【问题标题】:How to deal with old references to a resized hash table?如何处理对调整大小哈希表的旧引用?
【发布时间】:2018-03-05 15:01:28
【问题描述】:

我目前正在使用 C 语言实现哈希表。我正在尝试实现动态调整大小,但遇到了问题。

如果调整散列表的大小意味着创建一个大小为两倍(或一半)的新表、重新散列并删除旧表,我该如何处理用户可能对旧表所做的旧引用?示例代码(我只是为了这个例子省略了错误检查):

int main(int argc, char *argv[])
{
    ht = ht_create(5) /* make hashtable with size 5 */
    ht_insert("john", "employee"); /* key-val pair "john -> employee" */
    ht_insert("alice", "employee");
    char *position = ht_get(ht, "alice"); /* get alice's position from hashtable ht */


    ht_insert("bob", "boss"); /* this insert exceeds the load factor, resizes the hash table */

    printf("%s", position); /* returns NULL because the previous hashtable that was resized was freed */

    return 0;
}

在这种情况下,position 指向在哈希表中找到的alice 的值。当它被调整大小时,我们释放了哈希表并丢失了它。我该如何解决这个问题,这样用户就不必担心先前定义的指针被释放了?

编辑:我当前的哈希表实现

哈希.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "hash.h"

#define LOADFACTOR 0.75

typedef struct tableentry /* hashtab entry */
{
    struct tableentry *next;
    char *key;
    void *val;
} tableentry_t;

typedef struct hashtable
{
    datatype_t type;
    size_t size;
    size_t load; /* number of keys filled */
    struct tableentry **tab;
} hashtable_t;

/* creates hashtable */
/* NOTE: dynamically allocated, remember to ht_free() */
hashtable_t *ht_create(size_t size, datatype_t type)
{
    hashtable_t *ht = NULL;
    if ((ht = malloc(sizeof(hashtable_t))) == NULL)
        return NULL;
    /* allocate ht's table */
    if ((ht->tab = malloc(sizeof(tableentry_t) * size)) == NULL)
        return NULL;
    /* null-initialize table */
    size_t i;
    for (i = 0; i < size; i++)
        ht->tab[i] = NULL;
    ht->size = size;
    ht->type = type;
    return ht;
}

/* creates hash for a hashtab */
static unsigned hash(char *s)
{
    unsigned hashval;
    for (hashval = 0; *s != '\0'; s++)
        hashval = *s + 31 * hashval;
    return hashval;
}

static int *intdup(int *i)
{
    int *new;
    if ((new = malloc(sizeof(int))) == NULL)
        return NULL;
    *new = *i;
    return new;
}

static void free_te(tableentry_t *te)
{
    free(te->key);
    free(te->val);
    free(te);
}

/* loops through linked list freeing */
static void free_te_list(tableentry_t *te)
{
    tableentry_t *next;
    while (te != NULL)
    {
        next = te->next;
        free_te(te);
        te = next;
    }
}

/* creates a key-val pair */
static tableentry_t *alloc_te(char *k, void *v, datatype_t type)
{
    tableentry_t *te = NULL;
    int status = 0;
    /* alloc struct */
    if ((te = calloc(1, sizeof(*te))) == NULL)
        status = -1;
    /* alloc key */
    if ((te->key = strdup(k)) == NULL)
        status = -1;
    /* alloc value */
    int *d;
    char *s;
    switch (type)
    {
        case STRING:
            s = (char *) v;
            if ((te->val = strdup(s)) == NULL)
                status = -1;
            break;
        case INTEGER:
            d = (int *) v;
            if ((te->val = intdup(d)) == NULL)
                status = -1;
            break;
        default:
            status = -1;
    }
    if (status < 0)
    {
        free_te_list(te);
        return NULL;
    }
    te->next = NULL;
    return te;
}

static tableentry_t *lookup(hashtable_t *ht, char *k)
{
    tableentry_t *te;
    /* step through linked list */
    for (te = ht->tab[hash(k) % ht->size]; te != NULL; te = te->next)
        if (strcmp(te->key, k) == 0)
            return te; /* found */
    return NULL; /* not found */
}

