1.直接寻址法:当关键字的全域U很小时,直接寻址法是一种简单而有效的技术。设集合U={0,1,2...m-1},我们用一个数组或叫直接寻址表记为T[0...m-1],数组每一个位置对应U中的一个关键字。T[k]存放关键字k,若集合中没有k关键字则T[k]=NIL;
直接寻址法缺点非常明显:如果全域U非常大,接寻址表T可能就存不下。反言之,如果T中实际存储关键字太少,也可能造成大量空间的浪费。
2.散列法:在直接寻址法,关键字放在T[k]中,而在散列方式下关键字k放在T[h(k)]中:即利用散列函数(hash function)h,通过关键字k计算出下标位置。函数h将关键字的全域U映射到散列表(hash table)T[0...m-1]上;一般m比|U|小得多,于是会出现不同关键字映射到了同一位置,这种情况称为冲突。
3.散列函数构造规则:一个好的散列函数应该满足以下条件:
1)计算简单,以便提高转换速度;
2)关键字对应的地址空间分布均匀,以尽量减少冲突。即对关键字集合中的任何一个关键字,经散列映射到集合中任何一地址的概率基本相等的。尽量随机。
4.散列函数构造方法:
1)数字关键字的散列函数构造:
a.直接定址法,取关键字为某个线性函数值为散列地址。哈希函数:h(key)=a*key+b;相当于直接寻址法,对大量的数据不适用。
b.除留余数法:通过key除p的余数将关键字映射到表中相应的位置:哈希函数:h(key)=key%p;设散列表的长度为tableSiza则选取的p最好是p<=tableSize最大的一个素数。用素数求余分布在整个地址空间上的可能性比较大。
c.数字分析法:如果关键字位数较多,在特定情况系有的位数容易重复,而有的位数比较随机,比如11位的手机号,前三位和中间四位容易重复,而末尾的四位则比较随机,于是可以选择末尾4为作为散列地址.。如果11位手机号码是字符串,在C和C++中有将字符串转换为整数函数:h=atoi(key+7),key+7表示指针向后移动7位,即保留四位。
2)字符串关键字的散列函数构造:基本思想是将关键字转换为自然数,然后再根据自然数的方法来构造。如一个字符串可以表示为适当基数的整数,在ASCII中,若以128为基数,则标示符pt可以表示为112*128+116=14452,以10为基数表示为112*10+116=1236。不管什么方法,我们的原则是将关键字转换为自然数。
5.解决冲突的办法:链接法和开放寻址法。
1)链接法解决冲突:链接法就是把散列到同一个位置的元素都放在一个链表中。链表法虽然有效的解决了冲突,但是却需要额外的空间。采用链式法在最坏情况下性能很差:当所有的n个关键字都散列到了同一个位置,此时,最坏查找时间是O(n)。装填因子:给定一个可以存放n个元素,具有m个散列位置的散列表T,定义装填因子a=n/m即为平均每个位置储存的元素个数。如果设散列函数的计算时间是O(1),那么这种链接法平均查找时间为O(1+a),平均删除时间也是O(1+a),因为先要找到才可以删除。由于每次插入元素都在表头,所以插入时间为O(1);
2)开放寻址法解决冲突:开放寻址法所有的元素都放在散列表里面。当查找某个元素时,要系统的检查所有表项,直到找到所需的元素。由于开放寻址法没有元素放在表外,因此在开放寻址中,散列表可能被填满,固有装填因子a<=1.为了使用开放寻址来插入一个元素,需要连续的检查散列表,或者叫探查,直到找到一个空位置来存放此元素。为了加入探测,我们把散列函数扩充为二元:h(k,i)=(h1(k)+d(i)) mod m (0<=i<m),m是散列表长度。一般来说d(i)的选取不同可以得到解决冲突的不同方案。有三种常用的开放寻址法的探测技术:线性探查,二次探查,双重探查。
a.线性探查技术;线性探查散列函数为h(k,i)=(h1(k)+i) mod m,i=0,1,2...m-1.给定关键值k,首先查看位置T[h1(k,0)],i依次增大,直到探测到空位置或者i==m-1为止。线性探测方法容易实现,但是它存在一个问题:“一次群集”。随着被占用的位置不断增多,平均查找的时间也不段增多,连续被占用的位置越来越长,因而平均查找时间也会增大。
b.二次探查:二次探查公式h(k,i)=(h1(k)+c*i^2+d*i) mod m.i=0...m-1.该偏移量以二次的方式,这种探测效果要比一次探测好的多。如果存在h(k1,0)==h(k2,0),那么关键值k1和k2探测的序列相同,于是也可能导致一种轻度的聚集“二次聚集”。一般的用i^2来探查叫做平方探查法。
c.双重探查法:双重散列是开放寻址的最好方法之一,双重散列函数:
h(k,i)=(h1(k)+i*h2(k)) mod m. i=0,1,2...m-1.这次的探测不像线性探测或者二次探测,这里探测序列以两种不同的方式依赖于关键字k,效果为最佳。关于h2和m选取的关系h2(k)必须与表的大小m互质,这样才可以查找到整个表。方法一:取m=2^p,并设计一个总产生奇数的h2(k)。方法二:选取m为某一素数,并设计一个总是小于m的正整数函数h2(k),这样h2(k)就与m互素了。双重散列是一种”比较理想的“均匀散列。
下面代码假设关键字是自然数,采用的是双重探查技术。将表的大小设置为一个素数。
/*hash.h*/
#pragma once
#include<iostream>
#include<iomanip>
using namespace std;
enum EntryType{Empty,Delete,Legitimaxte};
class HashEntry{
public:
int element;
enum EntryType Info;
};
class HashTable{
private:
int cnum_=0; //记录冲突次数
HashEntry *theCells;
int tableSize_;
int nextNum_;
public:
HashTable(int size);
int getSize(){ return tableSize_; }
int getElement(int i);
int getCnum(){ return cnum_; }
void showHashTable();
int hashFunction(int key,int i);
void distroyTable();
int findElement(int key);
bool insertElement(int key);
bool deleteElement(int key);
};
/*hash.cpp*/
#include"hash.h"
HashTable::HashTable(int size){
int i;
bool flag ;
theCells = new HashEntry[size]; //申请表空间
for (i = 0; i<size; i++)
theCells[i].Info = Empty; //初始化
if (size % 2 == 0) //如果size是偶数,将它变为奇数
size--;
for (tableSize_ =size; tableSize_> 2; tableSize_--){
flag =true;
for (i = 3; i*i <=tableSize_; i+=2){
if (tableSize_%i == 0){
flag = false;
break;
}
}
if (flag)
break;
}
/*以上找到素数因子用来作为表长*/
nextNum_ = tableSize_ -2; //nextNum与tableSize_互质
}
int HashTable::getElement(int i){
if (theCells[i].Info == Legitimaxte)
return theCells[i].element;
return -1;
}
void HashTable::showHashTable(){
int i;
for (i = 0; i < tableSize_; i++){
switch (theCells[i].Info){
case 0:cout<<setw(11)<<setfill(' ')<< "Empty" << " "; break;
case 1:cout << setw(11)<<setfill(' ')<< "Delete" << " "; break;
case 2:cout << setw(11)<<setfill(' ')<< "Legitimaxte" << " "; break;
}
if (theCells[i].Info == Legitimaxte)
cout << theCells[i].element << endl;
else
cout << "NULL" << endl;
}
}
int HashTable::hashFunction(int key,int i){ //双重散列
return (key%tableSize_+i*(key%nextNum_+1))%tableSize_;
}
void HashTable::distroyTable(){
delete[]theCells;
tableSize_ = 0;
}
bool HashTable::insertElement(int key){
int i=0;
while (i < tableSize_&&theCells[hashFunction(key, i)].Info != Empty)
i++,cnum_++;
if (i == tableSize_)
return false; //插入失败
theCells[hashFunction(key, i)].element = key;
theCells[hashFunction(key, i)].Info = Legitimaxte;
return true;
}
int HashTable::findElement(int key){
int i = 0,pos;
while (i < tableSize_&&theCells[hashFunction(key, i)].Info != Empty&&theCells[hashFunction(key, i)].element!= key)
i++;
if (i == tableSize_)
return -1;
pos = hashFunction(key, i); //找到啦下标
return pos;
}
bool HashTable::deleteElement(int key){
int pos = findElement(key); //找到元素的位置
if (pos == -1)
return false;
theCells[pos].Info = Delete;
return true;
}
/*源.cpp*/
#include"hash.h"
#include<iostream>
using namespace std;
int main(){
/*测试*/
int i;
int a[10] = { 47, 7, 29, 11, 9, 84, 54, 20, 32, 46 };
HashTable H(20);
cout << "表大小: " << H.getSize() << endl;
for (i = 0; i < 10; i++)
H.insertElement(a[i]);
H.showHashTable();
cout << "总的冲突次数: " << H.getCnum()<< endl;
cout << "----------------------------------------------------------------------" << endl;
cout << "插入58后表为:" << endl;
H.insertElement(58);
H.showHashTable();
cout << "总的冲突次数: " << H.getCnum() << endl;
cout << "----------------------------------------------------------------------" << endl;
cout << "删除20后表为:" << endl;
H.deleteElement(20);
H.showHashTable();
cout << "----------------------------------------------------------------------" << endl;
return 0;
}