在整数数组中查找重复项

43

作为一道作业问题,给出了以下任务:

你有一个包含1到1,000,000之间整数的数组。其中一个整数在数组中出现了两次。你如何确定这个整数?你能想到一种使用较少额外内存的方法吗?

我目前的解决方案:

解决方案1

  1. 创建哈希表
  2. 遍历数组并将其元素存储在哈希表中
  3. 一旦找到已经存在于哈希表中的元素,则它就是重复的元素

优点

它运行时间为O(n),并且只需一次遍历

缺点

它使用O(n)的额外内存

解决方案2

  1. 使用归并排序对数组进行排序(O(nlogn)时间)
  2. 再次遍历数组,如果看到一个元素两次,那么你得到了重复的元素

优点

它不使用额外内存

缺点

运行时间大于O(n)

你们能想到更好的解决方案吗?


10
一个好的作业问题的良好示例,+1。 - jkeys
如果我没记错的话,归并排序是面向列表而不是面向数组的。转换肯定会消耗内存。你可能最好使用面向数组的快速排序。 - RBarryYoung
10
据我所知,归并排序仍然可以用于数组。不过,我只是评论说这是一个SO上作业问题应该有的典型示例。问题以及已经完成的工作都已发布。 - Thomas Owens
这个数组的长度确切地为1,000,001,且在1到1,000,000之间的每个数字在该数组中仅出现一次,唯一的例外是一个数字恰好出现了两次。这样说是否正确? - Svante
1
https://dev59.com/03I-5IYBdhLWcg3w99kH - Ravindra S
9个回答

34

这个问题有点含糊不清:当请求是“哪一个”时,它是指返回重复的还是重复的位置?如果是前者,则以下任何一种解决方案都可以;如果是后者,则第一种解决方案是唯一能帮助你的。

解决方案#1:假设数组是不可变的

构建一个位图;在迭代数组时设置第n 个位。如果该位已经设置,则已找到重复项。它在线性时间内运行,并适用于任何大小的数组。

位图将创建与数组中可能值相同的位数。在遍历数组时,检查数组中的n 个位。如果设置了它,则已找到重复项。如果没有,那么设置它。(执行此操作的逻辑可以在维基百科上看到的这篇文章的伪代码中看到位数组(Bit arrays)或使用System.Collections.BitArray类。)

解决方案#2:假设数组是可变的

对数组进行排序,然后进行线性搜索,直到当前值等于前一个值。使用所有解决方案中最少的内存。将排序算法改为在比较操作期间检测重复项并尽早终止,可获得额外的奖励分数。

解决方案#3:(假设数组长度=1,000,001)

  1. 对数组中的所有整数求和。
  2. 从中减去整数1到1,000,000(包括1,000,000)之和。
  3. 剩下的就是你要找的重复值。

这几乎不需要额外的内存,如果同时计算总和,则可以一次性完成。

缺点是需要完整地循环以找到答案。

优点是简单易用,且很可能比其他解决方案运行更快。


3
只有当数组包含介于1到1,000,000之间的所有整数,并且有一个重复数字(总共1,000,001个元素)时,这种方法才有效对吧? - T .
@lavino:你能谈谈位图解决方案吗? - Learner
2
这类似于哈希表,但你使用了位图代替哈希表,它可以稍微减小大小,但仍然使用O(n)额外的内存。 - Learner
3
无论数组大小如何(最多有1,000,001个元素),那个位图都需要1,000,000个比特或122.07 kB。 - T .
1
@lavino:你的算法时间复杂度为 O(n)(最优解),而且使用了 O(1) 的额外空间。还有什么更多想要的呢? - jason
显示剩余12条评论

10
假设数组中包含从1到1,000,000的所有数字,则从1到1,000,000的所有数字的总和为(1,000,000)*(1,000,000 + 1)/2 = 500,000 * 1,000,001 = 500,000,500,000。
因此,只需将数组中的所有数字相加,减去500,000,500,000,就会剩下出现了两次的数字。
时间复杂度为O(n),空间复杂度为O(1)。
如果这个假设不成立,可以尝试使用布隆过滤器 - 它们可以比哈希表更紧凑地存储(因为它们只存储存在的事实),但它们确实存在误报的风险。通过我们选择在布隆过滤器上花费多少内存来限制这种风险。
然后,我们可以使用布隆过滤器以O(n)的时间检测潜在的重复项,并在O(n)的时间内检查每个候选项。

这仅适用于范围内的所有整数都存在的情况。至少从我在学校听过的有关高斯的故事中是这样记得的... - raoulsson
是的,没错。我从问题描述中也这么认为。但看起来我的理解是错误的。 - rampion
以概率方式进行的优势是什么? - jason
内存仍然是O(n),但系数要小得多(取决于您选择的误报率)。 - rampion
@Jason - 布隆过滤器只会出现假阳性,而不会出现假阴性。因此,您不会错过正确的答案。您可能会面临假阳性的风险,但可以通过表格的大小来控制,然后您可以对剩下的少数进行O(n)检查。 - rampion

6

这段Python代码是快速排序的修改版

