Hash表的实现
目录 · Table of Contents
Hash表是一种特殊的数据结构。在Hash表中,记录在表中的位置和其关键字之间存在着一种确定的关系,这样我们就可以预先知道关键字在表中的位置,从而直接通过下标找到记录。使时间复杂度直接降为O(1).Hash表采用一个映射函数f(key) –> adress将关键字映射到该记录在表中的存储位置.
Hash函数的设计
- 直接定址法 取关键字或者关键字的某个线性函数为Hash地址,即address(key)=a*key+b;如知道学生的学号从2000开始,最大为4000,则可以将address(key)=key-2000作为Hash地址。
- 平方取中法 对关键字进行平方运算,然后取结果的中间几位作为Hash地址。假如有以下关键字序列{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取{72,89,00}作为Hash地址。
- 折叠法 将关键字拆分成几部分,然后将这几部分组合在一起,以特定的方式进行转化形成Hash地址。假如知道图书的ISBN号为8903-241-23,可以将address(key)=89+03+24+12+3作为Hash地址。
- 除留取余法 如果知道Hash表的最大长度为m,可以取不大于m的最大质数p,然后对关键字进行取余运算,address(key)=key%p。在这里p的选取非常关键,p选择的好的话,能够最大程度地减少冲突,p一般取不大于m的最大质数。
Hash处理冲突方法
- 开放定址法 为产生冲突的关键字地址 H(key) 求得一个地址序列: H0, H1, H2, …, Hs 1≤s≤m-1,Hi = (H(key) +di ) MOD m,其中: i=1, 2, …, s,H(key)为哈希函数;m为哈希表长;
- 再哈希法 构造若干个哈希函数,当发生冲突时,根据另一个哈希函数计算下一个哈希地址,直到冲突不再发生。即:Hi=Rhi(key) i=1,2,……k,其中:Rhi——不同的哈希函数,特点:计算时间增加
- 链地址法
采用数组和链表相结合的办法,将Hash地址相同的记录存储在一张线性表中,而每张表的表头的序号即为计算得到的Hash地址。如上述例子中,采用链地址法形成的Hash表存储表示为:

代码实现
1package com.sllt.qiao;
2public class MyMap<K, V> {
3 private int size;// 当前容量
4 private static int INIT_CAPACITY = 16;// 默认容量
5 private Entry<K, V>[] container;// 实际存储数据的数组对象
6 private static float LOAD_FACTOR = 0.75f;// 装载因子
7 private int max;// 能存的最大的数=capacity*factor
8
9 // 自己设置容量和装载因子的构造器
10 public MyMap(int init_Capaticy, float load_factor) {
11 if (init_Capaticy < 0)
12 throw new IllegalArgumentException("Illegal initial capacity: "
13 + init_Capaticy);
14 if (load_factor <= 0 || Float.isNaN(load_factor))
15 throw new IllegalArgumentException("Illegal load factor: "
16 + load_factor);
17 this.LOAD_FACTOR = load_factor;
18 max = (int) (init_Capaticy * load_factor);
19 container = new Entry[init_Capaticy];
20 }
21
22 // 使用默认参数的构造器
23 public MyMap() {
24 this(INIT_CAPACITY, LOAD_FACTOR);
25 }
26
27 /**
28 * 存
29 *
30 * @param k
31 * @param v
32 * @return
33 */
34 public boolean put(K k, V v) {
35 // 1.计算K的hash值
36 // 因为自己很难写出对不同的类型都适用的Hash算法,故调用JDK给出的hashCode()方法来计算hash值
37 int hash = k.hashCode();
38 //将所有信息封装为一个Entry
39 Entry<K,V> temp=new Entry(k,v,hash);
40 if(setEntry(temp, container)){
41 // 大小加一
42 size++;
43 return true;
44 }
45 return false;
46 }
47
48
49 /**
50 * 扩容的方法
51 *
52 * @param newSize
53 * 新的容器大小
54 */
55 private void reSize(int newSize) {
56 // 1.声明新数组
57 Entry<K, V>[] newTable = new Entry[newSize];
58 max = (int) (newSize * LOAD_FACTOR);
59 // 2.复制已有元素,即遍历所有元素,每个元素再存一遍
60 for (int j = 0; j < container.length; j++) {
61 Entry<K, V> entry = container[j];
62 //因为每个数组元素其实为链表,所以…………
63 while (null != entry) {
64 setEntry(entry, newTable);
65 entry = entry.next;
66 }
67 }
68 // 3.改变指向
69 container = newTable;
70
71 }
72
73 /**
74 *将指定的结点temp添加到指定的hash表table当中
75 * 添加时判断该结点是否已经存在
76 * 如果已经存在,返回false
77 * 添加成功返回true
78 * @param temp
79 * @param table
80 * @return
81 */
82 private boolean setEntry(Entry<K,V> temp,Entry[] table){
83 // 根据hash值找到下标
84 int index = indexFor(temp.hash, table.length);
85 //根据下标找到对应元素
86 Entry<K, V> entry = table[index];
87 // 3.若存在
88 if (null != entry) {
89 // 3.1遍历整个链表,判断是否相等
90 while (null != entry) {
91 //判断相等的条件时应该注意,除了比较地址相同外,引用传递的相等用equals()方法比较
92 //相等则不存,返回false
93 if ((temp.key == entry.key||temp.key.equals(entry.key)) && temp.hash == entry.hash&&(temp.value==entry.value||temp.value.equals(entry.value))) {
94 return false;
95 }
96 //不相等则比较下一个元素
97 else if (temp.key != entry.key && temp.value != entry.value) {
98 //到达队尾,中断循环
99 if(null==entry.next){
100 break;
101 }
102 // 没有到达队尾,继续遍历下一个元素
103 entry = entry.next;
104 }
105 }
106 // 3.2当遍历到了队尾,如果都没有相同的元素,则将该元素挂在队尾
107 addEntry2Last(entry,temp);
108
109 }
110 // 4.若不存在,直接设置初始化元素
111 setFirstEntry(temp,index,table);
112 return true;
113 }
114
115 private void addEntry2Last(Entry<K, V> entry, Entry<K, V> temp) {
116 if (size > max) {
117 reSize(container.length * 4);
118 }
119 entry.next=temp;
120
121 }
122
123 /**
124 * 将指定结点temp,添加到指定的hash表table的指定下标index中
125 * @param temp
126 * @param index
127 * @param table
128 */
129 private void setFirstEntry(Entry<K, V> temp, int index, Entry[] table) {
130 // 1.判断当前容量是否超标,如果超标,调用扩容方法
131 if (size > max) {
132 reSize(table.length * 4);
133 }
134 // 2.不超标,或者扩容以后,设置元素
135 table[index] = temp;
136 //!!!!!!!!!!!!!!!
137 //因为每次设置后都是新的链表,需要将其后接的结点都去掉
138 //NND,少这一行代码卡了哥哥7个小时(代码重构)
139 temp.next=null;
140 }
141
142 /**
143 * 取
144 *
145 * @param k
146 * @return
147 */
148 public V get(K k) {
149 Entry<K, V> entry = null;
150 // 1.计算K的hash值
151 int hash = k.hashCode();
152 // 2.根据hash值找到下标
153 int index = indexFor(hash, container.length);
154 // 3。根据index找到链表
155 entry = container[index];
156 // 3。若链表为空,返回null
157 if (null == entry) {
158 return null;
159 }
160 // 4。若不为空,遍历链表,比较k是否相等,如果k相等,则返回该value
161 while (null != entry) {
162 if (k == entry.key||entry.key.equals(k)) {
163 return entry.value;
164 }
165 entry = entry.next;
166 }
167 // 如果遍历完了不相等,则返回空
168 return null;
169
170 }
171
172 /**
173 * 根据hash码,容器数组的长度,计算该哈希码在容器数组中的下标值
174 *
175 * @param hashcode
176 * @param containerLength
177 * @return
178 */
179 public int indexFor(int hashcode, int containerLength) {
180 return hashcode & (containerLength - 1);
181
182 }
183
184 /**
185 * 用来实际保存数据的内部类,因为采用挂链法解决冲突,此内部类设计为链表形式
186 *
187 * @param <K>key
188 * @param <V>
189 * value
190 */
191 class Entry<K, V> {
192 Entry<K, V> next;// 下一个结点
193 K key;// key
194 V value;// value
195 int hash;// 这个key对应的hash码,作为一个成员变量,当下次需要用的时候可以不用重新计算
196
197 // 构造方法
198 Entry(K k, V v, int hash) {
199 this.key = k;
200 this.value = v;
201 this.hash = hash;
202
203 }
204
205 //相应的getter()方法
206
207 }
208}