生成一个整数,该整数不在给定的40亿个整数中


691

我得到了这个面试问题:

给定一个具有40亿个整数的输入文件,请提供一种算法来生成文件中不包含的整数。假设您有1 GB的内存。如果只有10 MB的内存,请执行后续操作。

我的分析:

文件大小为4×10 9 ×4字节= 16 GB。

我们可以进行外部排序,从而让我们知道整数的范围。

我的问题是在已排序的大整数集中检测丢失的整数的最佳方法是什么?

我的理解(阅读所有答案之后):

假设我们正在谈论32位整数,那么有2 32 = 4 * 10 9个不同的整数。

情况1:我们有1 GB = 1 * 10 9 * 8位= 80亿位内存。

解:

如果我们用一位代表一个不同的整数,那就足够了。我们不需要排序。

实现方式:

int radix = 8;
byte[] bitfield = new byte[0xffffffff/radix];
void F() throws FileNotFoundException{
    Scanner in = new Scanner(new FileReader("a.txt"));
    while(in.hasNextInt()){
        int n = in.nextInt();
        bitfield[n/radix] |= (1 << (n%radix));
    }

    for(int i = 0; i< bitfield.lenght; i++){
        for(int j =0; j<radix; j++){
            if( (bitfield[i] & (1<<j)) == 0) System.out.print(i*radix+j);
        }
    }
}

情况2:10 MB内存= 10 * 10 6 * 8位= 8000万位

解:

对于所有可能的16位前缀,有2 16个整数= 65536,我们需要2 16 * 4 * 8 = 2百万个位。我们需要构建65536个存储桶。对于每个存储桶,我们需要4个字节来保存所有可能性,因为最坏的情况是所有40亿个整数都属于同一个存储桶。

  1. 通过第一次遍历文件来构建每个存储桶的计数器。
  2. 扫描存储桶,找到命中率小于65536的第一个。
  3. 通过在文件的第二遍中构建在步骤2中发现高16位前缀的新存储桶
  4. 扫描步骤3中内置的存储桶,找到第一个没有命中的存储桶。

该代码与上面的代码非常相似。

结论:我们通过增加文件传递来减少内存。


对于迟到的人的澄清:问的问题不是说文件中没有正好包含一个整数-至少多数人不是这样解释的。但是,注释线程中的许多注释与任务的变化有关。不幸的是,引入注释线程的注释后来被其作者删除,因此现在看来,孤立的答复似乎误解了所有内容。很困惑,对不起。


32
@trashgod,错了。对于4294967295个唯一整数,您将剩余1个整数。要找到它,您应该对所有整数求和,并从所有可能的整数的预先计算的总和中减去它。
Nakilon

58
这是《 Programming Pearls》中的第二个“珍珠”,我建议您阅读本书中的整个讨论。见books.google.com/...
阿洛克辛格

8
@Richard一个64位int足够大。
cftarnas 2011年

79
int getMissingNumber(File inputFile) { return 4; }参考
约翰尼

14
不必存储从1到2 ^ 32的所有整数之和,因为在C / C ++等语言中,整数类型始终会保留诸如关联性和可通信性之类的属性。这意味着尽管总和不是正确的答案,但是如果您计算期望的溢出量,实际的总和溢出量,然后相减,结​​果仍然是正确的(前提是它本身不会溢出)。
dayturns'Aug

Answers:


529

假设“整数”表示32位:10 MB的空间已足够您计算带有给定16位前缀的输入文件中有多少个数字,一次通过所有可能的16位前缀输入文件。至少有一个水桶命中率低于2 16次。再进行一遍,以查找该存储桶中哪些可能的数字已被使用。

如果它表示大于32位,但仍为有界大小:请执行上述操作,忽略所有碰巧落在(有符号或无符号;您选择的)32位范围之外的所有输入数字。

如果“ integer”表示数学整数:请仔细阅读一次输入,并跟踪您所见过的最长数字中的最大数字长度。完成后,输出最大值加一再加上一个具有一位数字的随机数。(文件中的一个数字可能是一个大于10 MB的数字,要精确表示,但是如果输入是文件,则至少可以表示适合该文件的长度)。


24
完善。您的第一个答案只需要2次通过文件!
corsiKa 2011年

47
一个10 MB的bignum?那太极端了。
马克·兰索姆

12
@Legate,只需跳过大号,对它们不执行任何操作。由于无论如何您都不会输出过多的数字,因此无需跟踪您所看到的数字。
hmakholm在2011年

12
解决方案1的好处是,您可以通过增加通过次数来减少内存。
Yousf 2011年

