找到列表中不存在的最小整数

Pet*_*ebb 84 arrays algorithm

我的一位同事使用的一个有趣的访谈问题:

假设您有一个非常长的未排序的无符号64位整数列表.您如何找到列表中未出现的最小非负整数?

后续行动:现在已经提出了通过排序的明显解决方案,你能比O(n log n)更快地完成吗?

FOLLOW-UP:您的算法必须在具有1GB内存的计算机上运行

澄清:列表在RAM中,但它可能会消耗大量的内容.您将提前获得列表的大小,例如N.

Ant*_*sma 117

如果数据结构可以就地变异并支持随机访问,那么您可以在O(N)时间和O(1)额外空间中进行.只需按顺序遍历数组,并为每个索引将索引处的值写入value指定的索引,递归地将该位置的任何值放到其位置并丢弃值> N.然后再次遍历数组查找该点其中value与索引不匹配 - 这是不在数组中的最小值.这导致最多3N比较,并且仅使用一些值的临时空间.

# Pass 1, move every value to the position of its value
for cursor in range(N):
    target = array[cursor]
    while target < N and target != array[target]:
        new_target = array[target]
        array[target] = target
        target = new_target

# Pass 2, find first location where the index doesn't match the value
for cursor in range(N):
    if array[cursor] != cursor:
        return cursor
return N
Run Code Online (Sandbox Code Playgroud)

  • 您的解决方案会混淆域和范围(目标是值和索引).该范围受可用存储限制为128M元素,但域大小为2G.它将失败,其中一个条目的值大于可以分配到数组中的条目数.如果问题没有指定"非常长",那么答案是优雅的,即使它破坏了输入.在这个问题中,时空权衡是非常明显的,并且在所提供的约束条件下可能无法实现O(N)解. (12认同)
  • 小挑剔.你错过了一个简单的案例:列表是{0,...,N-1}.在这种情况下,传递1不执行任何操作,并且在传递2数组[cursor] == cursor中列表中的所有条目,因此算法不会返回.所以你最后需要一个'return N'语句. (9认同)
  • 它可以在较大的值下正常工作.可以忽略较大的值,因为它们与不在数组中的最小值无关.对于您的示例,第一遍将循环遍历数组,忽略由于目标<N而导致的所有值,然后在第二遍的第一次迭代时返回0. (7认同)
  • 只有当值和索引的范围相当时,此解决方案才有效. (4认同)
  • 第二遍可以使用二进制搜索代替线性搜索。 (2认同)
  • @AntsAasma你能解释为什么最多3N比较吗?传球2是N比较,那么为什么传球1最多有2N比较? (2认同)

Ste*_*n C 89

这是一个O(N)使用O(N)空间的简单解决方案.我假设我们将输入列表限制为非负数,并且我们想要找到列表中不存在的第一个非负数.

  1. 找到列表的长度; 让我们说它是N.
  2. 分配一系列N布尔值,初始化为所有布尔值false.
  3. 对于X列表中的每个数字,如果X小于N,则将X'th数组的元素设置为true.
  4. 从索引开始扫描数组0,查找第一个元素false.如果你找到第一个false索引I,那么I就是答案.否则(即所有元素都是true),答案是N.

实际上," N布尔数组"可能被编码为表示为数组byteint数组的"位图"或"位组" .这通常使用较少的空间(取决于编程语言)并允许false更快地完成第一次扫描.


这就是算法如何工作的原因.

假设N列表中的数字不是不同的,或者它们中的一个或多个大于N.这意味着在列表中不存在至少一个0 .. N - 1不在列表中的数字.因此,找到最小缺失数的问题必须减少到找到小于N的最小缺失数的问题.这意味着我们不需要跟踪大于或等于N...... 的数字,因为它们不是答案.

上一段的替代方案是列表是来自的数字的排列0 .. N - 1.在这种情况下,步骤3将数组的所有元素设置为true,步骤4告诉我们第一个"缺失"数字是N.


该算法的计算复杂度O(N)具有相对较小的比例常数.它通过列表进行两次线性传递,或者如果已知列表长度,则只进行一次传递.没有必要表示将整个列表保存在内存中,因此算法的渐近内存使用正是表示布尔数组所需要的; 即O(N)比特.

(相比之下,依赖于内存中排序或分区的算法假设您可以在内存中表示整个列表.在提出问题的形式中,这将需要O(N)64位字.)


@Jorn评论说,步骤1到3是计数排序的变体.从某种意义上说,他是对的,但差异很大:

  • 计数排序需要一组(至少)Xmax - Xmin计数器,其中Xmax列表中的最大数字是列表Xmin中的最小数字.每个计数器必须能够代表N个状态; 即,假设二进制表示,它必须具有整数类型(至少)ceiling(log2(N))位.
  • 要确定数组大小,计数排序需要首先通过列表来确定XmaxXmin.
  • 因此,最小的最坏情况空间要求是ceiling(log2(N)) * (Xmax - Xmin)比特.

相比之下,上面提出的算法只需要N在最差和最好的情况下使用位.

然而,这种分析导致直觉,即如果算法初始通过列表寻找零(并且如果需要则计算列表元素),如果它找到零,它将给出更快的答案,根本不使用空格.如果在列表中找到至少一个零的概率很高,那么绝对值得这样做.而这个额外的通行证并没有改变整体的复杂性.


