数据结构零基础入门(初学者也能看懂):哈希MAP

【引入】如果有如下键值对,如何存储和查询呢?

1:西瓜

2:苹果

3:桔子

使用数组arr[N]简单存储:

西瓜苹果桔子

存储时,将key值作为index,通过偏移,直接得到存储地点,将其赋值:

arr[1] = "西瓜";
arr[2] = "苹果";
arr[3] = "桔子";

查询时,将key值作为index,直接获取数据数据:

return arr[index];

这是非常高效的查询方式,无需逐个比较,直接偏移index个元素。

如果存储内容复杂一点,比如,记录下小朋友喜欢吃的水果,那上面的方法就不好用了:

小明:西瓜

小红:苹果

小猴:桔子

key已经不再是简单的index,不能直接用作数组的index了。

于是,需要通过某种方法,将key转换成简单的index:

PUT:
index = Hash("小明");
arr[index] = "西瓜";

GET:
index = Hash("小明");
return arr[index];

以上就是hashMAP的本质。

“Hash()”这个方法实现,暂时不做深究,这里列举常用的方法,有兴趣可以深入了解其实现原理:分离连接法、开放定址法、线性探测法、平方探测法。

通常,hash得到的是一串很大的数,而数组容量有限,通常处理如下:

PUT:
index = Hash("小明");
index = index % ARR_MAX_LEN;
arr[index] = "西瓜";

GET:
index = Hash("小明");
index = index % ARR_MAX_LEN;
return arr[index];

有几个问题:

一、不同的key最终可能得到相同的index,比如,“小明”哈希之后得到1,“小红”哈希之后得到11,而数组容量是10,“小明”和“小红”最终得到的index都是1。这种现象称之为“哈希冲突”;

二、模运算比较低效,如果查询特别频繁,这个效率比较低下。

三、数组的容量不好确定。初始设置太大,造成浪费;容量太小,会因为冲突太多,造成查询低效。

解决一、“哈希冲突”的解决方法有很多,常用的,是将index相同的数据,以链表形式挂到相应数组桶上。

解决二、模运算则通常被位运算取代:

PUT:
index = Hash("小明");
index = index & (ARR_MAX_LEN-1); // 使用位运算替代模运算
arr[index] = "西瓜";

GET:
index = Hash("小明");
index = index & (ARR_MAX_LEN-1); // 使用位运算替代模运算
return arr[index];

解决三、数组容量动态调整,初始为16,当数据量达到阈值(一般为容量的两倍)时,自动扩容,并对所有数据重新哈希。

typedef struct hashmap_s {
  unsigned long capability;
  unsigned long used;

  void **arr;
} hashmap_t;

hashmap_t hashmap;

bool hash_put (char *key, char *val) {
  if (hashmap->capability > (hashmap->used)<<1) {
    index = Hash(key);
    index = index & (hashmap->capability-1);
    hashmap->arr[index] = val;
    return true;
  }

  //rehash
  // malloc (hashmap->capability << 1) && 更新capability、used、arr
  // 将原来数据全部重新哈希,并放入新arr指向的空间。
  
}

char *hash_get (char *key) {
    index = Hash(key);
    index = index & (hashmap->capability-1);
    return hashmap->arr[index]; // 需要检查该空间是否曾被插入过数据
}

重哈希时,会导致本次查询速度很慢,redis中使用了分治策略,即,如果需要扩容,则生成第二张表:table2,在table 1中查询数据,查到则将该数据重hash并移动到table 2。经过无数次查询,总有一次,table 1中的数据全部迁移至table 2中,然后释放table1,table2替代table1。

同时,重哈希时,会导致内存使用增加(扩了一张更大的表),如果此时redis在执行某些占用大量内存的操作(比如备份),可能导致内存紧张。所以,在重哈希时,如果发现正在执行其他占用大量内存的操作,则尽可能延后重哈希。即,在(used > 4 * capability)时才执行重哈希。

为什么初始值是16呢?以及之后都是以2的幂次扩容呢?

因为2的幂次作为capability,那么(capability-1)转化为二进制会变成全1:

16-1=15

15的二进制:1111

index&1111B,可以保证每个桶都被使用到,这样冲突会最少。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值