【问题标题】:Algorithm for detecting duplicates in a dataset which is too large to be completely loaded into memory用于检测数据集中因太大而无法完全加载到内存中的重复项的算法
【发布时间】:2013-03-03 03:27:45
【问题描述】:

这个问题有最优解吗?

描述一种在包含一百万个电话号码的文件中查找重复项的算法。该算法在运行时只有 2 MB 可用内存,这意味着您不能一次将所有电话号码加载到内存中。

我的“幼稚”解决方案将是一个 O(n^2) 解决方案,它迭代值并仅以块的形式加载文件,而不是一次全部加载。

对于 i = 0 到 999,999

string currentVal = get the item at index i

for j = i+1 to 999,999
  if (j - i mod fileChunkSize == 0)
    load file chunk into array
  if data[j] == currentVal
    add currentVal to duplicateList and exit for

必须有另一种情况,您可以以一种真正独特的方式加载整个数据集并验证一个数字是否重复。有人有吗?

【问题讨论】:

  • 您想对重复项做什么?你只是想知道是否有重复吗?是否要删除重复项?你想知道重复的数量是否超过了某个阈值?
  • 副本将被删除,无论是一旦被发现或结束。
  • 2 MB 对于包含一百万个元素的Bloom filter 来说足够了。
  • 这不会涉及很多误报吗?
  • 每个电话号码是多少位?每次将特定范围加载到内存中时,您可能可以使用位向量并多次遍历文件。这会给你一个 O(n) 的解决方案。

标签: algorithm duplicates


【解决方案1】:

将文件分成 M 个块,每个块都足够大,可以在内存中排序。在内存中对它们进行排序。

对于每两个块的集合,我们将对两个块执行最后一步合并排序以生成一个更大的块 (c_1 + c_2) (c_3 + c_4) .. (c_m-1 + c_m)

指向磁盘上 c_1 和 c_2 上的第一个元素,并创建一个新文件(我们称之为 c_1+2)。

如果c_1的指向元素小于c_2的指向元素,将其复制到c_1+2并指向c_1的下一个元素。
否则,将 c_2 的指向元素复制到并指向 c_2 的下一个元素。

重复上一步,直到两个数组都为空。您只需要使用存储两个指向数字所需的内存空间。在此过程中,如果遇到 c_1 和 c_2 指向的元素相等,则发现有重复项,可以将其复制两次并递增两个指针。

生成的 m/2 数组可以以相同的方式递归合并 - 需要这些合并步骤的 log(m) 才能生成正确的数组。每个数字都将与其他数字进行比较,以找出重复项。

或者,@Evgeny Kluev 提到的一个快速而肮脏的解决方案是制作一个尽可能大的布隆过滤器,它可以合理地容纳在内存中。然后,您可以列出每个未通过布隆过滤器的元素的索引,并再次循环遍历文件,以测试这些成员是否重复。

【讨论】:

  • 感谢这个好主意!就我而言,我正在扫描数十个网络驱动器以寻找重复的文件名/大小组合。我决定分三步走。首先扫描驱动器,将每个文件名散列到 4096 个日志之一,将 filename_size_path 附加到日志中。其次,分别对每个日志文件进行排序。第三次打开所有日志,通过合并查找重复项。
  • 如果c_1中有重复项怎么办?您是否还需要将指向元素的 c_1 和 c_2s 与指向元素的 c_1+2 进行比较?
【解决方案2】:

我认为 Airza 的解决方案正在朝着一个好的方向发展,但由于排序不是您想要的,而且成本更高,您可以结合 angelatlarge 的方法进行以下操作:

取一个适合大小为 M/2 的内存的块 C。

获取块 Ci

  1. 遍历 i 并将每个元素散列到散列表中。如果元素已经存在,那么您知道它是重复的,您可以将其标记为重复。 (将其索引添加到数组或其他东西中)。

  2. 获取下一个块 Ci+1 并检查哈希表中是否已存在任何键。如果元素存在,则将其标记为删除。

  3. 重复所有块,直到您知道它们不会包含块 C 中的任何重复项i

  4. 使用块 Ci+1

  5. 重复步骤 1,2
  6. 删除了所有标记为删除的元素(可以在更合适的情况下完成,如果您必须转移其他所有内容,那么一次删除一个可能会更昂贵)。

    李>

这在 O((N/M)*|C|) 中运行,其中 |C|是块大小。请注意,如果 M > 2N,那么我们只有一个块,并且运行时间为 O(N),这对于删除重复项是最佳的。 我们只需对它们进行哈希处理并确保删除所有冲突。

