找到偶数出现的数字

Posted

技术标签:

【中文标题】找到偶数出现的数字【英文标题】:Find a number with even number of occurrences 【发布时间】:2012-09-02 18:59:57 【问题描述】:

给定一个数组,其中每个数字的出现次数为奇数,但出现次数为偶数的数字除外。找出偶数出现的数字。

例如

1, 1, 2, 3, 1, 2, 5, 3, 3

输出应该是:

2

以下是约束:

    数字不在范围内。 就地执行。 所需的时间复杂度为 O(N)。 数组可能包含负数。 数组未排序。

由于上述限制,我所有的想法都失败了:基于比较的排序、计数排序、BST、散列、蛮力。

我很想知道:XORing 会在这里工作吗?如果是,怎么做?

【问题讨论】:

不,不会。看反例:[1,1,1,5,2,2]。 1 异或 1 异或 1 异或 5 异或 2 异或 2 == 001 ^ 001 ^ 001 ^101 ^ 010 ^ 010 == 100 不确定复杂性,但你不能有两个哈希集,一个存储所有 seen 数字,一个存储一个数字当您看到它时,第二次看到它时将其删除,依此类推。最后,您将获得一组 (A) 包含所有数字,一组 (B) 包含所有奇数出现的数字。然后,您应该能够在线性时间内从 (A) 中减去 (B),这应该会产生结果。 (这假设了一个合适的哈希函数。)-: @aioobe:我相信通过“就地”,OP 正在寻找O(1) 空间解决方案。 (否则一个简单的直方图然后迭代就可以了) @Aashish:你有理由相信存在解决方案吗?如果有,那是什么原因?如果有人提供了一个输入,其中所有值都出现一次,但一个出现两次的除外,解决这个问题就像查找重复项一样。根据您的计算模型以及“数字不在范围内”的含义,在O(N) 时间找到重复项可能是不可能的。 您对输入值有所了解吗? “不在范围内”是什么意思?在 O(m) 中,m 是不同值的数量的 sorage 的解决方案会令人满意吗? 【参考方案1】:

这个问题已经困扰了我好几天的地铁行程。这是我的想法。

如果 A. Webb 是对的,并且这个问题来自采访或者是某种学术问题,我们应该考虑一下我们所做的(错误的)假设,并可能尝试探索一些简单的案例。

想到的两个极端子问题如下:

数组包含两个值:其中一个重复偶数次,另一个重复奇数次。 数组包含 n-1 个不同的值:所有值都出现一次,但一个值出现两次。

也许我们应该根据不同值的复杂度来拆分案例。

如果我们假设不同值的数量为 O(1),每个数组将有 m 不同的值,其中 m 独立于 n。在这种情况下,我们可以遍历原始数组擦除并计算每个值的出现次数。在示例中它会给出

1, 1, 2, 3, 1, 2, 5, 3, 3 -> First value is 1 so count and erase all 1
2, 3, 2, 5, 3, 3 -> Second value is 2, count and erase
-> Stop because 2 was found an even number of times.

这将解决复杂度为O(mn)第一个极端示例,其计算结果为O(n)

更好的是:如果不同值的数量是O(1),我们可以在哈希映射中计算值的出现次数,在读取整个数组后遍历它们并返回出现偶数次的值。这仍被视为O(1) 内存。

第二种极端情况是在数组中找到唯一重复的值。 这在O(n) 中似乎是不可能的,但在某些特殊情况下我们可以:如果数组有n 元素并且里面的值是1, n-1 + 重复值(或一些变体,例如x 和y 之间的所有数字)。在这种情况下,我们对所有值进行求和,从总和中减去n(n-1)/2,然后检索重复的值。

用数组内的随机值解决第二个极端情况,或者mn 上不恒定的一般情况,在恒定内存和O(n) 时间对我来说似乎是不可能的。

额外说明:在这里,XORing 不起作用,因为我们想要的数字出现偶数次,而其他数字出现奇数次。如果问题是“给出现 odd 次的数字,所有其他数字出现 even 次”我们可以对所有值进行 XOR 并找到奇数最后一个。

我们可以尝试使用这种逻辑寻找一种方法:我们需要一个类似函数的东西,对一个数字应用奇数次将产生 0,而偶数次将产生恒等式。不要认为这是可能的。

【讨论】:

