生成一个不在四十亿个给定整数中的整数

我被问到这个面试问题:

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

我的分析:

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

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

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

我的理解(看完所有答案后):

假设我们谈论的是32位整数,有232=4*109不同的整数。

案例1:我们有1 GB=1*109*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*106*8位=8000万位

解决方案:

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

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

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

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


对迟到的人澄清一下:正如所问的,这个问题并没有说只有一个整数不包含在文件中——至少大多数人不是这样解释的。然而,评论线程中的许多评论关于该任务的变化。不幸的是,介绍到评论线程的评论后来被其作者删除了,所以现在看起来孤立的回复只是误解了一切。这非常令人困惑,对不起。

121657 次浏览

正如Ryan所说,基本上,对文件进行排序,然后遍历整数,当跳过一个值时,你就有了它:)

编辑 at down选民:OP提到可以对文件进行排序,因此这是一个有效的方法。

使用BitSet.40亿整数(假设最多2^32个整数)打包到BitSet中,每字节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 GB RAM变体,您可以使用位向量。您需要分配40亿位==500 MB字节数组。对于您从输入读取的每个数字,将相应的位设置为“1”。完成后,遍历位,找到第一个仍然为“0”的位。它的索引就是答案。

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

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

假设每个整数为32位,如果完成位标记,它们将方便地适合1 GB的RAM。

假设“整数”表示32位:对于所有可能的16位前缀,10 MB的空间足以让您在一次通过输入文件时计算输入文件中具有任何给定16位前缀的数字。至少有一个存储桶被命中少于216次。进行第二次遍历以查找该存储桶中已经使用的哪些可能的数字。

如果它意味着超过32位,但仍然是有限的大小:如上所述,忽略所有碰巧落在(有符号或无符号;您选择的)32位范围之外的输入数字。

如果“整数”是指数学整数:通读输入一次,并跟踪你见过的最长数字的最大数量长度。完成后,输出最大加一一个多了一位的随机数。(文件中的一个数字可能是一个大数字,精确表示需要超过10 MB,但如果输入是一个文件,那么你至少可以表示任何适合它的长度)。

  • 最简单的方法是找到文件中的最小数字,并返回比该数字少1的值。对于n个数字的文件,这使用O(1)存储空间和O(n)时间。但是,如果数字范围有限,它将失败,这可能会使min-1不是一个数字。

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

  • 还提到了一个具有2^16个计数桶的2遍方法。它读取2*n个整数,因此使用O(n)时间和O(1)存储,但它不能处理超过2^16个数字的数据集。然而,它很容易通过运行4次而不是2次来扩展到(例如)2^60 64位整数,并且很容易适应使用小内存,通过仅使用内存中适合的桶数并相应增加通过次数,在这种情况下,运行时间不再是O(n),而是O(n*log n)。

  • 将所有数字一起异或的方法,到目前为止由rFrankel提到,由ircmaxell详细回答了stackoverflow#35185中提出的问题,正如ltn100指出的那样。它使用O(1)存储空间和O(n)运行时间。如果目前我们假设32位整数,异或产生一个不同数字的概率为7%。理由:给定~4G不同的数字异或在一起,并且ca.300M不在文件中,每个位位置的设置位数具有相同的奇数或偶数机会。因此,2^32个数字作为异或结果产生的可能性相等,其中93%已经在文件中。请注意,如果文件中的数字不完全不同,则XOR方法的成功概率会上升。

技巧问题,除非引用不当。只需通读文件一次即可获得最大整数n,并返回n+1

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

对于10 MB内存限制:

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

完成后,只需使用之前未创建的路径来创建请求的编号。

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

编辑

优化是可能的,如果两端的叶子已经创建并且有一个共同的父节点,那么它们可以被删除并且父节点被标记为不是解决方案。这会削减分支并减少对内存的需求。

编辑II

没有必要也完全构建树。你只需要在数字相似的情况下构建深分支。如果我们也剪枝,那么这个解决方案实际上可能有效。

编辑好吧,这并没有经过深思熟虑,因为它假设文件中的整数遵循一些静态分布。显然他们不需要,但即使如此,也应该试试这个:


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

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

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

