hash c语言实现原理,GitHub - nlifew/hashmap: c实现的原理性hashmap(HashTable+RBTree)

本文详细解析了Java HashMap的内部实现,包括其核心思想、哈希分布优化、负载因子、扩容机制以及链表转红黑树的过程。通过对hashCode的处理,提高哈希的均匀性,减少碰撞,并探讨了HashMap如何在Java8中引入红黑树以进一步提升性能。此外,文章还介绍了HashTable的基本原理,强调了正确实现Object.hashCode()和equals()的重要性。
摘要由CSDN通过智能技术生成

前言

作为一名 java 程序猿,java.util 和 java.lang 包下的源码是必须要看,而且是必须要看懂的。前者包含了许多在工程中经常使用的类,如 ArrayList,LinkedList,HashMap,LinkedHashMap等。 List 即线性表,这部分比较简单,网上也有很多这部分的源码解析。Map 用来保存键值对的映射关系,相对比较复杂,但也绝对不是天书那种难度的。

看大佬的代码是提高姿势水平的好办法。如果没办法静下心来,那就试试自己造轮子,这个过程会无形中驱使你仔细阅读源码。

HashMap 简介

HashMap 是 Map 接口的实现类,它利用 key 的 hash 作为键,可以非常快地完成增改删查操作。在 hash 较为均匀,或者负载因子较小,容量较大时,可以实现 O(1) 的时间复杂度;如果 hash 碰撞十分严重,或负载因子较大,容量较小,时间复杂度会转为 O(logN)

需要注意的是,java 的 HashMap 之前是使用 HashTable 实现的,这意味着在 hash 高度碰撞时,时间复杂度会恶化为 O(n)。自 Java8 开始,加入了另一种数据结构——红黑树(RBTree),它保证了 O(logN) 的时间复杂度

简短洁说,HashMap 的工作原理是,当插入新元素时,会先根据 Object.hashCode() 计算出 hashCode,利用 hashCode 定位到数组下标,然后会根据链表和红黑树进行不同的处理。插入完成后需要根据负载因子判断是否需要扩容,并进行再散列。这个过程可能会发生链表转红黑树,或是红黑树转链表的处理。移除和查找的话比较简单,和上面类似。

HashTable

核心思想

在线性表中,数组(Array)和链表(List)是两个代表。前者支持随机访问,但插入和删除开销较大;后者只支持顺序访问,但插入和删除非常快。HashTable 可以看作是这两个的组合,它的核心是一个数组,叫做”桶(bucket)“。当要把一个对象放到数组时,只要根据某种规则计算出它的数组下标,就可以在 O(1) 的时间内完成操作。换句话说,我们需要在 Object 和 数组下标 之间搭建一个桥梁。事实上,这个”桥梁“已经有了,那就是 Object.hashCode() 方法。

在实际开发中,如果一个类需要重写 Object.equals() 方法,原则上也需要重写 Object.hashCode() ,因为相同的对象,必须返回相同的 hashCode。为了保证数组下标小于数组长度,一个非常普通的思路就是对数组长度求余。比如现在的数组容量为 8,Object 的 hashCode 分别为 1,9,4,那么其下标就是 1 % 8 = 1,9 % 8 = 1,4 % 8 = 4。然而,hashCode 为 1 和 9 时,对应的是同一个数组下标,这种”不同的对象对应同一个数组下标“的现象叫做”hash的碰撞“,此时,我们就需要在 1 这个下标构建一个链表,以来保存这两个对象。(图随便画的,还请见谅)

4c37ed9778fd3fe27cccc23a9959322e.png

上面的思路十分简单,也很自然,不管你信不信,这就是 HashTable 的核心思想。然而,如果你仔细思考,会发现 hash 的均匀性是一个非常重要的问题。如果 Object.hashCode() 很差(比如总返回同一个值),或数组的长度非常短,会引起非常严重的 hash 碰撞。此时,整个数据结构退化为链表,时间复杂度恶化为 O(N)。

提高 hash 均匀性

Object.hashCode() 返回的是一个 int 类型,它占用 4 个字节,范围可达 4G 大小。但我们实际使用的数组大小往往远小于这个值。也就是说,在 hashCode 对 数组长度 求余时,高位信息被浪费掉了。

比如 786432,4390913,1,这三个数看起来相差很多,但对 8 求余时,

