最快的算法,以确定数组是否至少有一个重复

时间:2016-10-14 13:44:35

标签: c algorithm

我这里有一个非常奇怪的案例。我有一个包含数百万条目的文件,并想知道是否存在至少一个重复的。这里的语言不是很重要,但C似乎是速度的合理选择。现在,我想知道的是采取什么样的方法?速度是这里的主要目标。当然,我们希望一旦发现一个副本就停止查看,这很清楚,但是当数据进入时,我不知道它是如何排序的。我只知道它是一个字符串文件,由换行符分隔。现在请记住,我想知道的是,是否存在重复。现在,我发现了很多关于在数组中查找所有重复项的SO问题,但是大多数问题都是简单而全面的,而不是最快的。

因此,我想知道:找出一个数组是否包含至少一个副本的最快方法是什么?到目前为止,我在SO上找到的最接近的是:Finding out the duplicate element in an array。选择的语言并不重要,但因为它毕竟是编程,所以多线程是可能的(我只是不确定这是否是一种可行的方法)。

最后,字符串的格式为XXXNNN(3个字符和3个整数)。

请注意,这不是严格理论。它在一台机器(带有8GB RAM的Intel i7)上进行测试,因此我必须考虑进行字符串比较等的时间。这就是为什么我也想知道它是否可以是更快将字符串拆分为两个,并首先比较整数部分,因为int比较会更快,然后是字符串部分?当然,这也需要我拆分字符串并将后半部分转换为int,这可能会更慢......

6 个答案:

答案 0 :(得分:7)

  

最后,字符串的格式为XXXNNN(3个字符和3个整数)。

了解您的关键域对于此类问题至关重要,因此这使我们能够大规模简化解决方案(以及此答案)。

如果X∈{A..Z} N∈{0..9} ,则 26 3 * 10 3 = 17,576,000 可能的值...一个bitset(基本上是一个普通的,完美的Bloom过滤器,没有误报)将需要〜2Mb。

在这里:你可以生成所有可能的1700万个密钥的python脚本:

import itertools
from string import ascii_uppercase

for prefix in itertools.product(ascii_uppercase, repeat=3):
    for numeric in range(1000):
        print "%s%03d" % (''.join(prefix), numeric)   

和一个简单的C位集过滤器:

#include <limits.h>
/* convert number of bits into number of bytes */
int filterByteSize(int max) {
    return (max + CHAR_BIT - 1) / CHAR_BIT;
}
/* set bit #value in the filter, returning non-zero if it was already set */
int filterTestAndSet(unsigned char *filter, int value) {
    int byteIndex = value / CHAR_BIT;
    unsigned char mask = 1 << (value % CHAR_BIT);

    unsigned char byte = filter[byteIndex];
    filter[byteIndex] = byte | mask;

    return byte & mask;
}

出于您的目的,您可以这样使用:

#include <stdlib.h>
/* allocate filter suitable for this question */
unsigned char *allocMyFilter() {
    int maxKey = 26 * 26 * 26 * 10 * 10 * 10;
    return calloc(filterByteSize(maxKey), 1);
}
/* key conversion - yes, it's horrible */
int testAndSetMyKey(unsigned char *filter, char *s) {
    int alpha   = s[0]-'A' + 26*(s[1]-'A' + 26*(s[2]-'A'));
    int numeric = s[3]-'0' + 10*(s[4]-'0' + 10*(s[5]-'0'));
    int key = numeric + 1000 * alpha;
    return filterTestAndSet(filter, key);
}

#include <stdio.h>
int main() {
    unsigned char *filter = allocMyFilter();
    char key[8]; /* 6 chars + newline + nul */
    while (fgets(key, sizeof(key), stdin)) {
        if (testAndSetMyKey(filter, key)) {
            printf("collision: %s\n", key);
            return 1;
        }
    }
    return 0;
}

这是线性的,尽管显然可以优化密钥转换和文件输入。无论如何,样本运行:

useless:~/Source/40044744 $ python filter_test.py > filter_ok.txt
useless:~/Source/40044744 $ time ./filter < filter_ok.txt

real    0m0.474s
user    0m0.436s
sys 0m0.036s

useless:~/Source/40044744 $ cat filter_ok.txt filter_ok.txt > filter_fail.txt
useless:~/Source/40044744 $ time ./filter < filter_fail.txt
collision: AAA000

