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獲取元素的效能時表現會較好。
雜湊表的優勢體現在於空間換時間上,在設計雜湊表時需要注意以下情況[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+樹等。參考文章: