散列表(Hash table,也叫哈希表),是根据关键字(key value)而直接访问数据在内存中位置的数据结构,也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中的一个位置来访问记录,这加快了查找速度,这个映射函数称作散列函数,存放记录的数组成为散列表。 —维基百科
这种方法要求哈希表的每个位置有且仅有一个数据,当经过哈希之后得到的哈希值相同的时候,我们再按照一定的规则来寻找哈希表中是否有无数据的数组位置,如果有,则添加。而根据寻找规则的不同,又可以得到开放定址法不同的实现形式。
这种方法很简单,原理就是当出现在哈希表的某一位置出现冲突时,从该位置开始依次向后寻找是否有空闲位置,找到则将数据添加到该位置上。不过该方法有弊端,即
所谓聚集问题,是指存入哈希表的数据在位置上连成一片形成连续的序列,这样如果在以后存储数据的时候,如果出现冲突,那么就会导致这个连续的序列越来越长。而这越来越长的序列,反过来又会导致添加新数据的时候与这个连续序列产生冲突的概率大大增加,使得这个连续序列增长的越来越快,这样插入数据时处理冲突的时间就会越来越多,从而导致性能下降。换句话说,一旦出现聚集,就会导致进一步的聚集。
因为数据的存储与位置有关,因此当某个节点删除的时候,必须保留该位置(虽然可以将该位置的数据设置为某个代表无效值的值),不然将会影响以后其他数据的查找
示例代码(来自倪升武的博客):
public class HashTable {
private DataItem[] hashArray; //DateItem类是数据项,封装数据信息
private int arraySize;
private int itemNum; //数组中目前存储了多少项
private DataItem nonItem; //用于删除项的
public HashTable() {
arraySize = 13;
hashArray = new DataItem[arraySize];
nonItem = new DataItem(-1); //deleted item key is -1
}
public boolean isFull() {
return (itemNum == arraySize);
}
public boolean isEmpty() {
return (itemNum == 0);
}
public void displayTable() {
System.out.print("Table:");
for(int j = 0; j < arraySize; j++) {
if(hashArray[j] != null) {
System.out.print(hashArray[j].getKey() + " ");
}
else {
System.out.print("** ");
}
}
System.out.println("");
}
public int hashFunction(int key) {
return key % arraySize; //hash function
}
public void insert(DataItem item) {
if(isFull()) {
//扩展哈希表
System.out.println("哈希表已满,重新哈希化..");
extendHashTable();
}
int key = item.getKey();
int hashVal = hashFunction(key);
while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) {
++hashVal;//这里体现出线性探测依次查找的特点
hashVal %= arraySize;
}
hashArray[hashVal] = item;
itemNum++;
}
/*
* 数组有固定的大小,而且不能扩展,所以扩展哈希表只能另外创建一个更大的数组,然后把旧数组中的数据插到新的数组中。但是哈希表是根据数组大小计算给定数据的位置的,所以这些数据项不能再放在新数组中和老数组相同的位置上,因此不能直接拷贝(不然以后会无法正常查找到迁移之前的数据),需要按顺序遍历老数组,并使用insert方法向新数组中插入每个数据项。这叫重新哈希化。这是一个耗时的过程,但如果数组要进行扩展,这个过程是必须的。
*/
public void extendHashTable() { //扩展哈希表
int num = arraySize;
itemNum = 0; //重新记数,因为下面要把原来的数据转移到新的扩张的数组中
arraySize *= 2; //数组大小翻倍
DataItem[] oldHashArray = hashArray;
hashArray = new DataItem[arraySize];
for(int i = 0; i < num; i++) {
insert(oldHashArray[i]);
}
}
public DataItem delete(int key) {
if(isEmpty()) {
System.out.println("Hash table is empty!");
return null;
}
int hashVal = hashFunction(key);
while(hashArray[hashVal] != null) {
if(hashArray[hashVal].getKey() == key) {
DataItem temp = hashArray[hashVal];
hashArray[hashVal] = nonItem; //nonItem表示空Item,其key为-1
itemNum--;
return temp;
}
++hashVal;
hashVal %= arraySize;
}
return null;
}
public DataItem find(int key) {
int hashVal = hashFunction(key);
while(hashArray[hashVal] != null) {
if(hashArray[hashVal].getKey() == key) {
return hashArray[hashVal];
}
++hashVal;
hashVal %= arraySize;
}
return null;
}
}
class DataItem {
private int iData;
public DataItem (int data) {
iData = data;
}
public int getKey() {
return iData;
}
}
这种方案是,当出现冲突时,不是依次向后寻找空闲位置,而是在该位置的基础上添加一定的位移量之后再进行探测,但是这样会引发二次聚集现象。即对于很多经过哈希之后哈希值相同的数据,在它们存储的时候,越晚插入的数据,其步长就会越长。
为了消除聚集现象,现在需要的是一个可以依赖关键字产生探测序列的探测散列函数,而不是每个关键字都一样。即:不同的关键字即使映射到相同的位置,那么他们再次散列进行探测的时候,他们所用的探测值也不尽相同。经验说明,第二个散列函数需要具备以下特点:
专家发现如下形式的哈希函数工作的非常好:gap = prime - key%prime,其中prime是小于哈希表长度的质数。
代码取自倪升武的博客:
public class HashDouble {
private DataItem[] hashArray;
private int arraySize;
private int itemNum;
private DataItem nonItem;
public HashDouble() {
arraySize = 13;
hashArray = new DataItem[arraySize];
nonItem = new DataItem(-1);
}
public void displayTable() {
System.out.print("Table:");
for(int i = 0; i < arraySize; i++) {
if(hashArray[i] != null) {
System.out.print(hashArray[i].getKey() + " ");
}
else {
System.out.print("** ");
}
}
System.out.println("");
}
public int hashFunction1(int key) { //first hash function
return key % arraySize;
}
public int hashFunction2(int key) { //second hash function
return 5 - key % 5;//这里使用了前面专家证明的再哈希算法。这里取素数5
}
public boolean isFull() {
return (itemNum == arraySize);
}
public boolean isEmpty() {
return (itemNum == 0);
}
public void insert(DataItem item) {
if(isFull()) {
System.out.println("哈希表已满,重新哈希化..");
extendHashTable();
}
int key = item.getKey();
int hashVal = hashFunction1(key);
int stepSize = hashFunction2(key); //用hashFunction2计算探测步数
while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) {
hashVal += stepSize;
hashVal %= arraySize; //以指定的步数向后探测
}
hashArray[hashVal] = item;
itemNum++;
}
public void extendHashTable() {
int num = arraySize;
itemNum = 0; //重新记数,因为下面要把原来的数据转移到新的扩张的数组中
arraySize *= 2; //数组大小翻倍
DataItem[] oldHashArray = hashArray;
hashArray = new DataItem[arraySize];
for(int i = 0; i < num; i++) {
insert(oldHashArray[i]);
}
}
public DataItem delete(int key) {
if(isEmpty()) {
System.out.println("Hash table is empty!");
return null;
}
int hashVal = hashFunction1(key);
int stepSize = hashFunction2(key);
while(hashArray[hashVal] != null) {
if(hashArray[hashVal].getKey() == key) {
DataItem temp = hashArray[hashVal];
hashArray[hashVal] = nonItem;
itemNum--;
return temp;
}
hashVal += stepSize;
hashVal %= arraySize;
}
return null;
}
public DataItem find(int key) {
int hashVal = hashFunction1(key);
int stepSize = hashFunction2(key);
while(hashArray[hashVal] != null) {
if(hashArray[hashVal].getKey() == key) {
return hashArray[hashVal];
}
hashVal += stepSize;
hashVal %= arraySize;
}
return null;
}
}
讨论完了开放定址法,现在我们来讨论链地址法。在开放定址法中哈希表的每个位置只允许存在一个数据,而在链地址法中每个位置允许以链表的形式存在多个数据,即经过哈希散列之后哈希值相同的数据相互连接成链表然后将头部挂在哈希表的一个固定位置上。
代码示例:
public class HashChain {
private SortedList[] hashArray; //数组中存放链表
private int arraySize;
public HashChain(int size) {
arraySize = size;
hashArray = new SortedList[arraySize];
//new出每个空链表初始化数组
for(int i = 0; i < arraySize; i++) {
hashArray[i] = new SortedList();
}
}
public void displayTable() {
for(int i = 0; i < arraySize; i++) {
System.out.print(i + ": ");
hashArray[i].displayList();
}
}
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;
}
}
链表类:
public class SortedList {
private LinkNode first;
public SortedList() {
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("chain 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;
}
current = current.next;
}
return null;
}
public void displayList() {
System.out.print("List(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 data) {
iData = data;
}
public int getKey() {
return iData;
}
public void displayLink() {
System.out.print(iData + " ");
}
}
性能分析:无冲突情况下,插入删除均为O(1)级别,有冲突时,最坏情况下为O(n).