目录
1.哈希表的概念
2.哈希冲突
3.如何避免哈希冲突?
3.1哈希函数设计
3.2 负载因子的调节
4.解决哈希冲突
4.1闭散列
4.1.1线性探测
4.1.2二次探测
4.2开散列(哈希桶)
5.HashMap
6.HashSet
1.哈希表的概念
假设有一组数据,要让你去搜索其中的一个关键码,这种场景是很常见的,不同的数据结构的搜索的效率是不同的.顺序结构及平衡树中,元素关键码与其存储的位置之间没有对应关系,因此在查找一个元素时,必须要经过关键码的多次比较.
顺序查找时间复杂度为O(N),平衡树查找时间复杂度是O(log2 N),搜索的效率取决于搜索过程中比较的次数!
哈希表这个数据结构可以做到不经过任何比较,一次直接从表中得到要搜索的元素.
哈希表通过构造哈希函数使元素与元素存储位置之间能够建立一一映射的关系,那么查找的时间复杂度就是O(1).
向该结构插入元素时,根据待插入的元素的关键码,以哈希函数计算出元素存储的位置进行存放
搜索元素时同样方式,把求得的函数值作为元素的存储位置,在结构中按此位置取元素比较,相等则搜索成功
这种方式就是哈希(散列)方法,使用的函数称为哈希函数(散列函数),构造出来的结构称为哈希表结构(散列表)
2.哈希冲突
了解哈希冲突之前先看一个哈希表存储的例子 :
数据集合{1,5,9,8,6};哈希函数:hash(key) = key % capacity;capacity为存储元素底层空间的总大小
经过哈希函数的计算,1%10 = 1,5%10 = 5,9%10 = 9,8%10 = 8,6%10 = 6.
这样就将数据集合存进了哈希表,在搜索的时候不必进行多次比较,搜索效率大大提高
但是效率高的同时,也会出现其他问题,如果在上述哈希表中插入55,会出现什么问题?
我们发现哈希值是相同的,但是5下标已经存储了5,55没位置存了,即不同关键字通过相同的哈希函数计算出来相同的哈希地址,这种现象被称为哈希冲突.把具有相同哈希地址不同关键码的数据元素称为"同义词"
3.如何避免哈希冲突?
由于哈希表底层数组的容量往往下小于实际要存的关键字的数量,导致了哈希冲突的发生是必然的,我们能做的就是尽量降低哈希冲突率
3.1哈希函数设计
引起哈希冲突可能是因为哈希函数设计不够合理
哈希函数设计的原则:
函数定义域必须包括需要存储的全部关键码,散列表允许有m个地址时,值域必须在0-m-1之间
哈希函数计算出来的哈希地址能均匀分布在整个空间中
哈希函数应该比较简单
常见的哈希函数:
1.直接定制法
取关键字的某个线性函数为哈希地址:hash(key) = A*key+B
优点:简单均匀
缺点:需要先知道关键字分布情况
使用场景:适合查找较小且连续的情况
2.除留余数法
设散列表中允许的地址为m,取一个质数p(p<=m),按照hash(key) = key%p,将关键码转换成哈希地址
哈希函数设计的越精妙,越能降低哈希冲突率,但不能避免哈希冲突
3.2 负载因子的调节
散列表负载因子定义: = 表中元素个数/散列表的长度
与表中元素个数成正比, 越大,元素个数越多,反之, 越小,表中元素个数越少,哈希冲突概率越低
Java的系统库限制了负载因子为0.75,超过这个值将resize散列表
负载因子和冲突率的关系演示
因此当冲突率达到一定程度时,我们要通过降低负载因子来降低冲突率
哈希表的关键字个数是不变的,因此只能控制散列表的长度来降低冲突率
4.解决哈希冲突
在上述优化都不能有效降低哈希冲突率时,我们就要引入其他办法来解决哈希冲突
4.1闭散列
闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表没有被装满,说明在哈希表中还有其他位置,那么可以将key放到冲突位置的"下一个"位置,寻找下一个位置有两种探测方法
4.1.1线性探测
在上面的场景中,要插入55元素,通过计算哈希地址为5,但是5位置已经被5填入了,即发生哈希冲突
线性探测法:从发生哈希冲突的位置一次向后寻找,找到一个空位置填入待插入元素
缺点
采用闭散列方法解决哈希冲突时要注意不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索,如果突然删除5,那么搜索55时,就找不到了,因此线性探测法要删除一个元素采用标记的伪删除法来删除一个元素
线性探测还会把冲突的元素都放在一起,如果要放入65,75,95,那么这些元素会被堆积在相邻有空的位置
4.1.2二次探测
为了解决线性探测将冲突数据堆积在一起的缺点,二次探测提供了寻找空位置的方法
或者
其中i = 1,2,3......i表示某个位置第几次重复哈希冲突
H0是通过散列函数Hash(x)对关键码key进行计算得到的位置
m表示表的大小
如果放入55,65,75
有效的解决了线性探测将冲突元素堆积的情况
当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能插入,而且任何一个位置都不会被探查两次.因此只要表中有一半的空位置,就不会存在表满的问题,在搜索时可以不考虑装满的情况,但在插入时必须确保表的负载因子<=0.5,如果超出,先扩容
删除时也是采用标记的方法删除
这也造成了闭散列的一个缺点:空间里利用率低
4.2开散列(哈希桶)
这是Java中的HashMap用到的解决哈希冲突的方式
开散列法又叫开链法(链地址法),首先对关键码的集合用散列函数计算散列地址,具有相同的地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过单链表链接起来,链表头节点存在哈希表中!
对于上述的插入55,65,75
上图中开散列中每个桶放的都是发生哈希冲突的元素
开散列可以认为是把一个在大集合中的搜索问题,转化成在小集合中做搜索了
Java的HashMap中就是用这种数组加链表的方式解决哈希冲突,当数组长度超过64并且链表长度超过8的时候,就会变成红黑树,链表如果一直变长,那么搜索效率还是降低,链表的搜索效率是O(N),转化成红黑树存储,效率会提升!!
性能分析: 虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的, 也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是 O(1)
5.HashMap
我们使用HashMap实例化Map
public class Test {public static void main(String[] args) {HashMap<String,Integer> map = new HashMap<>();map.put("zhangsan",12);map.put("lisi",13);System.out.println(map);}
}
结果顺序不是我们put的顺序,是因为在put时,计算出的哈希值是不一样的,存放的位置就不同于put的顺序,原因不是比较造成的,是哈希出的地址不同
传入不可比较对象时,也可以添加
class Student{}
public class Test {public static void main(String[] args) {HashMap<Student,Integer> map = new HashMap<>();map.put(new Student(),12);map.put(new Student(),13);System.out.println(map);}
}
添加元素时没有对象的比较,因此可以添加null值为key
public class Test {public static void main(String[] args) {HashMap<Student,Integer> map = new HashMap<>();map.put(null,null);System.out.println(map);}
}
HashMap中不会存在相同的key,key相同则更新value
public class Test {public static void main(String[] args) {HashMap<String,Integer> map = new HashMap<>();map.put("hello",12);map.put("hello",22);System.out.println(map);}
}
HashMap的方法和TreeMap介绍的方法是相同的
使用HashMap统计一组数据中,每个数据出现的次数
public static void main(String[] args) {HashMap<Integer,Integer> map = new HashMap<>();int[] arr = {1,3,4,2,1,3,4};for (int i = 0; i < arr.length; i++) {int key = arr[i];if(map.get(key)==null){map.put(arr[i],1);}else{int val = map.get(key);//key出现的次数map.put(key,val+1);}}System.out.println("");for (Map.Entry<Integer,Integer> entry:map.entrySet()) {System.out.println("key: "+entry.getKey()+"出现次数: "+entry.getValue());}}
6.HashSet
使用HashSet实例化Set
public class Test {public static void main(String[] args) {HashSet<Integer> set = new HashSet<>();set.add(1);set.add(1);System.out.println("size: "+set.size());}
}
HashSet也不能重复添加元素,是因为底层也是一个HashMap,来保证key是唯一的
value是一个默认值
HashSet有去重的功能,使用HashSet 统计一组数据中,第一个重复的数
public static void main(String[] args) {HashSet<Integer> set = new HashSet<>();int[] arr = {5,3,4,2,1,3,4};for (int i = 0; i < arr.length; i++) {if(!set.contains(arr[i])){set.add(arr[i]);}else{System.out.println(arr[i]);break;}}}
在Map和Set一文中使用TreeSet对数据进行去重,使用HashSet也可以,并且效率更高