哈希表:又叫散列表,关键值通过哈希函数生成一个哈希地址映射到数组对应的存储位置上,查找时通过关键值直接访问数组。就好比把一个复杂的事务通过一种方式简化成一个小物件,这个小物件就代表了这个复杂的事物。
哈希函数:指的是关键值和存储位置建立的对应关系。
一般我们只需要一次查找就能找到目标位置,但有些关键字需要多次比较和查找才能找到。因为哈希表里,可能存在关键值不同但是由于哈希函数的计算方式导致生成的哈希地址相同的情况,产生了冲突,一般情况下,冲突是不可避免的,因为关键字集合往往比哈希地址集合大很多。
由此可见,选择合适的哈希函数就显得特别的重要,要尽可能的避免冲突。
哈希函数常见的构造方法:
1. 直接寻址法:即取关键值或关键字的某个函数变换值,直接映射到存储地址上(适用于关键字的数量和跨度不大),优点在于简单有效,避免了冲突。
2. 除留余数法:关键字对整数p取模直接作为存储地址。
3. 其它:分析数字法、随机数法。
总之,哈希函数并没有统一的方法,同一个哈希函数不一定适用于所有问题。一般有2个要求:计算简单,过于复杂的哈希函数会增加时间的开销;关键字尽可能均匀分到存储地址上,这样可减少冲突。
当冲突不可避免的时候,我们就需要考虑怎么处理冲突。
常见处理冲突的方法:
一.开放地址法(仍用数组):
如果发生冲突,那么就使用某种策略寻找下一存储地址,直到找到一个不冲突的地址或者找到关键字,否则一直按这种策略继续寻找。如果冲突次数达到了上限则终止程序,表示关键字不存在哈希表里。一般常见的策略有这几种:
1. 线性探测法,如果当前的冲突位置为d,那么接下来几个探测地址为d+1,d+2,d+3等,也就是从冲突地址往后一个一个的检测,遍历整个数组;
2. 线性补偿探测法,它形成的探测地址为d+m,d+2*m,d+3*m等,与线性探测法不同,这里的查找单位不是1,而是m,为了能遍历到哈希表里面所有位置,我们设置m和表长size互质。
3. 随机探测法,这种方法和前两种方法类似,这里的查找单位不是一个固定值,而是一个随机序列。
4. 二次探测法,它形成的探测地址为d+1^2,d-1^2,d+2^2,d-2^2等,这种方法在冲突位置左右跳跃着寻找探测地址。
开放地址法计算简单快捷,处理起来方便,但是也存在不少缺点。线性探测法容易形成”堆聚”的情况,即很多记录就连在一块,而且一旦形成堆聚,记录会越聚越多。另外,开放地址法都有一个缺点,删除操作显得十分复杂,我们不能直接删除关键字所在的记录,否则在查找删除位置后面的元素时,可能会出现找不到的情况,因为删除位置上已经成了空地址,查找到这里时会终止。
二.链地址法(使用链表)
该方法将所有哈希地址相同的节点构成一个单链表,单链表的头节点存在哈希数组里。链地址法常出现在经常插入和删除的情况下。
链地址法有以下优点:1.不会出现聚堆现象,哈希地址不同的关键字不会发生冲突;2.不需要重建哈希表,在开放地址法中,如果哈希表里存满关键字了就需要扩充哈希表然后重建哈希表,而在链地址法里,因为结点都是动态申请的,所以不会出现哈希表里存满关键字的情况;相比开放地址法,关键字删除更方便,只需要找到指定节点,删除该节点即可。
总结:开放地址法和链地址法各有千秋,适用于不同的情况。当关键字规模少的时候,开放地址法比链地址法更节省空间,因为用链地址法可能会存在哈希数组出现大量空地址的情况,而在关键字规模大的情况下,链地址法就比开放地址法更节省空间,链表产生的指针域可以忽略不计,关键字多,哈希数组里面产生的空地址就少了。
下面代码采用线性探测法处理冲突:
#include <iostream>
#include <string>
using namespace std;
class HashTable {
private:
string *elem;
int size;
public:
HashTable() {
size = 2000; //初始化设置哈希表的大小为2000
elem = new string[size];
for (int i = 0; i < size; i++) { //初始化为#
elem[i] = "#";
}
}
~HashTable() {
delete[] elem;
}
int hash(string &index) { //构造哈希函数
int code = 0;
for (size_t i = 0; i < index.length(); i++) {
code = (code * 256 + index[i] + 128) % size;
}
return code;
}
bool search(string &index, int &pos, int ×) { //查找哈希表中是否存在即将插入的关键值
pos = hash(index); //生成哈希地址
times = 0; //冲突次数初始化为0
while (elem[pos] != "#" && elem[pos] != index) { //找到可用的地址
times++;
if (times < size) {
pos = (pos + 1) % size;
} else {
return false;
}
}
if (elem[pos] == index) { //如果该关键值已经存在
return true;
} else {
return false;
}
}
int insert(string &index) { //插入关键值
int pos, times;
if (search(index, pos, times)) {
return 2; //存在返回2
} else if (times < size / 2) {
elem[pos] = index; //插入成功
return 1;
} else { //如果冲突次数大于哈希表的一半,证明产生了聚堆现象,重建哈希表
recreate();
return 0;
}
}
void recreate()
{
string *temp_elem; //存到临时空间
temp_elem = new string[size];
for(int i = 0; i < size; i++)
{
temp_elem[i] = elem[i];
}
int copy_size = size;
size *=2;
delete []elem;
elem = new string[size]; //扩大一倍的内存
for(int i = 0; i < size; i++)
{
elem[i] = "#";
}
for(int i = 0; i < copy_size; i++)
{
if(temp_elem[i] != "#")
{
insert(temp_elem[i]);
}
}
delete []temp_elem;
}
};
int main() {
HashTable hashtable;
string buffer;
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> buffer;
int ans = hashtable.insert(buffer);
if (ans == 0) {
cout << "insert failed!" << endl;
} else if (ans == 1) {
cout << "insert success!" << endl;
} else if (ans == 2) {
cout << "It already exists!" << endl;
}
}
int temp_pos, temp_times;
cin >> buffer;
if (hashtable.search(buffer, temp_pos, temp_times)) {
cout << "search success!" << endl;
} else {
cout << "search failed!" << endl;
}
return 0;
}