【问题标题】:Generate an integer that is not among four billion given ones生成一个不属于 40 亿给定整数的整数
【发布时间】:2011-11-01 11:47:49
【问题描述】:

我收到了这个面试问题:

给定一个包含四十亿整数的输入文件,提供一个算法来生成一个不包含在文件中的整数。假设您有 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 个 整数 = 65536,我们需要 216 * 4 * 8 = 200 万位。我们需要构建 65536 个存储桶。对于每个桶,我们需要 4 个字节来保存所有可能性,因为最坏的情况是所有 40 亿个整数都属于同一个桶。

  1. 通过第一次遍历文件构建每个桶的计数器。
  2. 扫描桶,找到第一个命中数少于 65536 的桶。
  3. 构建新的桶,我们在步骤 2 中找到了高 16 位前缀 通过文件的第二遍
  4. 扫描step3构建的bucket,找到第一个没有的bucket 成功了。

代码和上面的很相似。

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


对迟到的人的澄清:正如所问的那样,这个问题并不是说文件中不包含确切的一个整数——至少大多数人不是这样解释的。不过,评论线程中的许多 cmet 讨论了该任务的变体。不幸的是,将其引入评论线程的评论后来被其作者删除,所以现在看起来对它的孤立回复只是误解了一切。非常混乱,抱歉。

【问题讨论】:

  • @trashgod,错了。对于 4294967295 个唯一整数,您将剩下 1 个整数。要找到它,您应该对所有整数求和,然后从预先计算的所有可能整数的总和中减去它。
  • 这是“编程珍珠”中的第二颗“珍珠”,我建议您阅读本书中的整个讨论。见books.google.com/…
  • @Richard 一个 64 位的 int 就足够大了。
  • int getMissingNumber(File inputFile) { return 4; } (reference)
  • 你不能存储从 1 到 2^32 的所有整数的总和并不重要,因为像 C/C++ 这样的语言中的整数类型总是保留关联性和交流性等属性。这意味着虽然总和不是正确的答案,但如果你计算溢出的预期值,溢出的实际总和,然后减去,结果仍然是正确的(只要它本身没有溢出)。

标签: algorithm file search out-of-memory memory-limit


【解决方案1】:

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

EDIT at downvoters:OP 提到文件可以排序,所以这是一种有效的方法。

【讨论】:

  • 一个关键的部分是你应该边做边做,这样你只需要阅读一次。访问物理内存很慢。
  • @ryan 外部排序在大多数情况下是合并排序,因此在最后一次合并时您可以进行检查:)
  • 如果数据在磁盘上,则必须将其加载到内存中。这由文件系统自动发生。如果我们必须找到一个数字(问题没有说明其他情况),那么一次读取排序文件一行是最有效的方法。它使用的内存很少,并且不会比其他任何东西慢 - 必须读取文件。
  • 当你只有 1 GB 的内存时,你将如何对 40 亿个整数进行排序?如果您使用虚拟内存,则将需要很长时间,因为内存块会被分页进出物理内存。
  • @klas merge sort 就是为此而设计的
【解决方案2】:

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

添加更多细节 - 每次读取一个数字时,设置 BitSet 中的相应位。然后,遍历 BitSet 以找到第一个不存在的数字。事实上,您可以通过反复选择一个随机数并测试它是否存在来同样有效地做到这一点。

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

查看 BitSet API,它似乎只支持 0..MAX_INT,因此您可能需要 2 个 BitSet——一个用于 +'ve 数字,一个用于 -'ve 数字——但内存要求不会改变。

【讨论】:

  • 或者如果你不想使用BitSet ...试试位数组。做同样的事情;)
【解决方案3】:

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

【讨论】:

  • 未指定输入中的数字范围。如果输入由 80 亿到 160 亿之间的所有偶数组成,该算法如何工作?
  • @Mark,只需忽略 0..2^32 范围之外的输入。无论如何,您不会输出任何一个,因此无需记住要避免哪些。
  • @Mark 用于确定 32 位字符串如何映射到实数的算法取决于您。过程还是一样的。唯一的区别是如何将其作为实数打印到屏幕上。
  • 您可以使用bitSet.nextClearBit(0)download.oracle.com/javase/6/docs/api/java/util/…,而不是自己迭代
  • 值得一提的是,无论整数的范围如何,在传递结束时至少保证一位为 0。这是由于鸽巢原理。
