第11章-散列表
最后更新于:2022-04-01 07:35:33
# 一、概念
### 1.综述
散表表仅支持INSERT、SEARCH、DELETE操作。
把关键字k映射到槽h(k)上的过程称为散列。
多个关键字映射到同一个数组下标位置称为碰撞。
好的散列函数应使每个关键字都等可能地散列到m个槽位中
### 2.散表函数
(1)若函数为h(k)=k,就是直接寻址表
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-02_56b02bd0b042a.gif)
(2)除法散列法:h(k) = k mod m
(3)乘法散列法:h(k) = m * (k * A mod 1) (0<A<1)
(4)全域散列:从一组仔细设计的散列函数中随机地选择一个。(即使对同一个输入,每次也都不一样,平均性态较好)
3.冲突解决策略
(1)链接法
(2)开放寻址法
a.线性探测:h(k, i) = (h'(k) + i) mod m
b.二次探测:h(k, i) = (h'(k) + c1*i + c2 *i^2) mod m
c.双重散列:h(k, i) = (h1(k) + i * h2(k)) mod m
(3)完全散列:设计一个较小的二次散列表
# 二、代码
代码中用到了函数指针,函数指针的用法参考[函数指针总结](http://blog.csdn.net/mishifangxiangdefeng/article/details/7209060)
~~~
//Hash.h
#include <iostream>
using namespace std;
int m, NIL = 0;
//11.4 开放寻址法
typedef int (*Probing)(int k, int i);
int h(int k)
{
return k % m;
}
int h2(int k)
{
return 1 + k % (m-1);
}
//线性探测
int Linear_Probing(int k, int i)
{
return (h(k) + i) % m;
}
//二次探测
int Quadratic_Probint(int k, int i)
{
int c1 = 1, c2 = 3;
return (h(k) + c1 * i + c2 * i * i) % m;
}
//双重探测
int Double_Probint(int k, int i)
{
return (h(k) + i * h2(k)) % m;
}
int Hash_Insert(int *T, int k, Probing p)
{
int i = 0, j;
do{
j = p(k, i);
if(T[j] == NIL)
{
T[j] = k;
return j;
}
i++;
}
while(i != m);
cout<<"error:hash table overflow"<<endl;
}
int Hash_Search(int *T, int k, Probing p)
{
int i = 0, j;
while(1)
{
j = p(k, i);
if(T[j] == NIL || i == m)
break;
if(T[j] == k)
return j;
i++;
}
}
~~~
# 三、习题
### 11.1 直接寻址表
~~~
11.1-1
O(m),最坏情况时,集合中只有一个最小关键字
11.1-2
如果插入x,就把向量的第x位置为1
如果删除x,就把向量的第x位置为0
11.1-3
当存在关键字为key的卫星数据时,T[key]指向其中一个关键字为key的卫星数据。若不存在,T[key]指向空。
每一个卫星数据用一个结点表示,结点中有三个域,分别是:关键字key,卫星数据data,指向下一个所有相同关键字的卫星数据的指针next
DIRECT-ADDRESS-SEARCH(T, k)
return T[k];
DIRECT-ADDRESS-INSERT(T, x)
if T[key[x]] = NULL
then T[key[x]] <- x
else next[x] <- next[T[key[x]]]
next[T[key[x]]] <- x
~~~
11.1-4 见[算法导论-11.1-4](http://blog.csdn.net/mishifangxiangdefeng/article/details/7709567)
用一个栈来存储实际插入的数据,插入时栈向上扩展一个空间,删除时,用栈顶元素补充被删除元素的位置,栈向下回收一个空间,方法类似于P127-11.3-4.
这个非常大的数组中不直接存放数据,而是存放数据在Stack中的位置。
对于一个元素p,如果H[p] < 栈中总元素的个数 && p = Stack[H[p]],就是存入,否则就是不存在
### 11.2 散列表
11.2-3
所有操作的时间都是O(n/2),n是一个链表的长度
11.2-4 见[算法导论-11.2-4](http://blog.csdn.net/mishifangxiangdefeng/article/details/7712094)
插入操作时,从自由链表中取出一个空闲slot,填入关键字x,修改指针,链表相应的队列中,具体可以分为以下几种情况:
(1)x所属的slot未被占用,则
step1:把这个slot从自由链表中移出
step2:填入关键字x
step3:修改指针,在这种情况下其next和pre都置为-1
(2)x所属的slot已经被占用,令占用这个slot的关键是y,y也属于这个slot,则
step1:从自由链表中取出一个空闲的slot,这个slot肯定不是x所属的slot,只是拿过来用
step2:填入关键字x
step3:修改指针,把slot链表入到“以x所属的slot为头结点的队列”中
(3)x所属的slot已经被占用,令占用这个slot的关键是y,y不属于这个slot,通过(2)可知,这个情况是有可能的
step1:从自由链表中取出一个空闲的slot,这个slot肯定不是x所属的slot,也不是y所属的slot,只是拿过来用
step2:在新slot中填入关键字y,修改指针,让y使用这个新slot,而把原来的slot空出来还给x
step3:在x所属的slot中填入关键字x
step4:修改“x所属的slot”指针,类似(1)-step3
删除操作时,令待删除的关键字是x,释放x所占用的slot,具体可以分为以下几种情况
(1)x所占用的slot正是x所属的slot,且slot->next=-1,即所有关键字中只有x属于这个slot,x被删除后,slot就空闲了
step1:释放slot到自由链表中
(2)x所占用的slot正是x所属的slot,但还有别的关键字中只有x属于这个slot,应该优先使用关键所属于的slot,而释放“不自己关键字的、临时拿过来用的”slat
step1:从以slot为头结点的队列中另选一个slot2,slot2的关键字属于slot而不属于slot2,只是因为slot被占用,所以才用slot2
step2:把slot2的内容填入slot
step3:修改指针,让slot代替slot2存在于队列中,不同的是slot还是队列头
step4:释放slot2到自由链表中
(3)x所占用的slot不是x所属的slot,这个种情况下,这个slot一定不是队列头,还有别的关键字存在于队列中,并且占用了x所属的slot
step1:把x所占用的slot从“以x所属的slot为头的队列”中移出
step2:释放slot到自由链表中
查找操作,如果理解了插入和删除,查找操作就比较简单了,令待查找的关键字是x,也可分为几种情况
(1)x所属的slot未被占用,即不存在与x相同slot的关键字,当然也不存在x了
(2)x所属的slot被占用了,但它所存的关键不属于这个slot,与(1)相同,不存在与x相同slot的关键字
(3)x所属的slot被占用了,且它所存的关键属于这个slot,即存在与x相同slot的关键字,只是不知这个关键字是不是x,需要进一步查找
### 11.3 散列函数
11.3-1
从散列表中的第h(k)个表中搜索关键字为k的一项
11.3-2
每处理字符串中的一个字符,就取一次模
11.3-4
~~~
#include <cmath>
int main()
{
int n;
while(cin>>n)
{
double A = (sqrt(5.0)-1) / 2;
double t1 = A * n;
int t2 = (int)t1;
double t3 = t1 - t2;
int t4 = t3 * 1000;
cout<<t4<<endl;
}
}
~~~
h(61) = 700
h(62) = 318
h(63) = 936
h(64) = 554
h(65) = 172
### 11.4 开放寻址法
11.4-1
线性探测:22 88 0 0 4 15 28 17 59 31 10
二次探测:22 0 88 17 4 0 28 59 15 31 10
双重探测:22 0 59 17 4 15 28 88 0 31 10
代码如下:代码中用到了函数指针,函数指针的用法参考[函数指针总结](http://blog.csdn.net/mishifangxiangdefeng/article/details/7209060)
~~~
#include <iostream>
#include <string>
#include "Hash.h"
using namespace std;
void Print(int *T)
{
int i;
for(i = 0; i < m; i++)
cout<<T[i]<<' ';
cout<<endl;
}
int main()
{
string str;
int i, j;
m = 11;
int T[11], A[9] = {10, 22, 31, 4, 15, 28, 17, 88, 59};
Probing P[3] = {Linear_Probing, Quadratic_Probint, Double_Probint};
for(i = 0; i < 3; i++)
{
memset(T, 0, sizeof(T));
for(j = 0; j < 9; j++)
Hash_Insert(T, A[j], P[i]);
Print(T);
}
return 0;
}
~~~
11.4-2
删除一个元素后,将这个位置置为DELETED,在插入操作中,L3改为if T[j] = NIL or T[j] = DELETED
~~~
#define DELETED -1
int Hash_Insert(int *T, int k, Probing p)
{
int i = 0, j;
do{
j = p(k, i);
if(T[j] == NIL || T[j] == DELETED)
{
T[j] = k;
return j;
}
i++;
}
while(i != m);
cout<<"error:hash table overflow"<<endl;
}
void Hash_Delete(int *T, int k, Probing p)
{
int j = Hash_Search(T, k, p);
if(j != NIL)
T[j] = DELETED;
}
~~~