如果这是一个学术或面试问题,这就是你应该尝试回答的方式——说出来并解释你的思考过程。不过,我不会在面试的情况下跳到“不可能”,而是以“我想在有更多时间的时候多考虑一下”作为结束语。 也就是说,除非你证明这是不可能的。位,“......一个函数,对一个数字应用奇数次将产生 0,偶数次将产生身份。不要认为这是可能的。”确实可以证明是不可能的。如果 f(x) = 0。那么 f(f(x)) = f(0),一个常数,对于任何输入 x。当然,这不是解决问题的唯一方法。 @A.Webb 我不会在采访中说不可能。但我会提出我可以解决的所有案例,甚至是关于数字在 [1, n-1] 中的位置以及每个数字出现一次的非常具体的案例。【参考方案2】:

简介

这是一个可能的解决方案。这是相当做作且不实用的,但是问题也是如此。如果我的分析中有漏洞,我将不胜感激。如果这是一个“官方”解决方案的家庭作业或挑战问题,我也很想看看原始海报是否仍然存在,因为距离被问到已经过去了一个多月。

首先,我们需要充实问题的一些不明确的细节。所需的时间复杂度是O(N),但N 是什么?大多数评论员似乎都假设N 是数组中的元素数。如果数组中的数字具有固定的最大大小,这将是可以的,在这种情况下,Michael G 的基数排序解决方案将解决问题。但是,在原始发帖人没有澄清的情况下,我将约束 #1 解释为不需要固定最大位数。因此,如果n(小写)是数组中元素的数量,m 是元素的平均长度,那么要处理的总输入大小是mn。解决方案时间的下限是O(mn),因为这是验证解决方案所需的输入通读时间。因此,我们想要一个与总输入大小N = nm 成线性关系的解决方案。

例如,我们可能有n = m,即sqrt(N) 元素的平均长度为sqrt(N)。比较排序需要O( log(N) sqrt(N) ) < O(N) 操作,但这不是胜利,因为操作本身平均需要O(m) = O(sqrt(N)) 时间,所以我们回到O( N log(N) )

此外,如果m最大 长度而不是平均 长度,则基数排序将采用O(mn) = O(N)。如果假设数字落在某个有界范围内,则最大和平均长度将处于相同的顺序,但如果不是这样,我们可能会有一个小百分比的数字大且可变,而大百分比的数字数字少.例如,10% 的数字的长度可能为 m^1.1,而 90% 的长度可能为 m*(1-10%*m^0.1)/90%。平均长度为m,但最大长度为m^1.1,因此基数排序为O(m^1.1 n) > O(N)

为了避免有人担心我对问题定义的改动太大,我的目标仍然是描述一个时间复杂度与元素数量成线性关系的算法,即O(n)。但是,我还需要对每个元素的长度执行线性时间复杂度的操作,这样平均而言,这些操作在所有元素上将是O(m)。这些操作将是计算元素哈希函数和比较所需的乘法和加法。如果这个解决方案确实解决了O(N) = O(nm) 中的问题,那么这应该是最佳复杂度,因为验证答案需要相同的时间。

问题定义中省略的另一个细节是是否允许我们在处理数据时销毁数据。为简单起见,我将这样做,但我认为可以避免这种情况。

可能的解决方案

首先,可能存在负数的约束是空的。通过一次数据,我们将记录最小元素z和元素数量n。在第二次遍历中,我们将为每个元素添加(3-z),因此现在最小的元素为 3。(请注意,恒定数量的数字可能会因此溢出,因此我们应该对数据进行恒定数量的额外遍历首先测试这些解决方案。)一旦我们有了我们的解决方案,我们只需减去(3-z) 以将其恢复为原始形式。现在我们可以使用三个特殊的标记值012,它们本身不是元素。

第一步

使用median-of-medians selection algorithm 确定数组A 的第90 个百分位元素p,并将数组划分为两个集合ST,其中S 具有10% of n 元素大于pT 的元素小于p。这需要O(n) 步骤(平均步骤O(m) 总共需要O(N))时间。匹配p 的元素可以放入ST,但为了简单起见,遍历数组一次并测试p 并通过将其替换为0 来消除它。设置S 最初跨越索引0..s,其中s 大约是n10%,设置T 跨越剩余90% 的索引s+1..n

第 2 步

现在我们将遍历i in 0..s,并且对于每个元素e_i,我们将计算一个散列函数h(e_i)s+1..n。我们将使用universal hashing 来获得均匀分布。因此,我们的散列函数将进行乘法和加法,并在每个元素的长度上花费线性时间。

