c語言數據結構實現-哈希表/哈希桶(hashtable/hashbucket)

一、需求

以“key-value”的形式進行插入、查詢、刪除,是否可以考慮犧牲空間換時間的做法?

二、相關知識

哈希表(Hashtable)又稱爲“散列”,Hashtable是會根據索引鍵的哈希程序代碼組織成的索引鍵(Key)和值(Value)配對的集合。Hashtable 對象是由包含集合中元素的哈希桶(Bucket)所組成的。而Bucket是Hashtable內元素的虛擬子羣組,可以讓大部分集合中的搜尋和獲取工作更容易、更快速。[1]

哈希函數(Hash Function)爲根據索引鍵來返回數值哈希程序代碼的算法。索引鍵(Key)是被存儲對象的某些屬性值(Value)。當對象加入至 Hashtable時,它存儲在與對象哈希程序代碼相符的哈希程序代碼相關的Bucket中。當在Hashtable內搜尋值時,哈希程序代碼會爲該值產生,並且會搜尋與該哈希程序代碼相關的Bucket。例如,student和teacher會放在不同的Bucket中,而dog和god會放在相同的 Bucket中。所以當索引鍵是唯一從Hashtable獲取元素的性能時表現會較好。[1]

哈希表的優勢體現在於空間換時間上,在設計哈希表時需要注意以下情況[2]

1)Hash函數的選擇,一個好的哈希函數可以均勻地將數據樣本散列到表中;

2)衝突的解決方法,常用的衝突處理就是拉鍊法,即出現衝突時以鏈表的形式擴展;

3)表大小與關鍵字個數的平衡,設表大小爲M,關鍵字個數爲N,當裝填因子(k=N/M)越大則衝突越嚴重;

三、源碼實現

先放一個圖例,Hashtable由多個Bucket組成,Bucket以HashKey值爲索引,每個Bucket中存放着所有HashKey相同的(Key, Value)

如圖所示,BucketNum = 5, DataNum = 7, 可見 k = 1.4 有一些衝突,更能很好地看出拉鍊法是如何解決衝突問題的:

如Key=A Key=E Key=F 算出來的 HKey 均爲1,所以(A, ValueA) (B, ValueB) (C, ValueC) 均放入HKey = 1 的 Bucket中;


程序源碼源於Linux內核源碼修改:linux-3.10.25/security/selinux/ss/hashtab.c

以下直接分析源碼,先上結構體,其中hashtab標識了整個hash表,而**htable 爲buckets集合,hashtab_node則是鏈表節點

仔細看了一下,他這種寫法靈活性非常強,首先(key, datum)分別爲指針,按照調用者的用法就是外部申請好datum空間,告訴接口key 進行索引管理,而接口內部並不關心datum是什麼內容,查詢的時候只需要再把datum指針返回給調用者;

然後在主體結構hashtab中,預留了回調函數hash_value、keycmp,我理解就是相當於c++模板、java抽象類的思路,調用者的key可以是int、long、char*任何類型的,只需要定義好相關的合理實現即可;

struct hashtab_node {
    void *key;
    void *datum;
    struct hashtab_node *next;
};

struct hashtab {
    struct hashtab_node **htable;                               /* hash table */
    u32 size;                                                   /* number of slots in hash table */
    u32 nel;                                                    /* number of elements in hash table */
    u32 (*hash_value)(struct hashtab *h, void *key);            /* hash function */
    int (*keycmp)(struct hashtab *h, void *key1, void *key2);   /* key comparison function */
};
初始化則申請空間,並對回調函數進行賦值操作,由於是內核態編程,用戶態編程則用 calloc/malloc 去變通一下即可