【解决方案4】:

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

遍历整个文件后,扫描每个位,判断是否存在数字。

假设每个整数都是 32 位的,如果完成位标记,它们将很方便地放入 1 GB 的 RAM。

【讨论】:

  • 0.5 Gb,除非您将字节重新定义为 4 位 ;-)
  • @dty 我认为他的意思是“舒适”,因为在 1Gb 中会有很多空间。
【解决方案5】:

假设“整数”表示 32 位:10 MB 的空间足以让您计算输入文件中有多少具有任何给定 16 位前缀的数字,对于所有可能的 16 位前缀一次通过输入文件。至少有一个桶被击中少于 216 次。再做一遍,找出该桶中哪些可能的数字已被使用。

如果它意味着超过 32 位,但仍然是有界的大小:按照上述操作,忽略所有恰好落在(有符号或无符号;您的选择)32 位范围之外的输入数字.

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

【讨论】:

  • 完美。您的第一个答案只需要通过文件 2 次!
  • 一个 10 MB 的 bignum?这太极端了。
  • @Legate,跳过过大的数字,对它们什么也不做。由于无论如何您都不会输出过大的数字,因此无需跟踪您看到了哪些。
  • 解决方案 1 的好处在于,您可以通过增加通道数来减少内存。
  • @Barry:上面的问题并不表示缺少一个数字。它也没有说文件中的数字不重复。 (在面试中遵循实际提出的问题可能是个好主意,对吧?;-))
【解决方案6】:
  • 最简单的方法是找到文件中的最小数字,然后返回小于该数字的 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 位整数,并且通过仅使用尽可能多的 bin 并相应地增加通过次数来轻松适应使用微型内存,在这种情况下的运行时间不再是 O(n),而是 O(n*log n)。

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

