散列表:![](https://i-blog.csdnimg.cn/blog_migrate/484abd97ae6a6a943fba23562e9d894a.png)
能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构。也就是说,把关键字映射到表中的位置来直接访问记录,以加快访问速度。映射的方法称为散列函数,在映射过程中,不同的键值可能映射到同一位置,产生冲突。可用不同方式解决冲突。
散列函数:
1、除留余数法: H(key) = key mod P
H(key) 为键值key 通过映射函数得到在表中记录的位置
P为质数且一般为 小于或等于散列表大小(maxSize)的最大质数 或者不包含小于20质因子的合数
2、直接定址法: H(key) = key 或 H(key) = a * key +b
只能用于已知关键字并且查找表较小且连续的情况(不然浪费严重)
3、数字分析法:
在关键字位数比较大的情况下,选择关键字重复较少的几位数字作为关键字
4、平方取中法:
适用于不知道关键字的分布,而关键字位数不是很大的情况
5、折叠法:
将关键字从左到右分成几部分,最后一部分位数不够可稍短,将这几部分求和,再根据散列表的长度选择最后几位作为散列地址
不需要知道关键字的分布,适用于关键字位数较多的情况
6、随机数法:
关键字长度不等时,较适合采用
解决冲突:
1、开放寻址法:
1.1、线性探测:当键值映射到散列表的地址发生冲突时,映射的地址向右移动,直到找到空的地址再存入。( H(key) + i ) mod maxSize i = {1,2,3, ... i<maxSize}
1.2、二次探测:跟线性探测相似,只不过线性探测只向右一个方向探测,而二次探测为先右后左探测 ( H(key) + i ) mod maxSize i = {1,-1,2,-2, ... i<maxSize/2}
1.3、双重探测:不只运用一个散列函数,而是用到一组散列函数。当散列函数1给键值映射发生冲突时,调用散列函数2......直到不发生冲突
2、链表法:
链表法是一种更加常用的散列冲突解决办法,在散列表中,每个散列地址存放一条链表,所有散列值相同的元素都放到对应的链表中。
3、高级的动态扩容:
当装载因子(存放的键值数 / 散列表的大小)越大时,散列冲突的概率就越大。使得插入数据时要多次寻址或者拉很长的链,相应查找的过程也变得很慢。
动态扩容:当装载因子达到某个阈值时,将原散列表搬移到新的散列表(散列表更大)
阈值:过大频繁冲突,过小频繁扩容
因新的散列表的大小改变,导致原散列表的键值在新散列表的地址不再与原来相同,需重新散列。
如果刚扩容时,就一次性就把原散列表的键值搬到新的散列表中,当原散列表很大时,便很浪费时间。
高效扩容:
在扩容后,保留原散列表,在以后新散列表的存入键值时,同时将原散列表的一个键值存入,将原散列表慢慢的转存到新的散列表中,当原散列表转存完后,再将原散列表释放。
除留余数法+开放寻址法:
#define HashSize 20
#define INF 32767 // 标记作用,记录该位置是否有键值存放,也可以其他形式
typedef int keyType;
// 哈希表
struct HashTable {
HashTable(int size = HashSize) :maxSize(size), num(0) {
Key = new keyType[maxSize];
for (int i = 0; i < maxSize; ++i) {
Key[i] = INF;
}
}
keyType* Key; // 关键字数组
int maxSize;
int num;
};
// 找到合适质数
int PrimerNum[10] = { 7, 11, 17, 23,31, 41, 47,61, 71 ,97 };
int FindNum(keyType key, int* Num = PrimerNum) {
int num = 5;
for (int i = 0; i < 10; ++i) {
if (Num[i] < key) {
num = Num[i];
}
}
return num;
}
// 除留余数法法 + 开放定址法
bool HashPush(HashTable &hash, keyType key) {
if (hash.num == hash.maxSize) {
return false;
}
int div = FindNum(hash.maxSize);
// 除留余数法
int index = key % div;
if (INF == hash.Key[index]) {
hash.Key[index] = key;
hash.num++;
return true;
}
// 冲突解决:开放定址法 (二次探测)
int index1, index2;
index1 = index2 = index;
// 散列表没满必能找到空地址
while (INF != hash.Key[index1] && INF != hash.Key[index2]) {
index1 = (index1 + 1 + hash.maxSize) % hash.maxSize;
index2 = (index2 - 1 + hash.maxSize) % hash.maxSize;
}
if (INF == hash.Key[index1]) {
hash.Key[index1] = key;
}
else {
hash.Key[index2] = key;
}
hash.num++;
return true;
}
// 查找
bool HashFind(HashTable &hash, keyType key) {
int div = FindNum(hash.maxSize);
int index = key % div;
if (key == hash.Key[index]) {
return true;
}
int index1 = index;
int index2 = index;
for (int i = 0; i < hash.maxSize / 2; ++i) {
// 向右
index1 = (index + i + hash.maxSize) % hash.maxSize;
if (key == hash.Key[index1]) {
return true;
}
else if (INF == hash.Key[index1]) {
return false;
}
// 向左
index2 = (index2 - i + hash.maxSize) % hash.maxSize;
if (key == hash.Key[index2]) {
return true;
}
else if(INF==hash.Key[index2]) {
return false;
}
}
return false;
}
除留余数法+链式法:
#define hash_size 20
#define INF 32767
#define div 7
typedef int keyType;
class HashNode {
public:
HashNode(int KEY = INF) :key(KEY), next(NULL) {}
keyType key;
HashNode* next; // 拉链法
};
struct HashTable {
public:
HashTable(int size = 20) :maxSize(size), num(0) {
Key = new HashNode[maxSize];
}
HashNode* Key;
int maxSize;
int num;
};
bool HashPush(HashTable &hash, keyType key) {
if (hash.num == hash.maxSize) {
return false;
}
int index = key % div; // 除留余数法
if (INF == hash.Key[index].key) {
hash.Key[index].key = key;
hash.num++;
return true;
}
// 解决冲突: 链表法(拉链法)
HashNode* T = &hash.Key[index];
while (T->next) { // 找到链尾
T = T->next;
}
T->next = new HashNode(key);
hash.num++;
return true;
}
// 寻找
bool HashFind(HashTable &hash, keyType key) {
int index = key % div; // 除留余数法
if (key == hash.Key[index].key) {
return true;
}
HashNode* T = hash.Key[index].next;
while (T) {
if (T->key == key) {
return true;
}
T = T->next;
}
return false;
}