罗宾汉在C中散列

时间:2018-05-27 14:26:29

标签: c algorithm hashtable probing

我正在实施一个Hashtable来处理与robin hood散列的冲突。然而,之前我已经链接了,插入近100万个键的过程几乎是瞬间完成的。 Robin Hood散列也不会发生同样的情况,因为我觉得它更快,所以我觉得很奇怪。所以我想问的是我的插入功能是否正确实现。这是代码:

typedef struct hashNode{
    char *word;
    int freq; //not utilized in the insertion
    int probe;// distance from the calculated index to the actual index it was inserted.
    struct hashNode* next; //not utilized in the insertion
    struct hashNode* base; //not utilized in the insertion
}hashNode;
typedef hashNode* hash_ptr;

hash_ptr hashTable[NUM_WORDS] = {NULL}; // NUM_WORDS = 1000000
                                        // Number of actual entries = 994707

hash_ptr swap(hash_ptr node, int index){
    hash_ptr temp = hashTable[index];
    hashTable[index] = node;
    return temp;
}

static void insertion(hash_ptr node,int index){
    while(hashTable[index]){
        if(node->probe > hashTable[index]->probe){
            node = swap(node,index);
        }
        node->probe++;
        index++;
        if(index > NUM_WORDS) index = 0;

    }
    hashTable[index] = node;
}

将所有内容置于语境中:

  • node参数是新条目。
  • index参数是新条目的位置,如果它没有被占用。

2 个答案:

答案 0 :(得分:1)

Robin Hood算法非常聪明,但它依赖于具有良好的散列函数,就像任何其他开放散列技术一样。

最糟糕的情况是,考虑最糟糕的哈希函数:

int hash(const char* key) { return 0; }

由于这会将每个项目映射到同一个插槽,因此很容易看到探测的总数在条目数上是二次的:第一个插入在第一个探测上成功;第二个插入物需要两个探针;第三个三探针;依此类推,导致n(n+1)/2插入的n探针总数。无论您使用简单的线性探测还是罗宾汉探测,都是如此。

有趣的是,这个哈希函数可能对插入链式哈希表没有任何影响,如果 - 这是一个非常大的 if - 没有尝试验证插入的元素是独特。 (在您提供的代码中就是这种情况,并且它并非完全不合理;很可能将哈希表构建为固定查找表,并且已知要添加的条目是唯一的关于这一点的更多内容。)

在链式哈希实现中,非验证插入函数可能如下所示:

void insert(hashNode *node, int index) {
  node->next = hashTable[index];
  hashTable[index] = node;
}

请注意,即使您计划实施删除,也没有充分的理由为哈希链使用双向链表。额外的链接只是浪费内存和周期。

你可以在(几乎)没有时间构建链式哈希表的事实并不意味着该算法已经构建了一个好的哈希表。当需要在表中查找值时,将会发现问题:找到元素的平均探测数量将是表中元素数量的一半。 Robin Hood(或线性)开放式地址哈希表具有完全相同的性能,因为所有搜索都从表的开头开始。与使用该表的成本相比,开放式地址哈希表构建速度慢的事实可能几乎无关紧要。

我们不需要哈希函数,因为"总是使用0"功能产生二次性能。散列函数具有极小范围的可能值(与散列表的大小相比)就足够了。如果可能的值同样可能,则链式散列仍将是二次的,但平均链长将除以可能值的数量。但是,线性/ R.Hood探测散列的情况并非如此,特别是如果所有可能的散列值都集中在一个小范围内。例如,假设哈希函数是

int hash(const char* key) {
  unsigned char h = 0;
  while (*key) h += *key++;
  return h;
}

这里,散列的范围限于[0,255)。如果表大小远大于256,则会迅速降低到与常量散列函数相同的情况。很快就会填充哈希表中的前256个条目,并且在该点之后的每个插入(或查找)将需要在表格开头的线性增加的紧凑范围内进行线性搜索。因此,性能将与具有常量哈希函数的表的性能无法区分。

这些都不是为了激发链式哈希表的使用。相反,它指向使用良好散列函数的重要性。 (哈希一个密钥的结果均匀地分布在整个可能的节点位置范围内。)但是,通常情况下,聪明的开放寻址方案对于错误的哈希函数比简单链接更敏感。< / p>

开放寻址方案肯定很有吸引力,特别是在静态查找表的情况下。它们在静态查找表的情况下更具吸引力,因为删除确实很麻烦,因此不必实现密钥删除就会消除巨大的复杂性。删除最常见的解决方案是使用DELETED标记元素替换已删除的元素。查找探测器仍必须跳过DELETED标记,但如果查找后面将插入,则在查找扫描期间可以记住第一个DELETED标记,如果找不到该键,则由插入的节点覆盖。这是可以接受的,但负载因子必须使用预期的DELETED标记数计算,如果使用模式有时会连续删除大量元素,则表的实际负载因子将显着下降。