【讨论】:

    【解决方案7】:

    诡计问题,除非引用不正确。只需通读一次文件即可得到最大整数n,并返回n+1

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

    【讨论】:

    • 这是一个有效的解决方案......除非它不起作用。有用! :-)
    • 除非引用不正确,否则该问题不会限制整数类型,甚至不会限制使用的语言。许多现代语言的整数仅受可用内存限制。如果文件中的最大整数> 10MB,那么运气不好,第二种情况不可能完成任务。我最喜欢的解决方案。
    【解决方案8】:

    对于 10 MB 内存限制:

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

    完成后,只需走一条之前没有创建过的路径来创建请求的号码。

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

    编辑

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

    编辑二

    也不需要完全构建树。如果数字相似,您只需要构建深层分支。如果我们也剪树枝,那么这个解决方案实际上可能有效。

    【讨论】:

    • ... 10 MB 的大小如何?
    • 怎么样:将 BTree 的深度限制在 10MB 以内;这意味着您将在集合中得到结果 { false positive | positive } 并且您可以遍历它并使用其他技术查找值。
    【解决方案9】:

    编辑好的,这并没有完全考虑清楚,因为它假设文件中的整数遵循一些静态分布。显然他们不需要这样做,但即便如此,人们也应该试试这个:


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

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

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

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


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

    【讨论】:

    • 我很高兴看到其他人想到了这一点,但为什么它在底部呢?这是一个 1-pass 算法...... 10 MB 足以进行 250 万次猜测,而 93%^2.5M ≈ 10^-79000 确实需要第二次扫描的可能性微乎其微。由于二进制搜索的开销,如果您使用较少的猜测,它会更快!这在时间和空间上都是最佳的。
    • @Potatoswatter:很好,你提到了二分搜索。仅使用 5 次猜测时,这可能不值得开销,但肯定是 10 次或更多。您甚至可以进行 2 M 次猜测,但是您应该将它们存储在哈希集中以获得 O(1) 进行搜索。
    • @Potatoswatter Ben Haley 的等效答案接近顶部
    • 我喜欢这种方法,但建议对节省内存进行改进:如果有 N 位可用的索引存储,加上一些常量存储,定义一个可配置的可逆 32 位加扰函数(排列),选择任意排列,并清除所有索引位。然后从文件中读取每个数字,打乱它,如果结果小于N,则设置相应的位。如果文件末尾没有设置任何位,则在其索引上反转加扰函数。凭借 64KB 的内存,一次可以有效地测试超过 512,000 个数字的可用性。
    • 当然,使用这种算法,最坏的情况是数字是由您使用的同一个随机数生成器创建的。假设您可以保证不是这种情况,您最好的策略是使用线性同余随机数生成器来生成您的列表,以便您以伪随机方式遍历数字空间。这意味着如果你以某种方式失败,你可以继续生成数字,直到你覆盖了整个整数范围(找到了一个差距),而无需重复你的努力。
    【解决方案10】:

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

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

    【讨论】:

    • 如果文件中包含 MAXINT 怎么办?
    • @Petr Peller:BIGINT 库基本上会消除对整数大小的限制。
    • @oosterwal,如果允许这个答案,那么您甚至不需要阅读文件 - 只需打印尽可能大的数字。
    • @oosterwal,如果你的随机大数是你可以打印的最大的,并且它在文件中,那么这个任务就无法解决。
    • @Nakilon:+1 你的观点被采纳了。它大致相当于计算文件中的总位数并打印具有那么多位数的数字。
    【解决方案11】:

    关于这个问题的详细讨论已在Jon Bentley“第 1 列。破解牡蛎”中讨论过 编程珍珠 Addison-Wesley pp.3-10

    Bentley 讨论了几种方法,包括外部排序、使用多个外部文件的合并排序等,但 Bentley 建议的最佳方法是使用 bit fields 的单通道算法,他幽默地将其称为“Wonder Sort”:) 问题来了,40 亿个数字可以表示为:

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

    实现bitset的代码很简单:(取自solutions page

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

    HTH。

    【讨论】:

    • 我不知道这本书,但没有理由称它为“Wonder Sort”,因为它只是一个桶排序,有一个 1 位计数器。
    • 虽然更便携,但这段代码将被代码written to utilize hardware-supported vector instructions歼灭。我认为 gcc 在某些情况下可以自动将代码转换为使用向量操作。
    • @brian 我认为 Jon Bentley 不会在他的算法书中允许这样的事情。
    • @BrianGordon,与读取文件的时间相比,在 ram 中花费的时间可以忽略不计。忘记优化它。
    • @BrianGordon:或者你在谈论最后的循环以找到第一个未设置的位?是的,向量会加快速度,但是使用 64 位整数在位域上循环,寻找 != -1 仍然会使在单个内核上运行的内存带宽饱和(这是寄存器内的 SIMD,SWAR,位为元素)。 (对于最近的 Intel/AMD 设计)。您只需在找到包含它的 64 位位置后找出未设置的位。 (为此,您可以not / lzcnt。)公平一点,循环单比特测试可能无法得到很好的优化。
    【解决方案12】:

    与确定性方法相比,统计信息算法使用更少的通道数解决了这个问题。

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

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

    【讨论】:

    • 我很确定整数是有界的。如果不是,那么即使是初学者程序员也会想到算法“遍历数据以找到最大数,然后将其加 1”
    • 从字面上猜测随机输出可能不会让你在面试中获得很多分数
    • @Adrian,您的解决方案似乎很明显(对我来说,我在自己的答案中使用了它)但对每个人来说并不明显。这是一个很好的测试,看看你是否能找到明显的解决方案,或者你是否会过度复杂化你所接触的一切。
    • @Brian:我认为这个解决方案既富有想象力又实用。对于这个答案,我会给予很多赞誉。
    • 啊,工程师和科学家之间的界线就在这里。很好的答案本!
    【解决方案13】:

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

    如果您的 RAM (~10 MB) 较少,则需要稍微修改此解决方案。 10 MB ~ 83886080 位仍然足以为 0 到 83886079 之间的所有数字做一个位数组。所以你可以阅读你的整数列表;并且只记录位数组中 0 到 83886079 之间的#s。如果数字是随机分布的;以压倒性的概率(它大约相差 100% 10-2592069)你会发现一个缺失的 int)。事实上,如果您只选择 1 到 2048 之间的数字(只有 256 字节的 RAM),您仍然会在绝大多数情况下发现缺失的数字(99.9999999999999999999999999999999999999999999999999999999999999995%)。

    但假设不是有大约 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 个 bin,每个 bin 中有 65536 个数字。每个 bin 需要 4 个字节来存储其计数,因此您需要 218 个字节 = 256 kB。所以 bin 0 是 (0 to 65535=216-1),bin 1 是 (216=65536 to 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 个 bin 中每个 bin 中有多少整数,并找到一个不完整的 bin,它不包含所有 65536 个数字。然后你再次通读 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
    

    【讨论】:

    • 这么棒的答案。这实际上会起作用;并且有保证的结果。
    • @dr jimbob,如果垃圾箱中只有一个数字,而那个数字有 65535 个重复项怎么办?如果是这样,bin 仍将计数 65536,但所有 65536 数字都是相同的。
    • @Alcott - 我假设您有 2^32-1(或更少)个数字,因此根据鸽巢原则,您可以保证有一个少于 65536 个计数的垃圾箱来进行更详细的检查。我们试图只找到一个缺失的整数,而不是全部。如果您有 2^32 或更多数字,则不能保证缺少整数,也无法使用此方法(或者从一开始就保证缺少整数)。那么你最好的选择就是暴力破解(例如,读取数组 32 次;第一次检查前 65536 个 #s;一旦找到答案就停止)。
    • 聪明的 upper-16 / lower-16 方法早先由 Henning 发布:stackoverflow.com/a/7153822/224132。不过,我喜欢添加一组独特的整数的想法,它恰好缺少一个成员。
    • @PeterCordes - 是的,Henning 的解决方案早于我的解决方案,但我认为我的回答仍然有用(更详细地处理几件事)。也就是说,Jon Bentley 在他的《Programming Pearls》一书中建议在 stackoverflow 存在之前为这个问题提供一个多通道选项(参见 vine'th 的答案)(并不是我声称我们中的任何一个人都有意识地从那里偷了东西,或者 Bentley 是第一个分析这个问题——这是一个相当自然的解决方案开发)。当限制是您不再有足够的内存用于具有巨大位数组的 1 次通过解决方案时,两次通过似乎是最自然的。
    【解决方案14】:

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

    【讨论】:

    • 除非文件中的最大数字是 max int 否则你只会溢出
    • 在一个可能需要生成一个新整数并将其附加到“已使用整数”文件 100 次的真实世界程序中,该文件的大小是多少?
    • 我在想这个。假设int32 位,则输出2^64-1。完成。
    • 如果是每行一个整数:tr -d '\n' &lt; nums.txt &gt; new_num.txt:D
    【解决方案15】:

    位消除

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

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

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

    【讨论】:

      【解决方案16】:

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

      【讨论】:

        【解决方案17】:

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

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

        没有碰到maxint之类的风险,因为根据规则,对整数的大小或算法返回的数字没有限制。

        【讨论】:

        • 除非最大 int 在文件中,否则这将起作用,这是完全可能的......
        • 规则没有指定是32bit还是64bit什么的,所以按照规则指定,没有max int。整数不是计算机术语,它是识别正整数或负整数的数学术语。
        • 确实如此,但不能假设它是一个 64 位数字,或者有人不会为了混淆这些算法而偷偷输入最大 int 数。
        • 如果没有指定编程语言,“max int”的整个概念在上下文中是无效的。例如查看 Python 对长整数的定义。它是无限的。没有屋顶。您可以随时添加一个。您假设它是用一种具有整数最大允许值的语言实现的。
        【解决方案18】:

        这可以使用二进制搜索的变体在很小的空间内解决。

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

        2. 计算中点。

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

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

        5. 否则,请选择数字最少的范围,然后使用这个新范围从第 2 步开始重复。

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

        这与Henning's solution 基本相同,只是它使用两个 bin 而不是 16k。

        【讨论】:

        • 在我开始针对给定参数进行优化之前,这是我开始的。
        • @Henning:酷。这是一个很好的算法示例,可以轻松调整时空权衡。
        • @hammar,但如果这些数字出现多次怎么办?
        • @Alcott:那么该算法将选择更密集的 bin 而不是稀疏的 bin,但根据鸽巢原理,它永远无法选择完全满的 bin。 (两个计数中较小的总是小于 bin 范围。)
        【解决方案19】:

        他们可能想看看你是否听说过概率Bloom Filter,它可以非常有效地确定一个值是否不是大集合的一部分,(但只能以高概率确定它是设置。)

        【讨论】:

        • 由于可能设置了超过 90% 的可能值,您的布隆过滤器可能需要退化为许多答案已经使用的位域。否则,你最终会得到一个完全填充的无用位串。
        • @Christopher 我对布隆过滤器的理解是,在达到 100% 之前,你不会得到一个填充的位数组
        • ...否则你会得到假阴性。
        • @Paul 填充位数组会给您带来误报,这是允许的。在这种情况下,布隆过滤器很可能会退化为解决方案(可能为负)返回误报的情况。
        • @Paul:只要哈希函数的数量乘以条目的数量与您的字段长度一样大,您就可以获得一个填充的位数组。当然,这将是一个例外情况,但概率会很快上升。
        【解决方案20】:

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

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

        1 2 3 4 5 6 ...第一个丢失的数字...等

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

        【讨论】:

        • 不指定排序。
        【解决方案21】:

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

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

        (我知道这并不能准确地回答问题,但它是对一个非常相似的问题的一个很好的回答。)

        【讨论】:

        • 是的,很容易证明 [] 在缺少一个整数时有效,但如果缺少多个整数,它通常会失败。例如,0 ^ 1 ^ 3 ^ 4 ^ 6 ^ 7 为 0。[ 写 2x 为 2 的 x 次方,a^b 为 a xor b,所有 kx 的 xor 为零 -- k ^ ~k = (2^x)-1 for k
        • 正如我在对 ircmaxell 回复的评论中提到的那样:问题不是说“缺少一个数字”,而是说要找到文件中 40 亿个数字中未包含的数字。如果我们假设 32 位整数,那么文件中可能会丢失大约 3 亿个数字。存在的数字与缺失数字相异或的可能性仅为 7% 左右。
        • 这是我最初阅读问题时想到的答案,但仔细观察后,我认为这个问题比这更模棱两可。仅供参考,这是我在想的问题:stackoverflow.com/questions/35185/…
        【解决方案22】:

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

        【讨论】:

        • 如果你只想找到最大值为什么要排序?
        • 您如何使用不超过 1 GB 的内存对 40 亿个整数进行排序?
        • @Brian 哈哈。随着面试问题的进行,不了解复杂性所在通常比没有提出好的解决方案更糟糕。毕竟,理解问题通常是解决问题的一半以上。
        【解决方案23】:

        检查输入文件的大小,然后输出 任何 数字,该数字太大而无法用该大小的文件表示。 这似乎是一个廉价的技巧,但它是面试问题的创造性解决方案,它巧妙地回避了记忆问题,而且在技术上是 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 bitcount - 1,它总是大于 2 bitcount。从技术上讲,你必须击败的数字是 2 bitcount - (4 * 109 - 1),因为你知道有 (40 亿- 1) 文件中的其他整数,即使经过完美压缩,它们也会占用至少一位。

        【讨论】:

        • 为什么不只是Console.Write( 1 &lt;&lt; bitcount ) 而不是循环?如果文件中有 n 位,那么任何以 1 开头的 (_n_+1) 位数字都绝对保证更大。
        • @Emmet - 这只会导致整数溢出,除非文件小于 int 的大小(C# 中为 4 个字节)。 C++ 可能允许你使用更大的东西,但 C# 似乎只允许使用 &lt;&lt; 运算符的 32 位整数。无论哪种方式,除非您滚动自己的巨大整数类型,否则它将是一个非常小的文件大小。演示:rextester.com/BLETJ59067
        【解决方案24】:

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

        但是,如果唯一的因素是尽量减少内存使用量,而没有其他重要因素,那么这将是最佳解决方案。它甚至可能让你获得“最严重的违规行为”奖。

        【讨论】:

          【解决方案25】:

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

          (开玩笑的……有点。)

          【讨论】:

          • 我看到这已经被建议了 :) 为那些人点赞
          • 生日悖论使得这种解决方案不值得冒险,无需检查文件以查看您的随机猜测是否真的是一个有效的答案。 (生日悖论在这种情况下不适用,但重复调用此函数以生成新的唯一值确实会产生生日悖论。)
          • @PeterCordes 随机生成的 128 位数字正是 UUID 的工作原理——他们甚至在 Wikipedia 中计算碰撞概率时提到了生日悖论UUID page
          • 变体:在集合中找到最大值,加 1。
          • 我会对原始数组进行快速排序(没有额外的存储空间。)然后遍历数组并报告第一个“跳过”整数。完毕。回答了问题。
          【解决方案26】:

          2128*1018 + 1(即(2816*1018 sup> + 1 ) - 它不能成为今天的通用答案吗?这表示一个 16 EB 文件中无法容纳的数字,这是当前任何文件系统中的最大文件大小。

          【讨论】:

          • 你将如何打印结果?你不能把它放在一个文件里,在屏幕上打印需要几十亿年。今天的计算机不可能实现正常运行时间。
          • 从来没有说过我们需要在任何地方打印结果,只是“生成”它。所以这取决于你的意思是生成。无论如何,我的回答只是避免制定真正算法的一个技巧:)
          【解决方案27】:

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

          如果恰好有 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 在原始问题的 cmets 中指出了这一点
          【解决方案28】:

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

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

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

          【讨论】:

          • 问题正是关于isNotInFile 函数的算法。请确保您在回答之前理解问题。
          • 不,问题是“文件中没有哪个整数”,而不是“文件中是整数 x”。例如,确定后一个问题答案的函数可以将文件中的每个整数与所讨论的整数进行比较,并在匹配时返回 true。
          • 我认为这是一个合理的答案。除了 I/O,你只需要一个整数和一个 bool 标志。
          • @Aleks G - 我不明白为什么这被标记为错误。我们都同意它是所有算法中最慢的:-),但它可以工作并且只需要 4 个字节来读取文件。例如,原始问题并未规定文件只能读取一次。
          • @Aleks G - 对。我也没说过你也这么说。我们只是说 IsNotInFile 可以使用文件上的循环轻松实现:Open;While Not Eof{Read Integer;Return False if Integer=i;Else Continue;}。它只需要 4 个字节的内存。
          【解决方案29】:

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

          来自 Carbonetc 的 Reddit。

          【讨论】:

          • 爱它!即使这不是他正在寻找的答案......:D
          【解决方案30】:

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

          理论

          给定任何基于 0 的整数范围,其中包含 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 中设置每个位的次数yy 的值将等于y = x * 2,因为x 元素的n+1 位设置为0,x 元素的n+1 位设置为1。因为2x将始终是偶数,n+1 将始终将每个位设置为偶数次。

          因此,由于n=2 有效,n+1 有效,xor 方法将适用于n&gt;=2 的所有值。

          基于 0 的范围的算法

          这很简单。它使用 2*n 位内存,因此对于任何

          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亿个数字中未包含的数字。如果我们假设 32 位整数,那么文件中可能会丢失大约 3 亿个数字。存在的数字与缺失数字相异或的可能性仅为 7% 左右。
          • 如果您有一个不是从零开始的连续但缺少一个的范围,请添加而不是 xor。 sum(0..n) = n*(n+1)/2。所以missing = nmax*(nmax+1)/2 - nmin*(nmin+1)/2 - sum(input[])。 (来自@hammar 的答案的总结。)
          猜你喜欢
          • 2015-03-12
          • 2014-05-11
          • 2013-10-24
          • 2018-04-03
          • 2011-07-15
          • 2014-09-04
          • 2013-10-22
          • 1970-01-01
          相关资源
          最近更新 更多