Hash数据结构基础
哈希表:
- 一个基于数组的K-V存储结构,K经过hash函数后散列到数组的某个位置,去拿到所需的K-V对。适用于可预测数据量且不需要遍历的场合。
- 哈希表的效率:插入和删除需要O(1)的时间。(既能够具备数组的快速查询的优点又能融合链表方便快捷的增加删除元素的优势)
哈希表的缺点:
-
基于数组,由于数组是定长的,长度塞满时效率很低(碰撞太多)。
-
没有一种机制可以有序遍历(如从小到大)哈希表中的数据,如果需要则只能借助使用其他的数据结构。
Hash碰撞解决?—开放地址/链地址
多个对象通过hash算法后的的地址相同,就产生了“哈希碰撞”。解决哈希碰撞的方法一般有两种:
- 开放地址法:即在数组中向后找一个合适的空位存储新的碰撞的对象。
- 线性探测:若碰撞,则从第一次hash的地址开始逐一向后找,找到空位则存储新的对象。(这种算法会在数组的某一段聚集大量的值,会增大碰撞的概率。聚集会降低hash表的效率)
- 二次探测:若碰撞,则从第一次hash的地址开始继续向后找,但每次找的步长为 1、4、9、16、25…以平方数递增。(比上一种略好,但因为碰撞元素探测的补偿相同,故会造成更细小的碰撞,在数组上聚集很多小段连续元素)
- 再hash方法:通过另一个hash算法算出第二次的步长。这个步长为一个与hash地址有关的值。如:stepSize = constant - (key % constant) key为第一次hash后碰撞的地址,这个常量要求是小于数组长度的质素。(数组长度也是质数)
- 链地址法:即在发生碰撞的位置存储一个链表。
好的哈希函数:
-
可以快速的计算,有许多乘法/除法会比较慢不合适,除了取模之外可以考虑位运算,左移N位相当于除以2的N次方
-
结果要随机,哈希函数为了避免哈希碰撞,需要把key映射到数组的下标,越随机越好
-
使用质数取模,也就是数组长度要是质数,否则会有很大的聚集效应!
-
字符串hash的时候可以把每一位按照ascii码乘以27的N次方,再相加. 如china的c计算: (charAt(0)-97)*27^4 子母h:(charAt(1)-97)*27^3 …
-
如果最后数字太大可以使用[同余定理]拆分:
(A×B + C×D + E×F +...)%P == ((A×B)%P + (C×D)%P +...)%P
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
| package com.sam.phoenix.datastructure.e_HashTable;
import java.math.BigDecimal;
public class HashTableLine { private int maxDataSize = 0; private Node[]hashArray; private int length = 0; static final Node DELETEED_NODE = new Node(Integer.MIN_VALUE,null);
public HashTableLine(int size, float loadFactor){ if(loadFactor <= 0 || loadFactor>1){ System.out.println("装载因子必须大于0,小于1,因为输入非法,默认取0.5!!!!"); loadFactor = 0.5f; } int len = new BigDecimal(size).divide(new BigDecimal(loadFactor),0,BigDecimal.ROUND_HALF_EVEN).intValueExact(); hashArray = new Node[len]; maxDataSize = size; }
private int hashFunction(int key){ return key % hashArray.length; }
public void insert(int key, Object value){ if(length == maxDataSize){ System.out.println("已经满了,无法插入!!!!!!"); } int index = hashFunction(key); while(index < hashArray.length){ if(hashArray[index] == null || hashArray[index] == DELETEED_NODE){ hashArray[index] = new Node(key,value); length++; return; } index++; if(index == hashArray.length-1){ index = 0; } } }
public Object find(int key){ if(length == 0){ return null; } int index = hashFunction(key); while(index < hashArray.length){ if(hashArray[index] == null){ return null; } if(hashArray[index].getKey() == key){ return hashArray[index].getValue(); }else{ index++; } if(index == hashArray.length-1){ index = 0; } } return null; }
public boolean delete(int key){ int index = hashFunction(key); if(hashArray[index]==null){ return false; } while(index < hashArray.length){ if(hashArray[index] == null){ return false; } if(hashArray[index].getKey() == key){ hashArray[index] = DELETEED_NODE; length--; return true; }else{ index++; } if(index == hashArray.length-1){ index = 0; } } return false; }
public void display(){ for(int i = 0; i < hashArray.length; i ++){ if(hashArray[i]==null){ System.out.print("null,"); continue; } System.out.print(hashArray[i].getKey()+""+hashArray[i].getValue()+","); } System.out.println(); }
public int getSize() { return this.maxDataSize; } }
|
QA
-
装载因子为啥是0.75?
- 是一个时间和空间的这种,tradeoff
- 这决定扩容的阈值,太低会浪费空间,很早就开始扩容。不过碰撞概率低,查询效率高。
- 太高会碰撞加剧,插入和查询慢。但是节省空间。
-
长度为什么是n^2?
-
因为有一个取&的操作简化取模,保证冲突记录小。否则
-
key=2: 00000010
length-1=7: 00000111
-------------------------------------------
取& 00000010 = 2
key=3: 00000011
length-1=7: 00000111
-------------------------------------------
取& 00000011 = 3
key=2: 00000010
length-1=8: 00001000
-------------------------------------------
取& 00000000 = 0
key=3: 00000011
length-1=8: 00001000
-------------------------------------------
取& 00000000 = 0