目录
一、哈希函数的引入
二、解决哈希冲突的思路
2.1基于闭散列的思路
2.2基于开散列的思路
2.3负载因子
三、关于哈希函数的设计
四、基于拉链法实现哈希表
4.1哈希表的内部构造
4.2插入操作
4.3扩容操作
4.4搜索操作
4.5删除操作
哈希表其实就是基于数组衍生而来的,哈希表的查找时间复杂度近乎O(1)。用空间换时间的思想。
高效查找的奥秘就在于数组的随机访问特性(在数组中只要知道元素的下标-索引,可以立即在数组中取得该元素)。
一、哈希函数的引入
当有一组数据,要查询某个特定的元素是否在数组内,就将原数组的元素映射为新的布尔数组的下标,要查询某个元素是否存在,只需在对应的布尔数组中查看该索引即可。
当数组的元素跨度较大10w,100w等就得根据元素最大值来开辟数组,会浪费大量空间,当数组的元素存在负数,是无法直接进行映射的。这时引入了哈希函数。
哈希函数:将任意的key(所有数据类型)映射为数组下标。
哈希冲突:原本两个不同的key值,经过哈希函数运算后得到两个相同的int值(从数学角度一定存在)。
在处理整形映射为整形时,最常用的手段就是进行取模运算,一组很大的数组映射为一组很小的数组。例如100w和10w经过取模10之后都得到零(通过哈希冲突)。
二、解决哈希冲突的思路
2.1基于闭散列的思路
当发生哈希冲突时,找冲突位置的附近是都存在空闲位置,如果有就放入冲突元素。(好放难查更难删)。
线性探测:从不发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置即可。
要在哈希表中查询某个元素是否存在:要查找x是否存在,首先对x%n = y。如果发现y未知的索引对应的值不是x,那么就继续向后遍历,若一直向后遍历走到下一个空闲位置还没找到待查找元素,说明该元素不存在。删除也需要先找到再删除。
2.2基于开散列的思路
如果出现哈希冲突,就在对应的位置上将该数组元素变为一个链表(拉链法)。哈希表的数组其实存储的是每个链表的头结点,整个哈希表就是数组+链表的结构。
当要查找某个元素x是否存在,将x取模拿到对应链表的头结点,只要遍历这个链表即可。
基于这种方案解决的哈希冲突就想整张表的冲突问题转为几个链表的冲突问题。
这种思路简单实用,基本是各种哈希表解决冲突的方案首选。
若某个链表冲突非常严重,该立案表长度很长,查找元素又会为链表的遍历时间复杂度又为O(n)。
=》1.针对整个哈希表进行扩容(对原数组扩容),冲突链表上的元素放到新数组上时会进行重新取模,降低冲突概率。(JDK中当整个哈希表元素个数<64采用这种方案)
=》2.将某些冲突的链表再次拆拆解成子哈希表或树化(二叉搜索平衡树),原哈希表中其他位置不影响,不改动,只处理这个冲突严重的链表。(JDK中当整个哈希表元素个数>=64,且某个链表长度>=8,会将此链表转为BRTree,不进行第一种)
2.3负载因子
负载因子是描述一个哈希表冲突的情况。LOAD_FACTOR = 哈希表中世纪存储的元素个数 / 数组长度。
负载因子越大说明当前哈希表中冲突概率越大,查找效率越低。比较节省空间。
负载因子越小说明当前哈希表中冲突概率越小,查找效率就越高,比较浪费数组空间。
触发扩容(树化)的机制为数组元素>=哈希表长度*负载因子。
负载因子如何确定需要根据现有的业务场景进行性大量的试验论证,在查找效率和占用空间上取平衡。JDK的HashMap的负载因子默认设置为0.75。
三、关于哈希函数的设计
一般用现成的方案即可(由离散数学家研究的话题)。
一般来说,如果是对整型做哈希运算,模一个素数冲突概率较低。
如果对字符串进行哈希运算,就可以利用md5算法(主要给字符换计算哈希值)。
md5算法的三大特点:(1)定长,无论输入多大的字符串,得到的md5值长度固定。(2)分散,原数据稍微变化一点点,得到的md5值差异很大。(3)不可逆,通过原数据得到md5值很容易,反之通过md5值倒退原数据内容非常难。若两个字符串的md5相同,在工程领域可以认为他们俩是相通的字符串。
四、基于拉链法实现哈希表
数组+链表的实现。
4.1哈希表的内部构造
首先需要创建一个Node类,然后创建key,value两个int变量,在创建一个next的Node变量。然后创建构造方法。
再在MyHashMap类中创建size变量用来存储存入的数据的数量,创建一个负载因子double 类型的LOAD_FACTOR为0.75。以及Node类型的数组hashTable和int类型的取模值M。然后创建两个构造方法,一个无参一个有参。在创建一个hash方法用来确定key值取模后的索引位置。
public class MyHashMap {
private static class Node{
int key;
int value;
Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private int size;
private static final double LOAD_FACTOR = 0.75;
private Node[] hashTable;
private int M;
public MyHashMap() {
this(10);
}
public MyHashMap(int capacity) {
this.hashTable = new Node[capacity];
this.M = capacity;
}
public int hash(int key){
return Math.abs(key) % M;
}
}
4.2插入操作
在插入操作中首先要存储index为key取模之后的值也就是相应的索引位置。然后遍历这个索引位置下的链表,如果在链表中存在相同的key值则直接将新得value值取代即可,然后返回原来的value值。
如果没有相同的key值,那么就直接头插在当前的链表上,然后size++。最后判断是否需要扩容,然后返回value值。
public int put (int key,int value){
int index = hash(key);
for(Node x = hashTable[index];x!=null;x=x.next){
if(x.key == key){
int oldValue = x.value;
x.value = value;
return oldValue;
}
}
Node newNode = new Node(key,value);
newNode.next = hashTable[index];
hashTable[index] = newNode;
size++;
if (size >= hashTable.length * LOAD_FACTOR) {
resize();
}
return value;
}
4.3扩容操作
首先需要创建一个新的Node数组,长度设置为原数组长度的二倍,然后将取模的数值更新为新的数组长度。然后遍历原先的数组,再遍历当前位置的链表,然后根据新的索引插入新的数组,然后将this.hashTable指向新的数组。
private void resize() {
Node[] newTable = new Node[hashTable.length<<1];
this.M = newTable.length;
for(int i = 0;i<hashTable.length;i++){
Node x = hashTable[i];
while(x!=null){
Node next = x.next;
int k = hash(x.key);
newTable[k] = x;
x = next;
}
}
this.hashTable = newTable;
}
public static void main(String[] args) {
MyHashMap hashMap = new MyHashMap(4);
hashMap.put(1,11);
hashMap.put(5,55);//断点1
hashMap.put(2,22);//断点2
hashMap.put(6,66);
hashMap.put(3,33);
hashMap.put(1,111);
}
首先断点1过去后size=2,插入了两个元素,并没有进行扩容操作,M仍然为4。
断点2过去之后发现M变为8,插入了三个元素,成功进行了扩容操作。
4.4搜索操作
分为三种,查找对应key值或者value值是否存在,查找key值返回value值。
两个根据key值查找直接找到对应索引然后遍历链表即可。
根据value值查找则需要遍历整个数组+链表。
public boolean containsKey(int key) {
int index = hash(key);
for (Node x = hashTable[index];x != null;x = x.next) {
if (key == x.key) {
return true;
}
}
return false;
}
public int get(int key) {
int index = hash(key);
for (Node x = hashTable[index];x != null;x = x.next) {
if (x.key == key) {
return x.value;
}
}
throw new NoSuchElementException("hashtable has not this key!");
}
public boolean containsValue(int value) {
for (int i = 0; i < hashTable.length; i++) {
for (Node x = hashTable[i];x != null;x = x.next) {
if (x.value == value) {
return true;
}
}
}
return false;
}
public static void main(String[] args) {
MyHashMap hashMap = new MyHashMap(4);
hashMap.put(1,11);
hashMap.put(5,55);
hashMap.put(2,22);
hashMap.put(6,66);
hashMap.put(3,33);
hashMap.put(1,111);
System.out.println(hashMap.containsKey(6));
System.out.println(hashMap.get(5));
System.out.println(hashMap.containsValue(33));
}
4.5删除操作
类似于链表的删除操作,首先需要排除当前key的所有为空时肯定不存在则直接返回false,若当前头结点就是要找到的结点则删除头结点,然后遍历后面的链表,找到即删除返回true,未找到返回false。
public boolean remove (int key){
int index = hash(key);
Node head = hashTable[index];
if (head == null) {
return false;
}
if(head.key==key){
hashTable[index] = head.next;
head = head.next = null;
size--;
return true;
}
Node prev = head;
while (prev.next!=null){
if(prev.next.key==key){
prev.next = prev.next.next;
size--;
return true;
}
prev = prev.next;
}
return false;
}
public static void main(String[] args) {
MyHashMap hashMap = new MyHashMap(4);
hashMap.put(1,11);
hashMap.put(5,55);
hashMap.put(2,22);
hashMap.put(6,66);
hashMap.put(3,33);
hashMap.put(1,111);
System.out.println(hashMap.remove(1));
System.out.println(hashMap.remove(6));
System.out.println(hashMap.remove(9));
}