/* inserts the key-val pair */
hashtable_t *ht_insert(hashtable_t *ht, char *k, void *v)
{
    tableentry_t *te;
    /* unique entry */
    if ((te = lookup(ht, k)) == NULL)
    {
        te = alloc_te(k, v, ht->type);
        unsigned hashval = hash(k) % ht->size;
        /* insert at beginning of linked list */
        te->next = ht->tab[hashval]; 
        ht->tab[hashval] = te;
        ht->load++;
    }
    /* replace val of previous entry */
    else
    {
        free(te->val);
        switch (ht->type)
        {
            case STRING:
                if ((te->val = strdup(v)) == NULL)
                    return NULL;
                break;
            case INTEGER:
                if ((te->val = intdup(v)) == NULL)
                    return NULL;
                break;
            default:
                return NULL;
        }
    }
    return ht;
}

static void delete_te(hashtable_t *ht, char *k)
{
    tableentry_t *te, *prev;
    unsigned hashval = hash(k) % ht->size;
    te = ht->tab[hashval];
    /* point head to next element if deleting head */
    if (strcmp(te->key, k) == 0)
    {
        ht->tab[hashval] = te->next;
        free_te(te);
        ht->load--;
        return;
    }
    /* otherwise look through, keeping track of prev to reassign its ->next */
    for (; te != NULL; te = te->next)
    {
        if (strcmp(te->key, k) == 0)
        {
            prev->next = te->next;
            free_te(te);
            ht->load--;
            return;
        }
        prev = te;
    }   
}

hashtable_t *ht_delete(hashtable_t *ht, char *k)
{
    size_t i;
    if (lookup(ht, k) == NULL)
        return NULL;
    else
        delete_te(ht, k);

}

/* retrieve value from key */
void *ht_get(hashtable_t *ht, char *k)
{
    tableentry_t *te;
    if ((te = lookup(ht, k)) == NULL)
        return NULL;
    return te->val;
}

/* frees hashtable created from ht_create() */
void ht_free(hashtable_t *ht)
{
    size_t i;
    if (ht)
    {
        for (i = 0; i < ht->size; i++)
            if (ht->tab[i] != NULL)
                free_te_list(ht->tab[i]);
        free(ht);
    }
}

/* resizes hashtable, returns new hashtable and frees old */
static hashtable_t *resize(hashtable_t *oht, size_t size)
{
    hashtable_t *nht; /* new hashtable */
    nht = ht_create(size, oht->type);
    /* rehash */
    size_t i;
    tableentry_t *te;
    /* loop through hashtable */
    for (i = 0; i < oht->size; i++)
        /* loop through linked list */
        for (te = oht->tab[i]; te != NULL; te = te->next)
            /* insert & rehash old vals into new ht */
            if (ht_insert(nht, te->key, te->val) == NULL)
                return NULL;
    ht_free(oht);
    return nht;
}

哈希.h

/* a hash-table implementation in c */
/*
hashing algorithm: hashval = *s + 31 * hashval
resolves collisions using linked lists
*/

#ifndef HASH
#define HASH

typedef struct hashtable hashtable_t;

typedef enum datatype {STRING, INTEGER} datatype_t;

/* inserts the key-val pair */
hashtable_t *ht_insert(hashtable_t *ht, char *k, void *v);

/* creates hashtable */
/* NOTE: dynamically allocated, remember to ht_free() */
hashtable_t *ht_create(size_t size, datatype_t type);

/* frees hashtable created from ht_create() */
void ht_free(hashtable_t *ht);

/* retrive value from key */
void *ht_get(hashtable_t *ht, char *k);

hashtable_t *ht_delete(hashtable_t *ht, char *k);

#endif

