STL --C++哈希表封装unordered_map/set

一、实现思路

  • 目标 :实现 unordered_map 和 unordered_set,底层使用链地址法(哈希桶)解决冲突

  • 核心架构 :HashTable + HTIterator(迭代器) + HashFunc<K>(哈希仿函数) + KeyOfT(键提取)

二、核心类设计

1. HashNode<T> ------ 哈希桶节点
  • 单链表节点,存储数据 _data 和指向下一个节点的指针 _next

  • 构造函数初始化数据,_next 置空

cpp 复制代码
template <class T>
struct HashNode
{
	T _data;
	HashNode<T>* _next;

	HashNode(const T& data)
		:_data(data)
		, _next(nullptr)
	{	}
};
2. HashFunc<K> ------ 哈希仿函数
  • 通用版本:将 K 转为 size_t

  • 特化版本 HashFunc<string>:BKDR 哈希算法,乘以 33 和 累加字符 ASCII 值

cpp 复制代码
template <class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return size_t(key);
	}
};
template<>//BKDR 哈希
struct HashFunc<string>//仿函数特化string作为键值
{
	size_t operator()(const string& key) const//要对 const敏感点 不改变值的 尽量都加上const
	{
		size_t _num = 0;
		for (auto e : key)
		{
			_num *= 33;
			_num += e;
		}
		return _num;
	}
};
//每个哈希桶的结构---先写整形 再加其他类型的 这里以string为例 其他的思路大致是一样的 主要是找出唯一性
3. HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> ------ 迭代器
  • 成员 :_node 当前节点、_ht 指向哈希表的指针、_bucket 当前桶索引

  • operator++:

    cpp 复制代码
    	Self& operator++()//前置++
    	{
    		if (_node && _node->_next)
    		{
    			_node = _node->_next;
    			return *this;
    		}
    		if (_node == nullptr)
    		{
    			return *this; // 或者 assert(false)
    		}	
    		size_t nextBucket = _bucket + 1;// 从当前桶的下一个开始查找
    		while (nextBucket < _ht->_tables.size())
    		{
    			if (_ht->_tables[nextBucket])
    			{
    				_node = _ht->_tables[nextBucket];
    				_bucket = nextBucket;
    				return *this;
    			}
    			++nextBucket;//没有节点的桶跳过继续++
    		}
    		// 后面没有非空桶,直接返回 end()
    		_node = nullptr;
    		_bucket = _ht->_tables.size(); //把桶索引置为最大 size 表示结束end
    		return *this;
    	}
    	
    	Self operator++(int)
    	{
    		Self temp = *this;
    		++(*this);
    		return temp;
    	}
    • 如果 _node->_next 存在,直接前进

    • 否则从 _bucket + 1 开始找下一个非空桶,更新 _node 和 _bucket

    • 若找不到,则 _node = nullptr,_bucket = _ht->_tables.size()即 end()

  • operator--:

    cpp 复制代码
    	Self& operator--()//前置--
    	{
    		// 如果是 end() (_node == nullptr)退到最后一个元素
    		if (_node == nullptr)
    		{
    			for (size_t i = _ht->_tables.size(); i > 0; --i)
    			{
    				size_t bucket = i - 1;
    				if (_ht->_tables[bucket])//找到一个非空桶
    				{
    					// 跳到该桶的最后一个节点
    					Node* cur = _ht->_tables[bucket];
    					while (cur->_next) cur = cur->_next;
    					_node = cur;
    					_bucket = bucket;
    					return *this;
    				}
    			}
    			_node = nullptr;
    			_bucket = 0;
    			return *this;
    		}
    
    		// 不是桶的头节点
    		if (_node != _ht->_tables[_bucket])
    		{
    			// 从桶头开始遍历,找到前一个节点
    			Node* cur = _ht->_tables[_bucket];
    			while (cur->_next != _node) cur = cur->_next;
    			_node = cur;// _bucket 不变			
    			return *this;
    		}
    
    		//当前节点是桶头 需要向前找上一个非空桶
    		for (size_t i = _bucket; i > 0; --i)
    		{
    			size_t bucket = i - 1;
    			if (_ht->_tables[bucket])
    			{
    				// 跳到该桶的最后一个节点
    				Node* cur = _ht->_tables[bucket];
    				while (cur->_next) cur = cur->_next;
    				_node = cur;
    				_bucket = bucket;
    				return *this;
    			}
    		}
    
    		//没有上一个桶 说明当前是第一个元素 则退回 end()
    		_node = nullptr;
    		_bucket = 0;
    		return *this;
    	}
    	Self operator--(int)
    	{
    		Self temp = *this;
    		--(*this);
    		return temp;
    	}
    • 如果是 end()(_node == nullptr),从最后一个桶向前找最后一个非空桶的尾节点

    • 如果当前节点不是桶头,在桶内遍历找前驱

    • 如果是桶头,向前找上一个非空桶的尾节点

    • 若没有更前的桶,退回 end()

  • 解引用 :operator* 返回 _node->_data 的引用;operator-> 返回 _node->_data 的指针