编辑:我已经改变了算法的描述,使用"布尔数组",因为人们显然发现我使用位和位图的原始描述令人困惑.

  • @ adi92在您的示例中,列表将包含300个元素.这意味着如果有任何"缺失"值,它必须小于300.运行算法,我们创建一个300个插槽的位域,然后重复设置插槽1,2和3中的位,留下全部其他插槽 - 0和4到299 - 清除.当扫描位域时,我们发现插槽0中的标志清零,所以我们知道0就是答案. (4认同)
  • 请注意,可以更简单地理解这个算法,而不需要钻头:"创建一个大小为N的布尔数组"等.一旦你理解了这一点,转向按位版本在概念上很容易. (4认同)
  • @ adi92如果步骤3给出了一个所有位都设置为1的位图,则列表包含从0到N-1的每个值.这意味着列表中最小的非负整数是N.如果0和N-1之间的任何值都不在列表中,则不会设置相应的位.因此,最小的这样的值就是答案. (3认同)
  • 在提供抽象解决方案时,请使用概念上最简单的方法,并且不要过于专业化.你的解决方案为使用(抽象)布尔数组而尖叫,所以称之为.您可以通过`bool []`或位图实现此数组与一般解决方案无关. (2认同)
  • 我认为这个解决方案最好用"使用忽略N以上元素的计数排序,然后从头开始进行线性搜索找到第一个缺失的元素". (2认同)

Bar*_*own 13

由于OP现在已经指定原始列表保存在RAM中,并且计算机只有1GB的内存,我将会出局并预测答案为零.

1GB的RAM意味着该列表中最多可包含134,217,728个数字.但是有2 64 = 18,446,744,073,709,551,616可能的数字.因此零列在列表中的概率是137,438,953,472中的1.

相比之下,我今年闪电击中的可能性是70万分之一.我被陨石击中的可能性大约是10万亿分之一.因此,由于天体过早死亡而不是零回答,我在科学期刊上写的可能性要高十倍.

  • 仅当值均匀分布并随机选择时,才会保留计算.它们也可以按顺序生成. (11认同)
  • 那么,受访者选择这个答案的几率是多少? (10认同)
  • @Amarghosh这个问题的答案也是零. (8认同)
  • 问题并不是说数字是随机均匀选择的.它们是由设置此问题的人选择的.鉴于此,列表中0的概率比137,438,953,472中的1大*大*,甚至可能大于2中的1 :-) (6认同)

cdi*_*ins 10

正如在其他答案中指出的那样,您可以进行排序,然后直接扫描直到找到间隙.

您可以通过使用修改后的QuickSort将算法复杂度提高到O(N)并保留O(N)空间,从而消除不是包含间隙的潜在候选分区.

  • 在第一个分区阶段,删除重复项.
  • 分区完成后,查看下部分区中的项目数
  • 此值是否等于用于创建分区的值?
    • 如果是这样,则意味着差距在更高的分区中.
      • 继续快速排序,忽略下部分区
    • 否则,间隙位于下部分区中
      • 继续快速排序,忽略更高的分区

这节省了大量计算.

  • 如果您知道列表的长度,您还可以剔除任何大于len(列表)的值.根据鸽笼原则,任何"洞"必须小于len(列表). (2认同)

Bar*_*own 8

由于数字都是64位长,我们可以对它们使用基数排序,即O(n).排序'em,然后扫描'直到找到你要找的东西.

如果最小数字为零,则向前扫描直到找到间隙.如果最小数字不为零,则答案为零.


I. *_*edy 8

为了说明O(N)思考的一个陷阱,这里有一个O(N)使用O(1)空间的算法.

for i in [0..2^64):
  if i not in list: return i

print "no 64-bit integers are missing"
Run Code Online (Sandbox Code Playgroud)

  • 在N个元素列表中查找数字显然是O(N).我们这样做2 ^ 64次.虽然很大,但是2 ^ 64是一个常数.因此算法是C*O(N),它仍然是O(N). (12认同)
  • Nic,Will,它是O(n*N),其中n是列表的大小,N是域的大小(64位整数).虽然N是一个庞大的数字,但它仍然是一个常数,因此正如所述的问题的复杂性是O(n). (6认同)
  • 我必须放弃我之前的发言; 通过最严格的定义,这个操作确实是O(n). (3认同)

Ego*_*gon 5

对于节省空间的方法,所有值都是不同的,您可以在空间O( k )和时间上进行O( k*log(N)*N ).它节省空间,没有数据移动,所有操作都是基本的(增加减法).

  1. U = N; L=0
  2. 首先划分k区域中的数字空间.像这样:
    • 0->(1/k)*(U-L) + L,0->(2/k)*(U-L) + L,0->(3/k)*(U-L) + L...0->(U-L) + L
  3. 查找count{i}每个区域中有多少个数字().(N*k步骤)
  4. 找到第一个h未满的区域().这意味着count{h} < upper_limit{h}.(k步骤)
  5. 如果h - count{h-1} = 1你有答案
  6. U = count{h}; L = count{h-1}
  7. 转到2

这可以使用散列来改进(感谢Nic这个想法).

  1. 相同
  2. 首先划分k区域中的数字空间.像这样:
    • L + (i/k)->L + (i+1/k)*(U-L)
  3. inc count{j} 运用 j = (number - L)/k (if L < number < U)
  4. 找到第一个h没有k个元素的region()
  5. 如果count{h} = 1h是你的答案
  6. U = maximum value in region h L = minimum value in region h

这将运行O(log(N)*N).