跳到主要内容
哈希表原理与实现:线性探测及链地址法 | 极客日志
C++ 算法
哈希表原理与实现:线性探测及链地址法 综述由AI生成 哈希表通过哈希函数建立键值与存储位置的映射。哈希含义、冲突概念及负载因子,阐述了直接定址、除法散列、乘法散列等映射方法。重点讲解了开放定址法(线性探测、二次探测)和链地址法两种冲突解决策略,并提供了基于 C++ 模板的线性探测与链地址法完整代码实现,包含查找、插入、删除及扩容逻辑。
Stephaine Walsh 发布于 2026/3/17 更新于 2026/6/3 30 浏览1 哈希表的概念
1.1 哈希的含义
哈希(又称散列),是一种通过哈希函数来得到值与存储位置的映射关系的存储方式。
1.2 哈希冲突
哈希冲突指在使用哈希函数存储元素时,多个元素通过哈希函数计算得到的映射位置是一样的,此时它们就会争夺同一个存储位置。这种现象叫做哈希冲突,哈希冲突是不可避免的,但可以通过设计出好的哈希函数来减少冲突。
例如 x、y、z 通过哈希函数得到的映射位置都是 3,此时相当于它们在争夺这个存储位置,发生了哈希冲突。
1.3 负载因子
负载因子指哈希表内元素的占比。假设存储的元素个数为 N,哈希表长度为 M,负载因子为 N/M。负载因子越大,说明存储的元素越多,发生哈希冲突的概率越大,空间利用率越高;负载因子越小,说明存储的元素越少,发生哈希冲突的概率越小,空间利用率越低。
例如下图中哈希表的负载因子就等于 8 / 11 = 0.73。
2 哈希表的映射方法
2.1 直接定址法
2.1.1 概念
直接定址法直接用元素的值充当存储位置来进行映射。比如下标为 [0, 10] 的数组,用直接定址法来做映射,那么 0 直接映射到下标为 0 的存储位置上,1 直接映射到下标为 1 的存储位置上。
2.1.2 缺点
直接定址法只适合用于元素之间的值相差不大的情况。如果有一组数据,它里面元素的值相差过大,那么使用直接定址法会导致空间浪费。比如要存储 [0, 9999],虽然只有两个值但是要开辟 10000 个空间来存储,造成了很大的空间浪费,此时通过哈希函数来获得新的映射方案会更理想。
2.2 除法散列法(除留余数法)
除法散列法是通过取模的方式来获取余数,以该余数作为映射位置的一种映射方案。假设哈希表长度为 M,元素的值为 key,那么最终得到的映射位置:h(key) = key % M。
在使用除法散列法时,哈希表长度建议取不接近 2 的整数次幂的质数。
假设哈希表长 M 为 7,要映射的值 key 为 5,那么根据除留余数法最终可以得到映射的下标是 5 % 7 = 5。
2.3 乘法散列法
乘法散列法的思想是用元素的值 key 先乘上小数 A (0<A<1),再 %1.0 获得结果的小数部分,最后让 M 乘上这个小数部分,向下取整来得到映射位置 h(key) = floor(M * ((key * A) % 1.0))。floor 为向下取整,A 一般取 0.6180339887。
假设哈希表长 M 为 1024,key 为 1234,A = 0.6180339887,A * key = 762.6539420558,% 1.0 取小数部分 0.6539420558,乘以表长 1024 * 0.6539420558 = 669.6366651392,向下取整最终得到 669,也就是映射的位置。
2.4 全域散列法
如果哈希函数是固定不变的,此时可能会出现有人设计了一组数据,将这组数据通过已设计好的哈希函数进行存储,让数据都存储到一个位置上,发生多次哈希冲突,以此来降低效率。全域散列法就是为了解决这种问题存在的。
全域散列法的映射方案为 h(key) = ((a * key + b) % P) % M。P 为一个较大的质数,a 在 [1, P-1] 范围内随机选取一个值,b 在 [0, P-1] 范围内随机选取一个值,这样就能得到 (P - 1) * P 个哈希函数,在初始化哈希表时,随机选取其中的一个进行使用。
3 处理哈希冲突的办法
3.1 开放定址法
3.1.1 线性探测法 线性探测法的思路为发生冲突时,从发生冲突的位置依次向后寻找,直到找到没有数据的位置为止,将元素存入该位置。如果遇到表尾,则回到表头继续查找。
使用 h(key) = key % M 得到第一次要映射的位置 hash0,若该位置发生哈希冲突,则使用 h(key) = (hash0 + i) % M 来查找下一个要映射的位置 hashi,i 的取值为 [1, M-1]。如果负载因子小于 1,最少留下一个位置可以用来存储,则最多探测 M - 1 次得到要映射的位置。
假设一个元素 x 要映射到位置 hash0 上但是发生冲突,那么它就会继续向后查找 hash1,hash2,hash3,最后它存放在了 hash3 的位置上。此时元素 y,z 到来,第一次分别映射到 hash1 和 hash2 的位置上,进行查找后,最后它们都会对 hash3 的位置进行争夺,这种现象叫做堆积(群聚)。
线性探测法例:将 [19, 30, 5, 36, 13, 20, 21, 12] 这一组数据映射到长度 M 为 11 的哈希表中:
h (19 ) = 19 % 11 = 8
h (30 ) = 30 % 11 = 8 -> h (30 ) = (8 + 1 ) % 11 = 9
h (5 ) = 5 % 11 = 5
h (36 ) = 36 % 11 = 3
h (13 ) = 13 % 11 = 2
h (20 ) = 20 % 11 = 9 -> h (20 ) = (9 + 1 ) % 11 = 10
h (21 ) = 21 % 11 = 10 -> h (21 ) = (10 + 1 ) % 11 = 0
h (12 ) = 12 % 11 = 1
3.1.2 二次探测法 二次探测法的思路为发生冲突时,从发生冲突的位置依次向前向后寻找,直到找到没有数据的位置为止。
使用 h(key) = key % M 得到第一次要映射的位置 hash0,若该位置发生哈希冲突,则使用 h(key, i) = (hash0 + i^2) % M 或类似变体来查找下一个要映射的位置 hashi。通常 i 的取值为 [1^2, -(1)^2, 2^2, -(2)^2, …]。当 hashi < 0 时,要加上表长 M,否则无法存储。
二次探测法例:将 [19, 30, 52, 63, 11, 22] 这一组数据映射到长度 M 为 11 的哈希表中:
h (19 ) = 19 % 11 = 8
h (30 ) = 30 % 11 = 8 -> h (30 , 1 ) = (8 + 1 ) % 11 = 9
h (52 ) = 52 % 11 = 8 -> h (52 , 1 ) = (8 + 1 ) % 11 = 9 -> h (52 , -1 ) = (8 - 1 ) % 11 = 7
h (63 ) = 63 % 11 = 8 -> h (63 , 1 ) = (8 + 1 ) % 11 = 9 -> h (63 , -1 ) = (8 - 1 ) % 11 = 7 -> h (63 , 4 ) = (8 + 4 ) % 11 = 1
h (11 ) = 11 % 11 = 0
h (22 ) = 22 % 11 = 0 -> h (22 , 1 ) = (0 + 1 ) % 11 = 1 -> h (22 , -1 ) = (0 - 1 ) % 11 = -1 ,再 -1 + 11 = 10
3.2 链地址法 链地址法也叫哈希桶或拉链法。链地址法中,哈希表的每个位置上都有一个指针,当有元素映射到一个位置上时,就用该元素创建一个结点,挂到对应的指针上。当没有元素映射时,指针是空的。
链地址法例:将 [19,30,5,36,13,20,21,12,24,96] 映射到长度 M = 11 的哈希表中:
19 % 11 = 8 ,30 % 11 = 8 ,5 % 11 = 5 ,36 % 11 = 3 ,13 % 11 = 2
20 % 11 = 9 ,21 % 11 = 10 ,12 % 11 = 1 ,24 % 11 = 2 ,96 % 11 = 8
4 线性探测法的实现
4.1 枚举类型 State
EXIST:表示该位置存在值
EMPTY:表示该位置无值
DELETE:表示该位置上的值已删除,主要用来防止删除后影响查找操作
enum State {
EXIST,
EMPTY,
DELETE
};
4.2 数据类型 HashData template <class K , class V >
struct HashData {
pair<K, V> _data;
State _state;
};
4.3 本体 HashTable template <class K , class V , class Hash = HashFunc<K>>
class HashTable {
public :
private :
vector<HashData<K, V>> _table;
size_t _n;
};
4.4 仿函数 HashFunc 为了让负数、小数、字符串都可以被插入到哈希表中,需要把它们转化成整数。
负数、小数在转化时,要转化为 size_t 类型的数据,否则无法进行插入。
字符串在转化时,需要把每个字符的 ASCII 值相加,用最终的和来得到映射位置。但是像 "abc" 和 "bac" 这样的字符串,ASCII 值相加后值是一样的,为了防止这种问题,需要在每次加的时候乘以质数。
template <class K >
class HashFunc {
size_t operator () (const K& key) {
return (size_t )key;
}
};
template <>
class HashFunc <string> {
size_t operator () (const string& str) {
size_t key = 0 ;
for (auto & e : str) {
key += e;
key *= 131 ;
}
return key;
}
};
4.5 查找(Find) 先根据 h(key) = key % M 计算出 hash0,从 hash0 的位置开始进行查找。若该位置的状态为 EXIST(值存在)并且值等于 key,那么说明找到了需求的值,返回该值的地址。若该位置的状态为 DELETE(元素已删除)或存在值但是不相等,仍然需要向后进行查找。此时通过 h(key) = (hash0 + i) % M 的方式得到新的映射位置再与该位置上的值进行比较。若最后到 EMPTY 还没找到则说明哈希表内没有需求的值,返回空指针即可。
HashData<K, V>* Find (const K& key) {
Hash hash;
int hash0 = hash (key) % _table.size ();
int hashi = hash0;
int i = 1 ;
while (_table[hashi]._state != EMPTY) {
if (_table[hashi]._state == EXIST && _table[hashi]._data.first == key) {
return &_table[hashi];
}
hashi = (hash0 + i) % _table.size ();
i++;
}
return nullptr ;
}
4.6 插入(Insert) 为了进行去重,先根据 key 查找元素是否在哈希表内。在哈希表内则不需要进行插入,不在则进行插入。插入前,先根据负载因子来判断是否进行扩容,负载因子 > 0.7 时进行扩容。扩容时定义新的哈希表,对函数 Insert 进行复用,将原表的值放入新表,再交换两个表即可。扩容完后进行插入,使用 h(key) = key % M 计算出 hash0,若 hash0 的位置已有元素(EXIST),则通过 h(key) = (hash0 + i) % M 继续向后查找,直到找到无元素的位置(DELETE 或 EMPTY),将值插入。
bool Insert (const pair<K, V>& kv) {
if (Find (kv.first)) return false ;
if (_n * 10 / _table.size () >= 7 ) {
HashTable<K, V, Hash> newTable;
newTable._table.resize (__stl_next_prime(_table.size () + 1 ));
for (int i = 0 ; i < _table.size (); ++i) {
if (_table[i]._state == EXIST) {
newTable.Insert (_table[i]._data);
}
}
swap (newTable._table, _table);
}
Hash hash;
int hash0 = hash (kv.first) % _table.size ();
int hashi = hash0;
int i = 1 ;
while (_table[hashi]._state == EXIST) {
hashi = (hash0 + i) % _table.size ();
i++;
}
_table[hashi]._data = kv;
_table[hashi]._state = EXIST;
_n++;
return true ;
}
4.7 删除 使用 key 先在表内进行查找,若找不到则不需要删除。若找得到则直接将状态设置为 DELETE,表示元素被删除。设置为 DELETE 的目的为防止删除元素后影响查找。
bool Erase (const K& key) {
HashData<K, V>* ret = Find (key);
if (!ret) return false ;
ret->_state = DELETE;
_n--;
return true ;
}
由于 hash0 处被标记为了 EMPTY,所以查找时直接返回,但是 20 在该位置后面,所以查找结果出现了错误。
4.8 完整代码 template <class K >
struct HashFunc {
size_t operator () (const K& key) {
return (size_t )key;
}
};
template <>
struct HashFunc <string> {
size_t operator () (const string& str) {
size_t key = 0 ;
for (auto & e : str) {
key += e;
key *= 131 ;
}
return key;
}
};
enum State {
EXIST,
EMPTY,
DELETE
};
template <class K , class V >
struct HashData {
pair<K, V> _data;
State _state = EMPTY;
};
template <class K , class V , class Hash = HashFunc<K>>
class HashTable {
inline unsigned long __stl_next_prime(unsigned long n) {
static const int __stl_num_primes = 28 ;
static const unsigned long __stl_prime_list[__stl_num_primes] = {
53 , 97 , 193 , 389 , 769 , 1543 , 3079 , 6151 , 12289 , 24593 ,
49157 , 98317 , 196613 , 393241 , 786433 , 1572869 , 3145739 ,
6291469 , 12582917 , 25165843 , 50331653 , 100663319 , 201326611 ,
402653189 , 805306457 , 1610612741 , 3221225473 , 4294967291
};
const unsigned long * first = __stl_prime_list;
const unsigned long * last = __stl_prime_list + __stl_num_primes;
const unsigned long * pos = lower_bound (first, last, n);
return pos == last ? *(last - 1 ) : *pos;
}
public :
HashTable () {
_table.resize (__stl_next_prime(0 ));
_n = 0 ;
}
bool Insert (const pair<K, V>& kv) {
if (Find (kv.first)) return false ;
if (_n * 10 / _table.size () >= 7 ) {
HashTable<K, V, Hash> newTable;
newTable._table.resize (__stl_next_prime(_table.size () + 1 ));
for (int i = 0 ; i < _table.size (); ++i) {
if (_table[i]._state == EXIST) {
newTable.Insert (_table[i]._data);
}
}
swap (newTable._table, _table);
}
Hash hash;
int hash0 = hash (kv.first) % _table.size ();
int hashi = hash0;
int i = 1 ;
while (_table[hashi]._state == EXIST) {
hashi = (hash0 + i) % _table.size ();
i++;
}
_table[hashi]._data = kv;
_table[hashi]._state = EXIST;
_n++;
return true ;
}
HashData<K, V>* Find (const K& key) {
Hash hash;
int hash0 = hash (key) % _table.size ();
int hashi = hash0;
int i = 1 ;
while (_table[hashi]._state != EMPTY) {
if (_table[hashi]._state == EXIST && _table[hashi]._data.first == key) {
return &_table[hashi];
}
hashi = (hash0 + i) % _table.size ();
i++;
}
return nullptr ;
}
bool Erase (const K& key) {
HashData<K, V>* ret = Find (key);
if (!ret) return false ;
ret->_state = DELETE;
_n--;
return true ;
}
private :
vector<HashData<K, V>> _table;
size_t _n;
};
5 链地址法的实现 链地址法中,仿函数 HashFunc,枚举类型 State 和线性探测法中一致。
5.1 结点 HashNode 由于链地址法中,哈希表的每一个位置上都是一个链表,所以就要开辟链表中的结点并将数据存放进去。
template <class K , class V >
struct HashNode {
HashNode (const pair<K, V>& kv) : _kv(kv), _next(nullptr ) {}
pair<K, V> _kv;
HashNode<K, V>* _next;
};
5.2 本体 HashTable template <class K , class V , class Hash = HashFunc<K>>
class HashTable {
typedef HashNode<K, V> Node;
public :
private :
vector<Node*> _table;
size_t _n;
};
5.3 析构函数 由于哈希表中存了许多的结点,所以要释放掉这些结点。每一个哈希桶相当于是一个链表,对哈希桶按照链表的方式释放即可。定义 cur,删除 cur 前,先记录它的下一个节点 next,删除 cur 后,cur = next 向后移动。当 cur 为空时,说明当前的哈希桶删除完毕,把头结点指针置为空,移动到下一个哈希桶即可。
~HashTable () {
for (int i = 0 ; i < _table.size (); i++) {
Node* cur = _table[i];
while (cur) {
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr ;
}
}
5.4 查找(Find) 计算出元素要映射的位置 hashi,确定要查找的哈希桶,在该哈希桶内查找元素。找到了则返回元素,查不到则返回空指针。
HashNode<K, V>* Find (const K& key) {
Hash hash;
size_t hashi = hash (key) % _table.size ();
Node* cur = _table[hashi];
while (cur) {
if (cur->_kv.first == key) return cur;
cur = cur->_next;
}
return nullptr ;
}
5.5 插入(Insert) 插入之前,先查找该元素在表中是否存在。若存在则不进行插入,若不存在,先查看负载因子是否等于 1,等于 1 需要扩容。扩容时,由于原来的哈希表中已经有结点了,复用 Insert 的话会创建新结点,造成浪费,所以不进行复用,直接开辟一个新的顺序表,遍历原来的顺序表,将每个哈希桶中的值映射到新表中,将新表和旧表交换即可。扩容结束后,计算出 hashi 来确定新元素要映射的哈希桶,创建新结点,将该结点头插至对应的哈希桶内即可。
bool Insert (const pair<K, V>& kv) {
if (Find (kv.first)) return false ;
Hash hash;
if (_n == _table.size ()) {
vector<Node*> newTable;
newTable.resize (__stl_next_prime(_table.size () + 1 ), nullptr );
for (int i = 0 ; i < _table.size (); ++i) {
Node* cur = _table[i];
while (cur) {
Node* next = cur->_next;
size_t hashi = hash (cur->_kv.first) % newTable.size ();
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = nullptr ;
}
swap (newTable, _table);
}
size_t hashi = hash (kv.first) % _table.size ();
Node* newNode = new Node (kv);
newNode->_next = _table[hashi];
_table[hashi] = newNode;
++_n;
return true ;
}
5.6 删除(Erase) 删除的操作与链表的删除类似,需要一个前驱指针 prev 和当前节点指针 cur。计算出 hashi 来确定元素所在的哈希桶。当 prev 为空指针,cur 指向的为要删除的结点时,那么需要修改头结点的指针,使其指向 cur 的下一个结点,再删除结点 cur。当 prev 为非空指针,cur 指向的为要删除的结点时,就需要修改 prev 结点的 next,使其指向 cur 的下一个结点,再删除结点 cur。如果 cur 遍历至空都没有要删除的值,则不需要删除。
bool Erase (const K& key) {
Hash hash;
size_t hashi = hash (key) % _table.size ();
Node* cur = _table[hashi];
Node* prev = nullptr ;
while (cur) {
if (cur->_kv.first == key && prev == nullptr ) {
_table[hashi] = cur->_next;
--_n;
delete cur;
return true ;
} else if (cur->_kv.first == key && prev) {
prev->_next = cur->_next;
--_n;
delete cur;
return true ;
}
prev = cur;
cur = cur->_next;
}
return false ;
}
5.7 完整代码 template <class K >
struct HashFunc {
size_t operator () (const K& key) {
return (size_t )key;
}
};
template <>
struct HashFunc <string> {
size_t operator () (const string& str) {
size_t ret = 0 ;
for (auto & ch : str) {
ret += ch;
ret *= 131 ;
}
return ret;
}
};
template <class K , class V >
struct HashNode {
HashNode (const pair<K, V>& kv) : _kv(kv), _next(nullptr ) {}
pair<K, V> _kv;
HashNode<K, V>* _next;
};
template <class K , class V , class Hash = HashFunc<K>>
class HashTable {
typedef HashNode<K, V> Node;
inline unsigned long __stl_next_prime(unsigned long n) {
static const int __stl_num_primes = 28 ;
static const unsigned long __stl_prime_list[__stl_num_primes] = {
53 , 97 , 193 , 389 , 769 , 1543 , 3079 , 6151 , 12289 , 24593 ,
49157 , 98317 , 196613 , 393241 , 786433 , 1572869 , 3145739 ,
6291469 , 12582917 , 25165843 , 50331653 , 100663319 , 201326611 ,
402653189 , 805306457 , 1610612741 , 3221225473 , 4294967291
};
const unsigned long * first = __stl_prime_list;
const unsigned long * last = __stl_prime_list + __stl_num_primes;
const unsigned long * pos = lower_bound (first, last, n);
return pos == last ? *(last - 1 ) : *pos;
}
public :
HashTable () {
_table.resize (11 , nullptr );
_n = 0 ;
}
~HashTable () {
for (int i = 0 ; i < _table.size (); i++) {
Node* cur = _table[i];
while (cur) {
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr ;
}
}
bool Insert (const pair<K, V>& kv) {
if (Find (kv.first)) return false ;
Hash hash;
if (_n == _table.size ()) {
vector<Node*> newTable;
newTable.resize (__stl_next_prime(_table.size () + 1 ), nullptr );
for (int i = 0 ; i < _table.size (); ++i) {
Node* cur = _table[i];
while (cur) {
Node* next = cur->_next;
size_t hashi = hash (cur->_kv.first) % newTable.size ();
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = nullptr ;
}
swap (newTable, _table);
}
size_t hashi = hash (kv.first) % _table.size ();
Node* newNode = new Node (kv);
newNode->_next = _table[hashi];
_table[hashi] = newNode;
++_n;
return true ;
}
HashNode<K, V>* Find (const K& key) {
Hash hash;
size_t hashi = hash (key) % _table.size ();
Node* cur = _table[hashi];
while (cur) {
if (cur->_kv.first == key) return cur;
cur = cur->_next;
}
return nullptr ;
}
bool Erase (const K& key) {
Hash hash;
size_t hashi = hash (key) % _table.size ();
Node* cur = _table[hashi];
Node* prev = nullptr ;
while (cur) {
if (cur->_kv.first == key && prev == nullptr ) {
_table[hashi] = cur->_next;
--_n;
delete cur;
return true ;
} else if (cur->_kv.first == key && prev) {
prev->_next = cur->_next;
--_n;
delete cur;
return true ;
}
prev = cur;
cur = cur->_next;
}
return false ;
}
private :
vector<Node*> _table;
size_t _n;
};
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online