【问题讨论】:

  • 您可以在哈希表中添加一个条目,指明它使用的哈希算法。您可以使用低于 0x30000 的指针来指示它不是指向函数的指针,而是使用标准散列算法,并使用高于 0x30000 的指针作为指向函数的指针。这样,您需要做的就是将其重新散列为新的存储桶大小。您可以使用 0 作为“无效哈希函数”哈希函数的占位符。例如,0 可能表示“无效哈希函数”,1 可能表示标准哈希函数 #1,...,超过 0x30000 的指针可用于指示它是哈希函数指针。
  • 哦,你的意思是,如果有人有一个指向哈希表的弱指针,但该表已调整大小?这不是问题,因为您永远不会破坏桌子;您只需停止世界(防止任何东西使用信号量/锁与哈希表交互)并重新散列所有内容。以后任何使用 table 的尝试都将在执行散列时使用新的存储桶大小。
  • 所以你在调整大小后永远不会删除/释放旧的哈希表?这最终不会导致大量的内存空间浪费吗?
  • 为什么会这样?哈希表是一种抽象数据结构,它与关联数组相同,只是它是桶表而不是关联表。您只需复制存储桶即可。哈希函数负责访问哪个存储桶,然后您像常规关联数组一样搜索它。
  • 我在想,因为你会有一堆死的、空的、永远不会被使用的内存空间(来自旧的哈希表)。

标签: c hashtable


【解决方案1】:

您可以像标准库 (C++) 处理这个确切问题的方式一样处理它们:

对容器的某些操作(例如插入、擦除、调整大小)使迭代器无效。

例如std::unordered_map,它基本上是一个用桶实现的哈希表,它有以下规则:

  • 插入

unordered_[multi]{set,map}:所有迭代器在重新散列时失效 发生,但引用不受影响 [23.2.5/8]。不会发生重新散列 如果插入不会导致容器的大小超过 z * B 其中 z 是最大负载因子,B 是当前数量 桶。 [23.2.5/14]

  • 擦除

unordered_[multi]{set,map}:只有迭代器和对 擦除的元素无效 [23.2.5/13]

Iterator invalidation rules

迭代器的 C++ 概念是指针的泛化。所以这个概念可以应用于C。


您唯一的另一种选择是,不是将对象直接保存到容器中,而是添加另一个间接级别并保存某种代理。所以元素总是在内存中保持相同的位置。它是在调整大小/插入等方面移动的代理。但是您需要分析这种情况:添加的双重间接(肯定会以负面方式影响性能)并增加实现复杂性是否值得?拥有持久指针重要吗?

【讨论】:

  • 您在谈论 C++ 标准库,但这是一个 C 问题。 (此外,对 C++ 关联容器条目的引用仅因删除而无效,而不是因调整大小而无效。)
  • @rici 迭代器是指针的泛化,因此 C++ 的迭代器失效概念可以应用于 C 指针。
  • @rici 不同类型的容器有不同的规则。
  • 是的,我知道。但是这个问题是关于 C 中的哈希表的,所以关于 C++ 中的向量的答案似乎至少需要几个免责声明。 C++ 哈希表 do 解决了 OP 提出的问题。
  • 公平地说 C/C++ 很接近,您可以轻松地从动态库中导出 std::unordered_map c 接口,并将其视为 void *,或表示数字中键的数字 -> std: :unordered_map * 映射存储在库中,C 可以使用 C++ 服务,但需要一个指针 + 函数调用开销。
【解决方案2】:

不要使用哈希表作为数据的容器;只用它来引用数据,你不会有这个问题。

例如,假设您有键值对,使用具有 C99 灵活数组成员中实际数据的结构:

struct pair {
    struct pair  *next; /* For hash chaining */
    size_t        hash; /* For the raw key hash */

    /* Payload: */
    size_t        offset; /* value starts at (data + offset) */
    char          data[]; /* key starts at (data) */
};

static inline const char *pair_key(struct pair *ref)
{
    return (const char *)(ref->data);
}

static inline const char *pair_value(struct pair *ref)
{
    return (const char *)(ref->data + ref->offset);
}

然后你的哈希表可以很简单

struct pair_hash_table {
    size_t        size;
    struct pair **entry;
};

如果您有struct pair_hash_table *htstruct pair *foo,其中foo-&gt;hash 包含密钥的哈希,那么foo 应该在挂在ht-&gt;entry[foo-&gt;hash % ht-&gt;size]; 的单链表中。

假设您希望调整哈希表ht 的大小。您选择一个新的size,并为那么多struct pair * 分配足够的内存。然后,您遍历每个旧哈希条目中的每个单链表,将它们从旧列表中分离出来,并将它们添加到新哈希表中正确哈希表条目中的列表中。然后你就释放旧的哈希表entry数组,用新的替换它:

