【C++ 泛型编程】基于哈希表封装 unordered_set(附完整源码解析)
建议配合上一篇讲的哈希链式实现的博客一起食用哦
结尾附上详细代码!!
在 C++ 标准库中,unordered_set 是 “存储唯一元素、支持快速查找” 的关联容器,其底层依赖链地址法哈希表实现,核心优势是 “平均 O (1) 时间复杂度的增删查”。本文将基于你提供的完整代码(HashTable 哈希表 +unordered_set 初步实现),从 “封装逻辑、代码解析、使用场景” 三个维度,带你吃透 unordered_set 的实现原理,理解泛型编程的复用精髓。
一、封装核心思路:复用泛型哈希表,聚焦 “唯一 key 存储”
unordered_set 的核心需求是 “存储不重复的 key,支持快速存在性判断”,而你已实现的 HashTable 是泛型底层容器,恰好支持 “自定义存储类型、自定义 key 提取方式”—— 这是封装 unordered_set 的关键前提,无需重复写哈希表逻辑,只需 “适配”
核心复用逻辑(3 个关键匹配)
| 组件 | 哈希表(HashTable)的要求 | unordered_set 的适配方式 |
|---|---|---|
| 存储数据类型(T) | 支持任意可拷贝构造的类型 | 存储单个 key(类型 K),即 T=K |
| key 提取方式(KeyOfT) | 需提供 “从 T 中提取 key” 的仿函数 | 直接返回 T 本身(因 T 就是 key),简化 KeyOfT |
| 去重机制 | 插入时通过 key 判断是否重复(Find 接口) | 直接复用 HashTable 的 insert 去重逻辑,无需额外处理 |
简单说:unordered_set 本质是 “哈希表的上层适配”—— 把哈希表的 “存储类型” 限定为单个 key,“key 提取” 简化为直接返回自身,再暴露符合 unordered_set 语义的接口(如 insert 返回是否插入成功、迭代器遍历等)
二、unordered_set 完整代码解析(基于你的实现)
你提供的 unordered_set 代码已实现核心功能,下面逐块拆解 “为什么这么写”“核心设计巧思”,结合底层 HashTable 讲透依赖关系。
完整代码
#pragma once
#include "hashtable.h" // 依赖泛型哈希表,复用底层逻辑namespace my_hash
{template<class K, class Hash = Hash<K>>class unordered_set{// 1. 核心:KeyOfT 仿函数——从存储数据T中提取key// 因unordered_set存储的T就是K(单个key),直接返回自身即可struct KeyOfT{const K& operator()(const K& key){return key; // 哈希表需要通过key计算哈希值、去重,此处直接返回key}};public:// 2. 迭代器定义:复用HashTable的Iterator,需加typename(依赖模板参数)using iterator = typename HashTable<K, K, KeyOfT>::Iterator;using const_iterator = typename HashTable<K, K, KeyOfT>::Const_iterator;// 3. 插入接口:复用HashTable的insert,保持语义一致// 返回pair<iterator, bool>:迭代器指向插入/已存在的元素,bool表示是否插入成功pair<iterator, bool> insert(const K& key){return _hash.insert(key); // 直接调用哈希表的insert,无需额外逻辑}// 4. 迭代器接口:复用HashTable的Begin/End,适配unordered_set的遍历需求iterator begin(){return _hash.Begin(); // 哈希表的Begin返回第一个有效节点的迭代器}iterator end(){return _hash.End(); // 哈希表的End返回nullptr迭代器(结束标记)}const_iterator begin() const{return _hash.Begin(); // const版本,保证只读访问}const_iterator end() const{return _hash.End();}private:// 5. 底层存储:依赖HashTable,模板参数对应关系需精准匹配// HashTable<K, T, KeyOfT, Hash>:// K:key类型;T:存储的数据类型(此处T=K);KeyOfT:key提取仿函数;Hash:哈希函数HashTable<K, K, KeyOfT, Hash> _hash;};
}
关键代码拆解(3 个核心点)
1. KeyOfT 仿函数:哈希表的 “key 提取器”
这是 unordered_set 能复用 HashTable 的核心 ——HashTable 是泛型容器,不知道 “存储的数据 T 中,哪个部分是 key”,因此需要 KeyOfT 仿函数明确 “从 T 中拿什么当 key”。
- 对
unordered_set而言,T 就是 K(存储的就是单个 key),所以KeyOfT直接返回输入的 key 即可,逻辑极简; - 对比
unordered_map(存储pair<K,V>),KeyOfT需返回pair.first,这也是两者封装的核心差异
2. 迭代器复用:零成本适配
unordered_set 的迭代器直接复用 HashTable 的 Iterator,原因是:
HashTable的迭代器存储HashNode<T>指针,unordered_set中T=K,因此迭代器解引用后返回K&,完全符合unordered_set“遍历 key” 的需求;- 需加
typename关键字:HashTable<K, K, KeyOfT>::Iterator是 “依赖模板参数的类型”,编译器无法提前识别,必须用typename声明为类型
3. 接口设计:保持与标准库一致
你的实现中,insert、begin、end 等接口完全对齐 C++ 标准库 unordered_set:
insert返回pair<iterator, bool>:既告诉用户 “插入是否成功”(避免重复插入),又返回 “元素所在位置”,实用性极强;- 支持
const_iterator:满足只读场景(如遍历 const 对象),符合容器设计的完整性
三、核心功能验证:unordered_set 使用示例
下面通过完整的 main 函数,测试 unordered_set 的插入、遍历、查找、删除等核心功能
#include <iostream>
#include "unordered_set.h" // 包含你的unordered_set实现
using namespace std;
using namespace my_hash;int main()
{// 1. 定义unordered_set:存储int类型的key,默认哈希函数unordered_set<int> us;// 2. 插入元素(支持重复插入,但会失败)us.insert(10);us.insert(20);us.insert(10); // 重复插入,返回falseus.insert(30);us.insert(20); // 重复插入,返回false// 3. 遍历容器(无序,哈希表存储特性)cout << "遍历unordered_set(无序):" << endl;for (const auto& key : us) { // 范围for依赖begin()和end()cout << key << " ";}cout << endl; // 输出示例:10 20 30(顺序可能不同)// 4. 迭代器遍历(const版本)const unordered_set<int> cus = us;cout << "const迭代器遍历:" << endl;for (auto it = cus.begin(); it != cus.end(); ++it) {cout << *it << " ";}cout << endl;// 5. 查找元素auto it = us.find(20); // 调用HashTable的Find接口if (it != us.end()) {cout << "找到元素:" << *it << endl;} else {cout << "未找到元素" << endl;}// 6. 删除元素bool ret = us.erase(30); // 调用HashTable的Erase接口if (ret) {cout << "删除30成功" << endl;}// 7. 插入string类型key(测试哈希函数模板特化)unordered_set<string> us_str;us_str.insert("apple");us_str.insert("banana");us_str.insert("orange");cout << "string类型key遍历:" << endl;for (const auto& s : us_str) {cout << s << " ";}cout << endl;return 0;
}
输出结果(顺序可能不同,因哈希表无序)
遍历unordered_set(无序):
10 20 30
const迭代器遍历:
10 20 30
找到元素:20
删除30成功
string类型key遍历:
apple banana orange
关键验证点
- 去重性:重复插入的 10、20 未被存储,符合
unordered_set“元素唯一” 的特性; - 无序性:遍历顺序与插入顺序无关,是哈希表 “按桶存储” 的正常表现;
- 多类型支持:string 类型 key 能正常使用,得益于
HashTable中 string 的哈希函数模板特化; - const 兼容性:const 对象能通过 const_iterator 遍历,接口设计完整
现在附上详细代码:
#pragma once
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
namespace my_hash
{
template<class K> //仿函数, 用于转换为size_t
struct Hash
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//模板特化:专门处理 string 类型(避免二义性,优化哈希算法)
//当然要什么类型的转换,都可以特化该类型
template<>
struct Hash<string>
{
size_t operator()(const string& key)
{
size_t ans = 0;
for (auto e : key)
{
ans += e;
}
return ans;
}
};
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
: _data(data)
, _next(nullptr)
{
}
};
//前置声明,防止HTIterator找不到HashTable
template<class K, class T, class KeyOfT, class Hash>
class HashTable;
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct HTIterator
{
using Node = HashNode<T>;
using HT = HashTable<K, T, KeyOfT, Hash>;
using Self = HTIterator<K, T, Ref, Ptr, KeyOfT, Hash>;
Node* _node;
const HT* _ht;
HTIterator(Node* node, const HT* ht)
: _node(node)
, _ht(ht)
{
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &(_node->_data);
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
Self& operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else
{
Hash hash;
KeyOfT kot;
size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
hashi++;
while (hashi < _ht->_tables.size() && !_ht->_tables[hashi])
{
hashi++;
}
if (hashi == _ht->_tables.size())
_node = nullptr;
else
_node = _ht->_tables[hashi];
}
return *this;
}
};
template<class K, class T, class KeyOfT, class Hash = Hash<K>>
class HashTable
{
//模板友元,需要传模板参数
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
friend struct HTIterator;
using Node = HashNode<T>;
public:
using Iterator = HTIterator<K, T, T&, T*, KeyOfT, Hash>;
using Const_iterator = HTIterator<K, T, const T&, const T*, KeyOfT, Hash>;
//直接打表弄素数,关键是这个真的是c++ gil标准实现写法
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); //查找第一个 >= n 的数字
return pos == last ? *(last - 1) : *pos;
}
HashTable()
: _tables(__stl_next_prime(0)) //让他为素数,因为素数只能被他自己和1整除,可以更好的进行取模运算得到更多的不同风格值
, _n(0)
{
}
HashTable(const HashTable<K, T, KeyOfT, Hash>& other)
{
KeyOfT kot;
_tables.resize(other._tables.size());
_n = other._n;
for (size_t i = 0; i < other._tables.size(); i++)
{
Node* pcur = other._tables[i];
Node* tail = nullptr;
while (pcur)
{
Node* newNode = new Node(pcur->_data);
if (_tables[i])
{
tail->_next = newNode;
tail = newNode;
}
else
{
_tables[i] = newNode;
tail = newNode;
}
pcur = pcur->_next;
}
}
}
HashTable& operator=(HashTable<K, T, KeyOfT, Hash> other)
{
_tables.swap(other._tables);
swap(_n, other._n);
return *this;
}
~HashTable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* pcur = _tables[i];
while (pcur)
{
Node* next = pcur->_next;
delete pcur;
pcur = next;
}
_tables[i] = nullptr;
}
}
Iterator Begin()
{
if (_n == 0)
{
return End();
}
for (size_t i = 0; i < _tables.size(); i++)
{
Node* pcur = _tables[i];
if (pcur)
{
return Iterator(pcur, this);
}
}
}
Iterator End()
{
return Iterator(nullptr, this);
}
Const_iterator Begin() const
{
if (_n == 0)
{
return End();
}
for (size_t i = 0; i < _tables.size(); i++)
{
Node* pcur = _tables[i];
if (pcur)
{
return Const_iterator(pcur, this);
}
}
}
Const_iterator End() const
{
return Const_iterator(nullptr, this);
}
pair<Iterator, bool> insert(const T& data)
{
KeyOfT kot;
Iterator it = Find(kot(data));
if (it != End())
{
return { it, false };
}
Hash hash;
if (_n == _tables.size())
{
/*HashTable<K, V, Hash> newhash;
newhash._tables.size() = __stl_next_prime(_tables.size() + 1);
for (size_t i = 0; i < _tables.size(); i++)
{
Node* pcur = _tables[i];
while (pcur)
{
newhash.insert(pcur->_data);
pcur = pcur->_next;
}
_tables[i] = nullptr;
}
_tables.swap(newhash);
*/ //这样写有问题,因为newhash是临时对象,出了循环会调用析构函数,虽然vector会调用析构
//但Node*是我的自定义类型,无法析构,所以得自己实现析构函数
//如果用list写法就可以解决这个问题,因为list自带析构函数,但是同时也要面临其他的问题
//所以我们换一种扩容方法从根源上解决需要析构的问题
//当前方法是swap,_tables作为老数据只是进行了交换然后析构,他已经没用了
//但是我们又无法析构Node,何不直接将_tables的元素取出来,然后直接头插
//这样结束后_tables里面没有Node*,调用vector析构足矣
HashTable<K, T, KeyOfT, Hash> newhash;
newhash._tables.resize(__stl_next_prime(_tables.size() + 1));
for (size_t i = 0; i < _tables.size(); i++)
{
Node* pcur = _tables[i];
while (pcur)
{
Node* next = pcur->_next;
size_t hashi = hash(kot(pcur->_data)) % newhash._tables.size();
pcur->_next = newhash._tables[hashi];
newhash._tables[hashi] = pcur;
pcur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newhash._tables);
}
size_t hashi = hash(kot(data)) % _tables.size();
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
_n++;
return { Iterator(newnode, this), true };
}
Iterator Find(const K& key)
{
KeyOfT kot;
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* pcur = _tables[hashi];
while (pcur)
{
if (kot(pcur->_data) == key)
{
return Iterator(pcur, this);
}
pcur = pcur->_next;
}
return End();
}
bool Erase(const K& key)
{
KeyOfT kot;
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* pcur = _tables[hashi];
Node* prev = nullptr;
while (pcur)
{
if (kot(pcur->_data) == key)
{
if (prev)
{
prev->_next = pcur->_next;
}
else
{
_tables[hashi] = pcur->_next;
}
delete pcur;
_n--;
return true;
}
else
{
prev = pcur;
pcur = pcur->_next;
}
}
return false;
}
private:
vector<Node*> _tables; //本质上是一个指针数组,相当于每一个数组元素里面都放入一个list,所以也可以写为vector<list> _tables
size_t _n; //但是这样也会有新的问题出现需要解决,总之没有一个万能的方法,每个方法都有好处和坏处
};
}
#pragma once
#include "hashtable.h"
namespace my_hash
{
template<class K, class Hash = Hash<K>>
class unordered_set
{
struct KeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
using iterator = typename HashTable<K, K, KeyOfT>::Iterator;
using const_iterator = typename HashTable<K, K, KeyOfT>::Const_iterator;
pair<iterator, bool> insert(const K& key)
{
return _hash.insert(key);
}
iterator begin()
{
return _hash.Begin();
}
iterator end()
{
return _hash.End();
}
const_iterator begin() const
{
return _hash.Begin();
}
const_iterator end() const
{
return _hash.End();
}
private:
HashTable<K, K, KeyOfT, Hash> _hash;
};
}
