在计算机科学的数据结构体系中,哈希表(Hash Table) 是一种极具代表性且应用极为广泛的存储结构,凭借其卓越的性能优势,成为构建高效系统的核心基础组件。与线性表需要逐一遍历查找、树表需要逐层比较不同,哈希表通过哈希函数 建立关键字与存储地址的直接映射关系,能够在平均 O (1) 常数时间复杂度内完成查找、插入、删除操作,在海量数据处理场景中展现出不可替代的效率优势。
哈希表的核心价值在于突破了传统数据结构的性能瓶颈,以空间换时间的设计思想,实现了数据的快速存取。无论是软件开发中的字典、关联容器 ,互联网架构中的缓存系统、路由表 ,还是数据处理中的高频计数器、去重统计,哈希表都是最常用的底层支撑结构,是现代计算机系统高效运行的关键基石。
本文将围绕哈希表展开系统性讲解,从哈希函数设计、哈希冲突处理两大核心原理入手,详细分析开放寻址法、哈希桶(链地址法)的实现机制,并结合 C++ 语言完成完整代码实现,包括底层结构搭建、迭代器封装、深拷贝管理、扩容优化等关键内容,最终形成一套可落地、可理解、可扩展的哈希表实现方案,帮助全面掌握哈希表的底层逻辑与工程实践方法。
1. 哈希基础概念
1.1 哈希定义
哈希(Hash),又称散列,核心是通过哈希函数,将任意类型的关键字(Key),映射为固定范围的整数(存储地址),实现快速存取。
-
核心定义:用哈希函数将关键字(如int、string)转化为存储地址,无需遍历,直接定位,这是哈希表高效的关键。
-
核心思想:空间换时间------牺牲部分存储空间(预留哈希桶、空闲位置),换取查找、插入、删除操作均为O(1)的高效性能。
-
关键细节:同一关键字经同一哈希函数,必映射到同一地址;不同关键字可能映射到同一地址(哈希冲突),需通过对应方式处理。
核心思想:
- 给每个 Key 计算一个下标
index = h(key)。 - 将数据存储到这个下标对应位置。
- 查找时通过哈希函数直接找到位置,极大提升速度。
1.2 直接定地址法
当关键字范围集中时,直接定址法非常高效:
- 例如关键字在
[0, 99],可用数组下标直接表示关键字。 - 字符
[a, z]可以用 ASCII 码减'a'得到数组下标。
优点:
- 简单、快速,无哈希冲突问题。
缺点:
- 当关键字范围分散时,会浪费大量内存。
- 不适合稀疏或大范围数据。
cpp
#include <iostream>
#include <string>
using namespace std;
// 直接定址法演示:统计 a~z 每个字母出现次数
void directAddressHash(string s)
{
// a~z 一共26个字母,开辟大小为26的数组
int cnt[26] = {0};
// 直接定址核心:ch - 'a' 映射为数组下标 0~25
for (char ch : s)
{
int index = ch - 'a'; // 直接定址哈希函数
cnt[index]++;
}
// 遍历输出每个字母的出现次数
for (int i = 0; i < 26; i++)
{
if (cnt[i] != 0)
{
char ch = 'a' + i;
cout << "字符 " << ch << " 出现了:" << cnt[i] << " 次" << endl;
}
}
}
int main()
{
string str = "abacabxyzabc";
directAddressHash(str);
return 0;
}
1.3 哈希冲突
当两个不同的 Key ,经过哈希函数计算后,映射到同一个存储下标位置 时,就会产生哈希冲突。
在实际应用中,关键字随机、分布不确定,无论怎么设计哈希函数,都无法从根本上完全杜绝哈希冲突。
所以工程上只能做两件事:
- 设计分布尽量均匀 的哈希函数,让 Key 尽量散列,减少冲突发生的概率;
- 配套完善、成熟的哈希冲突解决方案,就算发生冲突,也能正常存入、正常查找,不影响哈希表整体性能。
cpp
#include <iostream>
using namespace std;
// 自定义哈希函数:除留余数法
int hashFunc(int key)
{
// 数组长度为10,对10取模
return key % 10;
}
int main()
{
// 哈希表数组,长度10
int hashTable[10] = {0};
// 两个完全不同的 key
int key1 = 12;
int key2 = 22;
int idx1 = hashFunc(key1);
int idx2 = hashFunc(key2);
cout << "关键字 " << key1 << " 映射下标:" << idx1 << endl;
cout << "关键字 " << key2 << " 映射下标:" << idx2 << endl;
if (idx1 == idx2)
{
cout << "发生了哈希冲突!" << endl;
}
return 0;
}
cpp
关键字 12 映射下标:2
关键字 22 映射下标:2
发生了哈希冲突!
这就是典型的哈希冲突。
2. 哈希函数设计
2.1 将关键字转换为整数
哈希表底层只能用整数 计算存储下标,但关键字可以是整型、字符串、pair、自定义结构体 等类型,所以必须先将关键字转换为整数:
整型关键字:直接强制类型转换为整数;
cpp
template<class K>
struct HashFunc
{
size_t operator()(const K& key) const
{
return (size_t)key;
}
};
cpp
template<>
struct HashFunc<int>
{
size_t operator()(int key) const
{
key ^= (key >> 16);
return (size_t)key;
}
};
字符串关键字:遍历字符,利用字符 ASCII 码加权运算,折算为一个整数;
cpp
// 字符串特化:把字符串每个字符转ASCII,加权算出一个整数
template<>
struct HashFunc<string>
{
size_t operator()(const string& key) const
{
size_t hash = 0;
// 遍历每个字符,将字符转为ASCII参与运算
for (auto ch : key)
{
hash += ch; // 累加字符ASCII值
hash *= 131; // 加权扰动,让哈希值分布更均匀
}
return hash;
}
};
复合类型 pair:拆解内部成员,依次加权合并,最终生成一个哈希整数。
cpp
// 把 pair<int,int> 这种组合关键字,换算成一个哈希整数
struct pairHash
{
size_t operator()(const pair<int, int>& kv) const
{
size_t hash = 0;
hash += kv.first;
hash *= 131;
hash += kv.second;
hash *= 131;
return hash;
}
};
转换后的整数再通过 hash % 表容量 就能算出存储下标。
2.2 常用哈希函数
2.2.1 除法散列法
除留余数法 是哈希函数中最常用、最简单的一种构造方法。
设哈希表底层数组长度为 m,对任意关键字 key,哈希函数公式:H(key)=keymodm也就是:哈希地址 = 关键字 % 哈希表容量
作用:把任意大的整数关键字,映射到 0 ~ m-1 之间的数组下标。
哈希表容量 m 优先选:
质数(素数) __stl_next_prime 就是专门干这事:每次扩容自动找下一个质数当表长,最大限度减少冲突。
cpp
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;
}
2.2.2 乘法散列法
公式:h(key)=⌊M×((A×key)mod1.0)⌋
常数 A 取黄金分割 25−1≈0.618。优点:对哈希表大小无特殊要求,不用刻意选质数,映射分布均匀。
2.2.3 全域散列法
公式:hab(key)=((a×key+b)modP)modM
思路:引入随机参数 、,初始化时随机选定并固定使用。给哈希函数增加随机性,避免恶意构造大量哈希冲突,提升整体稳定性。
3. 哈希冲突的处理策略
3.1 开放定址法
核心思想:
- 所有元素都直接存储在哈希表的数组中
- 如果一个 Key 映射位置已经被占用,就按某种规则探测下一个可用位置
特点:
- 表内所有元素互相影响
- 负载因子必须 <1(否则找不到空位置)
主要探测策略
1. 线性探测(Linear Probing)
- 原理:冲突后,从当前位置依次向后查找空槽
- 公式:

