关于C#:在小阵列上最有效的搜索?

Most efficient search on small array?

我有一个C数组,它很少(几乎从不)更新:

1
unsigned long user_values[8];

我希望能够进行大量的快速查找(在速度较慢的机器上每秒数百次),以检查数组中是否存在值,如果存在,则获取其索引。几乎所有时候,此查找都无法在数组中找到项。

通常,我会保持数组的排序,并使用二进制搜索,但我不确定在非常小的数据集上进行二进制搜索的效率。有没有更有效的方法来进行这种查找,考虑到它的小,已知的大小?


我建议使用类似于小散列的方法来立即检测大多数故障。像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define HASH(x)    ((x) & 0xff)   // This can be improved if needed
uint8_t possible[256];

void initHash()
{
    int i;

    memset(possible, 0, sizeof(possible));
    for (i=0;i<8;i++)
        possible[HASH(user_values[i])] = 1;
}

int find(unsigned long val)
{
    // Rule out most failures with a quick test.
    if (!possible[HASH(val)])
        return -1;

    // Now use either binary or linear search.
    ...
}

请注意,在256插槽哈希表中最多设置8个插槽,您将立即排除31/32或97%的故障。有三种明显的方法可以改善这一点:

  • 您可以使哈希表变大以过滤更多的失败,但代价是使用更多的内存。更好的是,您可以添加第二个哈希(例如,从第二个字节到最后一个字节的哈希)。这将只需要另外256个字节,但过滤掉传递第一个哈希的3%的97%。顺便说一下,这种多哈希算法被称为布卢姆滤波器。
  • 每个哈希索引可以使用1位,而不是一个字节,这使得哈希表的大小为1/8,但需要更长的计算来进行哈希检查。
  • 您可以提供更好的哈希函数,具体取决于您期望的数据类型。

  • 这个程序执行二进制搜索和线性搜索(很多次这样他们可以很容易地定时)。在通常找不到搜索值的条件下,线性搜索大约是二进制搜索的两倍。二值搜索需要3次迭代才能将8个元素减少到1,而线性搜索需要8次迭代。我用的是unsigned int,而不是unsigned long

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    #include <stdio.h>
    #include <time.h>

    #define ARRLEN 8
    #define LOOPS 0x7FFFFF

    unsigned user_values[ARRLEN] = { 1, 234, 8124, 8335, 10234, 11285, 637774, 788277 };

    int main (int argc, char *argv[]) {
        unsigned val;
        int bot, top, mid, loop;
        clock_t start, elap;

        if (argc < 2) return 1;
        if (sscanf (argv[1],"%u", &val) != 1) return 1;

        // binary search
        printf ("Binary search for %u:", val);
        start = clock();
        for (loop=0; loop!=LOOPS; loop++) {
            bot = 0;
            top = ARRLEN;
            while (top-bot > 1) {
                mid = ((top + bot) >> 1);
                if (user_values[mid] <= val)
                     bot = mid;
                else top = mid;
            }
        }
        elap = clock() - start;
        if (user_values[bot] == val)
             printf ("found");
        else printf ("not found");
        printf (" in count %u
    "
    , (unsigned)elap);

        // linear search
        printf ("Linear search for %u:", val);
        start = clock();
        for (loop=0; loop!=LOOPS; loop++) {
            for (bot=0; bot<ARRLEN; bot ++)
                if (user_values[bot] == val)
                    break;
        }
        elap = clock() - start;
        if (bot<ARRLEN)
             printf ("found");
        else printf ("not found");
        printf (" in count %u
    "
    , (unsigned)elap);

        return 0;
    }


    对于switch-case跳转表有一些技术,但是它要求编译器利用它,这在您的特定情况下可能会发生,也可能不会发生。不要将这些值保存在数组中(您编写的值几乎从未更改过),而是按字面意思将它们作为标签:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #define NOT_FOUND -1

    int index = NOT_FOUND; // or any other way to mark that number is not found
    switch (val)
    {
        case 0x00000000UL : // replace with array values
            index = 0; break;
        case 0x00000001UL :
            index = 1; break;
        case 0x00000002UL :
            index = 2; break;
        // ...
    };

    唯一的缺点是数字现在在编译时是"固定的"。因此,要更新它们,您需要重新编译整个程序,什么是不可接受的(?).


    你想加快多少速度?除非在你的集合(数组)中有一些可利用的模式,只有8个检查,否则任何一种方法都将获得最小的收益。编译器通常非常擅长优化这类事情。我发现,通过一些gcc编译,在for循环中使用指针可以为我购买百分之几。展开循环,因为您知道静态8偏移值可能值几个百分点(也可能不值)。

    这里是一个假设的可利用数据模式。如果您的集合/向量/列表/数组/调用它们您倾向于集群,并且您要测试的候选范围在0x00000000到0xffffffff范围内均匀分布,那么您可能会获得一点向量预排序,并简单地测试小于第一个或大于最后一个,这将是2 TE。STS通常会失败,如果失败,则会通过列表进行线性搜索。但这种情况实际上取决于不同的比例(窗户有多宽?预分带会增加多少开销?等)。只有根据真实数据进行测试才能知道。

    而且总是存在这样一种真正的危险:可利用的模式,虽然通常会让你加速20%,但在你的假设被违反的边缘情况下,会表现得非常糟糕,以数量级伤害你。