目录
1. 哈希表简述
散列表的英文名称为 hash table
,因此散列表又被称为哈希表,散列函数又被称为哈希函数
散列表通常基于数组来实现。借助散列函数对数组进行扩展,利用的是数组支持按照下标随机访问元素的特性
- 存放数据的时候,散列函数根据
f(key)
计算出数据应该存储的位置即数组下标,从而将不同的数据分散在不同的存储位置,这也是散列的由来 - 查找的时候,通过散列函数
f(key)
直接确定查找hash
值所在位置即数组下标。而不需要一个个比较,直接找到数据,提升效率 - 散列表存放元素的数组位置也被称为槽
slot
1.1. 哈希表与链表,树等查找数据的不同
最简单的顺序表结构查找包括简单的顺序查找、二分查找、插值查找、以及后来的树结构查找包括二叉排序树、平衡二叉树、多路查找树、红黑树等。它们有一个功能特点就是,要查找的元素始终要与已经存在的元素进行多次比较,才能查找该元素
散列表与线性表、树、图等结构不同的是,后几种结构数据元素之间都存在某种逻辑关系,而使用散列技术的散列表的数据元素之间不存在什么逻辑关系,元素的位置只与关键字 key
和散列函数 f(key)
有关联
1.2. 几种典型数据结构特点
- 数组:随机访问效率较高,插入和删除效率较低
- 链表:随机访问效率较低,插入和删除效率较高
- 散列表:结合了两者的优势,寻址效率高,插入删除效率高。但数据经过
hash
散列后是无序的 - 散列表
+
链表结构:结合了散列表和链表的优势,查找、插入和删除的效率较高,同时也支持顺序访问
2. 哈希函数
2.1. 哈希函数的要求
哈希函数设计的好坏决定了散列冲突的概率,也就决定哈希表的性能。哈希函数设计的基本要求
- 哈希函数计算得到的散列值是一个非负整数。因为数组下标是从
0
开始的 - 计算简单:如果散列算法需要很复杂的计算,会耗费很多时间,这对于需要频繁地查找来说,就会大大降低查找的效率了。因此散列函数的计算时间不应该超过其他查找技术与关键字比较的时间
- 散列地址分布均匀:虽然不能完全
hash
避免冲突,但是可能设计好的散列函数尽量让散列地址均匀地分布在存储空间中,这样可以保证存储空间的有效利用,并减少冲突的发生和为处理冲突而耗费的时间
2.2. 哈希函数的构造种类
2.2.1. 直接定址法
取关键字或关键字的某个线性函数值为散列地址(这种散列函数也叫自身函数)。f(key) = a×key+b
(a、b
为常数)
这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合査找表较小且连续的情况。由于这样的限制,在现实应用中虽然简单,但却并不常用
2.2.2. 余数法
通过选择适当的正整数 p
,按计算公式 hash(K) = K % p
来计算关键字 K
的散列地址。这种方法计算最简单,也不需根据全部关键码的分布情况研究如何从中析取数据,最常用
2.2.3. 平方取中法
将关键字 K
平方,取 K^2
中间几位作为其散列地址 hash(K)
的值
如有以下关键字序列 {421,423,436}
,平方之后的结果为 {177241,178929,190096}
,那么可以取 {72,89,00}
作为 Hash
地址
2.2.4. 随机数法
采用随机函数作为散列函数 hash(Key) = random(Key)
,其中 random
为随机函数。当关键码长度不等时,采用该方法较恰当
3. 哈希冲突的解决
3.1. 拉链法
在存储数据的过程中,如果发生冲突,可以利用单链表在已有数据的后面插入新数据,访问的数组下标元素作为链表的头部。这种解决冲突的方法被称为分离链接法,又被称为分离链接法、拉链法。除了链表之外,其他辅助结构都能解决冲突现象,二叉树或者另一张散列表;如果采用链表来解决哈希冲突,并且哈希函数设计的很好,那么链表应该是比较短的
jdk 1.8
之前的 HashMap
就是使用的单链表来处理 hash
冲突,为了降低链表过长造成的遍历性能损耗,在 jdk 1.8
中采用链表 +
红黑树的方法来处理散列冲突,当链表长度 > 8
时且数组长度 > 64
时,转换为红黑树,红黑树的查找效率明显高于单链表的。而 >=8
时,采用链表则完全可以接受,避免红黑树的复杂结构
3.2. 双散列法
准备两个散列函数。双散列一般公式为:F(i)= i * hash2(x)
,意思是用第二个散列函数算出 x
的散列值,然后在距离 hash2(x),2hash2(x)
的地方探测