786433 % 8 = 1

4390913 % 8 = 1

1 % 8 = 1

原因就是 int 的 高 24 位信息,都被忽略掉了。因此,我们需要先对 hashCode 做一个处理,使其高位信息仍能参与运算。java中是把高 16位和 hashCode 做异或运算。

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

同样的三个数,经过这样的处理,其数组下标分别为

(786433 ^ (786433 >>> 16)) % 8 = 5

(4390913 ^ (4390913 >>> 16)) % 8 = 2

(1 ^ (1 >>> 16)) % 8 = 1

可以看到,现在的碰撞的情况已经好多了。但上面的运算仍然不是很高效,因为求余运算是一个比较慢的过程。在 java 中,数组的长度必须是 2 的整次幂,这样就可以把求余优化为相与运算。比如,x % 16 和 x & (16 - 1) 的值是相同的,但后者的效率要高很多。

负载因子、扩容与再散列

从 HashTable 的原理上看,实际上就是用空间换时间。当数组长度过大,hash 分布更均匀,时间更短但浪费空间;当数组长度过小,hash 碰撞增加,时间更长但节省了空间。为了平衡二者,引入了一个概念叫做 ”负载因子(load_factor)“。当已存储元素的个数与数组容量的比值达到这个阈值时,我们就可以认为已经有相当数量的链表存在,就会考虑对数组扩容。在大多数情况下,这个值为 0.75,一般不建议修改。较小的值会加快速度,但占用内存会增大;反之亦然。

在扩容的过程中,由于数组大小变为原来的两倍,就需要重新调整每个元素的下标,这个过程叫做”再散列“。上面我们提到,数组的长度必须为 2 的整次幂,这样在再散列时,就能利用位操作得到更高的性能

Node[] newTab = (Node[])new Node[newCap];

table = newTab;

if (oldTab != null) {

for (int j = 0; j < oldCap; ++j) {

Node e;

if ((e = oldTab[j]) != null) {

oldTab[j] = null;

if (e.next == null)

newTab[e.hash & (newCap - 1)] = e;

else if (e instanceof TreeNode)

((TreeNode)e).split(this, newTab, j, oldCap);

else { // preserve order

Node loHead = null, loTail = null;

Node hiHead = null, hiTail = null;

Node next;

do {

next = e.next;

if ((e.hash & oldCap) == 0) {

if (loTail == null)

loHead = e;

else

loTail.next = e;

loTail = e;

}

else {

if (hiTail == null)

hiHead = e;

else

hiTail.next = e;

hiTail = e;

}

} while ((e = next) != null);

if (loTail != null) {

loTail.next = null;

newTab[j] = loHead;

}

if (hiTail != null) {

hiTail.next = null;

newTab[j + oldCap] = hiHead;

}

}

}

}

}

上面时扩容时的主要逻辑,非常巧妙的是,通过 hash 与 oldCap 相与的结果,将旧的链表拆分为两条新的链表,再保存到新的位置。假设现在的数组长度为 4,5 个对象的 hash 分别为 1,5,9,13,17,他们和 4 相与的结果为

1 & 4 = 0

5 & 4 = 4

9 & 4 = 0

13 & 4 = 4

17 & 4 = 0

其中,值为 0 的 1,9,17 在新数组的下标和旧下标相同,为 1;而5,13在新数组的下标为 旧数组容量 + 旧下标,即 4 + 1 = 5

链表转红黑树

正如上面所分析的,我们对 hash 的高 16 位进行异或,加入负载因子与扩容机制,以提高效率。即使如此,在 Object.hashCode() 很差时,仍会发生频繁的碰撞。举个例子,容量为 16 的数组,负载因子为 0.75,Object.hashCode() 总返回 1,在触发扩容之前,链表长度已经达到 12 了。这时我们就要转链表为红黑树,以进一步提高效率。

红黑树

红黑树是一种树形数据结构,通过左旋、右旋、变色等操作保持平衡,其最长路径最多为最短路径的 2 倍。它不像传统的二叉查找树会退化为链表,也不像完全二叉树那样难以保持平衡。这部分较为复杂,这里我推荐几篇较为详细的文章

代码实现

通过上面的分析,我们能够实现出一个比较简单的 HashMap 工具类。这个部分我们使用 C 语言实现。待稍稍整理再上传到 github

https://github.com/nlifew/hashmap

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值