struct hashtab *hashtab_create(u32 (*hash_value)(struct hashtab *h, void *key),
                               int (*keycmp)(struct hashtab *h, void *key1, void *key2),
                               u32 size)
{
    struct hashtab *p;
    u32 i;

    p = kzalloc(sizeof(*p), GFP_KERNEL);
    if (p == NULL)
        return p;

    p->size = size;
    p->nel = 0;
    p->hash_value = hash_value;
    p->keycmp = keycmp;
    p->htable = kmalloc(sizeof(*(p->htable)) * size, GFP_KERNEL);
    if (p->htable == NULL) {
        kfree(p);
        return NULL;
    }

    for (i = 0; i < size; i++)
        p->htable[i] = NULL;

    return p;
}
數據插入操作,流程非常明顯,先是哈希算法 hvalue=H(key),定位 h->htablep[hvalue],如果有衝突則遍歷bucket比對節點內部的key值;

但是有一點使用起來不太方便,就是key的保存他使用的是直接指針賦值,若使用同一個變量取地址進行傳參,這樣將會出現問題;

int hashtab_insert(struct hashtab *h, void *key, void *datum)
{
    u32 hvalue;
    struct hashtab_node *prev, *cur, *newnode;

    if (!h || h->nel == HASHTAB_MAX_NODES)
        return -EINVAL;

    hvalue = h->hash_value(h, key);
    prev = NULL;
    cur = h->htable[hvalue];
    while (cur && h->keycmp(h, key, cur->key) > 0) {
        prev = cur;
        cur = cur->next;
    }

    if (cur && (h->keycmp(h, key, cur->key) == 0))
        return -EEXIST;

    newnode = kzalloc(sizeof(*newnode), GFP_KERNEL);
    if (newnode == NULL)
        return -ENOMEM;
    newnode->key = key;
    newnode->datum = datum;
    if (prev) {
        newnode->next = prev->next;
        prev->next = newnode;
    } else {
        newnode->next = h->htable[hvalue];
        h->htable[hvalue] = newnode;
    }

    h->nel++;
    return 0;
}
瞭解了插入函數,那麼查詢函數也不會有太大困難,也是先計算hash值,若有衝突的情況,遍歷bucket去查找;

同理可知刪除節點也是類似的流程;

void *hashtab_search(struct hashtab *h, void *key)
{
    u32 hvalue;
    struct hashtab_node *cur;

    if (!h)
        return NULL;

    hvalue = h->hash_value(h, key);
    cur = h->htable[hvalue];
    while (cur != NULL && h->keycmp(h, key, cur->key) > 0)
        cur = cur->next;

    if (cur == NULL || (h->keycmp(h, key, cur->key) != 0))
        return NULL;

    return cur->datum;
}
最後是銷燬操作,就是遍歷所有buckets,逐一銷燬;

在這個接口中,我認爲是可以擴充的,一是可以加一個 free_callback 幫助用戶數據進行銷燬;其次傳參的時候可以用二級指針,調用結束後外部的變量設置爲NULL,避免了野指針的出現;

void hashtab_destroy(struct hashtab *h)
{
    u32 i;
    struct hashtab_node *cur, *temp;

    if (!h)
        return;

    for (i = 0; i < h->size; i++) {
        cur = h->htable[i];
        while (cur != NULL) {
            temp = cur;
            cur = cur->next;
            kfree(temp);
        }
        h->htable[i] = NULL;
    }

    kfree(h->htable);
    h->htable = NULL;

    kfree(h);
}

四、總結

本文簡單介紹了哈希表的原理,以及對內核的哈希源碼進行了分析,代碼裏的回調思想是值得推薦的。
對於哈希函數的選擇上,若key值爲數值型的,最高效的方式就是選擇&位運算的算法;若爲字符串型則有多種選擇的算法如:RS、JS、BKDR等。
在實際的使用中,hash表的可以用於大規模數據下的增加、刪除操作;但是若存在一些遍歷的需求,hash表在這塊的效率不高(需要遍歷所有的桶),這些情況則可以考慮別的數據結構如紅黑樹、B+樹等。

參考文章:

[1] http://www.nowamagic.net/academy/detail/3008086

[2] http://blog.csdn.net/freetourw/article/details/53493616

[3] http://blog.chinaunix.net/uid-27213819-id-3794127.html

發佈了46 篇原創文章 · 獲贊 35 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章