参考博客:https://blog.csdn.net/weixin_38169413/article/details/81612307
一、概念
散列技术是在记录的存储位置和他的关键字之间建立一个确定的对应关系f,是的每个关键字key对应一个存储位置f(key)。查找时,根据这个对应的关系找到给定值key的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上。我们把这种对应关系f成为散列函数,又称为哈希(Hash)函数。采用散列技术将记录存储在一块连续的存储空间中,这块连续空间称为散列表或哈希表(Hash-Table)。
二、散列表的构造方法
2.1 直接定址法
直接定址法使用下面的公式
f
(
k
e
y
)
=
a
×
k
e
y
+
b
f(key) = a × key + b
f(key)=a×key+b
比如统计出生年份,那么就可以使用f(key)=key−1990 来计算散列地址。
2.2 除留取余法
这种方法是最常用的散列函数构造方法,对于表长为m的散列公式为
f
(
k
e
y
)
=
k
e
y
  
m
o
d
  
p
(
p
≤
m
)
f(key) = key\;mod \;p (p≤m)
f(key)=keymodp(p≤m)
mod相当于c++中的取余%
2.3 数字分析法
分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址这里使用一个手机号码作为例子,手机号码是一个字符串,一般的说,后面4位是真正的用户号。
2.4 折叠法
把关键词分割成位数相同的几个部分,然后叠加。
2.5 平方取中法
三、冲突解决方法
常用处理冲突的思路:
换个位置: 开放地址法
同一位置的冲突对象组织在一起: 链地址法
3.1 开放地址法
一旦产生了冲突(该地址已有其它元素),就按某种规则去寻找另一空地址.若发生了第 i 次冲突,试探的下一个地址将增加
d
i
d_{i}
di , 基本公式是:
h
i
(
k
e
y
)
=
(
h
(
k
e
y
)
+
d
i
)
  
m
o
d
  
T
a
b
l
e
S
i
z
e
  
(
1
≤
i
<
T
a
b
l
e
S
i
z
e
)
h_{i}(key) = (h(key)+d_{i})\; mod\; TableSize\; (1 ≤ i < TableSize)
hi(key)=(h(key)+di)modTableSize(1≤i<TableSize)
这里面
d
i
d_{i}
di决定了不同的解决冲突方案: **线性探测、平方探测。**下面依次介绍各中方法。
3.1.1 线性探测法
线性探测法以增量序列
1
,
2
,
…
…
,
(
T
a
b
l
e
S
i
z
e
−
1
)
1,2,……,(TableSize−1)
1,2,……,(TableSize−1) 循环试探下一个存储地址。
【例1】 设关键词序列为{47, 7, 29, 11, 9, 84, 54, 20, 30},散列表表长TableSize=11 (装填因子 α=9/13≈0.69 ),散列函数为:
h
(
k
e
y
)
  
=
  
k
e
y
  
m
o
d
  