编辑:根据要求,我提供详细信息: * N是号码电话号码。

  • 块的大小取决于内存,它的大小应该是 M/2。 这是将加载文件块的内存大小,因为整个文件太大而无法加载到内存中。

  • 这会留下另外 M/2 个字节来保存哈希表2,和/或重复列表1

  • 因此,应该有 N/(M/2) 个块,每个块的大小为 |C| = M/2

  • 运行时间将是块的数量(N/(M/2)),乘以每个块的大小|C| (或 M/2)。总的来说,这应该是线性的(加上或减去从一个块更改为另一个块的开销,这就是为什么描述它的最佳方式是 O( (N/M) * |C| )

一个。加载一个块 CiO(|C|)
湾。遍历每个元素,测试并设置如果不存在 O(1) 将在其中进行插入和查找进行散列。
C。如果该元素已经存在,您可以将其删除。1
d。获取下一个块,冲洗并重复(2N/M 个块,所以 O(N/M)

1 删除一个元素可能会花费 O(N),除非我们保留一个列表并一次性删除它们,避免在删除一个元素时移动所有剩余的元素。 p>

2 如果电话号码都可以表示为整数 32 - 1,我们可以避免使用完整的哈希表,而只需使用标志映射,节省大量内存(我们只需要 N 位内存)

这里有一个比较详细的伪代码:

void DeleteDuplicate(File file, int numberOfPhones, int maxMemory)
{
    //Assume each 1'000'000 number of phones that fit in 32-bits.
    //Assume 2MB of memory
    //Assume that arrays of bool are coalesced into 8 bools per byte instead of 1 bool per byte
    int chunkSize = maxMemory / 2; // 2MB / 2 / 4-byes per int = 1MB or 256K integers

    //numberOfPhones-bits. C++ vector<bool> for example would be space efficient
    //  Coalesced-size ~= 122KB  | Non-Coalesced-size (worst-case) ~= 977KB 
    bool[] exists = new bool[numberOfPhones];

    byte[] numberData = new byte[chunkSize];
    int fileIndex = 0;
    int bytesLoaded;
    do //O(chunkNumber)
    {
        bytesLoaded = file.GetNextByes(chunkSize, /*out*/ numberData);
        List<int> toRemove = new List<int>(); //we still got some 30KB-odd to spare, enough for some 6 thousand-odd duplicates that could be found

        for (int ii = 0; ii < bytesLoaded; ii += 4)//O(chunkSize)
        {
            int phone = BytesToInt(numberData, ii);
            if (exists[phone])
                toRemove.push(ii);
            else
                exists[phone] = true;
        }

        for (int ii = toRemove.Length - 1; ii >= 0; --ii)
            numberData.removeAt(toRemove[ii], 4);

        File.Write(fileIndex, numberData);
        fileIndex += bytesLoaded;

    } while (bytesLoaded > 0); // while still stuff to load
}

【讨论】:

  • 能否详细说明运行时复杂度分析? N 是电话号码总数吗?当您说块大小时,您是指块数还是每个块中的电话号码数?快速解释一下 O((N/M) * |C|) 会很棒。谢谢!
  • 我添加了细节,我希望它能澄清答案。总的来说应该是 O(N),你只需要遍历整个文件一次。
  • 感谢您添加的详细信息!
【解决方案3】:

如果您可以存储临时文件,则可以将文件加载到块中,对每个块进行排序,将其写入文件,然后遍历块并查找重复项。您可以通过将数字与文件中的下一个数字以及每个块中的下一个数字进行比较来轻松判断该数字是否重复。然后移动到所有块中的下一个最小数字并重复直到用完数字。

由于排序,您的运行时间为 O(n log n)。

【讨论】:

    【解决方案4】:

    我喜欢@airza 解决方案,但也许还有另一种算法需要考虑:也许一百万个电话号码无法一次加载到内存中,因为它们的表达效率低下,即每个电话号码使用的字节数超过了必要的字节数。在这种情况下,您可以通过对电话号码进行散列并将散列存储在(散列)表中来获得有效的解决方案。哈希表支持字典操作(如in),让您轻松找到骗子。

    更具体地说,如果每个电话号码都是 13 个字节(例如格式为(NNN)NNN-NNNN 的字符串),则该字符串表示十亿个数字中的一个。作为一个整数,这可以存储为 4 个字节(而不是字符串格式的 13 个)。然后我们可能能够将这个 4 字节的“哈希”存储在哈希表中,因为现在我们的 10 亿个哈希数字占用了 3.08 亿个数字的空间,而不是 10 亿个。排除不可能的数字(区号 000555 等中的所有数字)可能会让我们进一步减小哈希大小。

    【讨论】:

    • 包含 1,000,000 个唯一元素的哈希表至少有 1,000,000 个元素。它们通常被认为是空间->速度权衡,这与我们在这里尝试做的相反
    • 抱歉不清楚。我编辑了答案以更清楚。
    猜你喜欢
    • 1970-01-01
    • 2018-12-04
    • 2015-05-26
    • 2020-11-24
    • 2016-01-06
    • 1970-01-01
    • 2011-08-17
    • 2017-10-13
    相关资源
    最近更新 更多