散列表–c++实现
散列表描述
理想散列
字典的另一种表示方法——散列(hashing)。它用一个散列函数(也称哈希函数)把字典的键值对映射到一个散列表(也称哈希表)的具体位置。
例如:对于字典元素p<k,v>,若散列函数为f,那么在理想状况下p在散列表中的位置为f(k)。
可用理想散列的情况:需记录的字典元素中的关键字范围与元素个数相差不大。
例:有100个字典元素,它们的关键字范围是1200到1300,则可以使用函数f(k) = k - 1200来把字典元素映射到散列表的位置0到100之间
不可用理想散列的情况:需记录的字典元素中的关键字范围远大于元素个数。
例:有100个字典元素,它们的关键字范围从10000到99999,则使用函数f(k) = k - 10000,而f 的值域为[0,89999],那么散列表需要有90000个位置,但其中只存放了100个字典元素,显然使用如此长的散列表是不明智的,不仅浪费空间,而且要把90000个元素初始化为null也需要很长时间。
散列函数和散列表
1. 桶和起始桶
当关键字的范围太大,不能用理想方法表示时,可以采用并不理想的散列表和散列函数:散列表位置的数量比关键字的个数少,散列函数把若干个不同的关键字映射到散列表的同一个位置。散列表的每一个位置叫做一个桶;对关键字为k的键值对,f(k) 是起始桶 ;桶的数量等于散列表的长度或大小。因为散列函数可以把若干个关键字映射到同一个桶,所以桶要能够容纳多个键值对。此处仅考虑两种极端情景,第一种情况是每一个桶只能储存一个键值对,第二种情况就是每一个桶都是一个可以容纳全部键值对的线性表。
2. 除法散列函数
在多种散列函数中,最常用的是除法散列函数,它的形式如下:
f(k) = k % D
其中k是关键字,D是散列表的长度(即桶的数量),%为求模运算符。散列表的位置索引从0到D - 1。
3. 冲突和溢出
假设一个散列表有11个桶,序号从0到10,而且每一个桶可以存储一个键值对,使用除法散列函数,对于关键字为40,58的两个键值对,但通过散列函数计算的散列表位置都是3,这个时候就发生了冲突。当两个不同的关键字所对应的起始桶相同时,就是冲突发生了。
因为一个桶可以存储多个键值对,因此发生冲突也没什么。只要起始桶足够大,所有对应同一个起始桶的键值对都可存储在一起。如果存储桶没有空间存储一个新键值对,就是溢出发生了。
在我们的假设中,每个桶只能存储一个键值对,因此冲突和溢出同时发生,若关键字为40的键值对已在散列表索引为3的位置处,那么关键字为58的键值对无法放入起始桶。这类问题由溢出处理方法来解决。最常用的方法是线性探查法(待会讨论)。
4. 好的散列函数
单论冲突而言不可怕,可怕的是它会带来溢出,除非一个桶可以容纳无限多个数对,否则插入时的溢出就难以处理。当映射到散列表中任何一个桶里的关键字数量大致相等时,冲突和溢出的平均数最少。均匀散列函数便是这样的函数。
均匀散列函数:假定散列有b个桶,且b>1,桶的序号从0到b-1。如果对所有的k,散列函数f(k) = 0,那么 f(k) 就不是一个均匀散列函数,因为它把所有的关键字都映射到一个0号桶里。这样的散列函数使冲突和溢出的数量最大。假设b = 11,关键字范围为[0,98]。一个均匀散列函数应该把大约每9个关键字映射到一个桶里。函数*f(k) =k%*对范围[0,r]内的关键字是均匀散列函数,其中r是正整数。
在实际应用中,关键字不是从关键字范围内均匀选择的,所以有的均匀散列函数表现好一些,有的差一些。那些在实际应用中性能表现好的均匀散列函数被称作良好散列函数。
线性探查
- 方法
现有一个长度为8的散列表如下
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
关键字为12,48,57的键值对通过除法散列函数插入散列表中,得到如下表格
48 | 57 | 12 | |||||
---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
此时如果想把关键字为28的键值对插入散列表中显然与12冲突并产生溢出,而处理最简单的方式是找到下一个可用的桶,这种解决溢出的方法叫做线性探查。
28因此被储存在5号桶,如下表
48 | 57 | 12 | 28 | ||||
---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
以此类推,所以在寻找可用桶时,把散列表当做一个环形表。
由此,可以设计散列表的搜索方法。假设要查找关键字为k的键值对,首先搜索起始桶f(k),然后把散列表当做环形表继续搜索下一个桶,知道以下情况之一发生为止:
a. 存有关键字k的桶已找到,即找到了要查找的键值对;
b.到达一个空桶;
c.回到起始桶f(k);
后两种情况说明关键字为k的键值对不存在。
删除一个键值对后要保证上述的搜索过程可以正常成进行,例如下散列表
48 | 57 | 11 | 12 | 28 | 19 | ||
---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
将28删除后,若是仅将散列表五号位置置为空,则无法搜索到19,因为通过19 % 8 = 3,然后从3号位置开始寻找19,在五号位置时便会发现空桶。所以在删除28时,除了将五号位的桶置为空,还需移动若干个桶(关键字与序列不匹配的),比如此处需要移动19到五号位置的桶处,直到到达一个空桶或回到删除位置为止。
散列表类的原型
在HashTable.h文件中,定义了pair节点,声明了HashTable类。
template<class K, class E>
struct pair {
K key;
E value;
};
template<class K, class E>
class HashTable {
private:
pair<K, E> **table;
int size;
int length;
public:
HashTable(int length); //构造函数
~HashTable(); //析构函数
pair<K, E> *find(const K &key) const; //根据关键字返回键值对的指针
int search(const K &key) const; //根据关键字返回键值对在散列表的索引
void insert(const pair<K, E> &pair); //散列表的插入
void del(const K &key); //根据关键字删除键值对
int getSize() const; //返回散列表键值对个数
};
散列表的实现
#include "HashTable.h"
template<class K, class E>
HashTable<K, E>::HashTable(int length) {
this->length = length;
size = 0;
table = new pair<K,E>* [length];
for (int i = 0; i < length; ++i) {
table[i] = nullptr;
}
}
template<class K, class E>
HashTable<K, E>::~HashTable() {
for (int i = 0; i < length; ++i) {
if(table[i] != nullptr) {
delete table[i];
table[i] = nullptr;
}
}
delete table[length];
}
template<class K, class E>
pair<K, E> *HashTable<K, E>::find(const K &key) const {
int i = key % length;
int j = i;
do {
if(table[j]->key == key) return table[j];
else if(table[j] == nullptr) return nullptr;
else{
j = (j+1) % length;
}
} while (j != i);
return nullptr;
}
template<class K, class E>
int HashTable<K, E>::search(const K &key) const {
int i = key % length;
int j = i;
do {
if(table[j]->key == key ) return j;
else if(table[j] == nullptr) return -1;
else{
j = (j+1) % length;
}
} while (j != i);
return -1;
}
template<class K, class E>
void HashTable<K, E>::insert(const pair<K, E> &tpair) {
int i = tpair.key % length;
int j = i;
do {
if(table[i] == nullptr){
table[i] = new pair<K,E>;
table[i]->key = tpair.key;
table[i]->value = tpair.value;
size++;
break;
} else if(table[i]->key == tpair.key){
table[i]->value = tpair.value;
break;
} else{
i = (i+1) % length;
}
} while (i != j);
}
template<class K, class E>
void HashTable<K, E>::del(const K &key) {
int i = key % length;
int j = i;
do {
if(table[i] != nullptr && table[i]->key == key){
delete table[i];
table[i] = nullptr;
int k = i;
do {
k = (k+1) % length;
if(table[k] == nullptr) break;
else if(table[k]->key % length == j){
pair<K,E> * p = table[k];
table[k] = nullptr;
size --;
insert(*p);
}
} while (k != i);
break;
} else i = (i + 1) % length;
} while (i != j);
size--;
}
template<class K, class E>
int HashTable<K, E>::getSize() const {
return size;
}
测试程序:
#include<iostream>
int main(){
HashTable<int, int> hashTable(5);
hashTable.insert(pair<int,int>{7,7});
hashTable.insert(pair<int,int>{8,8});
hashTable.insert(pair<int,int>{13,13});
std::cout << "key = 8 index = " << hashTable.search(8) << std::endl;
std::cout << "key = 7 index = " << hashTable.search(7) << std::endl;
std::cout << "key = 13 index = " << hashTable.search(13) << std::endl;
std::cout << "size = " << hashTable.getSize() << std::endl;
hashTable.del(8);
std::cout << "key = 13 index = " << hashTable.search(13) << std::endl;
std::cout << "size = " << hashTable.getSize() << std::endl;
hashTable.del(7);
std::cout << "size = " << hashTable.getSize() << std::endl;
return 0;
测试结果:
key = 8 index = 3
key = 7 index = 2
key = 13 index = 4
size = 3
key = 13 index = 3
size = 2
size = 1