内存消耗:几十个字节,复杂度:O(n),开销:可删除,因为大部分时间都将花费在不可避免的硬盘访问中,而不是无论如何比较int。


实际最坏的情况,当我们做没有假设一个静态分布时,是每个整数最多出现一次,因为只有 1-4000000000/2¾²约6% 的所有整数不会出现在文件中。所以你需要更多的猜测,但这仍然不会花费大量的内存。

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

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

关于这个问题的详细讨论已经在jonbentley“专栏1.破解牡蛎”编程珍珠 Addison-Wesley第3-10页中进行了讨论

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

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

实现bitset的代码很简单:(取自解决方案页面

#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的算法对文件进行一次遍历,setting数组中的适当位,然后使用上面的test宏检查该数组以找到丢失的数字。

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

嗯。

统计信息算法比确定性方法使用更少的传递来解决这个问题。

然后可以生成一个在O(1)时间内可能唯一的数字。像GUID这样的伪随机128位整数只会在不到640亿亿例中与集合中现有的40亿个整数中的一个碰撞。

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

如果它们是32位整数(可能来自选择接近232的~40亿数字),您的40亿数字列表最多将占用93%的可能整数(4*109/(232))。因此,如果您创建一个由232位组成的位数组,并且每个位初始化为零(这将占用229字节~500 MB的RAM;记住一个字节=23位=8位),请通读您的整数列表并为每个int设置相应的位数组元素从0到1;然后通读您的位数组并返回第一个仍然为0的位。

如果内存较少(~10 MB),则需要稍微修改此解决方案。10 MB~83886080位仍然足以对0到83886079之间的所有数字进行位数组。因此,您可以通读整数列表;并且在位数组中仅记录0到83886079之间的#。如果数字是随机分布的;以压倒性的概率(大约与10-2592069相差100%),您将找到丢失的int)。事实上,如果您只选择数字1到2048(只有256字节的RAM),您仍然会在绝大多数(99.99999999999999999999999999999999999999999999999999999999999995%)的时间内找到丢失的数字。

但是让我们说,而不是大约40亿的数字;你有232-1的数字和不到10 MB的RAM;所以任何小范围的整数只有很小的可能性不包含这个数字。

如果您保证列表中的每个int都是唯一的,您可以对数字进行求和,然后将缺少一个#的总和减去完整的总和(½)(232(232-1)=9223372034707292160以找到缺少的int。但是,如果一个int出现两次,此方法将失败。

然而,你总是可以分而治之。一个简单的方法是通读数组,并计算前半部分(0到231-1)和后半部分(231,232)的数字数量。然后选择数字较少的范围,并重复将该范围一分为二。(假设(231,232)中有两个较少的数字,那么你的下一次搜索将计算范围内的数字(231,3*230-1),(3*230,232)。不断重复,直到你找到一个数字为零的范围,你就有了答案。应该采取O(lg N)~32次读取数组。

这种方法效率低下。我们在每一步只使用两个整数(或大约8字节的RAM和4字节(32位)整数)。更好的方法是分成sqrt(232)=216=65536个箱,每个箱中有65536个数字。每个bin需要4个字节来存储其计数,所以你需要218字节=256 kB。所以bin 0是(0到65535=216-1),bin 1是(216=65536到2*216-1=131071),bin 2是(2*216=131072到3*216-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亿整数列表;并计算216箱中每个箱中有多少整数,并找到一个没有全部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

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

位消除

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

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 };

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

我将回答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;
}
}

为什么要这么复杂?你要求一个文件中不存在的整数?

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

没有撞到maxint什么的风险,因为根据规则,整数的大小或算法返回的数字没有限制。

这可以使用二分搜索的变体在很少的空间内解决。

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

  2. 计算中点。

  3. 遍历文件,计算有多少个数字相等,小于或高于中点值。

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

  5. 否则,选择具有最少数字的范围,并使用此新范围重复步骤2。

这将需要对文件进行多达32次线性扫描,但它只会使用几个字节的内存来存储范围和计数。

这与亨宁的解决方案基本相同,只是它使用两个箱而不是16k。