11
@Barry:上面的问题并不表示确实缺少一个数字。它也没有说文件中的数字也不会重复。(在面试中回答实际提出的问题可能是个好主意,对吗?;
Christopher Creutzig,2011年

197

统计确定的算法比确定性方法使用更少的遍数来解决此问题。

如果允许使用非常大的整数,则可以生成一个在O(1)时间内可能唯一的数字。像GUID这样的伪随机128位整数将仅与集合中现有的40亿个整数之一发生冲突,而不会发生于每640亿亿个案例中的十分之一。

如果整数限制为32位,则可以使用一次少于10 MB的数据生成一个唯一可能唯一的数字。伪随机的32位整数将与40亿个现有整数之一相撞的几率约为93%(4e9 / 2 ^ 32)。1000个伪随机整数全部发生碰撞的几率小于12,000万亿的整数(一次碰撞的可能性^ 1000)。因此,如果程序维护一个包含1000个伪随机候选者的数据结构并迭代已知的整数,从而消除候选者中的匹配项,则可以肯定找到至少一个不在文件中的整数。


32
我很确定整数是有界的。如果不是这样,那么即使是初学者,程序员也会想到该算法“对数据进行一次遍历以找到最大数目,然后将其加1”
Adrian Petrescu

12
从字面上看,猜测随机输出可能不会在面试中获得很多分
Brian Gordon

6
@Adrian,您的解决方案似乎很明显(对我来说,我是在自己的答案中使用它的),但对每个人来说都不是显而易见的。这是一个很好的测试,看看您是否可以找到明显的解决方案,或者是否会使所碰到的一切变得过于复杂。
Mark Ransom

19
@Brian:我认为这种解决方案既富于想象力又实用。对于这个答案,我将一视同仁。
理查德H

6
啊,这就是工程师和科学家之间的界线。很好的答案本!
TrojanName 2011年

142

有关此问题的详细讨论,已在Jon Bentley的 “专栏1:裂解牡蛎”中进行了编程Pearls Addison-Wesley pp.3-10

Bentley讨论了几种方法,包括外部排序,使用多个外部文件进行合并排序等。但是Bentley建议的最佳方法是使用位字段的单次通过算法,他幽默地称其为“ Wonder Sort” :)来解决这个问题,40亿数字可以表示为:

4 billion bits = (4000000000 / 8) bytes = about 0.466 GB

实现位集的代码很简单:(摘自解决方案页面

#define BITSPERWORD 32
#define SHIFT 5
#define MASK 0x1F
#define N 10000000
int a[1 + N/BITSPERWORD];

void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); }
void clr(int i) {        a[i>>SHIFT] &= ~(1<<(i & MASK)); }
int  test(int i){ return a[i>>SHIFT] &   (1<<(i & MASK)); }

Bentley算法对文件进行一次遍历set,设置数组中的适当位,然后使用test上面的宏检查该数组以查找丢失的数字。

如果可用内存小于0.466 GB,Bentley建议使用k-pass算法,根据可用内存将输入划分为多个范围。举一个非常简单的例子,如果只有1个字节(即用于处理8个数字的内存)可用并且范围从0到31,我们将其分为0到7、8-15、16-22等范围。并在每次32/8 = 4通过中处理此范围。

HTH。


12
我不知道这本书,但没有理由称其为“ Wonder Sort”,因为它只是一个带有1位计数器的存储桶排序。
flolo 2011年

3
虽然越来越多的便携,这个代码将被消灭的代码写入到利用硬件支持的矢量指令。我认为gcc在某些情况下可以自动将代码转换为使用向量运算。
Brian Gordon

3
@brian我认为乔恩·本特利(Jon Bentley)不允许这种事情进入他的算法书。
David Heffernan

8
@BrianGordon,与读文件相比,在ram中花费的时间可以忽略不计。忘记优化它。
伊恩

1
@BrianGordon:或者您是在谈论最后找到未设置位的循环?是的,向量将加快速度,但是会使用64位整数在位域上循环,寻找!= -1仍将饱和运行在单个核上的内存带宽(这是SIMD-in-a-register,SWAR,以位为元素)。(用于最新的Intel / AMD设计)。找到包含该位的64位位置后,只需找出未设置的位即可。(为此,您可以这样做not / lzcnt。)公平地说,单位测试的循环可能无法很好地进行优化。
彼得·科德斯

120

由于问题未指明我们必须找到文件中没有的最小可能数字,因此我们可以生成比输入文件本身更长的数字。:)


6
除非文件中最大的数字是max int,否则您将溢出
KBusc 2014年

在实际程序中,可能需要生成一个新的整数并将其附加到“使用的整数”文件中100次的那个文件的大小是多少?
迈克尔

2
我在想这个 假设int32位,只输出2^64-1。做完了
imallett 2015年

1
如果这是每行一个INT:tr -d '\n' < nums.txt > new_num.txt:d
与Shon

56

对于1 GB RAM变量,可以使用位向量。您需要分配40亿位== 500 MB字节数组。对于从输入中读取的每个数字,将相应的位设置为“ 1”。完成后,遍历位,找到第一个仍为“ 0”的位。它的索引就是答案。


4
未指定输入中的数字范围。如果输入由80亿到160亿之间的所有偶数组成,该算法将如何工作?
马克·兰索姆

27
@Mark,只需忽略0..2 ^ 32范围之外的输入。无论如何,您都不打算输出它们中的任何一个,因此无需记住要避免使用哪个。
hmakholm在2011年

@Mark可以使用哪种算法来确定32位字符串如何映射到实数取决于您。过程仍然相同。唯一的区别是如何将其作为实数打印到屏幕上。
corsiKa 2011年

4
相反迭代自己的,你可以使用bitSet.nextClearBit(0)download.oracle.com/javase/6/docs/api/java/util/...
starblue

3
值得一提的是,不管整数的范围如何,在传递结束时至少要保证1位为0。这是由于信鸽原理。
2011年

46

如果它们是32位整数(可能从接近2 32的约40亿个数字中选择),则40亿个数字的列表最多将占据93%的可能整数(4 * 10 9 /(2 32) )。因此,如果您创建一个2 32位的位数组,并且每个位都初始化为零(这将占用2 29个字节〜500 MB的RAM;记住一个字节= 2 3位= 8位),请通读整数列表,然后对于每个int,将相应的位数组元素设置为0到1;然后读取您的位数组并返回仍然为0的第一位。

