深入解讀Khash.h之 key、value相關操作

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_##namekh_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_keykh_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 */    

如果不爲空,就需要往後探測,於是就有三種狀態需要討論:

  1. 探測位置爲空,符合要求,此時x = site = h->n_buckets;
  2. 探測位置不爲空,位置不是刪除狀態,且哈希值相同,說明找到了相同key的元素,此時x=h->n_buckets
  3. 探測位置不爲空,要麼是刪除,要麼是哈希值不相同。最終回到了出發點,此時,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 */ \

這部分的邏輯比較複雜,需要畫圖理解下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章