sqlyog怎么查找表_哈希表:人人都能看懂的哈希表介绍

本文通过图书馆的例子解释哈希表的概念,哈希表是一种通过hash算法快速定位数据的数据结构,常用于降低查找复杂度。在Java中,HashMap是哈希表的常见实现,其使用hash算法和数组实现高效查找。哈希冲突是不可避免的,解决冲突的方法包括开放寻址法和链表法。HashMap的扩容策略是当元素数量超过容量*负载因子时进行扩容,扩容过程中利用旧哈希值的特定位来确定新位置,避免了全部重新计算hash值。
摘要由CSDN通过智能技术生成

1d2c9a74ba6536b30eb20fe1a7888f39.png
导读:通过图书馆的例子,讲解什么是哈希表,为什么需要哈希表这种数据结构。介绍什么是好的 hash 算法、什么是 hash 冲突,Java 中哈希表的运用。

我们之前学习什么是数据结构与算法的时候讲过,数据结构是数据的存放方式,哈希表也是一种数据结构。接下来我们一起学习,哈希表是如何存放数据,为什么需要用哈希表来存放数据。

我们还是用之前图书的例子来学习哈希表,假设现在有一个二十层楼高的图书馆,存放满了图书。你只知道书名,你要怎么找到自己想要的图书呢?

首先,你可以一层一层楼的按着每层楼的每个书柜挨个查找,用计算机话语,这个叫遍历。在最理想的情况下,你在开始的位置就找到了自己想要的图书,用计算机的话来说,就是 O(1) 的时间复杂度。最差的情况下,你可能在最后的位置才找到自己想要的图书,在计算机中就是 O(n) 的时间复杂度。

按照这种方式查找图书,二十层楼的图书馆,上百万的藏书,你可能找书的时间都比看书的时间久,那我们看看有没有什么更好的查找图书方式呢?

我们发现,像这种二十层楼高的超大型图书馆,一般都会有专门的计算机用来查找图书。你只需要输入你想要查找书名,计算机就会告诉你这本书的具体位置,在图书馆中的第几层,在那一层的那个书柜,在书柜中又是第几层。

我们这里用书名去查找图书对应的位置,其实就是一种映射。计算机用书名这个 key 去映射图书位置 ,这种决定那个 key 映射到那个位置的办法,就是我们的 hash 算法。

我们通过 hash 算法知道了图书对应的位置,我们就可以直接找到图书。在数据结构中,支持知道了位置,就可以直接找到数据的数据结构是数组,不知道的朋友可以看我之前的文章数组:最基础的数据结构。

为了支持知道位置,就可以随机访问(随机访问:可以用同等的时间访问到一组数据中的任意一个元素)数据的特性,所以我们底层需要用数组来实现,用数组来表示就 Library[locality]。

这种通过 hash 算法计算键(Key)而得到数据存放位置,直接进行数据访问的数据结构,就是哈希表。

我们知道书柜有很多层,每层又用隔板隔开,每个小隔间中可能存放不同书名的图书。这样我们通过不同书名查看位置时,就可能会得到相同的位置,翻译成计算机话语,这就叫 hash 碰撞。

通过 hash 碰撞,我们可以解释为什么在 Java 中 hashCode 相等,equals 不一定相等,但是 equals 相等,hashCode 一定相等,所以重写 equals 方法也需要重写 hashCode 方法。

就像图书位置相同,但是并不一定是同一本图书,一个书柜的隔间是可以存放多本图书的,但是同一本书,在图书馆中的位置一定相同。

而且 hash 算法是一定会有 hash 碰撞的,因为hash 算法就是将任意长度的输入,转换成相同长度的输出。我们可以看到输出长度是固定的,但是输入长度却是无限的,输入一定远远大于输出,所以一定会存在 hash 碰撞。

就好比图书馆中的图书名字有很多,书名的长短也不一样,但是查找图书时,计算机输出的图书位置都是固定第几层、第几个书柜、书柜第几层的那个隔间。

hash 碰撞概率,也直接决定了哈希表的性能。就像我们查找图书,如果很多不同的书名,查找出来的位置都是一样的,我们还是要去那个位置一本一本的查找,效率又会大大降低。

既然 hash 碰撞不能避免,我们应该做的就是尽量降低 hash 碰撞的概率。所以我们需要一个好的 hash 算法来降低 hash 碰撞的概率,那么什么又是好的 hash 算法呢?

首先好的 hash 算法,肯定是需要生成的 hash 值是尽可能随机并且均匀分布,以此来最大程度的降低 hash 碰撞。其次运算不能太过于复杂,不然会消耗大量时间。

有了好的 hash 算法,也只能降低 hash 碰撞的概率,那么出现了 hash 碰撞我们应该怎么办呢?

我们解决 hash 冲突常用的有两种方法,开放寻址法(open addressing)和链表法(chaining)。

开发寻址法核心思想就是出现了 hash 冲突,重新找一个位置,将数据插入。就像存放图书时,发现这个对应的柜子隔间已经存放了图书,就把需要存放的图书,放到下一个空闲的隔间。

