哈希表
哈希表基础
什么是哈希表
每一个字符都和一个索引相对应
O(1)的查找操作
哈希函数:“键”转换为“索引”
哈希冲突:不同的键经过哈希函数映射,对应了同一个索引
解决哈希冲突是关键问题
哈希表充分体现了算法设计领域的经典思想: 空间换时间
如果我们有无限的空间,可以用o(1)时间完成各项操作
如果我们只有1的空间,则只能用o(n)时间完成各项操作(线性表)
哈希表是时间和空间的平衡
哈希函数的设计是很重要的
哈希函数的设计
“键”通过哈希函数得到的“索引”越均匀越好
整型
小范围正整数直接使用
小范围负整数进行偏移 -100 ~ 100 ——> 0 ~ 200
大整数
比如身份证号4409…
通常做法:取模 ,比如,取后四位 等于mod 10000
取模一个简单通用的方法:模一个素数 背后数学原理超出课程范畴
浮点型
字符串
所占空间数量不固定
仍能转成整形处理
把一个字符串看出26进制的整型
B看成多少进制
上述哈希函数可以化简为:
根据哈希函数设计出函数
复合类型
转成整型处理
仍可以像字符串一样处理
总结
转成整型处理,并不是唯一方法
原则:
- 一致性:如果a == b,则hash(a) == hash(b),反过来不成立
- 高效性:计算高效简便
- 均匀性:哈希值均匀分布
java中的hashcode方法
java中基本类型不是对象
Integer i = 10;
System.out.println(i.hashCode());
double c = 3.1415926;
System.out.println(((Double)c).hashCode());
String s = "hhhasda";
System.out.println(s.hashCode());
整型输出结果为他本身
不能直接将整型转化为索引,因为不知道索引具体范围
double类型输出结果是一个整型
String类型输出结果是一个整型
自定义对象
/**
* @author laimouren
*/
public class Student {
private int grade;
private int cls;
private String firstName;
private String lastName;
public Student(int grade, int cls, String firstName, String lastName) {
this.grade = grade;
this.cls = cls;
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public int hashCode() {
int B = 31;
int hash = 0;
hash = hash * B + grade;
hash = hash * B + cls;
hash = hash * B + firstName.toLowerCase().hashCode();
hash = hash * B + lastName.toLowerCase().hashCode();
return hash;
}
@Override
public boolean equals(Object obj) {
if(this == obj){
return true;
}
if (obj == null){
return false;
}
if (getClass() != obj.getClass()){
return false;
}
Student another = (Student) obj;
return this.grade == another.grade && this.cls == another.cls
&& this.firstName.toLowerCase() == another.firstName.toLowerCase()
&& this.lastName.toLowerCase() == another.lastName.toLowerCase();
}
}
测试
Student student = new Student(1,1,"lai","lai");
System.out.println(student.hashCode());
结果
3451552
注意:如果一个类要作为哈希表的键,重写了hashcode()方法的同时,还得重写equals()方法,以解决哈希冲突
哈希冲突的处理 链地址法(Seperate Chaining)
哈希表本质就是一个数组
通过哈希函数计算出的结果如果是相同的,则放在链表后面
java8之前,每一个位置对应一个链表
java8开始,当哈希冲突达到一定程度,每一个位置从链表转化为红黑树(因为java中treemap的底层就是红黑树)
具体实现
import java.util.TreeMap;
public class HashTable<K, V> {
private TreeMap<K, V>[] hashtable;
private int size;
private int M;
public HashTable(int M){
this.M = M;
size = 0;
hashtable = new TreeMap[M];
for(int i = 0 ; i < M ; i ++)
hashtable[i] = new TreeMap<>();
}
public HashTable(){
this(97);
}
private int hash(K key){
return (key.hashCode() & 0x7fffffff) % M;
}
public int getSize(){
return size;
}
public void add(K key, V value){
TreeMap<K, V> map = hashtable[hash(key)];
if(map.containsKey(key))
map.put(key, value);
else{
map.put(key, value);
size ++;
}
}
public V remove(K key){
V ret = null;
TreeMap<K, V> map = hashtable[hash(key)];
if(map.containsKey(key)){
ret = map.remove(key);
size --;
}
return ret;
}
public void set(K key, V value){
TreeMap<K, V> map = hashtable[hash(key)];
if(!map.containsKey(key))
throw new IllegalArgumentException(key + " doesn't exist!");
map.put(key, value);
}
public boolean contains(K key){
return hashtable[hash(key)].containsKey(key);
}
public V get(K key){
return hashtable[hash(key)].get(key);
}
}
哈希表的动态空间处理
如何让时间复杂度变为O(1)?
固定地址空间是不合理的,需要resize()
- 平均每个地址承载的元素多过一定程度,即扩容 N/M >= upperTol
- 平均每个地址承载的元素少过一定程度,即缩容 N/M < lowerTol
具体实现如下:
import java.util.Map;
import java.util.TreeMap;
public class HashTable<K, V> {
private static final int upperTol = 10;
private static final int lowerTol = 2;
private static final int initCapacity = 7;
private TreeMap<K, V>[] hashtable;
private int size;
private int M;
public HashTable(int M){
this.M = M;
size = 0;
hashtable = new TreeMap[M];
for(int i = 0 ; i < M ; i ++)
hashtable[i] = new TreeMap<>();
}
public HashTable(){
this(initCapacity);
}
private int hash(K key){
return (key.hashCode() & 0x7fffffff) % M;
}
public int getSize(){
return size;
}
public void add(K key, V value){
TreeMap<K, V> map = hashtable[hash(key)];
if(map.containsKey(key))
map.put(key, value);
else{
map.put(key, value);
size ++;
if(size >= upperTol * M)
resize(2 * M);
}
}
public V remove(K key){
V ret = null;
TreeMap<K, V> map = hashtable[hash(key)];
if(map.containsKey(key)){
ret = map.remove(key);
size --;
if(size < lowerTol * M && M / 2 >= initCapacity)
resize(M / 2);
}
return ret;
}
public void set(K key, V value){
TreeMap<K, V> map = hashtable[hash(key)];
if(!map.containsKey(key))
throw new IllegalArgumentException(key + " doesn't exist!");
map.put(key, value);
}
public boolean contains(K key){
return hashtable[hash(key)].containsKey(key);
}
public V get(K key){
return hashtable[hash(key)].get(key);
}
private void resize(int newM){
TreeMap<K, V>[] newHashTable = new TreeMap[newM];
for(int i = 0 ; i < newM ; i ++)
newHashTable[i] = new TreeMap<>();
int oldM = M;
this.M = newM;
for(int i = 0 ; i < oldM ; i ++){
TreeMap<K, V> map = hashtable[i];
for(K key: map.keySet())
newHashTable[hash(key)].put(key, map.get(key));
}
this.hashtable = newHashTable;
}
}
哈希表的复杂度分析
回忆动态数组的均摊复杂度分析:平均复杂度为O(1)
同理,对于哈希表来说,元素从N增加到upperTol * N;地址空间翻倍
从而平均复杂度为O(1)
每个操作在O(lowerTol) ~ O(upperTol) ——>O(1)
缩容同理
更复杂的动态空间处理方法
扩容 M -> 2 M
扩容结果2M不是素数
解决方案:
根据下表来设置扩容
缺点:
哈希表牺牲了顺序性
更多哈希冲突的解决方法
开放地址法
原先实现的是封闭地址,只能对应的key访问
![在这里插入图片描述](https://img-blog.csdnimg.cn/b7bcdb3dfc9e4be78cf3ef5796e012e6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA6LWW5oC75Yay5Yay5Yay,size_20,color_FFFFFF,t_70,g_se,x_16)
开放地址法中,当负载率达到一定程度就需要扩容
只要负载率选的合适,时间复杂度就是O(1)
再哈希法 Rehashing
Coalesced Hashing
综合了Seperate Chaining 和Open Addressing