如何在一个打乱顺序的连续整数数组中找到重复元素?

74
我最近在某个地方看到了一个问题:
假设你有一个包含1001个整数的数组。这些整数是随机排列的,但你知道每个整数都在1到1000(含)之间。此外,每个数字仅出现一次,除了一个数字出现两次。假设你只能访问数组的每个元素一次,请描述一种算法来找到重复的数字。如果你的算法使用了辅助存储器,你能否找到不需要它的算法?
我感兴趣的是要知道第二部分,即不使用辅助存储的方法。你有任何想法吗?

13
很确定之前已经有人问过这个问题,但找不到确切的问题。序列中n个整数和重复的整数x的总和将是x + n(n-1)/2。 - Pete Kirkham
你可以在这里使用的另一个数学属性是阶乘。 (n1 * n2 * ..) / n! 给出所需的数字。 1000!阶乘实际上并不是那么大的数字 - http://justinwhite.com/big-calc/1000.html - Anurag
2
略有不同的问题,但答案相同:https://dev59.com/E3VD5IYBdhLWcg3wQZYg - starblue
1
再次提醒:https://dev59.com/4nNA5IYBdhLWcg3wGJ0V - starblue
3
请勿重复:https://dev59.com/KnRB5IYBdhLWcg3wro2B - starblue
显示剩余5条评论
19个回答

104

把它们全部加起来,然后从中减去如果只使用1001个数字时你所期望的总和。

例如:

Input: 1,2,3,2,4 => 12
Expected: 1,2,3,4 => 10

Input - Expected => 2

2
@Brian Rasmussen:额外的存储在哪里? - leppie
3
“@leppie: 保留计算出的总和,但我不确定原帖中的‘extra storage’具体指什么。不管怎样,我喜欢你的回答。” - Brian Rasmussen
4
@Brian,面试官可能是指“不要使用哈希表或数组”……我相当确定O(1)的存储空间,特别是单个变量,应该是可以接受的。 - Michael Aaron Safyan
7
这种方法完全有效。但是示例应该像这样:(1,3,2,4,2=>12)-(1+2+3+4=>10)= 2。 - SysAdmin
5
我不确定面试问题是否应该具有可扩展性 :) - Brian Rasmussen
显示剩余13条评论

77

更新2: 有些人认为使用异或运算符寻找重复数字是一种黑客行为或技巧。我的官方回答是:“我不是在寻找重复数字,而是在寻找位集数组中的重复模式。而异或运算符绝对比加法更适合操作位集。”

更新: 在我睡觉前,只是为了好玩,这里有一个“一行代码”的替代解决方案,它需要零额外存储(甚至没有循环计数器),只触及每个数组元素一次,是非破坏性的,但根本不可扩展 :-)

printf("Answer : %d\n",
           array[0] ^
           array[1] ^
           array[2] ^
           // continue typing...
           array[999] ^
           array[1000] ^
           1 ^
           2 ^
           // continue typing...
           999^
           1000
      );
请注意,编译器实际上会在编译时计算该表达式的后半部分,因此“算法”将在确切的1002个操作中执行。
如果数组元素的值也在编译时已知,则编译器将优化整个语句为常量。 :-)
原始解决方案:虽然它可以找到正确的答案,但不符合问题的严格要求。它使用一个额外的整数来保持循环计数器,并访问每个数组元素三次-两次读取和写入当前迭代,一次读取下一次迭代。
除了这个之外,这是一种破坏性算法,可以安全地扩展到任何N,最大值为MAX_INT。
for (int i = 1; i < 1001; i++)
{
   array[i] = array[i] ^ array[i-1] ^ i;
}

printf("Answer : %d\n", array[1000]);

我会留给你自己去思考为什么这个方法有效,但是给你一个简单的提示:-)

a ^ a = 0
0 ^ a = a

2
一种非破坏性的方法是在侧面维护一个累加器...我认为这也会使它更易读。 - Matthieu M.
2
@Matthiey M. - 但是一个非破坏性的解决方案需要额外的存储空间,因此违反了问题的要求。 - Franci Penov
1
@Dennis Zickefoose - 我并不是在争论带有额外整型变量的非破坏性解决方案不好。 :-) 但它确实违反了问题的要求,这就是为什么我选择了破坏性算法。至于循环计数器-没有办法避免这个,而且它是隐式地被允许的,因为问题说明代码允许对数组进行一次迭代,而没有循环计数器是不可能的。 - Franci Penov
1
@Pavel Shved - XOR 没有什么诀窍,它是一种具有众所周知的属性的数学运算,就像加法、乘法和其他运算一样。 - Franci Penov
1
@Pavel - 另外,你和我对问题的看法不同 - 因为我不是在寻找重复的数字,而是在寻找一组标志中的重复模式。当你用这种方式陈述问题时,现在使用加法就成了“诡计” :-) - Franci Penov
显示剩余7条评论

