一、hash表时如何提升查找效率
利用的前提:数组中根据下标访问元素的时间复杂度是O(1)
查找过程:
- 在一个Key-Set中查找指定Key的过程。
- 因为Key-Set中的key太多了,所以查找会变慢。
- 如果Key-Set中的Key较少,无论什么查找算法都比较快。
Hash表的大体思路:把一个Key-Set的查找过程转换为很多个小的Key-Set的查找过程。
在10000个Key中查找key
put(key, value)的一次过程
-
如何通过key得到存放的下标?
- 因为key是一个任意的数据类型,要得到的下标是一个int类型,所以需要经过哈希函数,把key转换成一个int类型的数字
- 要求:相同的key,得到的int类型数字一定是相同的
- int类型的数字不一定是合法的下标,还需要将其转换为一个合法的下标
- 因为key是一个任意的数据类型,要得到的下标是一个int类型,所以需要经过哈希函数,把key转换成一个int类型的数字
-
通过下标,找到小集合(冲突问题)
-
在小集合中,选择合适的算法,找到对应的key以及关联的value
冲突(Collision)
-
什么是冲突?
不同的key,经过hash函数后,得到了相同的hash值 -
为什么会产生冲突?
因为Key-Set中的key的数量是远远大于数组长度的。 -
冲突可以完全消除吗?
不可以。把M个数,放到N个下标中(M远远大于N)就一定会产生冲突。 -
对于冲突我们的原则:尽可能的减少冲突,如果真的遇到冲突了,也可以解决。
-
如何尽可能的减少冲突?
-
如果一定有冲突,冲突呈现一个较好的形态。
冲突就是上面的小集合,小集合的key数量越平均越好。
所以,需要设计比较好的哈希函数,使得到的下标尽可能的均匀。 -
插入一个新的key会有概率冲突
-
负载因子 = 所以key的数量 / 数组的长度
冲突率会随着负载因子变大而变大。 -
我们的目标是把冲突控制在一个可接受的范围里,我们可以通过把负载因子控制在一个阈值范围内来达到目的
-
负载因子 = key的数量 / 数组的长度
-
key的数量是不能改变的,通过增加数组的长度来减小负载因子。
- 为了控制冲突率,会设置一个阈值,当负载因子超过这个阈值时,需要增加数组长度,也就是所谓的扩容。
Java中默认阈值是 0.75(LoadFactor)—— 扩容因子 - 为什么Hash表中的扩容时机和顺序表是完全不同的?
顺序表的扩容是为了解决放不下的问题 —— 只要放得下,就不需要扩容
哈希表的扩容是为了降低冲突的问题 —— 放得下,也会扩容
- 为了控制冲突率,会设置一个阈值,当负载因子超过这个阈值时,需要增加数组长度,也就是所谓的扩容。
-
-
如何解决冲突?
- 数组内部解决(闭散列)
- 另起炉灶,把所有的冲突key放到另外的结合
- 可以使用链表——因为我们认为冲突的key不会太多
- 冲突的数量变多了,链表变成平衡搜索树
二、和Java语言强相关
1.hashMap.put(key, value)的过程
- 通过key得到一个下标index O(1)的时间复杂度
- Hash Map的key其实是一个泛型,本质就是一个Object的子类
- 通过key得到哈希值(hash)
**hashCode()**属于Object,用来求出key所对应的哈希值
int hash = key.hashCode(); - hash 不保证是在[0, arr.length) ,不一定是合法下标
再把hash转换为一个合法的下标
1.通过 hash % array.length 得到合法下标
Java中没有这种方式,因为mod操作相对比较慢
2.Java选择了另一种方式,需要一个前提,array.length一定是2的n次方;初始长度为16
int index = (array.length - 1) & hash
array.length = 16 二进制表示 0b10000
array.length - 1 = 15 二进制表示 0b01111
hash & (array.length - 1)
无论原来的hash是多大的一个数,只能保留4bit使得结果一定不超过16,最多到15,[0, 16)一定是一个合法下标。
但是这样,导致hash中真正被用的只有后4bit,因为没有用到所有bit,所以可能导致下标不均匀。
Java多做了一个事情:
hash = (hash >>> 16) ^ hash;
index = (array.length - 1) & hash;
使得下标尽可能均匀。
- 通过key得到哈希值(hash)
- Hash Map的key其实是一个泛型,本质就是一个Object的子类
自定义类(Person)作为HashMap的key,做到认为相同的Person对象,必须返回相同的哈希值。
import java.util.Objects;
public class Person {
private String name;
private int age;
private int gender;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
return age == person.age &&
gender == person.gender &&
name.equals(person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age, gender);
}
public static void main(String[] args) {
MyHashMap<Person, Integer> map = new MyHashMap<>();
Person p1 = new Person();
p1.name = "你好";
p1.age = 18;
p1.gender = 1;
Person p2 = new Person();
p2.name = "你好";
p2.age = 18;
p2.gender = 1;
map.put(p1, 108);
//因为覆写了hashCode 和 equals,代码才能正确运行
//返回108,否则返回 null
System.out.println(map.get(p2));
}
}
2.自己实现HashMap
public interface MyMap<K, V> {
V get(K key);
V put(K key, V value);
}
public class MyHashMap<K, V> implements MyMap<K, V> {
private static class MyEntry<K, V> {
K key;
V value;
MyEntry<K, V> next;
public MyEntry(K key, V value) {
this.key = key;
this.value = value;
}
}
private MyEntry<K, V>[] table = new MyEntry[16];
private int size = 0;
private static final double LOAD_FACTOR_THRESHOLD = 0.75;
@Override
public V get(K key) {
int hash = key.hashCode();
hash = (hash >>> 16) ^ hash;
int index = hash & (table.length - 1);
MyEntry<K, V> head = table[index];
MyEntry<K, V> cur = head;
while (cur != null) {
if (key.equals(cur.key)) {
return cur.value;
}
cur = cur.next;
}
return null;
}
@Override
public V put(K key, V value) {
int hash = key.hashCode();
hash = (hash >>> 16) ^ hash;
int index = hash & (table.length - 1);
MyEntry<K, V> head = table[index];
//在链表中查找
MyEntry<K, V> cur = head;
while (cur != null) {
if (key.equals(cur.key)) {
V oldValue = cur.value;
cur.value = value;
return oldValue;
}
cur = cur.next;
}
//没有找到节点
MyEntry<K, V> newNode = new MyEntry<>(key, value);
/**
* 头插
* newNode.next = head;
* table[index] = newNode;
*/
//尾插
if (head == null) {
table[index] = newNode;
} else {
MyEntry<K, V> last = head;
while (last.next != null) {
last = last.next;
}
last.next = newNode;
}
size++;
// 通过调整负载因子,来控制冲突率
if ((double)size / table.length >= LOAD_FACTOR_THRESHOLD) {
//扩容
resize();
}
return null;
}
private void resize() {
MyEntry<K, V>[] newTable = new MyEntry[table.length * 2];
// 遍历所有的 key
// 首先遍历所有的下标位置,找到一条条的链表
// 再次遍历每个链表,找到一个个的 key
for (int i = 0; i < size; i++) {
MyEntry<K, V> node = table[i];
while (node != null) {
// 为了简化,重新创建新结点
MyEntry<K, V> newNode = new MyEntry<>(node.key, node.value);
int hash = newNode.key.hashCode();
hash = (hash >>> 16) ^ hash;
int index = hash & (newTable.length - 1);
//头插
newNode.next = newTable[index];
newTable[index] = newNode;
node = node.next;
}
}
}
}
2.因为Java内部是用拉链法解决冲突的,用下标,只找到对应的小集合即可(默认是链表)
利用了数组下标访问时间复杂度是O(1)的特性
3.在小集合中,查看对应的key所在节点
Node node = array[index];
while (node != null) {
if (key.equals(node.key)) {
表示找到
}
node = node.next;
}
HashMap的树化过程(Treeify)
为什么要树化?
理想情况下,根据概率论中的泊松分布计算,每个下标处,key的个数不会太长
但实际中,还是有可能出现某个index位置处,key过多的情况。
原因:key的分布不是符合理想分布。(理想情况下,key的数量巨大时,都是符合高斯分布(正态分布)) key不是正态分布
如果某个下标处,链表的长度特别长,违背了哈希表的思想——把大数据集的查找转化为小数据集的查找。
所谓的小数据集也很大,所以哈希表的查找变慢了。
怎么解决?
Java,现在小数据集查找很慢,再次使用查找用的数据结果(搜索树)上去。
树化的情况是比较少的。
当链表长度超过阈值(8)时,将链表转换为红黑树。