【C++】 链式哈希表(Separate Chaining)


链式哈希表(Chaining Hash Table / 拉链法哈希表)

链式哈希表是最经典、工业界应用最广的哈希表实现方案之一,核心作用是解决哈希冲突问题:当不同的Key经过哈希函数计算后,映射到同一个桶位置时,用「链表挂载」的方式把所有冲突的键值对挂在同一个桶下,避免数据覆盖。

一、底层结构组成

它的整体结构是 「固定大小的桶数组 + 每个桶挂载的动态链表」​ 的两层设计:

组成部分 说明
桶数组(Slot Array) 底层是一个连续内存的数组,长度为 m(也叫槽数/容量),数组的每个位置称为一个「桶」,初始状态所有桶都是空指针
桶挂载的链表 每个桶不直接存键值对,而是作为链表的头节点,所有经过哈希计算落到该桶的 (Key, Value)对,都以链表节点的形式追加到这个链表中
负载因子 α 核心调控指标:α = 已存储元素个数 n / 桶数量 m,用来衡量哈希表的拥挤程度,也是触发扩容的核心判断条件

二、代码结构与核心定义

我们先来看一下代码的基础骨架。

1. 节点结构 (HashNode)

cpp 复制代码
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)
    {}
};

每个节点存储一个 pair<K, V>和一个指向下一个节点的指针。这是一个典型的单链表节点。

2. 哈希表主体 (HashTable)

cpp 复制代码
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
    typedef HashNode<K, V> Node;
private:
    vector<Node*> _tables; // 桶数组
    size_t _n = 0;         // 表中实际元素个数
};

这里使用了 vector<Node*>作为桶数组,而不是 vector<list<pair<K,V>>>。这样做的好处是减少了 list 容器的额外开销,更加贴近底层实现。


三、关键算法详解

1. 哈希函数与取模

为了将 Key 映射到桶下标,我们需要两步:

  1. 将 Key 转换为整数(HashFunc)。

  2. 对桶大小取模(%)。

    Hash hs;
    size_t hashi = hs(key) % _tables.size();

这里的 Hash是一个仿函数(Functor),允许用户自定义哈希规则。

2. 插入操作 (Insert)

插入逻辑分为三步:检查重复 -> 检查扩容 -> 头插。

负载因子与扩容策略

这是哈希表性能的精髓。代码中设定了独特的扩容阈值:

cpp 复制代码
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));
		for (size_t i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			// 当前桶的节点重新映射挂到新表
			while (cur)
			{
				Node* next = cur->_next;
				// 插入到新表
				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;
}

解析:

  • 负载因子 (Load Factor) α​ = 元素个数 / 桶个数 (α=sizen​)。

  • 大多数语言(如 Java)默认阈值是 0.75,但这份代码采用了 1.0。

  • 为什么是 1.0?​ 因为底层使用了质数作为桶大小(见下文),配合质数取模,冲突分布相对均匀,允许更高的负载因子而不至于性能急剧下降。

Rehash(重哈希)

扩容时,桶的大小变为原来的若干倍(通常是下一个质数),并遍历旧表将所有节点重新计算位置插入新表。

cpp 复制代码
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; //保存下一个链节点的地址,防止断链

        size_t hashi = hs(cur->_kv.first) % newtables.size();//找到新表的桶位置

        // 头插到新表桶
        cur->_next = newtables[hashi]; //将当前链节点next指针指向新表桶位置,等于接上链
        newtables[hashi] = cur; //将接上后的链再接入表节点

        cur = next;  //指针往后走,继续断链接链
    }
    _tables[i] = nullptr;//修改完成,原表置空
}
_tables.swap(newtables);

注意细节:这里在搬运节点时,并没有重新 new节点,而是直接改变了节点的 _next指针指向,这是一种高效的"指针窃取"操作。

3. 查找与删除

查找和删除都需要先定位桶,然后遍历链表。

查找 (Find)
cpp 复制代码
Node* Find(const K& key)
{
    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;
}
删除 (Erase)

删除单链表节点需要处理两种情况:删除头节点和删除中间节点。

cpp 复制代码
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;
        delete cur;
        return true;
    }
    prev = cur;
    cur = cur->_next;
}

完整代码

cpp 复制代码
namespace SeparateChaining
{
	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(1), 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));
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					// 当前桶的节点重新映射挂到新表
					while (cur)
					{
						Node* next = cur->_next;

						// 插入到新表
						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;
					}

					delete cur;

					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;
		}

	private:
		//vector<list<pair<K, V>>> _tables;
		vector<Node*> _tables;
		size_t _n = 0;  // 实际存储的数据个数
	};
}

相关推荐
大白话_NOI2 小时前
【洛谷 P1480】A/B Problem(高精度除法 Ⅰ)详细题解
c++
j7~2 小时前
【C++】C&C++内存管理--之内存分布,operatenew/new,operate/delete的底层原理.
c语言·c++·delete·内存泄漏·new·operate new·动态内存分布
拂拉氏2 小时前
【项目分享-知识讲解】 C++标准库 list类的模拟实现
开发语言·c++·list·封装·stl标准库
刃神太酷啦2 小时前
MySQL 库表操作 +数据类型+ 基础概念全梳理----《Hello MySQL!》(2)
java·c语言·数据库·c++·vscode·mysql·adb
L_090710 小时前
【C++】异常
开发语言·c++
liulilittle10 小时前
关于拥塞控制的几点思考
网络·c++·tcp/ip·计算机网络·信息与通信·tcp·通信
xqqxqxxq10 小时前
哈希表(HashMap)技术学习笔记
笔记·学习·散列表
QT-Neal12 小时前
C++ 编码规范
c++
啦啦啦啦啦zzzz13 小时前
数据结构:红黑树理论
数据结构·c++·红黑树