散列表
知识点讲解可直接点击这里
代码参考:
https://blog.csdn.net/qq_35644234/category_6521704.html?spm=1001.2014.3001.5482
【问题】如何快速搜索到需要的关键词?如果关键词不方便比较怎么办?
查找的本质:已知对象找位置
- 有序安排对象:全序、半序
- 直接 “算出” 对象位置:散列
散列查找法的两项基本工作:
- 计算位置:构造散列函数确定关键词存储位置
- 解决冲突:应用某种策略解决多个关键词位置相同的问题
时间复杂度几乎是常数:O(1) ,即查找时间与问题规模无关!
1. 什么是散列函数
装填因子(Loading Factor):设散列表空间大小为m,填入表中元素个数是n,则称α = n / m
为散列表的装填因子。
“散列(Hashing)” 的基本思想:
- 以关键字key为自变量,通过一个确定的函数h(散列函数),计算出对应的函数值h(key),作为数据对象的存储地址
- 可能不同的关键字会映射到同一个散列地址上,即
h(key i) = h(key j) (当key i ≠ key j)
,称为“冲突(collision)”。——需要某种冲突解决策略
2. 散列函数的构造方法
一个 “好” 的散列函数一般应考虑下列两个因素:
- 计算简单,以便提高转换速度
- 关键词对应的地址空间分布均匀,以尽量减少冲突
2.1 数字关键词 的散列函数构造
-
直接定址法
取关键词的某个线性函数值为散列地址,即
h(key) = a * key + b
(a、b为常数) -
除留余数法
散列函数为:
h(key) = key mod p
- 这里:p = tableSize = 17
- 一般,p取素数
-
分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址
-
折叠法
把关键词分割成位数相同的几个部分,然后叠加
-
平均取中法
2.2 字符关键词 的散列函数构造
3. 冲突处理方法
常用处理冲突的思路:
- 换个位置:开放地址法
- 同一个位置的冲突对象组织在一起:链地址法
一旦产生了冲突(该地址已有其他元素),就按某种规则去寻找另一空地址。
- 若发生了第 i 次冲突,试探的下一个地址将增加
di
,基本公式:hi(key) = (h(key) + di) mod tableSize
(1 ≤ i < tableSize)。 di
决定了不同的解决冲突方案:线性探测(di = i
)、平方探测(di = ±i²
)、双散列(di = i*h2(key)
)。
-
线性探测法:以增量序列 1,2,… …,(tableSize - 1) 循环试探下一个存储地址。
-
平方探测:以增量序列 1²,-1²,2²,-2²,… …,q²,-q² 且q ≤[tableSize / 2] 循环试探下一个存储地址
有定理显示:如果散列表长度tableSize是某个 4k + 3 (k是正整数)形式的素数时,平方探测法就可以探查到整个散列表空间。
-
分离链接法:将相应位置上的冲突的所有关键词存储在同一个单链表中
4. 散列表的性能分析
-
平均查询长度(ASL)用来度量散列表查找效率:成功、不成功
-
关键词的比较次数,取决于产生冲突的多少
影响产生冲突多少有以下三个因素:
- 散列函数是否均匀
- 处理冲突的方法
- 散列表的填装因子α
4.1 线性探测法的查找性能
可以证明,线性探测法的期望探测次数 满足下列公式:
4.2 平方探测法和双散列探测法的查找性能
可以证明,平方探测法和双散列探测法探测次数 满足下列公式:
4.3 分离链接法的查找性能
所有地址链表的平均长度定义成 装填因子α,α有可能超过1。
可以证明:其期望探测次数p为:
4.4 总结&比较
- 选择合适的 h(key),散列法的查找效率期望是 常数O(1),它几乎与关键字的空间的大小n无关!也适合于关键字直接比较计算量大的问题
- 它是以 较小的α为前提。因此,散列方法是一个以 空间换时间
- 散列方法的存储对关键字是随机的,不便于 顺序查找关键字,也不适合于 范围查找,或 最大值最小值查找。
开放地址法:
- 优点:散列表是一个数组,存储效率高,随机查找
- 缺点:散列表有 “聚集” 现象
分离链法:
- 散列表是顺序存储和链式存储的结合,链表部分的存储效率和查找效率都比较低
- 优点:关键字删除不需要 “懒惰删除” 法,从而没有存储 “垃圾” 。
- 缺点:太小的 α 可能导致空间浪费,大的 α 又将付出更多的时间代价。不均匀的链表长度导致时间效率的严重下降。
5. 实现一个散列表
5.1第一次实现
#include <iostream>
using namespace std;
#define MAXSIZE 100
typedef int ElemType;
struct HashTable {
ElemType* data; // 散列表存储数据数组
int count; // 插入数据的数量
int size; // 散列表的最大容量
};
void initHash(HashTable& H)
{
H.size = MAXSIZE;
H.count = 0;
H.data = new ElemType[H.size];
for (int i = 0; i < H.size; ++i) {
H.data[i] = -99999;
}
}
// 求散列函数,这里用 除留余数法
// 插入操作
bool insertHash(HashTable& H, ElemType key)
{
if (H.count == H.size) {
return false;
}
int index = key % H.size; // 计算散列地址
// 判断是否有冲突
while (H.data[index] != -99999) {
index = (index + 1) % H.size;
}
// 插入值
H.data[index] = key;
H.count++;
}
// 查找并返回索引位置
bool searchHash(HashTable& H, ElemType key, int& location)
{
int index = key % H.size; // 计算散列地址
// 判断求出的散列地址是否为该值
while (H.data[index] != key) {
index = (index + 1) % H.size;
// 不存在的情况,即一种散列地址处的值为初始值,另一种循环一圈到原位置
if (H.data[index] == -99999 || index == key % H.size) {
return false; // 说明关键字不存在
}
}
location = index; // 存在,索引位置
return true;
}
5.2 第二次实现
第一次实现的效果并不好,下面是第二次实现的成果
1. 开放地址法
/* !!!线性探测 & 平方探测!!!*/
#include<iostream>
using namespace std;
typedef int keyType;
// 质数的集合
int hashsize[] = { 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107,
109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223,
227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337,
347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457,
461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593,
599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719,
727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857,
859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997 };
// 每次存储关键字空间的大小都是取质数 最接近的质数
struct Hash {
keyType* data; // 记录哈希表中的元素
bool* isfull; // 记录哈希表是否有元素了,标志位--------true说明不为空,false说明为空
int count; // 哈希表中已存放元素的个数
int sizeindex; // 对应的最接近的质数索引~Hashsize
};
void createHash(Hash& H) {
H.count = 0;
H.sizeindex = 0;
H.data = new keyType[hashsize[H.sizeindex]]; // 初始哈希表的长度
H.isfull = new bool[hashsize[H.sizeindex]];
for (int i = 0; i < hashsize[H.sizeindex]; ++i) {
H.isfull[i] = false;
}
}
// 线性探测---查找对应的关键字
// p为最后插入的索引位置;c为偏移;hashsize为质数的集合--方便我们的计算
bool searchHash(Hash H, keyType key, int& p, int* hashsize) {
int c = 0;
p = key % hashsize[H.sizeindex]; // 计算要插入位置的索引
while (H.isfull[p] && H.data[p] != key && c < hashsize[H.sizeindex] - 1) {
++c;
//继续往下寻找下一个散列结点
p = (key + c) % hashsize[H.sizeindex];
}
if (H.data[p] == key) {
return true; // 查找成功
}
return false; // 查找失败
}
// 平方探测---查找对应的关键字
bool searchHash(Hash H, keyType key, int& p, int* hashsize) {
int c = 0;
p = key % hashsize[H.sizeindex]; // 计算要插入位置的索引
// 0^2,1^2,-1^2,2^2,-2^2,3^2,-3^2,4^2,-4^2...,q^2,-q^2),q<=m/2,m为散列表的长度
while (H.isfull[p] && H.data[p] != key && c < hashsize[H.sizeindex] - 1) {
if (c == 0) {
c = 1;
p = (key + c) % hashsize[H.sizeindex];
}
else if (c > 0) {
c = -c;
p = (key - c * c) % hashsize[H.sizeindex];
}
else { // (c < 0)
c = -c + 1;
p = (key + c * c) % hashsize[H.sizeindex];
}
}
if (H.data[p] == key) {
return true; // 查找成功
}
return false; // 查找失败
}
bool insertHash(Hash& H, keyType key, int* hashsize) {
int p; // 保存插入时索引的位置
if (searchHash(H, key, p, hashsize)) { // 此时key已存在,不需要再插入
return false;
}
// 下面的if语句判断在处理平方探测的时候还是有些问题的!
else if (H.count == hashsize[H.sizeindex] - 1) {// 此时哈希表已经满,得重新分配了
// 首先是把之前的内容保存起来
++H.sizeindex;
keyType* data = new keyType[hashsize[H.sizeindex]];
bool* isfull = new bool[hashsize[H.sizeindex]];
for (int i = 0; i < hashsize[H.sizeindex - 1]; ++i) {
data[i] = H.data[i];
isfull[i] = H.isfull[i];
}
// 给新增加的标志位 添加标志
for (int j = hashsize[H.sizeindex - 1]; j < hashsize[H.sizeindex]; ++j) {
H.isfull[j] = false;
}
delete[] H.data;
delete[] H.isfull;
H.data = data;
H.isfull = isfull;
}
else {
// 插入key
H.data[p] = key;
++H.count;
H.isfull[p] = true;
}
}
bool DeleteHash(Hash& H, keyType key, int* hashsize) {
int p;
if (!searchHash(H, key, p, hashsize)) {//没找到要删除的元素
return false;
}
else {
H.isfull[p] = false;
--H.count;
}
}
void print(Hash H, int* hashsize) {
cout << "当前的哈希表的长度:" << hashsize[H.sizeindex] << endl;
cout << "哈希表存有元素的个数:" << H.count << endl;
cout << "打印整个哈希表:" << endl;
for (int i = 0; i < hashsize[H.sizeindex]; ++i) {
if (H.isfull[i]) {
cout << H.data[i] << " ";
}
else {
cout << "空" << " ";
}
}
cout << endl;
}
2.链地址法
/*!!!链地址法!!!*/
#include<iostream>
using namespace std;
typedef int keyType;
// 质数的集合
int hashsize[] = { 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107,
109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223,
227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337,
347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457,
461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593,
599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719,
727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857,
859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997 };
struct Node {
keyType data; //每个结点的值
Node* next; //下一个结点
Node() {
next = nullptr;
}
};
// 每次都是取质数最接近的质数
struct Hash {
Node* elem; // 头结点链表
int sizeindex;
};
// cur指向key所在的当前结点, pre指向~前一结点
bool searchHash(Hash H, keyType key, Node*& cur, Node*& pre, int* hashsize) {
int index = key % hashsize[H.sizeindex];
Node* head = H.elem[index].next;
pre = nullptr;
while (head) { //遍历该链表
cur = head;
if (head->data == key) {
return true;
}
pre = head;
head = head->next;
}
return false;
}
// 插入
bool insertHash(Hash& H, keyType key, int* hashsize) {
Node* cur = nullptr;
Node* pre = nullptr;
if (searchHash(H, key, cur, pre, hashsize)) {
return false;
}
else {
Node* s = new Node;
s->data = key;
if (pre == nullptr) {
int index = key % hashsize[H.sizeindex];
H.elem[index].next = s;
return true;
}
cur->next = s;
}
return true;
}
//删除结点
bool deleteHash(Hash& H, keyType key, int* hashsize) {
Node* cur = nullptr;
Node* pre = nullptr;
if (!searchHash(H, key, cur, pre, hashsize)) {
return false;
}
else {
if (pre == nullptr) {
int index = key % hashsize[H.sizeindex];
H.elem[index].next = cur->next;
}
else {
pre->next = cur->next;
}
delete cur;
return true;
}
}
void print(Hash H, int* hashsize) {
Node* p = nullptr;
for (int i = 0; i < hashsize[H.sizeindex]; ++i) {
cout << i << " : ";
p = H.elem[i].next;
while (p) {
cout << p->data << " ";
p = p->next;
}
cout << "空" << endl;
}
}