一 概念
Hash表也称散列表,也有直接译作哈希表,Hash表是一种根据关键字值(key - value)而直接进行访问的数据结构。它基于数组,通过把关键字映射到数组的某个下标来加快查找速度,但是又和数组、链表、树等数据结构不同,在这些数据结构中查找某个关键字,通常要遍历整个数据结构,也就是O(N)的时间级,但是对于哈希表来说,只是O(1)的时间级。
注意,这里有个重要的问题就是如何把关键字转换为数组的下标,这个转换的函数称为哈希函数(也称散列函数),转换的过程称为哈希化
二、哈希函数
幂的连乘
我们将单词表示的数拆成数列,用适当的 27 的幂乘以这些位数(因为有26个可能的字符,以及空格,一共27个),然后把乘积相加,这样就得出了每个单词独一无二的数字。
比如把单词cats 转换为数字:
cats = 3*273 + 1*272 + 20*271 + 19*270 = 59049 + 729 + 540 + 19 = 60337
这个过程会为每个单词创建一个独一无二的数,但是注意的是我们这里只是计算了 4 个字母组成的单词,如果单词很长,比如最长的10个字母的单词 zzzzzzzzzz,仅仅是279 结果就超出了7000000000000,这个结果是很巨大的,在实际内存中,根本不可能为一个数组分配这么大的空间。
所以这个方案的问题就是虽然为每个单词都分配了独一无二的下标,但是只有一小部分存放了单词,很大一部分都是空着的。那么现在就需要一种方法,把数位幂的连乘系统中得到的巨大的整数范围压缩到可接受的数组范围中。
对于英语字典,假设只有5000个单词,这里我们选定容量为10000 的数组空间来存放(后面会介绍为啥需要多出一倍的空间)。那么我们就需要将从 0 到超过 7000000000000 的范围,压缩到从0到10000的范围。
第一种方法:取余,得到一个数被另一个整数除后的余数。首先我们假设要把从0-199的数字(用largeNumber表示),压缩为从0-9的数字(用smallNumber表示),后者有10个数,所以变量smallRange 的值为10,这个转换的表达式为:
smallNumber = largeNumber % smallRange
当一个数被 10 整除时,余数一定在0-9之间,这样,我们就把从0-199的数压缩为从0-9的数,压缩率为 20 :1。
我们也可以用类似的方法把表示单词唯一的数压缩成数组的下标:
arrayIndex = largerNumber % smallRange
这也就是哈希函数。它把一个大范围的数字哈希(转化)成一个小范围的数字,这个小范围的数对应着数组的下标。使用哈希函数向数组插入数据后,这个数组就是哈希表。
三、哈希冲突
把巨大的数字范围压缩到较小的数字范围,那么肯定会有几个不同的单词哈希化到同一个数组下标,即产生了冲突。
冲突可能会导致哈希化方案无法实施,前面我们说指定的数组范围大小是实际存储数据的两倍,因此可能有一半的空间是空着的,所以,当冲突产生时,一个方法是通过系统的方法找到数组的一个空位,并把这个单词填入,而不再用哈希函数得到数组的下标,这种方法称为开放地址法。比如加入单词 cats 哈希化的结果为5421,但是它的位置已经被单词parsnip占用了,那么我们会考虑将单词 cats 存放在parsnip后面的一个位置 5422 上。
另一种方法,前面我们也提到过,就是数组的每个数据项都创建一个子链表或子数组,那么数组内不直接存放单词,当产生冲突时,新的数据项直接存放到这个数组下标表示的链表中,这种方法称为链地址法。
四、链地址法
某个数据项的关键字值还是像通常一样映射到哈希表的单元,而数据项本身插入到这个单元的链表中。其他同样映射到这个位置的数据项只需要加到链表中,不需要在原始的数组中寻找空位。
有序链表:
|
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
|
package com.ys.hash;
public class SortLink {
private LinkNode first;
public SortLink(){
first = null;
}
public boolean isEmpty(){
return (first == null);
}
public void insert(LinkNode node){
int key = node.getKey();
LinkNode previous = null;
LinkNode current = first;
while(current != null && current.getKey() < key){
previous = current;
current = current.next;
}
if(previous == null){
first = node;
}else{
node.next = current;
previous.next = node;
}
}
public void delete(int key){
LinkNode previous = null;
LinkNode current = first;
if(isEmpty()){
System.out.println("Linked is Empty!!!");
return;
}
while(current != null && current.getKey() != key){
previous = current;
current = current.next;
}
if(previous == null){
first = first.next;
}else{
previous.next = current.next;
}
}
public LinkNode find(int key){
LinkNode current = first;
while(current != null && current.getKey() <= key){
if(current.getKey() == key){
return current;
}
}
return null;
}
public void displayLink(){
System.out.println("Link(First->Last)");
LinkNode current = first;
while(current != null){
current.displayLink();
current = current.next;
}
System.out.println("");
}
class LinkNode{
private int iData;
public LinkNode next;
public LinkNode(int iData){
this.iData = iData;
}
public int getKey(){
return iData;
}
public void displayLink(){
System.out.println(iData + " ");
}
}
} |
链地址法:
|
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
|
package com.ys.hash;
import com.ys.hash.SortLink.LinkNode;
public class HashChain {
private SortLink[] hashArray;//数组中存放链表
private int arraySize;
public HashChain(int size){
arraySize = size;
hashArray = new SortLink[arraySize];
//new 出每个空链表初始化数组
for(int i = 0 ; i < arraySize ; i++){
hashArray[i] = new SortLink();
}
}
public void displayTable(){
for(int i = 0 ; i < arraySize ; i++){
System.out.print(i + ":");
hashArray[i].displayLink();
}
}
public int hashFunction(int key){
return key%arraySize;
}
public void insert(LinkNode node){
int key = node.getKey();
int hashVal = hashFunction(key);
hashArray[hashVal].insert(node);//直接往链表中添加即可
}
public LinkNode delete(int key){
int hashVal = hashFunction(key);
LinkNode temp = find(key);
hashArray[hashVal].delete(key);//从链表中找到要删除的数据项,直接删除
return temp;
}
public LinkNode find(int key){
int hashVal = hashFunction(key);
LinkNode node = hashArray[hashVal].find(key);
return node;
}
|