深入解读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));

那么面对一个范围比较大的key,我们如何将key映射到有限大小的桶中呢? 在讲解实际代码之前,我们先以几个具体的例子讲解背后的逻辑。

假如目前的桶大小为16(n_buckets=16), 但是我们的key是10001,我们是无法直接将key放入桶中。由于32位整型的哈希函数就是返回原值,那么 k=10001,显然依旧无法放入桶中。这个时候,我们就需要引入一个取模的概念,所谓的取模,就是将我们的key和桶的大小相除,得到剩下的余数,也就是 10001 % 16 = 1,也就是在index=1的位置存放key。取模保证了我们再大的数字都能落在一个固定范围内,例如 1223423423423 % 16 = 15。为了提高取模运算,我们可以用一个等价的位运算10001 & 15来加速,15的二进制表示为0b01111,也就是无论一个数字有多大,最终只有最后的四位可以不为0.

取模操作会有一个问题,就是不同的数字会有相同的余数,例如17求余之后也是1。那么此时应该处理呢?解决方法有很多种,khash.h采用的方法就是,在冲突的地方往后找空位。也就是,我们可以在2的位置插入17。

在查询操作的时候,也不能直接返回模运算得到的位置,而是将位置的key和我们查询的key进行比较,如果相同才能返回。

除了插入和查询外,我们还有一个删除操作。为了提高效率,我们执行删除操作时,需要将对应位置标记为删除态即可,等到空间存在过多的删除位置时,我们才考虑做一次空间调整。

khash.h中kh_put_##name,kh_get_##name这两个函数都需要根据key计算index分别对应

  • 根据给定的key获取桶的位置,并在keys对应的位置上加入计算后的key
  • 根据给定的key查询桶的位置

有了桶具体位置之后,kh_key,kh_val,kh_del_##name就可以对key和value进行修改或删除。

接下来的部分就是具体的代码讲解

put

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 */ \

注意,这里的ret, 一共有-1, 0, 1, 2 这四种状态,分别表示操作失败,key在表中出现,桶为空,桶是删除状态

第二部分,根据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的求模结果
  • site: 上一个标记删除的位置
  • last: 第一次算出的i的位置
  • mask: 用于对i进行求模
  • step: 哈希冲突后的往后移动的步数

查找可用位置的代码如下

if (__ac_isempty(h->flags, i)) x = i; /* for speed up */    \
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; }                 \
    }                                                       \
    if (x == h->n_buckets) {\
        if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \
        else x = i;                                         \
    }                                                       \
}                                                           \

即便最简单的情况是__ac_isempty(h->flags, i),也就是待插入的位置为空,也要分为三种情况考虑,

  • 没有碰撞:x = i, site=h->n_buckets
  • 出现碰撞,往后探测过程中没有删除位置:x=site=h->n_buckets
  • 出现碰撞,往后探测过程中有删除位置: x=h->buckets, site=i

如果不为空,那么考虑key重复的情况,也就是待插入的位置的哈希值和当前key的哈希值相同,也就是!__ac_isdel(h->flags, i) && __hash_equal(h->keys[i], key)), 也分为两种请

  • 没有碰撞,x=site=h->n_buckets
  • 出现碰撞,往后探测过程中没有删除位置: x=site=h->n_buckets
  • 出现碰撞,往后探测过程中有删除位置: x=h->buckets, site=i

最麻烦的情况是,找了一圈,回到最初的位置i=last, 那么此时x=site=i

不存在所有元素都是delete,或者所有元素都是occupied的情况,因为一旦超过一个容纳上线,哈希表会自动扩容。

最后,如果x!=h->n_buckets, 我们是找了一圈,我们直接使用上一个标记为删除的位置,否则考虑两种情况

  1. 如果找到的位置为空,且中间有删除的位点的话,我们优先用删除的位置
  2. 换言之,找到的位置不为空,或者中间没有删除位置,那就用有相同哈希的位置或者中间的空位
if (x == h->n_buckets) {                                \
    if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ 
    else x = i;                                         \
}                                                       \

最后,分三种情况考虑

  • 如果是x的位置为空,标记状态,并增加size和n_occupied
  • 如果是x的位置标记为删除,标记状态,只增加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 */ \

get

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的哈希值和我们查询的key的哈希值相同。

delete

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;                                                  \
        }                                                               \
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容