11
h(key)\;=\;key\;mod\;11
h(key)=keymod11
用线性探测法处理冲突,列出依次插入后的散列表,并估算查找性能。
【解】 初步的散列地址如下表所示。
关键词(key) | 47 | 7 | 29 | 11 | 9 | 84 | 54 | 20 | 30 |
---|---|---|---|---|---|---|---|---|---|
散列地址h(key) | 3 | 7 | 7 | 0 | 9 | 7 | 10 | 9 | 8 |
可以看出,有多个关键词的散列地址发生了冲突,具体见下表。
关键词(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 | 3 | 1 | 3 | 6 |
具体的哈希表构建过程可以用下面的图表来表示
这里引出一下散列表的的查找性能分析,散列表的查找性能,一般使用下列方法:
成功平均查找长度(ASLs)
对于上面一题的散列地址冲突次数为
散列地址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)
A
S
L
s
=
(
1
+
7
+
1
+
1
+
2
+
1
+
4
+
2
+
4
)
/
9
=
23
/
9
≈
2.56
ASLs=(1+7+1+1+2+1+4+2+4)/9=23/9≈2.56
ASLs=(1+7+1+1+2+1+4+2+4)/9=23/9≈2.56
3.1.2 平方探测法
平方探测法以增量序列 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , q 2 , − q 2 1^{2},-1^{2},2^{2},-2^{2},...,q^{2},-q^{2} 12,−12,22,−22,...,q2,−q2,且 q ≤ ( T a b l e S i z e / 2 ) q≤(TableSize/2) q≤(TableSize/2)循环试探下一个存储地址。还是使用[例1],得到的冲突如下表
关键词(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 |
A
S
L
s
=
(
1
+
1
+
2
+
1
+
1
+
3
+
1
+
4
+
4
)
/
9
=
18
/
9
=
2
ASLs=(1+1+2+1+1+3+1+4+4)/9=18/9=2
ASLs=(1+1+2+1+1+3+1+4+4)/9=18/9=2
3.2 链地址法
链地址法就是将相应位置上冲突的所有关键词存储在同一个单链表中
【例2】 设关键字序列为
47
,
7
,
29
,
11
,
16
,
92
,
22
,
8
,
3
,
50
,
37
,
89
,
94
,
21
47,7,29,11,16,92,22,8,3,50,37,89,94,21
47,7,29,11,16,92,22,8,3,50,37,89,94,21 ,散列函数取为
h
(
k
e
y
)
=
k
e
y
  
m
o
d
  
11
h(key)=key\; mod\; 11
h(key)=keymod11,用分离链接法处理冲突。
【解】
表中有9个结点只需1次查找,5个结点需要2次查找,所以查找成功的平均查找次数为
A
S
L
s
=
(
9
+
5
∗
2
)
/
14
≈
1.36
ASLs=(9+5∗2)/14≈1.36
ASLs=(9+5∗2)/14≈1.36
效率比线性探测和平方探测都要高.
参考代码:
/*******************************************************************************
功 能:哈希表——链表法(+/- 1^2,2^2,3^2...)
创建时间: 2018-07-24
作 者:Elvan
修改时间: 2019-08-21
作 者:lzy
********************************************************************************/
#include <iostream>
#include <string>
#include <vector>
#include <cmath>
#include <malloc.h>
using namespace std;
#define MAXTABLESIZE 10000 //允许开辟的最大散列表长度
#define KEYLENGTH 100 //关键字的最大长度
typedef int ElementType;
struct LNode
{
ElementType data;
LNode *next;
};
typedef LNode *PtrToNode;
typedef PtrToNode LinkList;
struct TblNode
{
int tablesize; //表的最大长度
LinkList heads; //存放散列单元数据的数组
};
typedef struct TblNode *HashTable;
/*返回大于n且不超过MAXTABLESIZE的最小素数*/
int NextPrime(int n)
{
int p = (n % 2) ? n + 2 : n + 1; //从大于n的下一个奇数开始
int i;
while (p <= MAXTABLESIZE)
{
for (i = (int)sqrt(p); i > 2; i--)
{
if ((p % i) == 0)
break;
}
if (i == 2)
break; //说明是素数,结束
else
p += 2;
}
return p;
}
/*创建新的哈希表*/
HashTable CreateTable(int table_size)
{
HashTable h = (HashTable)malloc(sizeof(TblNode));
h->tablesize = NextPrime(table_size);
h->heads = (LinkList)malloc(h->tablesize * sizeof(LNode));
//初始化表头结点
for (int i = 0; i < h->tablesize; i++)
{
h->heads[i].next = NULL;
}
return h;
}
/*查找数据的初始位置*/
int Hash(ElementType key, int n)
{
//这里只针对大小写
return key % 11;
}
/*查找元素位置*/
LinkList Find(HashTable h, ElementType key)
{
int pos;
pos = Hash(key, h->tablesize); //初始散列位置
LinkList p = h->heads[pos].next; //从链表的第一个节点开始
while (p && key != p->data)
{
p = p->next;
}
return p;
}
/*插入新的元素*/
bool Insert(HashTable h, ElementType key)
{
LinkList p = Find(h, key); //先查找key是否存在
if (!p)
{
//关键词未找到,可以插入
LinkList new_cell = (LinkList)malloc(sizeof(LNode));
new_cell->data = key;
int pos = Hash(key, h->tablesize);
new_cell->next = h->heads[pos].next;
h->heads[pos].next = new_cell;
return true;
}
else
{
cout << "键值 "<<key<<" 已存在!" << endl;
return false;
}
}
/*移除元素*/
bool Remove(HashTable h, ElementType key)
{
LinkList p0 = Find(h,key);
if(p0)
{
int pos = Hash(key, h->tablesize);
LinkList p = h->heads[pos].next; //从链表的第一个节点开始
if(p == p0)//当该值是链表的第一个节点
{
h->heads[pos].next = p->next;
p->next = NULL;
free(p);
cout<< "已删除元素"<<key<<endl;
return true;
}
while (p && p->next != p0)
{
p = p->next;
}
p->next = p0->next;
p0->next = NULL;
free(p0);
cout<< "已删除元素"<<key<<endl;
return true;
}
else
{
cout<< "没有该元素: "<<key<<endl;
return false;
}
}
/*销毁链表*/
void DestroyTable(HashTable h)
{
int i;
LinkList p, tmp;
//释放每个节点
for (i = 0; i < h->tablesize; i++)
{
p = h->heads[i].next;
while (p)
{
tmp = p->next;
free(p);
p = tmp;
}
}
free(h->heads);
free(h);
cout<<"链表已销毁"<<endl;
}
/*打印哈希表*/
void Print(HashTable h)
{
cout<<"*****HashTable*****"<<endl;
for (int i = 0; i < h->tablesize; i++)
{
LinkList p = h->heads[i].next;
while (p)
{
cout << p->data << " "; //打印哈希表元素
p = p->next;
}
cout << endl;
}
}
int main(int argc, char const *argv[])
{
int a[] = {47, 7, 29,29, 11, 16, 92, 22, 8, 3, 50, 37, 89, 94, 21};
int n = 15;
HashTable h = CreateTable(n);
for (int i = 0; i < n; i++)
{
Insert(h, a[i]); //插入元素
}
cout<<"移除元素前"<<endl;
Print(h);
Remove(h,50);
cout<<"移除元素后"<<endl;
Print(h);
DestroyTable(h);
return 0;
}
执行代码后的结果如下图所示