1 前言
常用处理冲突的思路:
- 换个位置:
开放地址法
- 同一位置的冲突对象组织在一起:
链地址法
2 开放定址法(Open Addressing)
一旦产生了冲突(该地址已有其它元素),就按某种规则去寻找另一空地址
- 若发生了第 i 次冲突,试探的下一个地址将增加di,基本公式是:
hi(key) = (h(key)+di) mod TableSize ( 1≤ i < TableSize ) - di 决定了不同的解决冲突方案:
线性探测
、平方探测
、双散列
。
线性探测:di = i
平方探测:di = ± i2( +12, -12, +22, -22……)
双散列:di = i * h2(key)
2.1线性探测法(Linear Probing)
线性探测法:以增量序列 1,2,……(TableSize -1)循环试探下一个存储地址。
下面看一个例子来说明线性探测法:设关键词序列为 {47,7,29,11,9,84,54,20,30},
- 散列表表长TableSize =13 (装填因子 α = 9/13 ≈ 0.69);
- 散列函数为:h(key) = key mod 11。
散列表表长大于散列函数的分母也没关系,由于存在冲突,进行探测,冲突的元素可能移动到下标大于分母的位置。
用线性探测法处理冲突,列出依次插入后的散列表,并估算查找性能。
关键词 | 47 | 7 | 29 | 11 | 9 | 84 | 54 | 20 | 30 |
---|---|---|---|---|---|---|---|---|---|
散列地址 | 3 | 7 | 7 | 0 | 9 | 7 | 10 | 9 | 8 |
冲突次数 | 0 | 0 | 1 | 0 | 0 | 3 | 1 | 3 | 6 |
由于计算散列地址时,较多的元素计算出散列地址为7,图中出现了聚集的现象,明明还有空间,却都往一个地方挤。聚集地方的冲突会越来越多。
散列表查找性能分析
- 成功平均查找长度(ASLs)
- 不成功平均查找长度 (ASLu)
分析有散列表查找性能有两个指标,成功平均查找长度(ASLs),即成功查找到元素平均需要多少次查找;不成功平均查找长度 (ASLu),即元素不再散列表中,要查出该元素不在该散列表中,平均需要多少次查找。
下面来一个具体的例子亲身感受一下成功平均查找长度(ASLs)与不成功平均查找长度 (ASLu)的计算过程。
散列表如下:
h(key) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
key | 11 | 30 | 47 | 7 | 29 | 9 | 84 | 54 | 20 | ||||
冲突次数 | 0 | 6 | 0 | 0 | 1 | 0 | 3 | 1 | 3 |
【分析】
ASLs:查找表中关键词的平均查找比较次数(其冲突次数加1)
对应每一个已经存在的元素,查找这个元素需要查找的次数,如查找11,h(11)=0,地址0对应元素11,一次找到元素。查找元素20,h(20)=9,在地址9查找,不是,地址10,不是,地址11,不是,地址12,找到了,查找了4次。存在的元素一共有9个,每个元素查找次数之和除以表中元素个数:
ASLs = (1+7+1+1+2+1+4+2+4)/ 9 = 23/9 ≈ 2.56
ASLu:不在散列表中的关键词的平均查找次数(不成功)
一般方法:将不在散列表中的关键词分若干类,h(key) = key mod 11。这里h(key)值0-10,一共11种,就将所有不在散列表种的元素分成11种,分别对应每一种h(key)。
如查找22,h(22)=0;地址0,不是,地址1,不是,地址2,为空,说明元素22不存在,注意,一定要找到地址处为空才能判断该元素不存在。
将h(key)值为0-10的元素不成功查找次数相加再除以元素种类就得到不成功平均查找长度 。
ASLu = (3+2+1+2+1+1+1+9+8+7+6)/ 11 = 41/11 ≈ 3.73
再看一个例子:将acos、define、float、exp、char、atan、ceil、floor顺次存入一张大小为26的散列表中。
h(key)=key[0]-’a’,采用线性探测di=i。
【分析】
ASLs:表中关键词的平均查找比较次数
ASLs = (1+1+1+1+1+2+5+3)/ 8 = 15/8 ≈ 1.87
ASLu:不在散列表中的关键词的平均查找次数(不成功)
根据H(key)值分为26种情况:H值为0,1,2,…,25
ASLu = (9+8+7+6+5+4+3+2+1*18)/ 26 = 62/26 ≈ 2.38
2.2 平方探测法(Quadratic Probing)----- 二次探测
平方探测法:以增量序列12,-12,22,-22,……,q2,-q2 且q ≤ (TableSize/2)循环试探下一个存储地址。
看一个例子:
设关键词序列为 {47,7,29,11,9,84,54,20,30}
- 散列表表长TableSize = 11
- 散列函数为:h(key) = key mod 11
用平方探测法处理冲突,列出依次插入后的散列表,并估算ASLs。
关键字key | 47 | 7 | 29 | 11 | 9 | 84 | 54 | 20 | 30 |
---|---|---|---|---|---|---|---|---|---|
散列地址h(key) | 3 | 7 | 7 | 0 | 9 | 7 | 10 | 9 | 8 |
冲突次数 | 0 | 0 | 1 | 0 | 0 | 2 | 0 | 3 | 3 |
ASLs = (1+1+2+1+1+3+1+4+4)/ 9 = 18/9 = 2
平方探测法是跳着寻找位置的,那么就存在一个问题,假设散列表中有空间,平方探测(二次探测)就能找得到?下面通过例子来说明。
关键词key | 5 | 6 | 7 | ||
---|---|---|---|---|---|
散列地址h(key) | 0 | 1 | 2 | 3 | 4 |
散列函数为h(key)= key mod 5,用平方探测处理冲突
假设下一个插入11
h(11)=1
探测序列:1+1=2, 1-1=0, (1+22)mod 5=0, (1-22)mod 5=2,(1+32)mod 5=0, (1-32)mod 5=2, (1+42)mod 5=2,…
可以看到,存放11元素的地址在地址0与地址2之间跳,虽然地址3,地址4位置有空位,但是却找不到这个空间。有没有办法解决这个问题呢,有的;
解决方法:有定理显示:如果散列表长度TableSize是某个4k+3
(k是正整数)形式的素数
时,平方探测法就可以探查到整个散列表空间。
2.3 具体实验
下面采用开放定址法创建散列表,采用平方探测法实现插入与查找。使用的例子就是上面的例子,设关键词序列为 {47,7,29,11,9,84,54,20,30},散列表表长TableSize = 11,散列函数为:h(key) = key mod 11
解读一下查找元素位置这段代码,首先根据关键词算出哈希值,根据哈希值查找对应位置的元素,如果该位置为空或者关键词等于我们的关键词,退出,要么找到了,要么不存在。
否则就是该位置有元素且不是想要的元素,冲突了,继续寻找,每次冲突,都将冲突值CNum加1,
- 冲突1次,增量为+12,+((1+1)/2)2
- 冲突3次,增量为+22,+((1+3)/2)2
- 冲突5次,增量为+32,+((1+5)/2)2
公式为di = +(CNum + 1)*(CNum + 1) / 4
- 冲突2次,增量为-12,-((2)/2)2
- 冲突4次,增量为-22,-((4)/2)2
- 冲突6次,增量为-32,-((6)/2)2
公式为di = -CNum*CNum / 4
冲突次数与增量序列的对应关系如下:
di | +12 | -12 | +22 | -22 | +32 | -32 | …. |
---|---|---|---|---|---|---|---|
Cnum | 1 | 2 | 3 | 4 | 5 | 6 | …. |
根据哈希函数算出的地址值加上这个增量后的地址可能超出哈希表的大小,需要该该值调整为合法地址。
Position Find(HashTable H, ElementType Key)
{
Position CurrentPos, NewPos;
int CNum = 0; //记录冲突次数
NewPos = CurrentPos = Hash(Key, H->TableSize);//初始散列位置,哈希函数自己设计
//当该位置的单元非空,并且不是要找的元素时,发生冲突
while (H->Cells[NewPos].Info != Empty && H->Cells[NewPos].Data != Key)
{
//统计1次冲突,并判断奇偶次
if (++CNum % 2) //奇数次冲突
{
NewPos = CurrentPos + (CNum + 1)*(CNum + 1) / 4;//增量为+[(CNum+1)/2]^2
if (NewPos >= H->TableSize)
NewPos = NewPos % H->TableSize; //可能超出表大小,调整为合法地址
}
else //偶数次冲突
{
NewPos = CurrentPos - CNum*CNum / 4;//增量为-(CNum/2)^2
while (NewPos < 0)
NewPos += H->TableSize; //可能超出表大小,调整为合法地址
}
}
return NewPos;//此时NewPos或者是Key的位置,或者是一个空单元的位置(表示找不到)
}
散列表的类型是结构体,里面有两个元素,分别是表的最大长度(不是实际长度)和一个指针,指向存放散列表的数据的数组。
存放散列表中每一个元素数据也用结构体,一个表示存放的哪个数,另一个表示这个位置的状态。有三个状态,有元素,空,已删除。为什么有已删除这个类型呢,现在假设在查找某个不在散列表的元素,碰见被删除的元素应该继续查找,否则该位置之后可能会存在查找的元素,在插入元素的时候,空位与已删除相同,都可以插入元素。
typedef struct TblNode { //散列表类型
int TableSize; //表的最大长度
Cell *Cells; //存放散列单元数据的数组
}*HashTable;
//散列单元状态类型,分别对应:有合法元素、空单元、有已删除元素
typedef enum { Legitimate, Empty, Deleted } EntryType;
typedef struct HashEntry{ //散列表单元类型
ElementType Data; //存放元素
EntryType Info; //单元状态
}Cell;
采用开放定址法创建散列表,采用平方探测法实现插入与查找,完整代码如下:
#include<iostream>
using namespace std;
#define MAXTABLESIZE 100000 //允许开辟的最大散列表长度
typedef int ElementType; //关键词类型用整型
typedef int Index; //散列地址类型
typedef Index Position; //数据所在位置与散列地址是同一类型
//散列单元状态类型,分别对应:有合法元素、空单元、有已删除元素
typedef enum { Legitimate, Empty, Deleted } EntryType;
typedef struct HashEntry{ //散列表单元类型
ElementType Data; //存放元素
EntryType Info; //单元状态
}Cell;
//散列表类型
typedef struct TblNode { //散列表结点定义
int TableSize; //表的最大长度
Cell *Cells; //存放散列单元数据的数组
}*HashTable;
//返回大于N且不超过MAXTABLESIZE的最小素数
//给出的表的TableSize不一定是素数,我们实际分配的大小可以更大
int NextPrime(int N)
{
int i, p = (N % 2) ? N + 2 : N + 1;//从大于N的下一个奇数开始
while (p <= MAXTABLESIZE)
{
for (i = (int)sqrt(p); i>2; i--)
if (!(p%i)) break; //p不是素数
if (i == 2) break; //for正常结束,说明p是素数
else p += 2; //否则试探下一个奇数
}
return p;
}
HashTable CreateTable(int TableSize)
{
HashTable H;
int i;
H = (HashTable)malloc(sizeof(struct TblNode));
if (H == NULL)
{
cout<<"空间溢出!!!";
return NULL;
}
H->TableSize = NextPrime(TableSize); //保证散列表最大长度是素数
H->Cells = (Cell *)malloc(H->TableSize*sizeof(Cell));//声明单元数组
for (i = 0; i<H->TableSize; i++) //初始化单元状态为“空单元”
H->Cells[i].Info = Empty;
return H;
}
Position Hash(ElementType key,int TableSize)//自己设计的散列函数,除留余数法
{
return key%TableSize;
}
Position Find(HashTable H, ElementType Key)
{
Position CurrentPos, NewPos;
int CNum = 0; //记录冲突次数
NewPos = CurrentPos = Hash(Key, H->TableSize);//初始散列位置,哈希函数自己设计
//当该位置的单元非空,并且不是要找的元素时,发生冲突
while (H->Cells[NewPos].Info != Empty && H->Cells[NewPos].Data != Key)
{
//统计1次冲突,并判断奇偶次
if (++CNum % 2) //奇数次冲突
{
NewPos = CurrentPos + (CNum + 1)*(CNum + 1) / 4;//增量为+[(CNum+1)/2]^2
if (NewPos >= H->TableSize)
NewPos = NewPos % H->TableSize; //可能超出表大小,调整为合法地址
}
else //偶数次冲突
{
NewPos = CurrentPos - CNum*CNum / 4;//增量为-(CNum/2)^2
while (NewPos < 0)
NewPos += H->TableSize; //可能超出表大小,调整为合法地址
}
}
return NewPos;//此时NewPos或者是Key的位置,或者是一个空单元的位置(表示找不到)
}
bool Insert(HashTable H, ElementType Key)
{
Position Pos = Find(H, Key); //先检查Key是否已经存在
if (H->Cells[Pos].Info != Legitimate) //如果这个单元没有被占,说明Key可以插入在此
{
H->Cells[Pos].Info = Legitimate;
H->Cells[Pos].Data = Key;
return true;
}
else
{
cout<<"键值已存在";
return false;
}
}
bool Delete(HashTable H, ElementType Key)
{
Position Pos = Find(H, Key); //先检查Key是否已经存在
if (H->Cells[Pos].Info == Legitimate) //如果这个单元没有被占,说明Key可以插入在此
{
H->Cells[Pos].Info = Deleted;
return true;
}
else
{
cout<<"键值不存在";
return false;
}
}
void show(HashTable H)//按照排列好的哈希表打印每个元素
{
for (int i = 0; i < H->TableSize; i++)
{
if (H->Cells[i].Info == Legitimate)
cout << "Cell[" << i << "]:" << H->Cells[i].Data <<" ";
}
cout << endl;
}
int main()
{
int a[] = { 47, 7, 29, 11, 9, 84, 54, 20, 30 };
int lena = sizeof(a) / sizeof(a[0]);
HashTable tab = CreateTable(10);
cout << "tablesize:" << tab->TableSize<<endl<<endl;
for (int i = 0; i < lena; i++)
Insert(tab, a[i]);
int b[] = {11,30,20,47,84,7,29,9,54};
int lenb = sizeof(b) / sizeof(b[0]);
show(tab);
cout << endl;
Delete(tab, 30);
show(tab);
return 0;
}
在开放地址散列表中,删除操作要很小心。通常只能“懒惰删除”,即需要增加一个“删除标记(Deleted)”,而并不是真正删除它。以便查找时不会“断链”。其空间可以在下次插入时重用。
2.4 双散列探测法(Double Hashing)
双散列探测法: di 为i*h2(key),h2(key)是另一个散列函数
探测序列成:h2(key),2h2(key),3h2(key),……
- 对任意的key,h2(key) ≠ 0 !
- 探测序列还应该保证所有的散列存储单元都应该能够被探测到。
选择以下形式有良好的效果:
h2(key) = p - (key mod p)
其中:p < TableSize,p、TableSize都是素数。
2.5 再散列(Rehashing)
- 当散列表元素太多(即装填因子α太大)时,查找效率会下降;
实用最大装填因子一般取 0.5 <= α<= 0.85 - 当装填因子过大时,解决的方法是加倍扩大散列表,这个过程叫做“
再散列(Rehashing)
”
注意:散列表扩大时,原有元素需要重新计算放置到新表中
3、总结
- 冲突处理两种方法(开放定址法,分离链接法)
- 开放定址法四种方法(线性探测法,平方探测法,双散列探测法,再散列)
- 采用开放定址法创建散列表,平方探测实现插入,删除与查找的具体实现