目录
[1. 哈希表的核心组成(三部分):](#1. 哈希表的核心组成(三部分):)
[2. 哈希冲突](#2. 哈希冲突)
[3. 负载因子](#3. 负载因子)
[4. 哈希函数](#4. 哈希函数)
[4.1 直接定址法(最简单,无冲突,局限性大)](#4.1 直接定址法(最简单,无冲突,局限性大))
[4.2 除留余数法(最通用,工业级常用)](#4.2 除留余数法(最通用,工业级常用))
[4.3 乘法散列法(高效,哈希分布均匀)了解](#4.3 乘法散列法(高效,哈希分布均匀)了解)
[4.4 全域散列法(抗攻击、抗极端数据)了解](#4.4 全域散列法(抗攻击、抗极端数据)了解)
[4.5 其它方法 了解](#4.5 其它方法 了解)
[5. 哈希冲突解决方式](#5. 哈希冲突解决方式)
[5.1 开放定址法](#5.1 开放定址法)
[5.1.1 线性探测](#5.1.1 线性探测)
[5.1.2 平方探测(二次探测)](#5.1.2 平方探测(二次探测))
[5.1.3 双重哈希探测(了解)](#5.1.3 双重哈希探测(了解))
[5.2 链地址法](#5.2 链地址法)
[(2)析构函数写 or 不写](#(2)析构函数写 or 不写)
哈希表是编程中最常用的高效数据结构之一,它的核心思想通过哈希函数将键(Key)映射到数组的特定位置(称为"桶"或"槽",Bucket),从而实现平均O(1)时间复杂度的增、删、改、查操作。
1. 哈希表的核心组成(三部分):
- 数组(桶数组):存储键值对的基础结构,每个元素称为一个"桶"。
- 哈希函数(Hash Function):将任意类型的键转换为数组的索引(桶位置)。
- 冲突处理机制:解决不同键映射到同一桶的问题。
2. 哈希冲突
当两个不同的键通过哈希函数得到相同的桶索引时,称为哈希冲突。冲突不可避免,但好的哈希函数可以减少冲突。
3. 负载因子
负载因子描述了哈希表的"拥挤程度"------哈希表中已存储的元素数量占哈希表总桶(数组)容量的比例。比例越高,哈希表越拥挤,冲突概率越高;比例越低,哈希表越宽松,空间浪费越多。
负载因子(loadFactor)= 哈希表当前元素数(size) / 哈希表总容量(capacity)
4. 哈希函数
哈希函数是哈希表的"灵魂",直接决定了哈希表的性能。
4.1 直接定址法(最简单,无冲突,局限性大)
公式:H(key) = key 或 H(key) = a * key + b(线性变换)。
直接用Key本身或key的线性变换(如H(key) = key )作为哈希值,适合key的取值范围连续且数值范围不大的场景。
这种方法无哈希冲突、计算极快,但是key范围较大时(比如1~10000000),哈希桶数量对应也要扩大,空间浪费严重。
4.2 除留余数法(最通用,工业级常用)
将键除以某个质数M(通常是桶数组的长度),取余数作为索引。
公式:H(key) = key % M。
M的选择是关键,**M选择 "不接近 2 的整数次幂的质数",**本质是同时规避两类冲突问题:
1. 选 "质数":规避 "公约数扎堆" 问题
质数的约数只有1和自身,能避免key因公约数集中映射到同一哈希值,比如:
- 若M=12(合数),key=6、12、18、24的公约数都是6,哈希值全为0,冲突暴增;
- 若M=13(质数),上述key的哈希值分别为6、12、5、11,分布均匀。
2. 选 "不接近 2 的整数次幂":规避 "低位主导" 问题
2的整数次幂(如 16=2⁴、32=2⁵)的致命缺陷是:**key % 2ⁿ等价于取key的二进制最后n位,高位信息完全丢失。**即使把M选为"接近2的幂的质数"(如17=16+1、31=32-1)。依然会残留"低位主导"的问题------因为这类质数和2的幂差值极小,key % M的结果仍主要由key的低位决定,高位信息利用不足。
接近2的幂的质数(M=17质数,接近16=2⁴)VS远离2的幂的质数(M=19 质数,远离16=2⁴),key分别取16、32、48、64、80:
- 当M=17时,key % 17哈希值依次为16、15、14、13、12,呈现连续递减规律,仍受低位主导,冲突风险高。
- 当M=19时,key % 17哈希值依次为16、13、10、7、4,分布更分散,高位信息充分利用,冲突风险低。
若M=2ⁿ,对 2ⁿ取模,num % 2ⁿ = num & (2ⁿ - 1) = 保留数字二进制的最后 n 位,高位所有信息都被丢弃。对 2 的幂取模只保留最后 n 位,那么就说明二进制后n位相同的值,哈希值就相同,会增加冲突概率。比如{25,41},如果p是16即2^4,25的二进制后8位是00011001,41的二进制后8位是00101001,那么计算出的哈希值都是9,保留的是它们的后4位1001。若M=10ⁿ,num % 10ⁿ = num的最后n位十进制数字,对 10ⁿ取模,等价于只保留数字十进制的最后 n 位,与上面同理。
Java HashMap的处理逻辑
Java HashMap并没有直接使用"选质数做除留余数"的通用方案,而是对除留余数法做了工业级优化: Java的HashMap实际采用2的幂作为数组长度,通用的2的幂取模会丢失高位信息,但是它底层用32位扰动函数(hashCode ^ (hashCode >> 16))通过移位+异或的方式将高低位混合,让高位也参与后续的取模运算,彻底解决"丢失高位信息"的致命缺陷。
假设key = 0x12345678(十六进制,对应十进制305419896),桶数组的长度M = 16 (2^4):
- h>>16 = 0x00001234;(把原始哈希值右移16位,无符号右移,高位补0)
- hash = 0x12345678 ^ 0x00001234 = 0x1234444C;(十进制305418316,混合后的哈希值既保留了高16位0x1234的特征,又融合了低16位0x5678的信息)。
- 用位运算代替取模(效率高):index = hash & (M- 1)= 305418316 & 15 =12 等价于305418316 % 16 = 12;最终被映射到数组下标12的位置。
4.3 乘法散列法(高效,哈希分布均匀)了解
乘法散列法是哈希函数设计的经典方法,对哈希表的大小M没有要求,它通过 "键值 × 黄金比例常数 ,提取乘积的小数部分 ,再用哈希表大小M乘以该小数部分并对结果向下取整, 得到哈希地址。
公式:**h(key) = floor(M × ((A × key)%1.0)),**其中floor表示对表达式向下取整,A是黄金比例常数,取值为= ( √5 − 1)/2 = 0.6180339887....(由knuth提出,该值能最大化哈希值的分布均匀性,有效降低冲突概率)。
例如已知M=1024、key=5678、A=0.6180339887,计算 h(5678):A*key=0.6180339887×5678=3509.1969878386,取小数部分为0.1969878386,M × ((A × key)%1.0)=1024×0.1969878386=201.7155467264,那么h(5678)=201。
4.4 全域散列法(抗攻击、抗极端数据)了解
如果存在一个恶意的对手,它针对我们提供的散列函数,特意构造出一个发生严重冲突的数据集,导致所有关键字集中冲突(哈希表性能大幅下降),就像快递分拣站一样,每个快递的快递单key,哈希函数就是分拣规则,比如按手机号后4位分货架,如果有人恶意搞事,比如所有快递是手机号后四位都填"6666",按这个规则,所有快递都堆一个货架(冲突爆炸),分拣站直接瘫痪(哈希表就会退化成链表,效率从 O (1) 变 O (n)。
全域散列法通过给散列函数增加随机性,让对手无法针对性构造冲突数据,避免最坏情况。公式:h (key) = ((a × key + b)%P )%M,其中P需选一个大于所有可能键值的质数,a随机选取[1,P-1]之间的整数,b随机选取[0,P-1]之间的整数,由a、b取不同值,可构成P×(P−1)个散列函数的 "全域函数组",初始化哈希表时,从全域函数组中随机选一个散列函数,后续的增、删、查、改操作都固定使用该函数。
例如:P=17,M=6,a=3,b=4,则h(8) = ((3×8 + 4) % 17) % 6 = 5;
综上所述全域散列法就是给哈希表准备一堆 "靠谱的哈希函数",每次用的时候随机挑一个,既保证了哈希表 O (1) 的操作效率,又提升了其抗攻击、抗极端数据的稳定性。
注:乘法散列法和全域散列法可参照《算法导论》
4.5 其它方法 了解
有些书籍中还提到了数字分析法、折叠法、平方取中法、随机数法等,这些方法更局限于一些特定的场景。
数字分析法(了解)
取键的某些特征位(如身份证号的出生日期部分)作为索引,适用于键是固定长度的数字串。
折叠法(了解)
将键拆分成若干段,叠加后取模得到索引(如键是"123456",拆成"12"+"34"+"56"=102,再取模)。
平方取中法(了解)
将键平方后,取中间几位作为索引(如键是"123",平方得"15129",取中间三位"512"作为索引)。
随机数法(了解)
以键本身作为 "种子",通过随机数生成器生成一个与键绑定的随机整数,再对哈希表的桶数(M)取模,得到该键对应的哈希地址。
5. 哈希冲突解决方式
5.1 开放定址法
当通过哈希函数计算出的桶位置被占用(冲突)时,不创建链表,而是按照某种探测规则在哈希桶数组中"挨个找",直到找到空闲的桶位置存放元素。
开放定址法的 3 种经典探测方式:
5.1.1 线性探测
冲突后每次往后找一个位置,**最简单但易"堆积",**冲突元素扎堆成"块",后续探测需遍历整个块,效率骤降。
hashi = (hash0 + i) % 表长M
其中:hash0:初始哈希位置(Key % M),i = {1,2,3......M-1},因为负载因子小于1, 则最多探测M-1次,一定能找到一个存储key的位置。

5.1.2 平方探测(二次探测)
线性探测的探测序列是hash0+1、hash0+2、hash0+3......,容易出现"一次聚集"(冲突的桶连成一片);二次探测通过平方数偏移打破聚集,探测序列更分散,公式:
hashi = (hash0 ± i²) % 表长M
其中hash0:初始哈希位置(Key % M),i = {1,2,3......M/2 },二次探测当 (hash0 - i²)<0时需要+=M
注:模拟实现以单向平方探测为例

平方探测的短板:
平方探测其步长仅由 "探测次数 i" 决定,**与 key 本身无关,**若两个不同 key 的初始哈希位置相同(比如 key1=12、key2=25,m=13 时h1均为12),则它们的探测序列完全一致(i=1→12+1=13 %13=0;i=2→12+4=16 %13=3;i=3→12+9=21%13=8...);
这种 "初始位置相同→探测序列完全重叠" 的现象称为二次聚集 ,会导致冲突 key 扎堆在同一组探测序列上,即使数组有其他空闲桶,也只能在重叠序列上竞争,最终探测次数急剧增加。平方探测存在天然的探测盲区, 平方探测的探测序列最多只能覆盖约 50% 的桶:
5.1.3 双重哈希探测(了解)
双重散列是 开放定址法 中性能最优、聚集问题最少的冲突解决策略,核心思想是:用 两个独立的哈希函数共同生成探测序列,彻底解决线性探测的 "一次聚集"、平方探测的 "二次聚集" 问题,让探测序列的随机性、均匀性达到开放定址法的最优水平。是开放定址法在工业级场景(如高性能哈希库、嵌入式开发)的主流选择。
第一个哈希函数h1(key):计算初始哈希位置(和普通哈希函数作用一致);第二个哈希函数h2(key):计算探测步长(步长随键变化,而非固定值);
公式:h(key) = (h1(key) + i×h2(key)) % M
第二个哈希函数的约束条件:
- h2(key)不等于0:步长不能为 0,否则每次探测都停在初始位置,永远无法解决冲突;
- h2(key)与M互为质数:确保探测序列能遍历哈希表中所有桶,避免探测盲区。
第二个哈希函数的设计方案:
M为质数:可设计为h2(key) = 1 + (key % (M - 1))(步长范围[1,M-1],天然与质数M互质)。
M为 2 的幂(如 16):h2(key) = 奇数 (奇数与2^k互质,确保遍历所有桶)。
例如,M=13 质数,设两个哈希函数为:h1(key) = key % 13(初始位置,利用质数M保证初始分布均匀);
h2(key) = 1 + (key % 12)(步长:范围1~12,与13互质,满足双重散列的约数);
待插入key = 27,h1(27) = 1,假设位置12已被占用,进入探测:
h2(27) = 1 + (27 % 12) = 4-->步长为4;
- 第一次探测(i = 1):pos(1) = (1 + 1 * 4) % 13 = 5-->探测位置5。
- 第二次探测(i = 2):pos(2) = (1 + 2 * 4) % 13 = 9-->探测位置9。
- 第三次探测(i = 3):pos(3) = (1 + 3 * 4) % 13 = 0-->探测位置0。
- 第四次探测(i = 4):pos(4) = (1 + 4 * 4) % 13 = 4-->探测位置4。
- 第五次探测(i = 5):pos(5) = (1 + 5 * 4) % 13 = 8-->探测位置8。
依此类推,直到找到空闲桶。
待插入key = 40,h1(40) = 1,h2 = 1+(key%12) = 5 探测序列为6-->11-->3-->8-->0-->......。即使两个key的初始位置相同(如key=27与key=40,h1均为1),只要h2(key)不同(几乎必然,因为h2是独立哈希),探测序列就会完全不同,对比它们的探测序列,很少重叠,彻底消除二次聚集,冲突不会"扎堆",哈希分布更均匀。
开放定址法模拟实现
(1)设计逻辑
开放定址法的核心是哈希冲突时通过探测序列寻找空闲桶存储元素,但直接删除桶内元素会破坏探测链,导致后续冲突元素查找失败(如下图删除值 30 后,查找与其冲突的 20 时会因探测链断裂而失败)。为解决该问题,需为哈希表的每个存储位置增设状态标识位{EXIST,EMPTY,DELETE},删除值 30 后,将它的状态位设为DELETE,那么查找20时遇到 EMPTY 就停止,遇到DELETE就继续查找,就可以找到20。
(2)负载因子阈值
不同探测方式的负载因子控制策略:
开放定址法对负载因子的敏感度高于链地址法------负载因子过高会直接导致探测链急剧变长,性能从O(1)断崖式下跌到O(n),核心是:
通用安全阈值:≤0.7(无论使用哪种开放定址法,只要不超这个值,就能保证哈希表的性能基本稳定);细分阈值:
- 线性探测(最易聚集):≤0.5(工业级实践如 Java ThreadLocalMap (ThreadLoal 的底层存储组件)就严格控制在 0.5 左右);
- 平方探测:≤0.7(聚集问题减轻,可适度放宽);
- 双重哈希探测(分布最均匀):≤0.8(阈值上限,仍不建议接近);
(3)扩容重哈希逻辑
扩容时,必须对原表中所有有效元素执行重新哈希映射到新表,而非直接复制原表数据, 因为"探测序列依赖桶数组长度" 这一核心特性。
(4)无法直接取模问题
工业级字符串哈希实现(BKDRHash,最常用)
当哈希表的key是string类型或像Date这种自定义类类型,无法直接取模,那么就在哈希表的模版类中增加一个哈希仿函数,让它默认能处理整型,对string等特殊类型做模版特化,实现低冲突的哈希转换,自定义类型(如Date)则手动实现仿函数。
仿函数的作用:仅将key转换为整型size_t;
字符串转换成整型,可以把字符的ASCII码值相加即可,但是直接相加的话,像"abcd"和"bcda"两个字符串计算出的结果是相同的,那么使用除留余数法计算出的哈希值就是冲突的,"累加字符ASCII"哈希函数仅适合学习演示,实际开发中绝对不能用,核心问题是字符顺序无关导致的冲突爆炸。
BKDRHash是多项式哈希的优化版,由Brian Kernighan 和 Dennis Ritchie 设计,兼顾效率和低冲突,是各大编程语言标准库的首选(如C++std::hash<string>底层基于此)。工业级字符串哈希的核心是多项式哈希 (hash*BASE + 字符值),工业级优先选BKDRHash(BASE = 131/13131等质数 )。
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key; //处理如果key是负数等情况
}
};
//string类型的模版特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
const size_t BASE = 131; //将131作为基数
size_t hash = 0;
for (auto ch : s)
{
//BKDR算法(对比直接累加字符实现,减少冲突)
hash = hash * BASE + ch;
}
return hash;
}
};
(5)代码实现
namespace open_address //开放定址法模拟实现
{
//质数表
inline unsigned long __stl_next_prime(unsigned long n)
{
// Note: assumes long is at least 32 bits.
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;
}
//哈希仿函数
template<class K>
struct HashFunc
{
size_t operator()(const K& key) const
{
return (size_t)key; //处理如果key是负数等情况
}
};
//string类型的模版特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& s) const
{
const size_t BASE = 131; //核心:多项式哈希,BASE选131(质数,低冲突)
size_t hash = 0;
for (auto ch : s)
{
//BKDR算法(对比直接累加字符实现,减少冲突)
hash = hash * BASE + ch;
}
return hash;
}
};
//桶的状态枚举
enum State { EMPTY, EXIST, DELETE }; //定义桶的状态:空、已占用、已删除
//存储数据类型
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
//哈希表结构
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
HashTable()
:_tables(__stl_next_prime(0))//初始容量为质数表中≥0的最小质数(53)
, _n(0)
{}
bool Insert(const pair<K, V>& kv)
{
//检查是否已存在,存在则返回false
if (Find(kv.first))
return false;
//负载因子检查:负载因子控制在0.7以下 >= 0.7就扩容
double load_factor = _n * 1.0 / _tables.size();
if (load_factor >= 0.7)
{
Rehash(); //扩容
}
size_t hash0 = hashfunc(kv.first) % _tables.size(); //除留余数法 初始哈希位置
size_t i = 0;//探测次数
size_t hashi = hash0;
//探测可用桶,仅当桶为EXIST时继续探测,EMPTY/DELETE均可复用
while (_tables[hashi]._state == EXIST)
{
//线性探测
i++;
hashi = (hash0 + i) % _tables.size(); //取模,避免越界
////平方探测+1 +4 +9 +16 +...... (这里仅实现单向平方探测)
//i++;
//hashi = (hash0 + i * i) % _tables.size(); //取模,避免越界
}
//插入数据
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_n;
return true;
}
void Print()const
{
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._state == EXIST)
{
cout << "位置" << i << " " << _tables[i]._kv.first << " " << _tables[i]._kv.second << endl;
}
}
}
HashData<K, V>* Find(const K& key)
{
size_t hash0 = hashfunc(key) % _tables.size(); //除留余数法
size_t i = 0;
size_t hashi = hash0;
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
//线性探测
i++;
hashi = (hash0 + i) % _tables.size(); //取模,避免越界
////平方探测
//i++;
//hashi = (hash0 + i * i) % _tables.size(); //取模,避免越界
}
return nullptr;
}
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
_n--; //有效数据-1
return true;
}
return false;
}
private:
//扩容逻辑
void Rehash()
{
//创建新表,新容量为质数表中大于等于_tables.size() + 1 的下一个质数
vector<HashData<K, V>> newtables(__stl_next_prime(_tables.size() + 1));
for (const auto& data : _tables)
{
if (data._state == EXIST)
{
size_t hash0 = hashfunc(data._kv.first) % newtables.size(); //除留余数法
size_t i = 0;
size_t hashi = hash0;
while (newtables[hashi]._state == EXIST)
{
//线性探测找空桶
i++;
hashi = (hash0 + i) % newtables.size();
////平方探测+1 +4 +9 +16 +......
//i++;
//hashi = (hash0 + i * i) % _tables.size(); //取模,避免越界
}
newtables[hashi]._kv = data._kv;
newtables[hashi]._state = EXIST;
}
}
//新旧表交换
_tables.swap(newtables);
}
private:
vector<HashData<K, V>> _tables; //桶数组
size_t _n = 0;//有效数据个数
Hash hashfunc;// 哈希函数仿函数对象
};
}
5.2 链地址法
链地址法**(开散列 / 拉链法 / 哈希桶)** 是哈希表的核心冲突解决策略,其核心思想是:将哈希表设计为 "桶数组 + 每个桶挂载独立链表 / 平衡树" 的双层结构,冲突元素不占用数组空闲位置,而是直接挂载到对应桶的链式结构中,是工业级通用哈希表(如 C++ unordered_map、Java HashMap)的首选实现方案。
哈希表的核心是 "桶数组",数组节点不直接存储数据 ,仅存储指向链式结构(链表 / 红黑树)的指针:
- 无数据映射到该桶时,指针为空;
- 多个数据哈希冲突到该桶时,冲突数据被组织成链表(或红黑树),挂在该桶指针下;

链地址法模拟实现
(1)负载因子阈值
开放定址法的负载因子必须<1,而链地址法的负载因子就没有限制了,可以 >1。STL 的unordered_map/unordered_set 等容器,将最大负载因子控制在 1,当负载因子超过 1 时触发扩容,以此平衡冲突概率与空间利用率。
(2)析构函数写 or 不写
哈希表不定义析构函数,自动vector的析构函数可行吗?
不可以。关于vector<Node*>析构:
因为Node*是内置类型(指针类型),内置类型的析构什么都不做,vector的析构逻辑是:销毁数组中每个元素(销毁Node*指针时,只是把指针变量本身从内存中清除,比如0x123456这个值被删掉,但指针指向的Node对象依然存在,变成"孤儿内存"),再释放vector自身的底层数组内存。
所以在析构哈希表时,我们就需要自定义析构函数,替vector完成它"管不到"的清理操作。
vector<Node*>释放内存的核心是:先手动遍历 delete 每个指针指向的对象,再清空 vector。
(3)扩容重哈希逻辑
思路1:新建哈希表对象newHT 并扩容其桶数组,遍历原桶数组链表,为每个原节点复制新节点(new创建)并插入新表,再交换原 / 新表的桶数组,局部变量newHT析构时利用哈希表析构函数释放原节点。(不推荐,太拉胯)
缺点:性能极差:N 个节点对应 N 次new/delete,系统调用开销大;内存浪费:Rehash 过程中原 / 新节点同时存在,临时占用 2 倍内存,创建完整的HashTable对象,但只用到它的_tables,其余成员都是冗余的,浪费内存。
实现
//扩容逻辑(不推荐!!!!!)
void Rehash()
{
//创建新哈希表,新容量为质数表中大于等于_tables.size() + 1 的下一个质数
HashTable<K, V, Hash> newHT;
newHT._tables.resize(__stl_next_prime(_tables.size() + 1));
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* newnode = new Node(cur->_kv.first, cur->_kv.second); //复制节点
size_t hash = hashfunc(cur->_kv.first) % newHT._tables.size();/除留余数法算出映射在新数组的位置
newnode->_next = newHT._tables[hash];
newHT._tables[hash] = newnode;
cur = cur->_next;
}
}
_tables.swap(newHT._tables); //交换原表和新表的桶数组
//局部变量newHT会自动调用自定义析构函数销毁
}
思路2:新建对应容量的桶数组newtables ,采用 "迁移节点" 策略不构造新节点,仅将原节点的指针 "摘下来",重新挂载到新表的桶下,然后交换原 / 新桶数组,vector类型局部变量newtables析构时调用自己的析构函数,因为其内部存储的是一堆nullptr(来自原桶数组的置空操作),不存在内存泄漏问题。迁移节点(最优实现,工业级首选)
性能最优:仅指针移动,避免了new/delete 的开销,时间 / 内存开销极小;内存高效:无冗余节点,无额外内存占用;
实现:
//扩容逻辑(推荐写法!!!)
void Rehash()
{
//创建新的桶数组,新容量为质数表中大于等于_tables.size() + 1 的下一个质数
vector<Node*> newtables(__stl_next_prime(_tables.size() + 1));
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next; //暂存下一个节点
//仅移动指针,将原节点挂载到新桶(无new,无复制)
size_t hash = hashfunc(cur->_kv.first) % newtables.size(); //除留余数法算出映射在新数组的位置
cur->_next = newtables[hash];
newtables[hash] = cur;
cur = next;
}
_tables[i] = nullptr; //原桶数组节点置空
}
_tables.swap(newtables);
//局部变量newtables会自动调用vector的析构函数销毁(里面元素都是nullptr)
}
(4)链地址法的不同实现(有无红黑树)
Java HashMap 和 C++ unordered_map 的链地址法对比(异同):
- 相同:都是桶数组 + 链地址法;
- 不同:Java 1.8 + 有红黑树优化,C++ unordered_map(GCC)只有单向链表,无红黑树优化;Java 扩容是 2 倍(2 的幂),C++ 是 > 2 倍的最小质数;Java 用位运算算索引,C++ 用除留余数法。
(5)代码实现
namespace SeparateChaining //链地址法模拟实现
{
//质数表
inline unsigned long __stl_next_prime(unsigned long n)
{
// Note: assumes long is at least 32 bits.
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;
}
//哈希仿函数
template<class K>
struct HashFunc
{
size_t operator()(const K& key) const
{
return (size_t)key; //处理如果key是负数等情况
}
};
//string类型的模版特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& s) const
{
const size_t BASE = 131; //核心:多项式哈希,BASE选131(质数,低冲突)
size_t hash = 0;
for (auto ch : s)
{
//BKDR算法(对比直接累加字符实现,减少冲突)
hash = hash * BASE + ch;
}
return hash;
}
};
//哈希节点
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{}
};
//哈希表结构
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
//构造函数
HashTable()
:_tables(__stl_next_prime(0))
, _n(0)
{}
//拷贝构造函数
HashTable(const HashTable& other)
{
//初始化当前表的桶数组
_tables.resize(other._tables.size());
_n = other._n;
//遍历每个桶,深拷贝节点
for (size_t i = 0; i < other._tables.size(); i++)
{
Node* cur = other._tables[i];
Node* tail = nullptr;//尾指针
while (cur)
{
Node* newnode = new Node(cur->_kv);
if (_tables[i] == nullptr)
{
_tables[i] = newnode;
tail = newnode;
}
else
{
tail->_next = newnode;
tail = newnode;
}
cur = cur->_next;
}
}
}
//赋值运算符重载
HashTable& operator=(HashTable tmp)
{
_tables.swap(tmp._tables);
swap(_n, tmp._n);
return *this;
}
//析构函数
~HashTable()
{
clear();
}
//清空表
void clear()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
_n = 0;
}
//插入
bool Insert(const pair<K, V>& kv)
{
//检查是否已存在,存在则返回false
if (Find(kv.first))
return false;
//负载因子==1时扩容
if (_n == _tables.size())
{
Rehash();
}
size_t hash = hashfunc(kv.first) % _tables.size();
//头插
Node* newnode = new Node(kv);
newnode->_next = _tables[hash];
_tables[hash] = newnode;
++_n;
return true;
}
//查找
Node* Find(const K& key)
{
size_t hash = hashfunc(key) % _tables.size();
Node* cur = _tables[hash];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
//删除
bool Erase(const K& key)
{
size_t hash = hashfunc(key) % _tables.size();
Node* cur = _tables[hash];
Node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
//分两种情况讨论
//1.如果删除的是链的第一个节点
if (prev == nullptr)
{
_tables[hash] = cur->_next;
}
else //2.如果删除的是链的中间节点或最后一个节点
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
//判空
bool Empty()
{
return _n == 0;
}
//获取有效元素个数
size_t Size()
{
return _n;
}
void Print()const
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
cout << "位置" << i << " " << cur->_kv.first << " " << cur->_kv.second << endl;
cur = cur->_next;
}
}
}
private:
//扩容逻辑(推荐写法!将原链表节点摘取下来)
void Rehash()
{
//创建新的桶数组,新容量为质数表中大于等于_tables.size() + 1 的下一个质数
vector<Node*> newtables(__stl_next_prime(_tables.size() + 1));
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next; //暂存下一个节点
//仅移动指针,将原节点挂载到新桶(无new,无复制)
size_t hash = hashfunc(cur->_kv.first) % newtables.size(); //除留余数法算出映射在新数组的位置
cur->_next = newtables[hash];
newtables[hash] = cur;
cur = next;
}
_tables[i] = nullptr; //原桶数组节点置空
}
_tables.swap(newtables);
//局部变量newtables会自动调用vector的析构函数销毁(里面元素都是nullptr)
}
private:
vector<Node*> _tables; //指针数组
size_t _n = 0; //表中存储的数据个数
Hash hashfunc; //哈希仿函数对象
};
}
