目录
1.概念:
在我们之前学到的顺序存储结构如链表,或者平衡二叉树,它们搜索的时候需要经过多次比较,顺序结构搜索的时间复杂度为O(N),而平衡二叉树搜索的时间复杂度为。
而我们理想的搜索方式是不做任何比较,一次直接从表中得到想要的元素,如果构造一种存储结构,通过某种函 数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快 找到该元素。
我们在向该结构插入一个元素的时候,可以通过某种函数计算出存储位置并在该位置存放该元素,在向该结构搜索元素时,可以可以通过某种函数计算出它存储的位置并进行访问。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。
例如:
在一个容量为10的数组中,依次插入1、4、5、6、7、9,那如果按照传统的从左往右给它们进行存放,效果会是这样的:
那么此时我们如果要找“7”这个元素,我们就要从左往右依次遍历,直到在下标为4的时候才找到元素“7”.
如果我们使用一个哈希函数来对它们要存放的位置进行计算,我们在下次查找该元素时也可以通过哈希函数直接找到它的位置,假设哈希函数为:Hash = Key % Capacity(Capacity = 10)那么效果会是这样的:
那么我们如果要找“7”这个元素的时候,就可以直接通过哈希函数来计算了:7 % 10 = 7 -> 那么元素“7”所在的位置就在7下标的位置。
2.哈希冲突
试想一下,如果我们在该数组中插入“44”元素,那么44 % 10 = 4,显然“44”元素应该需要插入在下标为4的位置上,但是下标为4的位置已经被元素“4”占了,那么这种Hash(key ) == Hash(key )的情况就是哈希冲突。
由于我们哈希表的容量往往小于所要存储的元素关键字的数量的,那么这时候哈希冲突就无法避免了,我们要做的就是降低哈希冲突的概率。
3.哈希避免:
3.1哈希函数:
哈希函数的设计原则:
1.哈希函数的定义域必须包括需要存储的所有关键码。
2.哈希函数计算出来地址能够均匀的分配在整个区域中。
3.哈希函数应该尽可能简单。
常用的哈希函数:
1. 直接定制法--(常用) 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况 面试题:Loading Question... - 力扣(LeetCode)
2.除留余数法--(常用) 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数: Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
3.平方取中法--(了解) 假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对 它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分 布,而位数又不是很大的情况。
4. 折叠法--(了解) 比特就业课 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和, 并按散列表表长,取后几位作为散列地址。 折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。
5. 随机数法--(了解) 选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数 函数。 通常应用于关键字长度不等时采用此法。
3.2负载因子:
负载因子: = 填入表中的个数 / 散列表的长度。
3.3冲突解决:
闭散列法:
闭散列法:也叫开放定址法,如果哈希表还没有满,那么可以把Key存放到冲突位置的下一个空位置去。
1.线性探测:
我们要在这个数组中插入元素“44”,发现元素“44”所要存放的位置与元素“4”冲突了,那么我们就继续往下找位置,到下表为5的位置发现与元素“5”冲突了,再继续往下找,直到找到下表为8的空位置,那么就可以存放元素“44”了。
2.二次探测:
同样我们要在这个数组中插入元素“44”,发现元素“44”所要存放的位置与元素“4”冲突了,那么我们使用二次探测去寻找下一个空位置: (i = 1,2,3...),可得下一个空位置位(4 + 1) % 10 = 5,那么到下表为5的位置发现与元素“5”冲突了,再继续进行计算:(4 + 2 *2) % 10 = 8,找到下表为8的空位置,那么就可以存放元素“44”了。
开散列/哈希桶
开散列法又称链地址法(开链法),首先用关键码用哈希函数计算其散列地址,将具有相同散列地址的元素存放在一个子集合中,每一个子集合称为一个桶,桶里的元素用单链表连接起来,然后再将每个桶的头节点存储在哈希表中。
从上图可以看出,开散列法就是将哈希冲突的元素存放在一个桶里。
模拟实现:
package HashBucket;
import java.util.HashSet;
import java.util.Set;
public class HashBucket<K, V> {
private class Node<K, V> {
private K key;
private V value;
Node next;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
}
private Node<K, V>[] array = new Node[8];
private int size; // 当前的数据个数
private static final double LOAD_FACTOR = 0.75;
private static final int DEFAULT_SIZE = 8;//默认桶的大小
public void put(K key, V value) {
int hashNum = key.hashCode();
int index = hashNum % array.length;
Node cur = array[index];
while(cur != null)
{
if(cur.key.equals(key)){
cur.value = value;
break;
}
cur = cur.next;
}
Node newNode = new Node(key, value);
newNode.next = array[index];
array[index] = newNode;
this.size++;
if(loadFactor() > LOAD_FACTOR)
{
resize();
}
}
private void resize() {
Node[] newArray = new Node[array.length * 2];
for(int i = 0; i < array.length; i++)
{
Node cur = array[i];
while(cur != null)
{
int newhash = cur.key.hashCode();
int index = newhash % newArray.length;
Node curNext = cur.next;
cur.next = newArray[index];
newArray[index] = cur;
cur = curNext;
}
}
array = newArray;
}
private double loadFactor() {
return size * 1.0 / array.length;
}
public HashBucket() {//无参构造函数
this.array = new Node[8];
this.size = 0;
}
public V get(K key) {
int hashNum = key.hashCode();
int index = hashNum % array.length;
Node cur = array[index];
while(cur != null){
if(cur.key.equals(key))
{
return (V) cur.value;
}
cur = cur.next;
}
return null;
}
}
在冲突过于严重时有两种解决方法:
1.在冲突严重的桶里存放另一个哈希表。
2.在冲突严重的桶里存放一颗搜索树。
HashMap的补充:
1.为什么HashMap内部的bucket数组的长度一直都是2的整数次幂?
原因1:可以使用key.hash & (table.length - 1) 位运算的方法来快速寻址,原理是这样的:
以table.length = 16 ( )为例,将它用二进制表示为10000,假设key.hash = 7,如果我们用Hash(key) = key% p来寻址,结果为7 % 16 = 7。那么当用key.hash & (table.length - 1)的方法来寻址结果也为7,如下图:
用代码表示:
int length = 16;//2^4
String str = "helloworld";
int key = str.hashCode();
int a = key % length;
int b = key & (length - 1);
System.out.println(a == b);//true
原因2:在HashMap扩容的时候,可以保证同一个桶中的元素均匀散列到新的桶中,确切地讲就是在同一个桶中的元素在扩容后,一半会留在原来的桶中,一半会放在新的桶中。
2.HashMap默认的数组是多大?
HashMap默认的数组容量是16,就算在构造HashMap的时候传入了不是2的整数次幂的数,那么HashMap也会找到一个最接近2的整数次幂的数来初始化数组桶。
3.HashMap什么时候开辟bucket数组来占用内存?
在第一次调用put的时候调用resize方法。
4.HashMap什么时候扩容?
当HashMap
中的元素熟练超过阈值时,阈值计算方式是capacity * loadFactor
,在HashMap
中loadFactor
是0.75。
5.桶中的元素列表何时转换为红黑树,何时转换会链表,为什么要这样设计?
在同一个桶的元素数量大于等于8的时候转换为红黑树,在同一个桶的元素小于等于6的时候转换回链表,原因是避免红黑树和链表的频繁转换,减少性能损耗。
6.JDK8中为什么要引入红黑树,是为了解决什么场景的问题?
引入红黑树是为了避免hash
性能急剧下降,引起HashMap
的读写性能急剧下降的场景,正常情况下,一般是不会用到红黑树的,在一些极端场景下,假如客户端实现了一个性能拙劣的hashCode
方法,可以保证HashMap
的读写复杂度不会低于O(lgN)
7.HashMap
如何处理key
为null
的键值对?
放置在桶数组中下标为0的桶中。
HashMap和HashTable 的异同?
1.二者的存储结构和解决冲突的方法都是相同的。
2.HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
3.HashTable 中 key和 value都不允许为 null,而HashMap中key和value都允许为 null(key只能有一个为null,而value则可以有多个为 null)。但是如果在 Hashtable中有类似 put( null, null)的操作,编译同样可以通过,因为 key和 value都是Object类型,但运行时会抛出 NullPointerException异常。
4.Hashtable扩容时,将容量变为原来的2倍+1,而HashMap扩容时,将容量变为原来的2倍。
5.Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在计算hash值对应的位置索引时,用 %运算,而 HashMap在求位置索引时,则用 &运算。
摘自大佬的图:
附:HashMap put方法逻辑图(JDK1.8)
最后给一些习题:
138. 复制带随机指针的链表 - 力扣(LeetCode)
本篇内容部分摘自: