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

一、实现思路

  • 目标 :实现 unordered_mapunordered_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:节点存储的数据类型(mappair<const K,V>setconst K

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

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

成员vector<Node*> _tablessize_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 提取器区分 mapset 的数据类型

  • 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_mapoperator[] 如果键不存在会发生什么?

:插入一个默认值 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

相关推荐
hansang_IR10 分钟前
【题解】[COCI 2024/2025 #2] 流明 / Blistavost
c++·算法·动态规划·dp
不正经学生16 分钟前
C语言指针进阶:const 和野指针——给指针加锁,向野指针宣战
c语言·开发语言·c++·算法·c#
程序喵大人17 分钟前
【C++进阶】STL算法与函数对象 - 01 让算法去处理一段迭代器范围
开发语言·c++·算法·函数对象
旖旎夜光1 小时前
LeetCode 738:单调递增的数字(贪心算法) —— 题解
数据结构·c++·算法·leetcode·贪心算法
旖旎夜光2 小时前
LeetCode 1262:可被三整除的最大和 (贪心算法)—— 题解
数据结构·c++·算法·leetcode·贪心算法
蒸蒸yyyyzwd2 小时前
cpp 选手转 ai infra 后端秋招学习笔记 day1
c++·transformer
A_cainiao_A2 小时前
【ggml系列】【第四篇】ggml_graph_compute 多线程计算引擎与算子分发源码深度解析
开发语言·c++·矩阵
影视飓风TIM2 小时前
C++ STL 完整入门笔记[9]:C++ 仿函数、priority_queue与STL算法底层笔记
数据结构·c++·笔记
晚风叙码3 小时前
C++ 模板深度解析:类型模板、非类型模板、特化与分离编译
开发语言·c++·算法
依然鸣3 小时前
蓝桥杯多校模拟赛(第二场)题解
数据结构·c++·经验分享·学习·算法·图论