他们可能想看看你是否听说过概率布隆过滤器,它可以非常有效地绝对确定一个值是否不是一个大集合的一部分(但只能以很高的概率确定它是集合的成员)。

也许我完全错过了这个问题的重点,但是您想从排序整数文件中找到一个缺失的整数?

呃……真的吗?让我们想想这样的文件会是什么样子:

1 2 3 4 5 6…第一个失踪号码…等等。

这个问题的解决方案似乎微不足道。

如果您在范围[0,2^x-1]中缺少一个整数,那么只需将它们全部异或在一起。例如:

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

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

我可能读得太仔细了,但问题是“生成一个文件中不包含的整数”。我只是对列表进行排序并将1添加到最大条目。砰,文件中不包含的整数。

检查输入文件的大小,然后输出任何数字,即太大,无法用该大小的文件表示。这可能看起来像是一个廉价的技巧,但它是面试问题的创造性解决方案,它巧妙地回避了内存问题,它在技术上是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*109-1),因为您知道文件中还有(40亿-1)个其他整数,即使进行了完美的压缩,它们也至少会占用一个位。

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

但是,如果唯一的因素是最大限度地减少内存使用,而其他任何事情都不重要,那么这将是最佳解决方案。它甚至可能让你获得“最严重滥用规则”奖。

如果你不假设32位约束,只需返回一个随机生成的64位数字(如果你是悲观主义者,则返回128位)。碰撞的几率是1 in 2^64/(4*10^9) = 4611686018.4(大约140亿)。大多数时候你是对的!

(开玩笑的…)

2128*1018+1(即(2816*1018+1)-它不能成为今天的通用答案吗?这代表了一个不能在16 EB文件中保存的数字,这是任何当前文件系统中的最大文件大小。

我认为这是一个已解决的问题(见上文),但有一个有趣的侧面案例需要记住,因为它可能会被问到:

如果正好有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";
}
}

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

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

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

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

来自Carbonetc的Reddit。

如果我们假设数字的范围总是2^n(2的偶数幂),那么异或就可以了(如另一张海报所示)。至于为什么,让我们证明一下:

的理论

给定任何具有2^n元素且缺少一个元素的基于0的整数范围,您可以通过简单地将已知值异或运算以产生缺失的数字来找到该缺失的元素。

的证明

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

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

现在,如果我们看一下,每个位都被设置了两次。因此,由于它被设置了偶数次,并且数字的异或将产生0。如果缺少一个数字,异或将产生一个数字,当与缺失的数字异时,该数字将导致0。因此,缺失的数字和结果异的数字完全相同。如果我们删除2,结果的异或将是10(或2)。

现在,让我们看看n+1。让我们调用每个位在n中设置的次数,x和每个位在n+1y中设置的次数。y的值将等于y = x * 2,因为有x元素将n+1位设置为0,和x元素将n+1位设置为1。并且由于x0总是偶数,n+1总是将每个位设置偶数次。

因此,由于n=2有效,而n+1有效,因此xor方法将适用于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;

出于某种原因,当我读到这个问题时,我想到了对角化方法。我假设任意大整数。

读取第一个数字。用零位进行左键填充,直到40亿位。如果第一位(高阶)为0,则输出1;否则输出0。(你不必左键填充:如果数字中没有足够的位,你只需输出1。)对第二个数字做同样的事情,除了使用它的第二位。以这种方式继续浏览文件。你将一次输出一个40亿位的数字,该数字将与文件中的任何数字不相同。证明:它与第n个数字相同,那么他们会同意第n位,但他们不通过构造。

您不需要对它们进行排序,只需重复划分它们的子集即可。

第一步就像快速排序的第一步。选择一个整数x,并使用它通过数组将所有小于x的值放在左边,大于x的值放在右边。找到x的哪一边有最多的可用槽(不在列表中的整数)。这很容易通过比较x的值和它的位置来计算。然后在x的那一边重复子列表上的分区。然后在子列表上重复具有最多可用整数的分区,等等。到一个空范围的比较总数应该是大约40亿,给予或接受。

您可以通过在某些树结构中存储未访问整数的范围来加快读取现有整数后查找丢失的整数。