23

Franci Penov的解决方案的非破坏性版本。

可以通过使用 XOR 运算符来实现。

假设我们有一个大小为5的数组:4, 3, 1, 2, 2
它们在索引位置上分别是:               0, 1, 2, 3, 4

现在对所有元素和所有索引进行XOR运算。我们得到2,这就是重复的元素。这是因为0没有参与XOR运算。剩下的n-1个索引和数组中相同的n-1个元素配对,数组中唯一没有配对的元素将是重复的元素。

int i;
int dupe = 0;
for(i = 0; i < N; i++) {
    dupe = dupe ^ arr[i] ^ i;
}
// dupe has the duplicate.

该解决方案最好的特点是它不会遇到基于加法的解决方案中出现的溢出问题。

由于这是一道面试题,最好从基于加法的解决方案开始,识别溢出限制,然后给出基于XOR的解决方案:)

这种方法使用了一个额外的变量,因此并不完全符合问题要求。


2
老实说,我不太理解这些基于XOR的解决方案。基本上,我们正在尝试将“索引”与元素的值进行匹配。在匹配的情况下,结果将为零,对于重复的值,异或结果将为非零。对于一个简单的数组--> {1,2,2},我们将执行1(元素值)^1(索引)^0(先前的异或结果)--> 0; 2^2^0 --> 0; 3^2^0 --> 1。这里1是根据XOR解决方案的最终结果值。除非我漏掉了非常明显的东西,否则我不认为这是有效的答案。 - Prabhjot
@codaddict 我认为循环应该从i初始化为1开始。 - Raman Singh
1
@codaddict 因为清晰易懂的说明和提到了溢出(并且是非破坏性的),给你点赞 +1。即使整数有偏移,比如{ 1043, 1042, 1044, 1042 },通过与{ 0,1042,1043,1044 }进行异或运算,同样可以实现。 - legends2k

15

将所有数字相加。最终的结果将会是 1+2+...+1000+重复的数字。


8
为了解决这个问题,我们需要找到一个整数数组中重复次数为偶数的所有元素,除了一个元素之外,它的重复次数为奇数。然后,我们只需返回该元素即可。
要实现这个解决方案,我们可以使用异或运算符(^)。首先,我们将数组中的第一个元素与第二个元素进行异或运算,并将结果存储在一个变量中。接下来,我们将变量与数组中的下一个元素进行异或运算,直到遍历完整个数组。最终,我们将得到唯一出现奇数次的元素的值。
acc = 0
for i in array: acc = acc ^ i

您目前遇到的问题是一个适应性问题。关键是,您需要找到重复两次的元素,因此您需要调整解决方案以弥补这个怪癖。

acc = 0
for i in len(array): acc = acc ^ i ^ array[i]

虽然 Francis 的解决方案会破坏整个数组(顺便说一下,它只能破坏第一个或最后一个元素...),但它最终实现的是相同的目的。但由于需要额外的索引存储空间,如果您还使用了额外的整数,我认为您将得到原谅。限制很可能是因为他们想防止您使用数组。如果要求使用 O(1) 空间(1000 可以看作 N,因为这里是任意的),则表述会更加准确。

我已根据您的答案发布了一行Python代码,链接为https://dev59.com/ynE85IYBdhLWcg3wzWzM#2612318。 - jfs

6

将所有数字相加。整数1..1000的总和为(1000*1001)/2。与您得到的结果之差即为您的数字。


4

Python的一行解决方案

arr = [1,3,2,4,2]
print reduce(lambda acc, (i, x): acc ^ i ^ x, enumerate(arr), 0)
# -> 2

为什么它有效的解释在@Matthieu M.的答案中。


+1,干得好:尽管这不是一个代码高尔夫比赛,但使用Python的内置循环更快 :) - Matthieu M.

3

如果你知道我们有精确的数字1-1000,你可以将结果相加并从总数中减去500500sum(1, 1000))。这将给出重复的数字,因为sum(array) = sum(1, 1000) + repeated number


3

嗯,有一种非常简单的方法来解决这个问题...在1到1000之间的每个数字都恰好出现一次,除了重复的数字...所以,从1到1000的总和为500500。因此,算法如下:

sum = 0
对于数组中的每个元素:
   sum += 数组的该元素
number_that_occurred_twice = sum - 500500

1
n = 1000
s = sum(GivenList)
r = str(n/2)
duplicate = int( r + r ) - s

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接