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

相关推荐
闻道且行之9 小时前
TurboOCR:基于PP-OCRv6的极速Windows离线OCR工具,深度解析3.4GB依赖背后的技术架构
c++·人工智能·python·qt·机器学习·ocr
c2385612 小时前
Bug 猎手入门指南
c++·算法·bug
玖玥拾14 小时前
C++ 数据结构 八大基础排序算法专题
数据结构·c++·算法·排序算法
YYYing.14 小时前
【C++大型项目之高性能服务器框架 (七) 】Socket与ByteArray模块
服务器·c++·后端·框架·高性能·c/c++
从零开始的代码生活_15 小时前
C++ list 原理与实践:双向链表、迭代器与简化实现
开发语言·c++·后端·学习·算法·链表·list
charlie11451419115 小时前
现代C++工程实践——WeakPtr 第二关:核心骨架与控制块
开发语言·c++
2401_8697695915 小时前
内容9 string 1
c++
ComputerInBook16 小时前
C & C++ 中的关键字 volatile 之详解
c语言·c++·volatile·编译器优化
XH华16 小时前
C++语言第二章类和对象(下)
开发语言·c++