def findDuplicate(arr):
    orig_len = len(arr)
    if orig_len <= 1:
        return None
    pivot = arr.pop(0)
    greater = [i for i in arr if i > pivot]
    lesser = [i for i in arr if i < pivot]
    if len(greater) + len(lesser) != orig_len - 1:
        return pivot
    else:
        return findDuplicate(lesser) or findDuplicate(greater)

我认为它可以在O(n logn)的时间内找到重复项。它使用了堆栈上的额外内存,但我相信它可以重新编写以仅使用原始数据的一份副本:

def findDuplicate(arr):
    orig_len = len(arr)
    if orig_len <= 1:
        return None
    pivot = arr.pop(0)
    greater = [arr.pop(i) for i in reversed(range(len(arr))) if arr[i] > pivot]
    lesser = [arr.pop(i) for i in reversed(range(len(arr))) if arr[i] < pivot]
    if len(arr):
        return pivot
    else:
        return findDuplicate(lesser) or findDuplicate(greater)

产生 greaterlesser 的列表推导式通过调用pop()破坏了原始数据。如果从中删除greaterlesser后,arr不为空,则必须存在重复项,且重复项为pivot

对于排序数据,代码会出现常见的堆栈溢出问题,因此需要使用随机枢轴或迭代解决方案将数据排队:

def findDuplicate(full):
    import copy
    q = [full]
    while len(q):
        arr = copy.copy(q.pop(0))
        orig_len = len(arr)
        if orig_len > 1:
            pivot = arr.pop(0)
            greater = [arr.pop(i) for i in reversed(range(len(arr))) if arr[i] > pivot]
            lesser = [arr.pop(i) for i in reversed(range(len(arr))) if arr[i] < pivot]
            if len(arr):
                return pivot
            else:
                q.append(greater)
                q.append(lesser)
    return None

然而,现在代码需要在循环的顶部进行数据的深度复制,这会改变内存需求。

计算机科学就到此为止。Python中的排序算法可能导致我的代码被朴素算法覆盖:

def findDuplicate(arr):
    arr = sorted(arr)
    prev = arr.pop(0)
    for element in arr:
        if element == prev:
            return prev
        else:
            prev = element
    return None

2
与其先对数组进行排序再检查,我建议编写一个比较排序函数的实现,一旦发现重复项就退出,这样不需要额外的内存需求(取决于你选择的算法),最坏情况下的时间复杂度为O(nlogn)(再次取决于算法),而不是最好(和平均,取决于...)情况下的O(nlogn)时间复杂度。
例如,可以实现基于原地合并排序的算法。
参考链接:http://en.wikipedia.org/wiki/Merge_sort

2
提示: 使用 A XOR A == 0 和 0 XOR A == A 的属性。

这对干什么有帮助?假设你将所有数字进行异或,而没有任何关于结果应该是什么的想法,那么这样做就没有任何诊断意义。 - hughdbrown
如果数组包含1,000,001个元素,则该解决方案比计算总和更好(需要更少的内存)。计算a [1]异或1异或a [2]异或2... - sdcvvc

0
def singleton(array):
  return reduce(lambda x,y:x^y, array)

对于某些输入是有效的(例如,range(1000) + [101]),但对于其他输入则无效(例如,range(1001) + [101])。 - ojrac

0
作为您解决方案(2)的变体,您可以使用 基数排序。不需要额外的内存,并且可以在线性时间内运行。您可以认为时间也受数字表示大小的影响,但您已经为此给出了限制:基数排序在时间 O(k n) 内运行,其中 k 是您可以每次传递排序的位数。这使得整个算法对于排序是 O(7n),对于检查重复数字是 O(n) —— 这是 O(8n)=O(n)。

优点:

  • 没有额外的内存消耗
  • O(n)

缺点:

  • 需要八次 O(n) 传递。

我不确定基数排序的时间复杂度是O(n)。在这种情况下,键的分布使得除了一个重复对之外,没有两个键相等。对于要检查重复项的任意最大列表,基数排序与其他高效排序完全相同,具体来说,它必须在最多log(n)次遍历中对最多n个元素进行排序,因此基数排序的时间复杂度为O(n log(n))。如果键的分布不同,例如固定大小的哈希值,并且键的数量显着大于键的域,则基数排序的时间复杂度为O(n)。 - SingleNegationElimination

0
那么如何解决查找所有重复项的问题呢?能否在O(n ln n)时间内完成?(排序和扫描)(如果您想恢复原始数组,请携带原始索引并在结束后重新排序,这可以在O(n)时间内完成)

使用计数排序可以使用额外的内存,是的。或者,使用基数排序而不使用额外的内存(但是,你可以争论使用基数排序有点“作弊”,因为它实际上是O(kn),其中k是数字的最大位数,而k与log n成比例--然而,这个问题的边界已经给出,所以k是固定的)。 - Jay

0

通过将整数排序到它们应该在的位置上来对整数进行排序。如果出现“冲突”,那么你就找到了正确的数字。

空间复杂度为O(1)(只是可以被覆盖的相同空间) 时间复杂度小于O(n),因为你会在到达末尾之前统计出冲突。


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