如果您的RAM较少(〜10 MB),则需要对该解决方案进行一些修改。10 MB〜83886080位仍然足以对0到83886079之间的所有数字进行位数组处理。因此,您可以通读int列表;并且仅在您的位阵列中记录0到83886079之间的#号。如果数字是随机分布的;出现压倒性的可能性(相差100%约10 -2592069),您会发现缺少整数。实际上,如果您仅选择数字1至2048(仅具有256个字节的RAM),那么您仍然会发现丢失的数字占了绝大多数(99.999999999999999999999999999999999999999999999999995%)。

但是,可以说不是拥有大约40亿个数字;您有类似2 32-1的数字和少于10 MB的RAM;因此,任何小范围的整数都只有很小的可能性不包含数字。

如果你保证,在列表中的每个INT是独一无二的,你可以总结数字和减去一个#缺少完整的总和(1/2)的总和(2 32(2 32 - 1)= 9223372034707292160寻找失踪的INT 。但是,如果int发生两次,则此方法将失败。

但是,您始终可以分而治之。幼稚的方法,将是通过阵列读取和计数是在所述第一半部分(0到2号的数目31 -1)和第二半(2 31,2 32)。然后选择数量较少的范围,然后重复将该范围二等分。(说,如果有两个(2数量少31,2 32),那么你下一次搜索会数范围(数字2 31,3 * 2 30 -1),(3 * 2 30,2 32),保持重复直到找到一个范围为零的范围并且您有答案。应该通过数组读取O(lg N)〜32次。

该方法效率低下。我们在每个步骤中仅使用两个整数(或大约8个字节的RAM和一个4字节(32位)的整数)。更好的方法是将sqrt(2 32)= 2 16 = 65536个bin 划分为每个bin中的65536个数字。每个bin需要4个字节来存储其计数,因此您需要2个18个字节= 256 kB。所以仓0是(0〜65535 = 2 16 -1),箱1是(2 16 = 65536 2 * 2 16 -1 = 131071),仓2是(2 * 2 16 = 131072到3 * 2 16 - 1 = 196607)。在python中,您会有类似以下内容:

import numpy as np
nums_in_bin = np.zeros(65536, dtype=np.uint32)
for N in four_billion_int_array:
    nums_in_bin[N // 65536] += 1
for bin_num, bin_count in enumerate(nums_in_bin):
    if bin_count < 65536:
        break # we have found an incomplete bin with missing ints (bin_num)

仔细阅读约40亿个整数列表;并计算2 个16个 bin 中的每个中有多少个int,并找到一个不完整的65536个数字的incomplete_bin。然后,您再次通读40亿个整数列表;但这一次只注意整数在该范围内;找到它们时会翻转一下。

del nums_in_bin # allow gc to free old 256kB array
from bitarray import bitarray
my_bit_array = bitarray(65536) # 32 kB
my_bit_array.setall(0)
for N in four_billion_int_array:
    if N // 65536 == bin_num:
        my_bit_array[N % 65536] = 1
for i, bit in enumerate(my_bit_array):
    if not bit:
        print bin_num*65536 + i
        break

3
如此棒的答案。这实际上可以工作;并保证了结果。
乔纳森·迪金森

@dr jimbob,如果容器中只有一个数字,并且那个数字有65535个重复项,该怎么办?如果是这样,则垃圾箱仍将计数65536,但是所有65536个数字都是相同的。
奥尔科特

@Alcott-我假设您有2 ^ 32-1(或更少)个数字,因此根据信鸽原理,您可以确保只有一个少于65536个计数的垃圾箱,以进行更详细的检查。我们正在尝试查找仅一个缺失的整数,而不是全部。如果您有2 ^ 32或更多的数字,则不能保证缺少整数,并且不能使用此方法(或者从一开始就保证有缺失的整数)。最好的选择就是蛮力(例如,读32次数组;第一次检查前65536个#;找到答案后立即停止)。
jimbob博士2011年

亨宁(Henning)早些时候发布了聪明的upper-16 / lower-16方法:stackoverflow.com/a/7153822/224132。不过,我喜欢将唯一的一组整数丢失一个唯一的整数的加法加法。
彼得·科德斯

3
@PeterCordes-是的,Henning的解决方案早于我的解决方案,但我认为我的回答仍然很有用(更详细地研究几件事)。就是说,乔恩·本特利(Jon Bentley)在他的《编程珍珠》一书中建议在存在堆栈溢出之前针对此问题(请参见vine'th的答案)进行多遍选择(不是我声称我们中的一个有意识地从那里偷走了,或者本特利是第一个这样做的人)分析这个问题-这是一个很自然的解决方案)。当限制是您不再有足够的内存来容纳带有巨型位阵列的1遍解决方案时,两遍似乎是最自然的选择。
jimbob博士

37

为什么要这么复杂?您要求文件中不存在整数?

根据指定的规则,您唯一需要存储的是文件中到目前为止遇到的最大整数。读取完整个文件后,返回大于1的数字。

没有命中maxint或任何东西的风险,因为根据规则,对整数的大小或算法返回的数字没有限制。


4
除非最大int在文件中,否则这将是可行的,这是完全可能的...
PearsonArtPhoto 2011年

13
规则没有指定它是32位还是64位或其他任何值,因此根据指定的规则,没有max int。整数不是计算机术语,它是标识正整数或负整数的数学术语。
皮特

的确如此,但是不能假设它是64位数字,否则就不会有人仅仅为了混淆这样的算法而潜入max int数字。
PearsonArtPhoto 2011年

24
如果未指定任何编程语言,则整个“ max int”概念在上下文中无效。例如,查看Python对长整数的定义。它是无限的。没有屋顶。您可以随时添加一个。您假设正在使用具有最大允许整数值的语言来实现它。
皮特

32

使用二进制搜索的变体可以解决这一问题。

  1. 从允许的数字范围开始,04294967295

  2. 计算中点。

  3. 循环浏览文件,计数等于或小于中点值的数字个数。

  4. 如果没有数字相等,那么就完成了。中点数字就是答案。

  5. 否则,请选择编号最少的范围,并使用新范围从步骤2开始重复。

这将需要整个文件进行32次线性扫描,但是仅使用几个字节的内存来存储范围和计数。

这与Henning的解决方案基本相同,除了它使用两个垃圾箱而不是16k。


2
这就是我开始为给定参数进行优化之前的过程。
hmakholm在2011年

@亨宁:太酷了。这是一个很好的算法示例,可以很容易地调整时空权衡。
hammar 2011年

@hammar,但是如果那些数字出现不止一次该怎么办?
Alcott

@Alcott:然后该算法将选择较密集的容器,而不是稀疏的容器,但是根据信鸽原理,它永远无法选择完全充满的容器。(两个计数中的较小者将始终小于bin范围。)
Peter Cordes

27

编辑好的,这还没有经过深思熟虑,因为它假定文件中的整数遵循某种静态分布。显然他们并不需要,但是即使如此,也应该尝试这样做:


共有约43亿个32位整数。我们不知道它们在文件中的分布方式,但是最坏的情况是香农熵最高的情况:均等分布。在这种情况下,文件中不出现任何一个整数的概率为

((2³²-1)/2³²)⁴⁰⁰⁰≈.4

香农熵越低,则平均获得该概率越高,但是即使在这种最坏的情况下,我们也有90%的机会在用随机整数进行5次猜测后找到了一个不出现的数字。只需使用伪随机数生成器创建此类数字,然后将其存储在列表中即可。然后在int之后读取int并将其与您的所有猜测进行比较。匹配时,删除此列表条目。浏览完所有文件后,您可能还剩下一个以上的猜测。使用其中任何一个。在几乎没有猜测的罕见(即使在最坏的情况下为10%)事件中,获取一组新的随机整数,这次可能更多(10-> 99%)。

内存消耗:几十个字节,复杂度:O(n),开销:可忽略不计,因为大多数时间将花费在不可避免的硬盘访问上,而不是始终比较整数。


当我们假设静态分布时,实际最坏的情况是每个整数都出现最大值。一次,因为这样文件中就不会出现所有整数的1-4000000000 /2³²≈6%。因此,您还需要更多猜测,但这仍然不会浪费大量内存。


5
我很高兴看到其他人想到了这一点,但是为什么它一直在底部呢?这是一种1遍算法…10 MB足以应付2.5 M的猜测,而93%^ 2.5M≈10 ^ -79000确实是需要进行第二次扫描的机会很小。由于二进制搜索的开销,如果您使用更少的猜测,它将更快。这在时间和空间上都是最佳的。
Potatoswatter

1
@Potatoswatter:很好,您提到了二进制搜索。仅使用5个猜测时,这可能不值得开销,但肯定是10个或更多。您甚至可以进行2 M个猜测,但是您应该将它们存储在散列集中以获取O(1)进行搜索。
2011年

1
@Potatoswatter Ben Haley的等效答案接近顶部
Brian Gordon

1
我喜欢这种方法,但建议采用一种节省内存的方法:如果有N位可用的索引存储,再加上一些常量存储,则定义一个可配置的可逆32位加扰函数(置换),选择一个任意置换,然后清除所有置换索引位。然后从文件中读取每个数字,对其进行加密,如果结果小于N,则设置相应的位。如果文件末尾未设置任何位,请反转其索引上的加扰功能。凭借64KB的内存,可以一次有效地测试超过512,000个数字的可用性。
超级猫

2
当然,使用这种算法,最坏的情况是数字是由您使用的同一随机数生成器创建的。假设可以保证不是这种情况,那么最佳策略是使用线性同余随机数生成器生成列表,以便以伪随机方式遍历数字空间。这意味着,如果您以某种方式失败,则可以继续生成数字,直到覆盖完所有整数(找到一个差值)为止,而无需重复进行任何工作。
Dewi Morgan Morgan 2015年

25

如果在[0,2 ^ x -1] 范围内缺少一个整数,则将它们全部异或。例如:

>>> 0 ^ 1 ^ 3
2
>>> 0 ^ 1 ^ 2 ^ 3 ^ 4 ^ 6 ^ 7
5

(我知道这并不能完全回答问题,但这是对非常相似的问题的很好的回答。)


1
是的,当缺少一个整数时0 ^ 1 ^ 3 ^ 4 ^ 6 ^ 7,很容易证明[ ]有效,但是如果缺少多个整数,它通常会失败。例如,为0。[写2 x表示 2的x次幂,而a ^ b表示xor b,则所有k <2 x 的xor为零-k ^〜k =(2 ^ x)-当k <2 ^(x-1)时为1,并且当j = k + 2 **(x-2)时k ^〜k ^ j ^〜j = 0-因此除一个数外的所有数的异或就是值失踪者之一]
詹姆斯·沃尔德比-jwpat7 2011年

2
正如我在ircmaxell答复的评论中提到的那样:问题不是说“缺少一个数字”,而是说找到文件中40亿个数字中未包括的数字。如果我们假设32位整数,那么文件中可能缺少大约3亿个数字。当前数字与缺失数字匹配的可能性只有大约7%。
詹姆斯·沃尔德比-jwpat7 2011年

这是我最初阅读该问题时所想到的答案,但是仔细检查后,我认为这个问题比这更模棱两可。仅供参考,这是我想的问题:stackoverflow.com/questions/35185/...
李瑟顿

18

他们可能希望查看您是否听说过概率布隆过滤器,该过滤器可以非常有效地绝对确定某个值是否不属于大型集合(但只能以很高的概率确定该值是否属于该集合)。


4
设置了可能超过90%的可能值后,您的Bloom Filter可能需要退化为位域,因此已经使用了许多答案。否则,您将最终得到一个无用的完全填充的位串。
Christopher Creutzig

@Christopher我对Bloom过滤器的理解是,直到达到100%,您才得到填充的位数组
Paul

...否则,您会得到假阴性。
保罗

@Paul填充位数组会给您带来误报,这是允许的。在这种情况下,布隆过滤器很可能会退化为负数的解返回假正数的情况。
2011年

1
@Paul:只要哈希函数的数量乘以条目的数量与字段的长度一样大,就可以得到一个填充的位数组。当然,这将是一个例外情况,但是概率将很快上升。
Christopher Creutzig

17

根据原始问题的当前措辞,最简单的解决方案是:

在文件中找到最大值,然后将其加1。


5
如果文件中包含MAXINT怎么办?
Petr Peller

@Petr Peller:BIGINT库实际上将消除对整数大小的限制。
oosterwal 2011年

2
@oosterwal,如果允许使用此答案,则您甚至不需要读取文件-只需打印尽可能大的数字即可。
Nakilon 2011年

1
@oosterwal,如果您的随机大数字是您可以打印的最大数字,并且已在文件中,则此任务无法解决。
Nakilon 2011年

3
@Nakilon:+1您的观点被接受了。大致相当于计算文件中的位数并打印出具有该位数的数字。
oosterwal

14

使用BitSet。将40亿个整数(假设最多2 ^ 32个整数)打包成8个字节(每字节)是2 ^ 32/2 ^ 3 = 2 ^ 29 =大约0.5 Gb。

要添加更多细节-每次读取数字时,请在BitSet中设置相应的位。然后,在BitSet上进行传递以找到第一个不存在的数字。实际上,您可以通过重复选择一个随机数并测试是否存在来同样有效地执行此操作。

实际上,BitSet.nextClearBit(0)会告诉您第一个未设置的位。

从BitSet API来看,它似乎仅支持0..MAX_INT,因此您可能需要2个BitSet-一个用于+'ve数字,一个用于-'ve数字-但内存需求没有变化。


1
或者,如果您不想使用BitSet...,请尝试使用位数组。做同样的事情;)
jcolebrand 2011年

12

如果没有大小限制,最快的方法是获取文件的长度,并生成文件的长度+ 1个随机数字(或仅为“ 11111 ...” s)。优点:您甚至不需要读取文件,并且可以将内存使用率降至几乎为零。缺点:您将打印数十亿个数字。

但是,如果唯一的因素是最大程度地减少内存使用,而没有其他重要的事情,那么这将是最佳解决方案。它甚至可能使您获得“最糟糕的规则滥用”奖。


11

如果我们假设数字范围将始终为2 ^ n(2的偶数次幂),则“异或”将起作用(如另一幅海报所示)。至于为什么,让我们证明一下:

理论

给定任何从零开始的整数范围,其中有2^n一个元素丢失一个元素,您可以通过简单地将已知值异或在一起以得出丢失的数字来找到该丢失的元素。

证据

让我们看一下n =2。对于n = 2,我们可以表示4个唯一整数:0、1、2、3。它们的位模式为:

  • 0-00
  • 1-01
  • 2-10
  • 3-11

现在,如果我们看的话,每一位都精确地设置了两次。因此,由于将其设置为偶数,并且数字的“异或”运算将产生0。如果缺少单个数字,则“异或”将产生一个数字,当与缺失的数字进行异或运算时,将导致0。因此,丢失的数字和所得的异或数字完全相同。如果我们删除2,则结果xor将为10(或2)。

现在,让我们看一下n + 1。让我们调用设置每个位的次数nx以及设置每个位的次数n+1 y。的值y等于,y = x * 2因为有些x元素的n+1位设置为0,而x元素的n+1位设置为1。由于2x将始终为偶数,n+1因此始终将每个位设置为偶数次。

因此,由于n=2作品,n+1作品,异或方法进行的所有值工作n>=2

基于0的范围的算法

这很简单。它使用2 * n位内存,因此对于<= 32的任何范围,将使用2个32位整数(忽略文件描述符消耗的任何内存)。它使文件一次通过。

long supplied = 0;
long result = 0;
while (supplied = read_int_from_file()) {
    result = result ^ supplied;
}
return result;

基于任意距离的算法

只要总范围等于2 ^ n,该算法就适用于任何起始数字到任何终止数字的范围...这基本上是重新确定范围的最小范围为0。但是它确实需要2次通过通过文件(第一个获取最小值,第二个计算缺失的int)。

long supplied = 0;
long result = 0;
long offset = INT_MAX;
while (supplied = read_int_from_file()) {
    if (supplied < offset) {
        offset = supplied;
    }
}
reset_file_pointer();
while (supplied = read_int_from_file()) {
    result = result ^ (supplied - offset);
}
return result + offset;

任意范围

我们可以将此修改后的方法应用于一组任意范围,因为所有范围都将至少一次穿越2 ^ n的幂。仅在缺少单个位时有效。它需要2次传递未排序的文件,但是每次都会找到一个丢失的数字:

long supplied = 0;
long result = 0;
long offset = INT_MAX;
long n = 0;
double temp;
while (supplied = read_int_from_file()) {
    if (supplied < offset) {
        offset = supplied;
    }
}
reset_file_pointer();
while (supplied = read_int_from_file()) {
    n++;
    result = result ^ (supplied - offset);
}
// We need to increment n one value so that we take care of the missing 
// int value
n++
while (n == 1 || 0 != (n & (n - 1))) {
    result = result ^ (n++);
}
return result + offset;

基本上,将范围重新设定为0左右。然后,在计算异或时,它会计算要附加的未排序值的数量。然后,它将1加到未排序值的计数上以处理缺失值(计算缺失值)。然后,继续对n值进行异或运算,每次将其递增1,直到n为2的幂为止。然后将结果重新基于原始值。做完了

这是我在PHP中测试的算法(使用数组而不是文件,但概念相同):

function find($array) {
    $offset = min($array);
    $n = 0;
    $result = 0;
    foreach ($array as $value) {
        $result = $result ^ ($value - $offset);
        $n++;
    }
    $n++; // This takes care of the missing value
    while ($n == 1 || 0 != ($n & ($n - 1))) {
        $result = $result ^ ($n++);
    }
    return $result + $offset;
}

以任意范围的值(我测试过包括否定值)将数组放入其中,而该范围内缺少一个值,则每次都会找到正确的值。

另一种方法

由于我们可以使用外部排序,所以为什么不检查间隙呢?如果我们假设文件在运行此算法之前已排序:

long supplied = 0;
long last = read_int_from_file();
while (supplied = read_int_from_file()) {
    if (supplied != last + 1) {
        return last + 1;
    }
    last = supplied;
}
// The range is contiguous, so what do we do here?  Let's return last + 1:
return last + 1;

3
问题不是说“缺少一个数字”,而是说找到文件中40亿个数字中未包括的数字。如果我们假设32位整数,那么文件中可能缺少大约3亿个数字。当前数字与缺失数字匹配的可能性只有大约7%。
詹姆斯·沃尔德比-jwpat7 2011年

如果您有一个不为零的连续但缺失的范围,请添加而不是xor。 sum(0..n) = n*(n+1)/2。所以missing = nmax*(nmax+1)/2 - nmin*(nmin+1)/2 - sum(input[])。(总结来自@hammar的回答。)
Peter Cordes,

9

技巧问题,除非被引用不正确。只需一次读取文件即可获得最大整数n,然后返回n+1

当然,您需要一个备份计划,以防n+1引起整数溢出。


3
这是一个可行的解决方案,除非无效。有用!:-)
dty

除非引用不正确,否则问题不会对整数类型甚至所用语言造成限制。许多现代语言都具有仅受可用内存限制的整数。如果文件中的最大整数> 10MB,那么运气不好,第二种情况无法执行任务。我最喜欢的解决方案。
尔根·斯特罗贝尔

9

检查输入文件的大小,然后将其输出任何数目是太大而不能由一个文件,该文件大小表示。这看似便宜,但它是解决面试问题的创造性方法,它巧妙地避开了内存问题,从技术上讲是O(n)。

void maxNum(ulong filesize)
{
    ulong bitcount = filesize * 8; //number of bits in file

    for (ulong i = 0; i < bitcount; i++)
    {
        Console.Write(9);
    }
}

应该打印10位计数 -1,它将始终大于2位计数。从技术上讲,你要拍数为2 位计数 - (4×10 9 - 1) ,因为你知道有(4十亿- 1)在文件中其他的整数,甚至与完美的压缩,他们至少会占用每个一位。


为什么不只是Console.Write( 1 << bitcount )循环呢?如果文件中有n位,则绝对保证前(1)位的(_n_ + 1)位数字更大。
Emmet

@Emmet-这只会导致整数溢出,除非文件小于int的大小(在C#中为4个字节)。C ++可能会让您使用更大的东西,但C#似乎只允许<<运算符使用32位整数。无论哪种方式,除非您滚动自己的巨型整数类型,否则文件大小都非常小。演示:rextester.com/BLETJ59067
贾斯汀·摩根

8
  • 最简单的方法是在文件中找到最小数目,然后返回最小数目。这将使用O(1)存储和n个文件的O(n)时间。但是,如果数字范围受到限制,它将失败,这可能会使min-1不是数字。

  • 已经提到了使用位图的简单明了的方法。该方法使用O(n)时间和存储空间。

  • 还提到了具有2 ^ 16个计数桶的2遍方法。它读取2 * n个整数,因此使用O(n)时间和O(1)存储,但是它不能处理具有2 ^ 16以上数字的数据集。但是,通过运行4次而不是2次,它很容易扩展为(例如)2 ^ 60个64位整数,并且通过仅使用与内存匹配的尽可能多的bin并相应地增加通过次数,可以轻松地适应使用微型内存。在这种情况下,运行时间不再是O(n),而是O(n * log n)。

  • 到目前为止,rfrankel和ircmaxell都提到了将所有数字进行异或运算的方法,这回答了ltn100所指出的stackoverflow#35185中提出的问题。它使用O(1)存储和O(n)运行时间。如果目前假设32位整数,则XOR产生不同数字的概率为7%。理由:给定约4G的不同数字XOR的总和。300M不在文件中,每个位位置的置1位数具有相等的奇数或偶数机会。因此,有2 ^ 32个数字具有作为XOR结果的同等可能性,其中93%已经归档。请注意,如果文件中的数字不是全部不同,则XOR方法成功的可能性会增加。


7

由于某种原因,我一读到这个问题就想到了对角化。我假设任意大的整数。

阅读第一个数字。左填充零位,直到有40亿位。如果第一个(高阶)位为0,则输出1;否则为0。否则输出0。(不必真正按左移键:如果数字中位数不足,只需输出1。)对第二个数字进行相同的操作,除了使用第二个数字。以这种方式继续浏览文件。您将一次输出一个40亿位数字,该数字将与文件中的任何数字都不相同。证明:它与第n个数字相同,然后他们会在第n个比特上达成共识,但并非出于构造目的。


+1用于创造力(这是单通道解决方案中最小的最坏情况输出)。
hmakholm在2011年

但是,没有40亿个对角线化,只有32个。您最终将得到一个32位数字,该数字与列表中的前32个数字不同。
Brian Gordon

@Henning几乎没有通过;您仍然必须从一元转换为二进制。编辑:好吧,我想这是对文件的一次传递。没关系。
布赖恩·戈登

@Brian,这里哪里有“一元”的东西?答案是一次构造一个二进制答案,它只读取输入文件一次,使其一次通过。(如果需要十进制输出,那么会出现问题-那么您最好每三个输入数字构造一个十进制数字,并接受输出数字的对数增加10%)。
hmakholm在2011年

2
@Henning这个问题对于任意大的整数没有意义,因为正如许多人指出的那样,只是找到最大的数字并加一个,或者从文件本身构造一个非常长的数字是微不足道的。这种对角线化解决方案特别不合适,因为i您可以只输出1位40亿次,而不是在第位进行分支,而在末尾再抛出1。我可以在算法中包含任意大的整数但我认为问题在于输出缺少的32位整数。只是没有其他办法。
布莱恩·戈登

6

您可以使用位标志来标记是否存在整数。

遍历整个文件后,扫描每一位以确定该数字是否存在。

假设每个整数为32位,则如果完成位标记,它们将方便地放入1 GB RAM中。


0.5 Gb,除非您已将字节重新定义为4位;-)
dty

2
@dty我认为他的意思是“舒适”,因为1Gb中将有很多空间。
corsiKa 2011年

6

从文件中删除空格和非数字字符并追加1。您的文件现在包含一个未在原始文件中列出的数字。

从Carbonetc的Reddit。


爱它!即使这不是他一直在寻找的答案...:D
Johann du Toit

6

为了完整起见,这是另一个非常简单的解决方案,很可能需要很长时间才能运行,但占用的内存却很少。

假设所有可能的整数为从int_min到的范围int_max,并且 bool isNotInFile(integer)一个函数,如果文件不包含某个整数,则返回true,否则返回false(通过将该特定整数与文件中的每个整数进行比较)

for (integer i = int_min; i <= int_max; ++i)
{
    if (isNotInFile(i)) {
        return i;
    }
}

问题恰恰与isNotInFile功能算法有关。在回答之前,请确保您了解问题。
Aleks G

2
不,问题是“文件中没有哪个整数”,而不是“文件中是整数x”。例如,用于确定后一个问题答案的函数可以仅将文件中的每个整数与所讨论的整数进行比较,并在匹配时返回true。
deg

我认为这是一个合理的答案。除了I / O,您只需要一个整数和一个bool标志。
Brian Gordon

@Aleks G-我不明白为什么这被标记为错误。我们都同意这是所有算法中最慢的算法:-),但是它可以工作并且只需要4个字节即可读取文件。原始问题并未规定该文件只能读取一次。
Simon Mourier 2011年

1
@Aleks G-对。我也没说过你说过 我们只是说可以使用文件循环来简单地实现IsNotInFile:打开;如果不是Eof {Read Integer;如果Integer = i,则返回False;否则继续;}。它仅需要4个字节的内存。
西蒙·穆里尔

5

对于10 MB的内存限制:

  1. 将数字转换为其二进制表示形式。
  2. 创建一个二叉树,其中left = 0和right = 1。
  3. 使用其二进制表示形式将每个数字插入树中。
  4. 如果已经插入数字,则将已经创建了叶子。

完成后,只需采用之前未创建的路径即可创建请求的号码。

40亿个数字= 2 ^ 32,意味着10 MB可能不够。

编辑

有可能进行优化,如果创建了两个末端叶子并具有一个公共父叶,则可以将其删除并将该父叶标记为不是解决方案。这样可以减少分支并减少对内存的需求。

编辑二

也无需完全构建树。如果数字相似,则只需要建立深分支。如果我们也削减分支,那么该解决方案实际上可能会起作用。


6
...以及如何容纳10 MB?
hmakholm在2011年

怎么样:将BTree的深度限制在10MB以内;这意味着您将获得一组{误报| },您可以遍历该代码并使用其他技术找到值。
乔纳森·迪金森

5

我将回答1 GB版本:

问题中没有足够的信息,因此我将首先陈述一些假设:

整数是32位,范围为-2,147,483,648至2,147,483,647。

伪代码:

var bitArray = new bit[4294967296];  // 0.5 GB, initialized to all 0s.

foreach (var number in file) {
    bitArray[number + 2147483648] = 1;   // Shift all numbers so they start at 0.
}

for (var i = 0; i < 4294967296; i++) {
    if (bitArray[i] == 0) {
        return i - 2147483648;
    }
}

4

只要我们在做创造性的回答,这就是另一个。

使用外部排序程序对输入文件进行数字排序。这将适用于您可能拥有的任何数量的内存(如果需要,它将使用文件存储)。通读排序的文件并输出缺少的第一个数字。


3

消除位

一种方法是消除位,但是实际上可能不会产生结果(可能不会)。伪代码:

long val = 0xFFFFFFFFFFFFFFFF; // (all bits set)
foreach long fileVal in file
{
    val = val & ~fileVal;
    if (val == 0) error;
}

位计数

跟踪位数;并使用数量最少的位生成值。同样,这不能保证产生正确的值。

范围逻辑

跟踪列表的排序范围(按开始排序)。范围由以下结构定义:

struct Range
{
  long Start, End; // Inclusive.
}
Range startRange = new Range { Start = 0x0, End = 0xFFFFFFFFFFFFFFFF };

浏览文件中的每个值,然后尝试将其从当前范围中删除。此方法没有内存保证,但应该做得很好。


3

2 128 * 10 18 +1(即(2 816 * 10 18 +1)-今天不能成为一个普遍的答案吗?这表示无法保存在16 EB文件中的数字,这是任何当前文件系统中的最大文件大小。


您将如何打印结果?您不能将其放在文件中,并且在屏幕上打印将花费数十亿年。今天的计算机不可能达到正常运行时间。
vsz 2011年

从来没有说过我们需要在任何地方打印结果,只需“生成”它即可。所以这取决于您的意思是generate。无论如何,我的回答只是避免制定出真正的算法的一个技巧:)
Michael Sagalovich

3

我认为这是一个已解决的问题(请参见上文),但是请牢记一个有趣的附带情况,因为可能会被问到:

如果正好有4,294,967,295(2 ^ 32-1)个32位整数没有重复,因此仅缺少一个整数,则有一个简单的解决方案。

从零开始运行总计,然后为文件中的每个整数添加32位溢出的整数(有效地,runningTotal =(runningTotal + nextInteger)%4294967296)。完成后,将4294967296/2添加到运行总计中,再次出现32位溢出。从4294967296减去它,结果是缺少的整数。

仅运行一次即可解决“只有一个丢失的整数”问题,并且只有64位RAM专用于数据(运行总数为32位,下一个整数读取32位)。

结论:如果我们不关心整数结果必须具有多少位,则更通用的规范非常容易匹配。我们只是生成了一个足够大的整数,该整数不能包含在我们得到的文件中。同样,这会占用绝对最少的RAM。参见伪代码。

# Grab the file size
fseek(fp, 0L, SEEK_END);
sz = ftell(fp);
# Print a '2' for every bit of the file.
for (c=0; c<sz; c++) {
  for (b=0; b<4; b++) {
    print "2";
  }
}

@Nakilon和TheDayTurns在对原始问题的评论中指出了这一点
Brian Gordon

3

就像Ryan所说的那样,对文件进行排序,然后遍历整数,并在其中跳过一个值时就拥有它:)

