用于检测数据集中的重复项的算法,该数据集太大而不能完全加载到存储器中

时间:2013-03-14 16:36:51

标签: algorithm duplicates

这个问题有最佳解决方案吗?

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

我的'天真'解决方案是一个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

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

4 个答案:

答案 0 :(得分:7)

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

对于每组两个块,我们将在两个块上执行mergesort的最后一步,以生成一个更大的块(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提到的一个快速而肮脏的解决方案是制作一个尽可能大的内存过滤器。然后,您可以列出未通过布隆过滤器的每个元素的索引,并第二次循环遍历该文件,以便测试这些成员是否有重复。

答案 1 :(得分:3)

我认为Airza的解决方案正朝着一个良好的方向前进,但由于排序不是你想要的,而且它更贵,你可以通过结合angelatlarge的方法来做到以下几点:

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

获取块C i

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

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

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

  4. 使用块C i + 1 重复步骤1,2

  5. 删除了所有标记为删除的元素(可以在期间完成,无论什么更合适,如果你必须转移其他所有内容,当时删除一个可能会更贵)。

  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 |)

    一个。加载块C i O(| C |)
    湾迭代每个元素,测试并设置是否存在 O(1)将在其中进行插入和查找。 C。如果元素已经存在,则可以删除它。 1
    d。获取下一个块,冲洗并重复(2N / M块,所以 O(N / M)

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

    2 如果电话号码都可以表示为整数< 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
    }
    

答案 2 :(得分:1)

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

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

答案 3 :(得分:1)

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

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