什么是哈希(散列)表 一文读明白它
arrayIndex=hugeNumber%arraySize
那么问题来了,这种方式对不同的关键字,可能得到同一个散列地址,即同一个数组下标,这种现象称为冲突,那么我们该如何去处理冲突呢?一种方法是开放地址法,即通过系统的方法找到数组的另一个空位,把数据填入,而不再用哈希函数得到的数组下标,因为该位置已经有数据了;另一种方法是创建一个存放链表的数组,数组内不直接存储数据,这样当发生冲突时,新的数据项直接接到这个数组下标所指的链表中,这种方法叫做链地址法。下面针对这两种方法进行讨论。
1.开放地址法
1.1 线性探测法
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;
}
}
二次聚集不是一个严重的问题,但是二次探测不会经常使用,因为还有好的解决方法,比如再哈希法。
1.2 再哈希法
- 和第一个哈希函数不同;
- 不能输出0(否则没有步长,每次探索都是原地踏步,算法将进入死循环)。
stepSize=constant-key%constant
其中 constant 是质数,且小于数组容量。
再哈希法要求表的容量是一个质数,假如表长度为15(0-14),非质数,有一个特定关键字映射到0,步长为5,则探测序列是 0,5,10,0,5,10,以此类推一直循环下去。算法只尝试这三个单元,所以不可能找到某些空白单元,最终算法导致崩溃。如果数组容量为13, 质数,探测序列最终会访问所有单元。即 0,5,10,2,7,12,4,9,1,6,11,3,一直下去,只要表中有一个空位,就可以探测到它。下面看看再哈希法的代码:
(左右滑动查看完整代码~)
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;
}
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;
}
}
2. 链地址法
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 + " ");
}
}
THE END