本文主要从以下几个方面来介绍哈希表:1、哈希表的作用。2、哈希表的组成,自写一个哈希表需要哪些知识。3、哈希表的一个实例
1. 哈希表的作用
在我们日常的数据处理中,有时会遇到以下这种情况:每个数据都有唯一的标识符(或我们希望只有唯一的标识符,有相同标识符的元素只取一个),并且这些数据都有相应的一个或多个的值。理论上讲我们当然可以用链表来储存这些数据,但由于链表的查询速度较慢,因此当数据量较大时,链表就显得不那么适用了,这时就需要一个新的数据结构来储存这些数据,这就是哈希表的由来。
总的来说,哈希表的作用是储存一些拥有唯一标识符的数据。
2.哈希表的组成
哈希表之所以可以满足上述的要求,是因为从我们可以构造一种映射,这种映射可以把任意的输入都变成一个长度固定的数[1],并且在有限时间内由这个生成的数反解得到原输入是几乎不可能的。这是我们构造哈希表的底气所在。这种映射不止一种,(例如MD5和SHA1都是这种映射)。当然,在我们实际应用中可以用现成的函数来得到数据标识符对应的哈希值,不需要自己额外写代码来实现。
实现哈希表需要以下几种知识,这里只列出,不做详细解释。1、可变长度数组。2、链表
哈希表本质上是一个以链表的表头为元素的数组,我们记这个数组的长度为L。那么由每个数据的标识符对应的哈希值(我们记它为H)就可以得到这个数据在数组中的位置(H%L)。进而把这个数据挂在相应的表头的链表上就可以了。
3.哈希表的实例
在实际哈希表的实现中,我们需要考虑以下几个问题:
1、就像前文[1]中所说,我们是将任意长度大小的输入变成一个固定长度的数。这也就意味着我们将一个无限集合映成了一个有限集合。这显然不可能是一个一一对应。因此就可能会出现两个不一样的标识符对应同一个哈希值的情况,这就是我们常说的哈希碰撞。
2、我们创建哈希表的目的是为了不生成一个过长的链表(查询速度慢)。然而我们创建的哈希表(本质上是数组)的长度是固定的,那么当数据量不断变大时,链表过长的现象是不可能避免的。因此当我们存入一定量的数据后,我们应当对这个哈希表(数组)进行扩容。
对于第一个问题,我们只需要在比较完哈希值之后,再比较一下标识符是否相等就可以了
对于第二个问题,则需要制作一个判定标准来判断什么时候数据量相对于已有的哈希表是过大的。(一般我们认为如果大于75%的表头不是空的话,数据量就过大了,应当对哈希表进行扩容)
具体代码如下:
public class MyHashMap<K,V> {
int arraySize;
int size;
int length;
Node<K, V>[] myHashMap;
final int def_length = 16;
final double threshold = 0.75;
public MyHashMap(){
myHashMap = new Node[def_length];
size = 0;
arraySize = 0;
length = def_length;
}
public void show(){
String str = "{";
for (int i = 0; i < length; i++) {
Node<K,V> tempt = myHashMap[i];
while (tempt != null){
str += tempt.value+",";
tempt = tempt.next;
}
}
System.out.println(str);
}
public void expansion() {
size = 0;
arraySize = 0;
length = 2*length;
Node<K,V>[] newMap = new Node[length];
for (int i = 0; i < myHashMap.length; i++) {
Node<K,V> tempt = myHashMap[i];
if (tempt != null){
Node<K,V> node = new Node<>();
node.key = tempt.key;
node.hashcode = tempt.hashcode;
node.value = tempt.value;
int index = tempt.hashcode & (length-1);
Node<K,V> newNode = newMap[index];
if (newNode == null){
newMap[index] = node;
arraySize++;
size++;
}else {
while (newNode.next!=null){
newNode = newNode.next;
}
newNode.next = tempt;
newNode.next.next = null;
size++;
}
while (tempt.next!=null){
tempt = tempt.next;
node = new Node<>();
node.key = tempt.key;
node.hashcode = tempt.hashcode;
node.value = tempt.value;
index = tempt.hashcode & (length-1);
newNode = newMap[index];
if (newNode == null){
newMap[index] = node;
arraySize++;
size++;
}else {
while (newNode.next!=null){
newNode = newNode.next;
}
newNode.next = node;
size++;
}
}
}
}
myHashMap = newMap;
}
public void add(K key,V value) {
Node<K, V> node = new Node<>();
node.key = key;
node.value = value;
node.hashcode = key.hashCode();
int index = node.hashcode & (length - 1);
Node<K, V> tempt = myHashMap[index];
V oldValue = null;
if (tempt == null) {
myHashMap[index] = node;
arraySize++;
}else {
if (node.hashcode == tempt.hashcode && node.key ==tempt.key || node.key.equals(tempt.key) ){
oldValue = tempt.value;
tempt.value = value;
}else{
while (tempt.next != null){
tempt = tempt.next;
if (node.hashcode == tempt.hashcode && node.key ==tempt.key || node.key.equals(tempt.key) ){
oldValue = tempt.value;
tempt.value = value;
break;
}
}
}
if (oldValue == null){
tempt.next = node;
}
}
size++;
if (arraySize >= threshold*length){
expansion();
}
}
public V get(K key){
V theV = null;
for (int i = 0; i < length; i++) {
Node<K,V> tempt = myHashMap[i];
if (tempt == null){
continue;
}
if (tempt.key.equals(key)){//==
theV = tempt.value;
}else{
while (tempt.next!=null){
tempt = tempt.next;
if (tempt.key.equals(key)){
theV = tempt.value;
}
}
}
}
return theV;
}
最后,记录一下写代码时遇到的一些问题
问题主要出在哈希表的扩容上。对于链表中每一个节点的传递,我们都应该新令一个节点,它初对下一个节点的指向外,都与原节点相同,并把这个新节点挂在相应的链表上。而不建议把老节点直接挂在新链表上,这样会导致老节点之后的节点由于没有节点指向它而出现丢失的情况。