在删除不是问题的情况下,开放式地址哈希表具有一些重要的优点。特别是,在有效载荷(密钥和相关值,如果有的话)很小的情况下,它们的开销要低得多。在链式散列表的情况下,每个节点必须包含next指针,并且散列表索引必须是指向节点链的指针的向量。对于其键仅占用指针空间的哈希表,开销为100%,这意味着负载因子为50%的线性探测开放寻址哈希表占用的空间比索引表的链表少一点vector已完全占用,其节点按需分配。

线性探测表不仅更具存储效率,而且还提供了更好的参考局部性,这意味着CPU的RAM缓存将被用于更大的优势。通过线性探测,可以使用单个高速缓存行(因此只有一个慢速内存引用)执行八个探测,这几乎是探测随机分配的表条目的链表的八倍。 (在实践中,加速度不会超过这个极限,但速度可能会快两倍。)对于性能真正重要的情况下的字符串键,您可能会考虑存储长度和/或第一个散列条目本身中的键的几个字符,因此指向完整字符串的指针大多只使用一次,以验证成功的探测。

但是开放寻址的空间和时间优势都取决于散列表是条目的数组,而不是像实现中那样指向条目的指针数组。将条目直接放入哈希索引可避免每个条目(或至少每个链)的指针可能显着的开销,并允许有效使用内存缓存。这是您在最终实施中可能想要考虑的事情。

最后,开放式寻址使删除变得复杂的情况不一定如此。在cuckoo哈希(以及它近年来受到启发的各种算法)中,删除并不比链式哈希中的删除困难,甚至可能更容易。在cuckoo哈希中,任何给定的键只能位于表中的两个位置之一(或者,在某些变体中,k个位置之一用于某个小常量k),并且只需要查找操作检查这两个地方。 (插入可能需要更长时间,但对于小于50%的负载因子,仍然可以预期O(1)。)因此,您只需将条目从其中删除即可删除条目;这对查找/插入速度没有明显影响,并且插槽将被透明地重用,而无需任何进一步的干预。 (在不利方面,节点的两个可能位置不相邻,它们可能位于不同的缓存行上。但是对于给定的查找,它们只有两个。一些变体具有更好的参考局部性。)

关于你的罗宾汉实施的最后评论:

  1. 我并不完全相信99.5%的负载系数是合理的。也许没关系,但99%和99.5%之间的差异是如此微小,以至于没有明显的理由来诱惑命运。此外,通过使表的大小为2的幂(在这种情况下为1,048,576)并使用位掩码计算余数,可以消除散列计算期间相当慢的余数运算。最终结果可能会明显加快。

  2. 在哈希条目中缓存探测计数确实有效(尽管我早先有疑问),但我仍然认为建议的缓存哈希值的方法更优越。您可以轻松计算探头距离;它是搜索循环中当前索引与从缓存哈希值计算的索引(或缓存的起始索引位置本身,如果您选择缓存的内容)之间的差异。该计算不需要对哈希表条目进行任何修改,因此它的缓存更友好且稍快,并且它不需要更多空间。 (但无论哪种方式,都存在存储开销,这也会降低缓存友好性。)

  3. 最后,正如评论中所述,您的环绕代码中存在一个错误的错误;它应该是

    if(index >= NUM_WORDS) index = 0;
    

    使用严格的大于测试,您的下一次迭代将尝试使用索引NUM_WORDS处的条目,该条目超出范围。

答案 1 :(得分:0)

只需将其保留在此处即可:99%的填充率是合理的。下界是95%,也不是90%。我知道他们在报纸上说过,他们错了。错了像往常一样使用60%-80%的开放地址

插入时,Robin Hood哈希不会更改数目或冲突,冲突的平均(和总数)保持不变。只有它们的分布发生变化:Robin Hood改善了最坏的情况。但对于平均值,它与线性,二次或双哈希算法相同。

  • 在75%的命中率之前,您会遇到1.5次碰撞
  • 在80%的情况下发生2次碰撞
  • 以90%发生约4.5次碰撞
  • 95%发生约9次碰撞
  • 99%发生30次碰撞

我在10000个元素的随机表上进行了测试。 Robin Hood哈希不能更改此平均值,但是它将1%的最坏情况下的冲突次数从150-250次未击中(填充率为95%)提高到大约30-40次。