编辑人士:行动党提到可以对文件进行排序,因此这是一种有效的方法。


一个关键部分是您应该随手做,这样一来您只需阅读一次。访问物理内存很慢。
瑞安·阿莫斯

@ryan外部排序在大多数情况下是合并排序,因此在最后一次合并时,您可以进行检查:)
棘轮怪胎

如果数据在磁盘上,则必须将其加载到内存中。这由文件系统自动发生。如果我们必须找到一个数字(否则问题就不会出现),那么一次读取一行排序的文件是最有效的方法。它占用很少的内存,并且没有比其他任何东西都要慢的速度-必须读取文件。
托尼·恩尼斯

当您只有1 GB的内存时,如何对40亿个整数排序?如果您使用虚拟内存,则由于存储块被分页移入和移出物理内存,这将花费很长的时间。
克拉斯·林德贝克(KlasLindbäck)

4
@klas merge sort是为此设计的
棘轮怪胎

2

如果您不采用32位约束,则只需返回随机生成的64位数字(如果您是悲观主义者,则返回128位)。碰撞的机会是1 in 2^64/(4*10^9) = 4611686018.4(大约40亿分之一)。您大多数时候都是对的!

(开玩笑……有点。)


我看到这已经建议:)为这些人upvotes
彼得·吉布森

生日悖论使这种解决方案不值得冒险,而无需检查文件以查看您的随机猜测是否实际上是有效答案。(生日悖论在这种情况下不适用,但是反复调用此函数以生成新的唯一值的确会造成生日悖论。)
Peter Cordes

@PeterCordes随机生成的128位数字正是UUID的工作方式-他们甚至在计算Wikipedia UUID页面
Peter Gibson

变体:查找最大的一组,加1
菲尔

我将对原始数组进行快速排序(不增加存储空间),然后遍历数组并报告第一个“跳过”整数。做完了 回答了问题。
42
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.