real    0m0.467s
user    0m0.452s
sys 0m0.016s

不可否认,输入文件缓存在内存中以进行这些运行。

答案 1 :(得分:4)

合理的答案是保持算法的复杂性最小。我鼓励您使用HashTable来跟踪插入的元素;最终的算法复杂度为O(n),因为HashTable中的搜索理论上是O(1)。在你的情况下,我建议你在阅读文件时运行算法。

public static bool ThereAreDuplicates(string[] inputs)
        {
            var hashTable = new Hashtable();
            foreach (var input in inputs)
            {
                if (hashTable[input] != null)
                    return true;

                hashTable.Add(input, string.Empty);
            }
            return false;
        }

答案 2 :(得分:3)

fast 但效率低下的内存解决方案将使用

// Entries are AAA####
char found[(size_t)36*36*36*36*36*36 /* 2,176,782,336 */] = { 0 };  // or calloc() this
char buffer[100];

while (fgets(buffer, sizeof buffer, istream)) {
  unsigned long index = strtoul(buffer, NULL, 36);
  if (found[index]++) {
    Dupe_found();
    break;
  }
}

该帖子的问题在于它需要“最快的算法”,但没有详述内存问题及其对速度的相对重要性。所以速度必须是王者而上述浪费的时间很少。它确实满足了“一旦发现一个副本就停止查找”的要求。

答案 3 :(得分:3)

由于你有几百万个条目,我认为最好的算法是计算排序。计数排序完全符合您的要求:它通过计算每个元素存在的次数来对数组进行排序。所以你可以编写一个对数组进行计数排序的函数:

void counting_sort(int a[],int n,int max)
{
     int count[max+1]={0},i;

     for(i=0;i<n;++i){
      count[a[i]]++;
       if (count[a[i]]>=2) return 1;
      }
      return 0;

}

首先应找到max元素(在O(n)中)。计数排序的渐近时间复杂度为O(max(n,M)),其中 M 是数组中的最大值。因此,如果M的大小顺序为数百万,那么你有几百万个条目,这将在O(n)中工作(或者对于计数排序更少,但因为你需要找到M它是O(n)) 。如果你也知道M不可能超过数百万,那么你肯定会得到O(n)而不仅仅是O(max(n,M))。

您可以在此处查看计数排序可视化以更好地理解它 https://www.cs.usfca.edu/~galles/visualization/CountingSort.html

请注意,在上面的函数中,我们没有实现精确的计数排序,当我们找到一个更有效的副本时我们会停止,因为你只想知道是否有重复。

答案 4 :(得分:2)

根据有多少不同的东西你可以选择:

  • 对整个数组进行排序,然后查找重复元素,复杂度O(n log n),但可以在适当的位置完成,因此内存将为O(1)
  • 构建所有元素的集合。根据所选的集合实现,可以是O(n)(当它将被哈希设置时)或O(n log n)(二叉树),但这样做会花费你一些内存。

答案 5 :(得分:2)

查明数组是否包含至少一个副本的最快方法是使用位图,多个CPU和(原子或非原子)“测试和设置位”指令(例如80x86上的lock bts)。

一般的想法是将数组划分为“总元素/ CPU数量”大小的片段,并将每个片段分配给不同的CPU。每个CPU通过计算一个整数并对与该整数对应的位执行原子“测试和设置位”来处理它的数组。

但是,这种方法的问题在于您正在修改所有CPU正在使用的内容(位图)。更好的想法是给每个CPU一个整数范围(例如,CPU编号N从“(最小 - 最大)* N / CPU”到“(最小 - 最大)*(N + 1)/ CPU”的所有整数。这意味着所有CPU都从整个数组中读取,但每个CPU只修改它自己的位图私有部分。这避免了缓存一致性协议(“读取高速缓存行的所有权”)所涉及的一些性能问题,并且还避免了对原子指令的需要。

然后下一步是看你如何将“3个字符和3个数字”字符串转换为整数。理想情况下,这可以/将使用SIMD完成;这将要求数组采用“数组结构”格式(而不是更可能的“结构数组”格式)。另请注意,您可以先将字符串转换为整数(在“每个CPU执行字符串的子集”方式),以避免每个CPU转换每个字符串并将更多内容打包到每个缓存行中。