首先存储[0…4294967295],每次读取一个整数时,都会拼接它所处的范围,当它变为空时删除一个范围。最后,你会得到范围中缺少的整数的确切集合。所以如果你看到5作为第一个整数,你会有[0…4]和[6…4294967295]。

这比标记位慢得多,因此它只是10MB情况下的解决方案,前提是您可以将树的较低级别存储在文件中。

存储这种树的一种方法是使用区域的开头作为键,区域的结尾作为值的B树。最坏的情况是当你得到所有奇数或偶数整数时,这意味着为树存储2^31个值或数十GB…哎哟。最好的情况是一个排序文件,你只需要为整个树使用几个整数。

所以不是真正的正确答案,但我想我会提到这样做的方式。我想我会失败的面试;-)

我想出了以下算法。

我的想法是:遍历整数的整个文件一次,每个位的位置都计算它的0和1。0和1的数量必须是2^(numOfBits)/2,因此,如果数量小于预期,我们可以将其用于结果数字。

例如,假设整数是32位,那么我们需要

int[] ones = new int[32];
int[] zeroes = new int[32];

对于每个数字,我们必须迭代32位并增加0或1的值:

for(int i = 0; i < 32; i++){
ones[i] += (val>>i&0x1);
zeroes[i] += (val>>i&0x1)==1?0:1;
}

最后,文件处理完毕后:

int res = 0;
for(int i = 0; i < 32; i++){
if(ones[i] < (long)1<<31)res|=1<<i;
}
return res;

注:在某些语言中(例如Java)1<<31是一个负数,因此,(长)1<<31是正确的方法

只要我们在做创造性的答案,这里还有一个。

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

老问题了,不知道“非功能性”需求怎么提,觉得应该有个线索吧?如果这个问题不是在书上写的,而是在别的地方提出来的,书里讨论了各种可能性,正反两面都有。这似乎是面试中的aske,经常出现,让我感到困惑的是,在不知道软需求的情况下,怎么能给出肯定的答案呢?比如“查漏号肯定很快,因为一秒钟用了x次”。

我认为这样的问题或许可以给出一个合理的答案。

  • 我会将所有数字合并排序到一个新文件中,每int使用4个字节。当然,一开始这样做会很慢。但它可以用很小的内存量完成(你不需要把所有的都保存在RAM中)
  • 使用二进制搜索来检查预先排序的文件中是否存在该数字。由于我们保持每个值4字节,这没有问题

缺点:

  • 文件大小
  • 缓慢的第一次排序-但只需要一次

优点:

  • 快速查找

对于一本书来说,这是一个非常好的问题。但我认为这是一个奇怪的问题,当要求一个最佳解决方案时,当要解决的问题还不完全清楚时。

当然,以有限的经验(刚刚开始在大学学习Java),你可以通过一组int运行,如果找不到数字,则处理桶。这既可以释放空间,又可以检查每个数据单元。如果找到你要找的,将其添加到计数变量中。这将需要很长时间,但是,如果你为每个部分制作了多个变量,并通过每个变量运行检查计数,并确保它们同时退出/处理,变量存储应该不会增加?并将加快检查过程。只是一个想法。

给定一个包含40亿个整数的输入文件,提供一个算法 生成一个不包含在文件中的整数。假设你 有1 GiB内存。跟进你会做什么,如果你只有 10 MiB内存

文件大小为4*109*4 bytes=16 GiB

对于32位无符号整数

0 <= Number < 2^32
0 <= Number < 4,294,967,296

我提出的解决方案:C++没有错误检查

#include <vector>
#include <fstream>
#include <iostream>
using namespace std;


int main ()
{
const long SIZE = 1L << 32;


std::vector<bool> checker(SIZE, false);


std::ifstream infile("file.txt");  // TODO: error checking


unsigned int num = 0;


while (infile >> num)
{
checker[num] = true ;
}


infile.close();


// print missing numbers


for (long i = 0; i < SIZE; i++)
{
if (!checker[i])
cout << i << endl ;
}


return 0;
}

复杂度

  • 空间~232位=229字节=219 KB=29 MB=1/2 GB
  • 时间~单程
  • 完整~是