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

相关推荐
1000世界小札14 分钟前
《大话数据结构》第9章精读:归并排序与快速排序完整 C++ 实现
数据结构·c++·算法
2601_956121973 小时前
背包基础篇(01、完全、分组、多重、混合)
c++·算法·动态规划
fpcc3 小时前
跟我学C++中级篇——编译期的条件选择
开发语言·c++
ShineWinsu7 小时前
对于C++:C++20中线程、初始化、Lambda与内存视图等特性的解析
c++·c++20
张张的快乐时光呀!9 小时前
记录使用 MFC 打开文件对话框读取一张图片并做简单的灰度转换算法简单学习过程
c++·mfc
东华万里10 小时前
第41篇 C++类与对象核心知识梳理:从底层原理到面试实战
开发语言·c++·大学生专区
qq_5896660511 小时前
C语言、C++与C#的区别详解
c语言·c++·c#
万法若空11 小时前
排列组合恒等式
c++·算法
一直C11 小时前
【数据结构】哈希表+算法复杂度与经典排序查找(C语言)
java·linux·开发语言·数据结构·算法·ubuntu·散列表
淡海水11 小时前
04-02-哈希-Dictionary-TKey-TValue-上-核心数据结构
数据结构·算法·c#·哈希算法·编译·字典·dictionary