key、value相關操作
當我們的鍵值對中的key=1001, 我們是不可能申請一個1001大小的數組用於存放key。否則,當我們要存放key=1和key=10001,我們就會浪費大量的內存空間。爲了根據key查詢對應的value,我們也需要申請同樣大小的空間,那麼就會浪費大量的空間。
實際上,我們只會申請和桶等大小的內容空間,來放key和value。
khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t));
khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t));
kh_put_##name
,kh_get_##name
和kh_del_##name
這三個函數就是和key的操作有關。
kh_get_##name
的含義是 查詢,根據給定的key,在哈希表中查找是否有元素,並返回哈希表對應的位置。由於存在哈希衝突的可能,因此查詢過程中還需要比較查詢的key和哈希表記錄的key值是否相同。
SCOPE khint_t kh_get_##name(const kh_##name##_t * h, khkey_t key) \
{ \
if (h->n_buckets) { \
khint_t k, i, last, mask, step = 0; \
mask = h->n_buckets - 1; \
k = __hash_func(key); i = k & mask; \
last = i; \
while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
i = (i + (++step)) & mask; \
if (i == last) return h->n_buckets; \
} \
return __ac_iseither(h->flags, i)? h->n_buckets : i; \
} else return 0; \
}
一共定義了5個變量
- k: k是哈希函數計算結果
- i: 存放key和value值的索引(index)
- last: 上一個index, 如果前後一樣,就用桶的最後一個位置
- mask: 用於取模,將key限制在桶大小以內
- step: 哈希碰撞後,往後移動
我們以key=10001, 桶的大小(n_buckets)=16爲例。mask = 16(0b10000) - 1 = 15(0b01111)
由於32位整型的哈希函數就是返回原值,那麼 k=10001. 通過按位與的位運算,我們計算出i = 10001 & 15 = 1. 這其實等價於對16取模(10001 % 16), 但是計算效率更高。
接着,我們就需要確定計算的位置是否爲我們查詢key所對應的的位置,顯然,當位置不爲空(!__ac_isempty(h->flags, i)
)時,如果哈希值不同(!__hash_equal(h->keys[i], key)
),就說明是哈希衝突了,我們需要往後移動。或者位置不爲空,並且這個位置還被標記爲刪除狀態(__ac_isdel(h->flags, i)
),說明也不是我們要找到的結果。
根據key得到桶中實際位置後,我們可以用kh_key
和kh_val
設置對應位置上的內容
- 對於整型的key和value,我們只需要設置value的值即可。
- 對於整型的key,字符串的value,你在設置value的時候,傳遞的是內存中存放對應字符串的指針。
- 對於字符串的key和字符串的value,那麼你分別需要指向key和指向value的指針。
也可以用kh_del_##name
刪除該位置。操作比較簡單,就是根據你提供的index(注意他是需要你提供key計算好的index),來標記桶對應位置爲刪除狀態,但不會實際釋放對應位置上key和value的內容。刪除的時候,我們得保證索引位置不是桶的最後一個位置,也不是空狀態或者刪除狀態。
SCOPE void kh_del_##name(kh_##name##_t * h, khint_t x) \
{ \
if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \
__ac_set_isdel_true(h->flags, x); \
--h->size; \
} \
}
比查詢和刪除複雜的函數是kh_put_##name
, 也就是插入操作,對應khash.h的307-348行,具體邏輯如下:
判斷當前佔用元素(n_occupied)是否超出了可容納元素的上限(upper_bound) ,也分爲兩種情況,一種是真的滿了,另一種是大部分都是刪除狀態), 如果是的話,就需要調整哈希表的大小(在下一部分介紹),並直接返回桶的最後一個位置。
if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \
if (h->n_buckets > (h->size<<1)) { \
if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \
*ret = -1; return h->n_buckets; \
} \
} else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \
*ret = -1; return h->n_buckets; \
}
} /* TODO: to implement automatically shrinking; resize() already support shrinking */ \
否則,先根據key計算index,找到待插入桶的位置,一共有7個變量
khint_t x;
khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \
x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \
- x: 最終返回的位置
- k: 根據key計算哈希值
- i: 哈希值基於mask的求模結果
- last: 第一次算出的i的位置
- mask: 用於對i進行求模
- step: 哈希衝突後的往後移動的步數
最佳情況是該位置爲空,那麼可以設置x。
if (__ac_isempty(h->flags, i)) x = i; /* for speed up */
如果不爲空,就需要往後探測,於是就有三種狀態需要討論:
- 探測位置爲空,符合要求,此時
x = site = h->n_buckets;
- 探測位置不爲空,位置不是刪除狀態,且哈希值相同,說明找到了相同key的元素,此時
x=h->n_buckets
- 探測位置不爲空,要麼是刪除,要麼是哈希值不相同。最終回到了出發點,此時,
x=site=上一個標記爲刪除的i
else { \
last = i; \
while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
if (__ac_isdel(h->flags, i)) site = i; \
i = (i + (++step)) & mask; \
if (i == last) { x = site; break; } \
} \
對於情況1和情況2,情況1, x=i, 情況2, x= site.
if (x == h->n_buckets) { \
if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \
else x = i; \
} \
最後,分三種情況考慮
- 如果是x的位置爲空,那麼就是設置爲00, 增加size和n_occupied
- 如果是x的位置標記爲刪除,那麼設置爲00,只增加size
- 否則,不做任何操作,返回狀態是0
if (__ac_isempty(h->flags, x)) { /* not present at all */ \
h->keys[x] = key; \
__ac_set_isboth_false(h->flags, x); \
++h->size; ++h->n_occupied; \
*ret = 1; \
} else if (__ac_isdel(h->flags, x)) { /* deleted */ \
h->keys[x] = key; \
__ac_set_isboth_false(h->flags, x); \
++h->size; \
*ret = 2; \
} else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \
這部分的邏輯比較複雜,需要畫圖理解下。