1.基本思想
哈希查找算法又称散列查找算法,是一种借助哈希表(散列表)查找目标元素的方法,查找效率最高时对应的时间复杂度为 O(1)。哈希查找算法适用于大多数场景,既支持在有序序列中查找目标元素,也支持在无序序列中查找目标元素。
讲解哈希查找算法之前,我们首先要搞清楚什么是哈希表。哈希表(Hash table)又称散列表,是一种存储结构,通常用来存储多个元素。和其它存储结构(线性表、树等)相比,哈希表查找目标元素的效率非常高。每个存储到哈希表中的元素,都配有一个唯一的标识(又称“索引”或者“键”),用户想查找哪个元素,凭借该元素对应的标识就可以直接找到它,无需遍历整个哈希表。
使用数组构建哈希表,最大的好处在于:可以直接将数组下标当作已存储元素的索引,不再需要为每个元素手动配置索引,极大得简化了构建哈希表的难度。我们知道,在数组中查找一个元素,除非提前知晓它存储位置处的下标,否则只能遍历整个数组。哈希表的解决方案是:各个元素并不从数组的起始位置依次存储,它们的存储位置由专门设计的函数计算得出,我们通常将这样的函数称为哈希函数。哈希函数类似于数学中的一次函数,我们给它传递一个元素,它反馈给我们一个结果值,这个值就是该元素对应的索引,也就是存储到哈希表中的位置。
举个例子,将 {20, 30, 50, 70, 80} 存储到哈希表中,我们设计的哈希函数为 y=x/10,最终各个元素的存储位置为:20->2;30->3;50->5;70->7;80->8。
假设我们想查找元素 50,只需将它带入 y=x/10 这个哈希函数中,计算出它对应的索引值为 5,直接可以在数组中找到它。借助哈希函数,我们提高了数组中数据的查找效率,这就是哈希表存储结构。
构建哈希表时,哈希函数的设计至关重要。假设将 {5, 20, 30, 50, 55} 存储到哈希表中,哈希函数是 y=x%10,可以知道,5 和 55 以及 20、30 和 50 对应的索引值是相同的,它们的存储位置发生了冲突,我们习惯称为哈希冲突或者哈希碰撞。设计一个好的哈希函数,可以降低哈希冲突的出现次数。哈希表提供了很多解决哈希冲突的方案,比如线性探测法、再哈希法、链地址法等。
2.哈希冲突的解决办法
哈希冲突是指不同的键值被哈希函数映射到了同一个槽位上的情况。为了解决哈希冲突,常用的方法包括:
-
开放地址法(Open Addressing):如果发生哈希冲突,就在哈希表中寻找下一个空闲槽位,直到找到合适的位置。开放地址法的实现方式包括线性探测、二次探测和双重哈希等。
-
链地址法(Chaining):将哈希表中每个槽位都设为链表的头节点,每个链表节点存储键值对。当发生哈希冲突时,将新的键值对插入到对应槽位的链表中。
-
拉链法(Separate Chaining):与链地址法类似,不同之处在于每个槽位存储一个链表的头指针,每个链表节点存储一个键值对。
-
建立公共溢出区(Overflow Area):当发生哈希冲突时,将键值对插入到公共溢出区中。这种方法可以避免哈希表的大小固定,但查找时间可能会变长。
-
完美哈希(Perfect Hashing):在设计哈希函数时,避免冲突的发生。完美哈希函数的设计需要考虑键值的特点和分布情况,一般适用于静态数据集合。
3.开放地址法的详解
开放地址法是一种解决哈希冲突的方法。当哈希函数将两个或多个不同的键值映射到同一个槽位时,就会发生哈希冲突。开放地址法的思想是在哈希表中寻找下一个可用的槽位,直到找到一个不冲突的位置为止。常见的开放地址法包括线性探测、二次探测和双重哈希等。
- 线性探测:线性探测是一种最简单的开放地址法,它的思想是在哈希表中依次查找下一个可用的槽位,直到找到一个不冲突的位置为止。具体实现时,当发生哈希冲突时,将查找位置向后移动一个固定的偏移量,直到找到一个空闲槽位或者遍历整个哈希表都没有找到。线性探测的偏移量通常为1,即依次查找相邻的槽位。线性探测的优点是实现简单,空间利用率高,适用于存储的键值对数量较少的情况。但是,它容易产生“聚集现象”,即连续的哈希冲突导致查找时间变长。
- 二次探测:二次探测是一种改进的开放地址法,它的思想是在哈希表中查找下一个可用的槽位时,依次查找一系列固定的偏移量,而不是像线性探测那样只查找一个偏移量。具体实现时,偏移量的选择可以采用一些数学公式,如:i^2,i^2+i,-i^2,-i^2+i 等。其中,i表示探测次数。二次探测可以避免线性探测的聚集现象,但仍然可能产生“二次聚集现象”。
- 双重哈希:双重哈希是一种改进的开放地址法,它的思想是使用两个哈希函数来定位下一个可用的槽位。具体实现时,先使用第一个哈希函数计算出一个槽位,如果该槽位已经被占用,则使用第二个哈希函数计算出下一个槽位,以此类推,直到找到一个空闲槽位。双重哈希可以避免线性探测和二次探测的聚集现象,但需要使用两个哈希函数,实现较为复杂。
代码设计如下:
//
// Created by A on 2023/6/13.
//
#include <iostream>
#include <vector>
#include <cassert>
#include <stack>
#include <queue>
using namespace std;
class NewHashTable {
public:
NewHashTable(int destSize);
~NewHashTable();
bool NeedRehash();
void Insert(int newData);
int Find(int destData);
private:
int m_size;
int m_use;
int *m_pData;
};
NewHashTable::NewHashTable(int destSize)
: m_size(destSize), m_use(0), m_pData(new int[m_size]) {
for (int idx = 0; idx < m_size; ++idx) m_pData[idx] = -INT_MIN;
}
NewHashTable::~NewHashTable() {
delete[] m_pData;
}
void NewHashTable::Insert(int newData) {
if (NeedRehash()) {
throw "Need Rehash";
}
int hashIdx = newData % m_size;
if (m_pData[hashIdx] == INT_MIN) {
m_pData[hashIdx] = newData;
++m_use;
return;
}
for (int idx = 1; idx < m_size; ++idx) {
hashIdx = (newData + idx) % m_size;
if (m_pData[hashIdx] == INT_MIN) break;
}
m_pData[hashIdx] = newData;
++m_use;
}
int NewHashTable::Find(int destData) {
int hashIdx = destData % m_size;
if (m_pData[hashIdx] == INT_MIN) return -1;
if (m_pData[hashIdx] == destData) return hashIdx;
for (int idx = 1; idx < m_size; ++idx) {
hashIdx = (destData + idx) % m_size;
if (m_pData[hashIdx] == INT_MIN) return -1;
if (m_pData[hashIdx] == destData) return hashIdx;
}
return -1;
}
bool NewHashTable::NeedRehash() {
return m_use >= m_size;
}
int main() {
NewHashTable hashTable(5);
while (true) {
std::cout << "请输入操作(INSERT FIND RPINT):" << std::endl;
std::string choice;
std::cin >> choice;
if ("INSERT" == choice) {
if (hashTable.NeedRehash()) {
std::cout << "没有剩余空间,请先Rehash" << std::endl;
} else {
std::cout << "请输入数据:";
int newData;
std::cin >> newData;
hashTable.Insert(newData);
}
} else if ("FIND" == choice) {
std::cout << "请输入待查数据:";
int newData;
std::cin >> newData;
std::cout << "DestIdx = " << hashTable.Find(newData) << std::endl;
} else if ("PRINT" == choice) {
hashTable.Print();
} else;
}
return 0;
}