cpp 复制代码
	Ref operator*()
	{
		return _node->_data;
	}
    Ptr operator->()
    {
	    return &(_node->_data);
    }
4. HashTable<K, T, KeyOfT, Hash>
  • 模板参数:

    • K:键类型

    • T:节点存储的数据类型(map 为 pair<const K,V>,set 为 const K)

    • KeyOfT:从 T 中提取 K 的仿函数

    • Hash:哈希函数,默认为 HashFunc<K>

成员 :vector<Node*> _tables,size_t _n

  • 核心接口:

    • Insert(const T& data)

    • Find(const K& key):找不到返回 end()

    • Erase(const K& key):

    • Begin()/End():

  • 扩容机制 :当 _n == _tables.size() 时,使用素数表(__stl_next_prime)扩容,重新哈希所有节点

具体哈希表结构见文章末尾留的仓库

5. unordered_map<K, V> 与 unordered_set<K> ------ 容器封装
  • 通过 KeyOfT 提取器区分 map 和 set 的数据类型

  • map 存储 pair<const K, V>,set 存储 const K

  • map::operator[] 实现:调用 insert({key, V()}),返回迭代器指向的 second

面试常考题目及答案

Q1:哈希表解决冲突的常见方法有哪些?你的实现用了哪种?

答 :常用方法有链地址法 和开放地址法(线性探测、二次探测、双重哈希),这里 实现使用链地址法,每个桶是一个单链表,冲突元素挂在同一桶的链表上

Q2:为什么哈希表的扩容因子通常为 1?

答 :_n == _tables.size() 时扩容,即负载因子为 1。这是在空间与时间之间的权衡:

  • 负载因子小(如 0.5):冲突少,查询快,但浪费内存

  • 负载因子大(如 2):节省内存,但冲突增多,性能下降

    STL 的 unordered_map 默认负载因子为 1,这里实现保持一致

Q3:迭代器 operator++ 和 operator-- 的时间复杂度是多少?

答:均摊 O(1) 虽然跳桶时需要线性扫描,但每个桶在遍历过程中只被检查一次,整体遍历所有元素的时间复杂度为 O(N)

Q4:unordered_map 的 operator[] 如果键不存在会发生什么?

答 :插入一个默认值 V() 并返回其引用 这是通过 insert({key, V()}) 实现的,如果键不存在则插入,存在则返回已有值的引用

Q5:为什么 unordered_map 的键是 const K?

答:防止用户通过迭代器修改键,破坏哈希表的内部结构(修改键会导致哈希值变化,使元素无法被正确查找)

Q6:拷贝构造为什么要深拷贝?浅拷贝会有什么后果?

答 :如果使用编译器默认的浅拷贝,两个对象会共享同一块内存(vector<Node*> 中的指针指向相同的 HashNode 对象) 析构时会导致双重释放,程序崩溃

Q7:扩容时,为什么需要重新计算哈希值?

答:扩容后桶的数量变化,哈希值取模的结果会改变 如果不重新哈希,所有元素都会跑到错误的桶中,导致查找失败

Q8:你的哈希函数支持哪些类型?如何扩展支持自定义类型?

答 :默认支持整数类型和 string(通过特化) 自定义类型只需特化 HashFunc<MyType>,提供 size_t operator()(const MyType& key) 接口就可以

我的gitee仓库

https://gitee.com/jiangmingpeng0716/c-learning-process

相关推荐
凉茶钱1 小时前
【C++】string 常用函数详解
开发语言·c++·string
(Charon)2 小时前
【C++面试】程序崩溃如何定位:从Segmentation fault到GDB、Core Dump与ASan
开发语言·c++
汉克老师2 小时前
GESP2026年9月认证C++七级( 第三部分编程题(1、必经之路))精讲
c++·gesp·小学生·学c++编程
ebiobiz2 小时前
Zig 嵌入式底层库选择与 build.zig 组织指南
c语言·c++·嵌入式硬件·mcu
漂流瓶jz3 小时前
UVA-11491 奖品的价值 题解答案代码 算法竞赛入门经典第二版
c++·算法·图论·题解·aoapc·算法竞赛入门经典·uva
weixin_307779137 小时前
C++代码实现MATLAB中的dlarray函数功能
开发语言·c++·算法·matlab
Frank_refuel10 小时前
C++11之一场名为“搬家”的 C++ 之旅
开发语言·c++
hetao173383712 小时前
2026-10-03~04 hetao1733837 的刷题记录
c++·算法
longlongzihan13 小时前
从LeetCode 70. 爬楼梯看动态规划:如何精准推导状态转移方程?
c++·算法·leetcode·动态规划
潼心1412o16 小时前
C++初阶(长期更新)第6讲:类和对象(下)
开发语言·c++·算法