链表法就是把 hash 冲突的数据,新增加一条链表把他们串联起来。比如存放图书时,这个隔间已经存放图书了,我们新增加一些连着的隔间,用这些新增加连着的隔间存放 hash 碰撞的图书。

了解完哈希表的基本概念,我们看一下哈希表在 Java 中的运用。Java 中最常用到哈希表的就是 HashMap ,其中 HashSet、LinkedHashMap、LinkedHashSet 这些也是基于 HashMap 实现的,所以了解完 HashMap ,其他几个也就了解了。

我们主要学习 HashMap 的 hash 算法、HashMap 遍历、HashMap 的扩容。(都是基于 JDK 1.8)

下面是 Java 中 hash 算法的实现,因为 HashMap 是支持存储 null 键值对的,所以 key 为 null 直接返回 0。

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

Java 中的hashCode 通过将该对象的内部地址转换成一个整数,如果这里 key 不为 null ,那么 key 的 hash 值为 key 的 hashCode和 key 的 hashCode 右移 16 位进行「异或」运算的结果。

之所以是和右移 16 位 hashCode 进行「异或」运算,是为了保留高 16 位的信息,减少 hash 碰撞的概率。

知道了 hash 值我们通过 tab[i = (n - 1) & hash] 这句代码求出元素在 HashMap 中的位置,这句代码的意思是数组长度减 1 和 key 的 hash 值进行「与」运算。

这个相当于是对数组进行取模,从而确认元素存放在数组中的位置,这样做的前提是数组长度必须是 2 的次方,这里也解释了为什么 HashMap 的长度必须是 2 的幂

学习完 HashMap 的 hash 算法,我们看一下 HashMap 如何按照数据的添加顺序进行遍历。HashMap 对于 Key、Value、Entry 不用按照添加顺序的遍历可以参考之前的文章Java容器框架学习整理。

哈希表存放数据是经过 hash 算法打乱之后无规律存放的,那么我们要如何实现按照数据的插入顺序来遍历打印的呢?这里我们就需要用到 LinkedHashMap 。

LinkedHashMap 是在 HashMap 的基础上增加了一条双链表,维护了元素插入的顺序,下面我们看一段 LinkedHashMap 的示例。

HashMap<Integer,Integer> map = new LinkedHashMap();
map.put(6, 6);
map.put(0, 0);
map.put(9, 9);
​
Iterator<Map.Entry<Integer, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    System.out.print(iterator.next() + ",");
 }

上面的代码会按照我们添加数据的顺序输出 6=6,0=0,9=9 。

我们前面说了,哈希表为了实现随机访问,底层采用数组实现。数组就往往会遇到动态扩容的问题,那么我们看看 HashMap 是如何进行扩容的、在什么情况下需要进行扩容。

HashMap 是否扩容是根据「临界值(threshold)」来决定的,threshold = capacity * load factor 。公式中的 capacity 表示 HashMap 的容量,load factor 是加载因子,HashMap 默认 load factor 是 0.75f ,也可以自己初始化 HashMap 时指定 load factor。

当 HashMap 中的元素数量大于 threshold 时,HashMap 就会通过 resize() 进行扩容。

一般 resize() 扩容是原来的两倍,然后再将旧的哈希表中的元素存放到新的哈希表中。那么如何确认旧哈希表中元素在新哈希表中的位置呢?最简单的操作就对每个元素重新进行 hash 来确认在新哈希表中的位置,但是这样比较耗时,我们看一下 JDK 1.8 中是怎么确认旧哈希表中元素在新哈希表中的位置。

HashMap 进行扩容时也是按照 2 的倍数来进行扩容的,因此旧哈希表中的元素在新哈希表中位置要么是在原位置,要么是在原位置再移动2次幂的位置。

我们通过下图来解释这句话,图中 n 为哈希表的容量,图(a)表示扩容前的 key1 和 key2 两个 key 确定索引位置的示例,图(b)表示扩容后 key1 和 key2 两个 key 确定索引位置的示例,其中hash1 是 key1 对应的哈希与高位运算结果。

caa0f222e94ace18c2dc57d6c5bfac82.png

元素在重新计算 hash 之后,因为 n 变为 2 倍,那么 n-1 的标志位范围在高位多 1bit (红色),因此新的位置就会发生这样的变化:

684403b4cfac03233fda7611c2e1b360.png

这样就不需要重新计算旧哈希表中的每个元素的 hash 值,只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成「原索引+oldCap」。

不得不说 JDK 的这个设计确实非常的巧妙,省去了重新计算 hash 值的时间,同时由于新增的 1 bit 是 0 还是 1 可以认为是随机的,因此 resize 的过程,均匀的把之前的冲突的元素放入了新哈希表中对应的位置。

参考

美团技术团队:Java 8系列之重新认识HashMap​zhuanlan.zhihu.com
6297066a7b7b32634788f9af9f63f509.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值