int resize_pair_hash_table(struct pair_hash_table *ht, const size_t new_size)
{
    struct pair **entry, *curr, *next;
    size_t        i, k;

    if (!ht || new_size < 1)
        return -1; /* Invalid parameters */

    entry = malloc(new_size * sizeof entry[0]);
    if (!entry)
        return -1; /* Out of memory */

    /* Initialize new entry array to empty. */
    for (i = 0; i < new_size; i++)
        entry[i] = NULL;

    for (i = 0; i < ht->size; i++) {

        /* Detach the singly-linked list. */
        next = ht->entry[i];
        ht->entry[i] = NULL;

        while (next) {
            /* Detach the next element, as 'curr' */
            curr = next;
            next = next->next;

            /* k is the index to this hash in the new array */
            k = curr->hash % new_size;

            /* Prepend to the list in the new array */
            curr->next = entry[k];
            entry[k] = curr;
        }
    }

    /* Old array is no longer needed, */
    free(ht->entry);

    /* so replace it with the new one. */
    ht->entry = entry;
    ht->size = size;

    return 0; /* Success */
}

请注意,struct pair 中的 hash 字段未修改,也未重新计算。

拥有原始哈希(相对于模表大小)意味着即使不同的键使用相同的槽,您也可以加快键搜索:

struct pair *find_key(struct pair_hash_table *ht,
                      const char *key, const size_t key_hash)
{
    struct pair *curr = ht->entry[key_hash % ht->size];

    while (curr)
        if (curr->hash == key_hash && !strcmp(key, pair_key(next)))
            return curr;
        else
            curr = curr->next;

    return NULL; /* Not found. */
}

在 C 中,逻辑与运算符 &amp;&amp; 是短路的。如果左侧不为真,则根本不计算右侧,因为在这种情况下整个表达式永远不会为真。

以上,这意味着比较键的原始哈希值,只有当它们匹配时,才会比较实际的字符串。如果你的散列算法甚至好到一半,这意味着如果密钥已经存在,通常只进行一次字符串比较;如果键不存在于表中,通常不会进行字符串比较。

【讨论】:

  • 传统观点认为,将计算得到的散列存储在散列表条目中并不是最优的,因为它增加了内存需求而没有提供显着的价值。具有相同哈希的不同字符串具有相同前缀的可能性极小,因此在比较字符串之前比较哈希值通常会增加执行的比较次数。它确实省去了重新计算调整大小的麻烦,但指数调整大小意味着一个条目的哈希值平均只会重新计算一次。
  • (另一方面,在比较字符串值之前比较条目的键和目标键的长度可能是一个胜利——两个同样散列的字符串的长度可能不同,但更重要的是,如果您事先知道两个字符串的长度相同,则可以优化比较代码。)
  • @rici:我不知道你的传统智慧是从哪里来的(我自己不相信权威,只是因为他们很受尊重);我脾气暴躁,看实际可衡量的东西来决定我使用什么。在键值哈希表的情况下,我通常存储键哈希以及键和值的长度,甚至将数据对齐和填充到本机字长以允许更快的比较。代码的简单性(和长期可维护性)以及简单实现的效率很好地弥补了额外的内存成本——无需任何“技巧”即可获得高性能的代码。
  • @rici:也就是说,我也喜欢使用简单但不够完美的哈希函数,比如 DJB2 xor 哈希变体(对于通常是文本的东西)。所以,我不是说你错了;我只是说我不同意你的说法,绝对不是没有实际已知的例子或实际证据。受欢迎程度并不能证明:虽然有数十亿只苍蝇,但这并不意味着便便一定很好吃。
  • 我提出了一个易于测试的声明,即链中两个完整哈希不同的概率小于两个键的第一个 sizeof(hash) 字节不同的概率。这两个比较都可以在相同的时间内完成。如果哈希比较,如果目标键存在,它们最终会比较,那么键比较也必须完成。所以哈希比较是浪费循环。你的苍蝇类比很可爱,但理性的论点会在智力上更令人满意。
猜你喜欢
  • 2011-06-24
  • 1970-01-01
  • 2017-04-12
  • 2013-12-21
  • 2012-10-14
  • 2021-03-14
  • 2012-11-27
  • 2014-04-21
相关资源
最近更新 更多