我们将对碰撞使用修改后的线性探测策略:

    h(e_i)T 的成员占用(意思是A[ h(e_i) ] < p 但不是标记12)或者是0。这是一个哈希表未命中。通过交换插槽ih(e_i) 中的元素插入e_i

    h(e_i)S 的成员(意思是A[ h(e_i) ] > p)或标记12 占用。这是一个哈希表冲突。进行线性探测,直到遇到 e_i 的重复项或 T0 的成员。

    如果是T 的成员,这又是一个哈希表未命中,因此通过交换到插槽i 插入e_i,如(1.)

    如果与 e_i 重复,则这是哈希表命中。检查下一个元素。如果该元素是12,我们已经不止一次看到e_i,将1s 更改为2s,反之亦然,以跟踪其奇偶性变化。如果下一个元素不是12,那么我们之前只见过e_i 一次。我们希望将2 存储到下一个元素中,以表明我们现在已经看到了偶数次e_i。我们寻找下一个“空”槽,即被T 的成员占用的槽,我们将移动到槽i 或0,然后将元素向上移动到索引h(e_i)+1 向下,所以我们在h(e_i) 旁边留出空间来存储我们的奇偶校验信息。请注意,我们不需要再次存储 e_i 本身,所以我们没有用完额外的空间。

所以基本上我们有一个功能性哈希表,其中槽数是我们希望哈希的元素的 9 倍。一旦我们开始获得命中,我们也开始存储奇偶校验信息,所以我们最终可能只有 4.5 倍的插槽数,仍然是一个非常低的负载因子。有几种碰撞策略可以在这里工作,但由于我们的负载因子很低,平均碰撞次数也应该很低,线性探测应该以适当的时间复杂度平均解决它们。

第三步

一旦我们将0..s 的元素散列到s+1..n 中,我们就会遍历s+1..n。如果我们找到 S 的一个元素后跟一个2,那就是我们的目标元素,我们就完成了。 S 的任何元素 e 后跟 S 的另一个元素表示 e 仅遇到一次并且可以清零。同样e 后跟1 表示我们看到e 的次数为奇数,我们可以将e 和标记1 归零。

冲洗并按需要重复

如果我们没有找到我们的目标元素,我们重复这个过程。我们的第 90 个百分位数分区会将 10% 的 n 剩余最大元素移动到 A 的开头,并将剩余元素(包括空的 0-marker 插槽)移动到末尾。我们像以前一样继续进行散列。我们最多必须这样做 10 次,因为我们每次处理 10% 的 n

结论分析

通过中位数算法进行分区的时间复杂度为 O(N),我们做了 10 次,仍然是 O(N)。每个哈希操作平均占用O(1),因为哈希表负载很低,并且在total 中执行了O(n) 哈希操作(10 次重复中的每一次大约占 n 的 10%)。每个n 元素都有一个为它们计算的散列函数,时间复杂度与它们的长度成线性关系,所以平均来说所有元素O(m)。因此,总的散列操作是O(mn) = O(N)。所以,如果我已经正确分析了这个,那么总的来说这个算法是O(N)+O(N)=O(N)。 (如果加法、乘法、比较和交换的操作被假定为相对于输入的常数时间,它也是O(n)。)

请注意,该算法没有利用问题定义的特殊性质,即只有一个元素出现偶数次。我们没有利用问题定义的这种特殊性质,这为存在更好(更聪明)算法的可能性提供了可能性,但最终也必须是 O(N)。

【讨论】:

【参考方案3】:

见以下文章:Sorting algorithm that runs in time O(n) and also sorts in place, 假设最大位数是常数,我们可以在 O(n) 时间内对数组进行就地排序。

然后是计算每个数字的出现次数,平均需要 n/2 时间才能找到出现次数为偶数的数字。

【讨论】:

消除这种可能的解决方案可能是约束 #1 的原因——你不能假设最大位数是恒定的。 老实说,恒定的最大位数是一个公平的假设,在我们这个领域很常见。我们不假设这个数字是 int32 吗? 我同意 Michael G,但我不认为这个问题的目的是通过实际解决方案解决常见问题,而是在人为限制下解决学术或面试问题。

以上是关于找到偶数出现的数字的主要内容,如果未能解决你的问题,请参考以下文章

在数字列表中查找下一个奇数或下一个偶数

找出区间[A, B]内所有数字的奇数字位出现次数为偶数,偶数字位出现次数为计数的数的个数。(数位DP)

codeforces 703D Mishka and Interesting sum 偶数亦或 离线+前缀树状数组

每个奇数和偶数位置分块一个字符串

C语言编程 调整数组使奇数全部都位于偶数前面

1295. 统计位数为偶数的数字