- 问题:连续冲突会形成"堆积",降低效率

2.二次探测(Quadratic Probing)
- 原理:按二次方跳跃式探测
- 公式:

- 可以缓解线性探测的堆积问题
- 问题:仍可能未使用表中全部位置

3.1.1开放定址法实现
cpp
enum State { EXIST, EMPTY, 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 {
private:
vector<HashData<K,V>> _tables;
size_t _n = 0; // 元素个数
public:
bool Insert(const pair<K,V>& kv) {
if (_n * 10 / _tables.size() >= 7) resize();
size_t hash0 = Hash()(kv.first) % _tables.size();
size_t i = 0, hashi;
do {
hashi = (hash0 + i) % _tables.size(); // 线性探测
if (_tables[hashi]._state != EXIST) break;
++i;
} while (i < _tables.size());
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_n;
return true;
}
};
3.2 链地址法
核心思想:
- 哈希表只存储指针,不直接存数据
- 冲突的数据挂在对应桶下形成链表
- 当桶中数据过多,可以升级为红黑树(如 Java 8 HashMap)
特点:
- 每个桶可以存多个元素
- 负载因子可 >1,扩容灵活
- 极端情况下查找效率下降,但一般不会严重


- 冲突时元素通过链表挂到同一个桶
- 查找时遍历链表即可
3.2.1 链地址法实现
cpp
template<class K, class V>
struct HashNode {
pair<K,V> _kv;
HashNode* _next;
HashNode(const pair<K,V>& kv) : _kv(kv), _next(nullptr) {}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable {
private:
vector<HashNode<K,V>*> _tables;
size_t _n = 0;
public:
bool Insert(const pair<K,V>& kv) {
size_t hashi = Hash()(kv.first) % _tables.size();
auto newnode = new HashNode<K,V>(kv);
newnode->_next = _tables[hashi]; // 头插法
_tables[hashi] = newnode;
++_n;
return true;
}
};
头插法:
cpp
auto newnode = new HashNode<K,V>(kv);
newnode->_next = _tables[hashi]; // 头插法
_tables[hashi] = newnode;
_tables[hashi]是哈希桶数组的一个元素,指向该桶链表的头节点;- 新节点的
next先指向原来的头节点; - 再让桶指针指向新节点,新节点就变成了链表的新头。


4. 扩容
4.1 开放定址法 扩容规则
负载因子 < 1
- 开放寻址法底层是数组
- 每个位置只能存一个数据
- 装满了就不能再存
- 所以负载因子必须小于 1
- 一般达到 0.7 就必须扩容
4.2 链地址法 扩容规则
负载因子可以 > 1
- 每个位置是链表 / 红黑树
- 一个桶可以挂很多数据
- 数据再多也能存
- 所以负载因子可以超过 1
- 一般达到 1.0 扩容
4.3 扩容方法
- 创建新表 容量 = 旧容量 × 2(取最近质数)
- 重新哈希把旧表所有数据用新容量重新计算下标全部插入新表
- 替换旧表新表替代旧表,旧表释放
cpp
// 扩容条件:负载因子 >= 0.7
if ((double)_n / (double)_tables.size() >= 0.7)
{
// 1. 创建新表:容量取 下一个质数
HashTable<K, V, Hash> newht(__stl_next_prime(_tables.size()+1));
// 2. 遍历旧表,重新哈希映射到新表
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i]._state == EXIST)
{
newht.Insert(_tables[i]._kv);
}
}
// 3. 新表替换旧表
_tables.swap(newht._tables);
}
5. 实现
哈希表设计核心:
- 选择合适哈希函数
- 冲突处理策略(开放定址 / 链地址)
- 负载因子与扩容机制
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<vector>
using namespace std;
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;
}
};
// 特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& key) const
{
size_t hash = 0;
for (auto ch : key)
{
hash += ch;
hash *= 131;
}
return hash;
}
};
namespace open_adrress
{
enum State
{
EXIST,
EMPTY,
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(size_t n = __stl_next_prime(0))
:_tables(n)
, _n(0)
{}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
// 扩容,负载因子==0.7就扩容
if ((double)_n / (double)_tables.size() >= 0.7)
{
//HashTable<K, V> newht(_tables.size() * 2);
HashTable<K, V, Hash> newht(__stl_next_prime(_tables.size() + 1));
// 遍历旧表,将旧表的数据全部重新映射到新表
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i]._state == EXIST)
{
newht.Insert(_tables[i]._kv);
}
}
_tables.swap(newht._tables);
}
Hash hs;
size_t hash0 = hs(kv.first) % _tables.size();
size_t hashi = hash0;
size_t i = 1;
// 线性探测
while (_tables[hashi]._state == EXIST)
{
hashi = hash0 + i;
i++;
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_n;
return true;
}
HashData<K, V>* Find(const K& key)
{
Hash hs;
size_t hash0 = hs(key) % _tables.size();
size_t hashi = hash0;
size_t i = 1;
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._state == EXIST
&& _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
// 线性探测
hashi = hash0 + i;
i++;
hashi %= _tables.size();
}
return nullptr;
}
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
--_n;
return true;
}
else
{
return false;
}
}
private:
vector<HashData<K, V>> _tables;
size_t _n; // 实际存储的数据个数
};
void TestHashTable1()
{
int a[] = { 19, 30, 5, 36, 13, 20, 21, 12 };
HashTable<int, int> ht;
for (auto e : a)
{
ht.Insert({ e, e });
}
cout << ht.Find(20) << endl;
cout << ht.Find(30) << endl;
ht.Erase(30);
cout << ht.Find(20) << endl;
cout << ht.Find(30) << endl;
ht.Insert({ -3,3 });
ht.Insert({ 13,3 });
ht.Insert({ 33,3 });
ht.Insert({ 323,3 });
ht.Insert({ 23,3 });
for (size_t i = 0; i < 100; i++)
{
ht.Insert({ rand(),i });
}
}
struct pairHash
{
size_t operator()(const pair<int, int>& kv) const
{
size_t hash = 0;
hash += kv.first;
hash *= 131;
hash += kv.second;
hash *= 131;
cout << hash << endl;
return hash;
}
};
// 21:07
void TestHashTable2()
{
//HashTable<string, string, StringHashFunc> dict;
HashTable<string, string> dict;
dict.Insert({ "sort", "排序" });
dict.Insert({ "left", "左边" });
dict.Insert({ "sort", "xxx" });
unordered_map<pair<int, int>, int, pairHash> um;
um.insert({ {1,3},3 });
um.insert({ {3,1},3 });
}
}
namespace hash_bucket
{
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(size_t n = __stl_next_prime(0))
:_tables(n, nullptr)
, _n(0)
{}
~HashTable()
{
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;
}
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
Hash hs;
// 负载因子到1扩容
if (_n == _tables.size())
{
// 扩容
vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);
// 遍历旧表,将旧表的数据全部重新映射到新表
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
// cur头插到新表
size_t hashi = hs(cur->_kv.first) % newtables.size();
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtables);
}
size_t hashi = hs(kv.first) % _tables.size();
// 头插
Node* newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
Node* Find(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
--_n;
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _tables;
size_t _n; // 实际存储有效数据个数
};
void TestHashTable1()
{
int a[] = { 19,30,5,36,13,20,21,12,24,96 };
HashTable<int, int> ht;
for (auto e : a)
{
ht.Insert({ e, e });
}
for (size_t i = 0; i < 100; i++)
{
ht.Insert({ rand(),i });
}
}
void TestHashTable2()
{
int a[] = { 19,30,5,36,13,20,21,12,24,96 };
HashTable<int, int> ht(11);
for (auto e : a)
{
ht.Insert({ e, e });
}
ht.Erase(30);
ht.Erase(24);
for (auto e : a)
{
ht.Erase(e);
}
}
void TestHashTable3()
{
HashTable<string, string> dict;
dict.Insert({ "sort", "排序" });
dict.Insert({ "left", "左边" });
dict.Insert({ "sort", "xxx" });
}
}