【C++—STL】哈希表底层封装与unorderedset/unorderedmap模拟实现

2025博客之星年度评选已开启 10w+人浏览 529人参与

目录

引言

一. 源码及框架分析

二. 模拟实现unorderedset/unorderedmap

2.1 实现出复用的哈希表

2.1.1 实现复用的哈希表的整体框架

2.1.2 实现KeyOfT仿函数

2.1.3 迭代器的实现

1)底层迭代器的实现

2)哈希表中封装迭代器

2.2 封装哈希表实现unorderedset

2.3 封装哈希表实现unorderedmap

2.3.1 实现operator[ ]

2.3.2 完整代码实现

结语


引言

        在C++标准模板库(STL)中,unordered_map和unordered_set是高效的无序关联容器,底层基于哈希表实现,提供平均O(1)时间复杂度的查找、插入和删除操作。这些容器在实际应用中广泛用于缓存、数据去重和快速查询场景。理解其内部机制有助于更好地优化代码和处理边缘情况。


一. 源码及框架分析

SGI-STL30版本源代码中没有unordered_map和unordered_set,SGI-STL30版本是C++11之前的STL 版本,这两个容器是C++11之后才更新的。但是SGI-STL30实现了哈希表,只容器的名字是hash_map和hash_set,他是作为非标准的容器出现的,非标准是指非C++标准规定必须实现的,源代码在 hash_map/hash_set/stl_hash_map/stl_hash_set/stl_hashtable.h中 hash_map和hash_set的实现结构框架核心部分截取出来如下:

// stl_hash_set
template <class Value, class HashFcn = hash<Value>,
class EqualKey = equal_to<Value>,
class Alloc = alloc>
class hash_set
{
private:
typedef hashtable<Value, Value, HashFcn, identity<Value>,
EqualKey, Alloc> ht;
ht rep;
public:
typedef typename ht::key_type key_type;
typedef typename ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;
typedef typename ht::const_iterator iterator;
typedef typename ht::const_iterator const_iterator;
hasher hash_funct() const { return rep.hash_funct(); }
key_equal key_eq() const { return rep.key_eq(); }
};

// stl_hash_map
template <class Key, class T, class HashFcn = hash<Key>,
class EqualKey = equal_to<Key>,
class Alloc = alloc>
class hash_map
{
private:
typedef hashtable<pair<const Key, T>, Key, HashFcn,
select1st<pair<const Key, T> >, EqualKey, Alloc> ht;
ht rep;
public:
typedef typename ht::key_type key_type;
typedef T data_type;
typedef T mapped_type;
typedef typename ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;
typedef typename ht::iterator iterator;
typedef typename ht::const_iterator const_iterator;
};

// stl_hashtable.h
template <class Value, class Key, class HashFcn,
class ExtractKey, class EqualKey,
class Alloc>
class hashtable {
public:
typedef Key key_type;
typedef Value value_type;
typedef HashFcn hasher;
typedef EqualKey key_equal;
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node<Value> node;
vector<node*,Alloc> buckets;
size_type num_elements;
public:
typedef __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey,
Alloc> iterator;
pair<iterator, bool> insert_unique(const value_type& obj);
const_iterator find(const key_type& key) const;
};
template <class Value>
struct __hashtable_node
{
__hashtable_node* next;
Value val;
};

通过源码可以看到,结构上hash_map和hash_set跟map和set的完全类似,复用同一个hashtable实现key和key/value结构,hash_set传给hash_table的是两个 key,hash_map传给hash_table的是pair<const key, value>


二. 模拟实现unorderedset/unorderedmap

2.1 实现出复用的哈希表


2.1.1 实现复用的哈希表的整体框架

和set/map类似,我们将哈希表中存储的节点类型定义为模板类型,set传递的模板参数是K,map传递的模板参数是pair<K,V>,这样,同一个哈希表就能实现复用

template<class T>
struct HashNode
{
	T _data;
	HashNode<T>* next;

	HashNode(const T& data)
		:_data(data)
		,next(nullptr)
	{}
};

2.1.2 实现KeyOfT仿函数

用上述方法修改之后,又会有一个新的问题,unordered_set中存储的是key,unordered_map中存储的是pair<K,V>,

为了让 unordered_map unordered_set 复用代码,底层的 HashTable 必须足够通用。最大的难点在于:

  • Set 存储的是 Key

  • Map 存储的是 pair<const Key, Value>

  • 哈希表在计算哈希值、比较相等时,都需要获取 Key。对于 Set,数据本身就是 Key;对于 Map,需要从 Pair 中提取 Key。

为了解决上述差异,我们在 HashTable 的模板参数中引入了一个 KeyOfT 仿函数策略

template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
    // ...
    // K: 键的类型
    // T: 存储的数据类型(Set是K,Map是pair<const K, V>)
    // KeyOfT: 从T中提取K的仿函数
};

这样,上层容器只需传入不同的仿函数:

  • unordered_set: 传入 SetKeyOfT,直接返回数据本身

  • unordered_map: 传入 MapKeyOfT,返回 pair<K,V>.first

template<class K, class Hash = HashFunc<K>>
class unordered_set
{
	// 内部类(把key取出来)
	struct SetKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
}
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
	// 内部类(把key取出来)
	struct MapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;
		}
	};
}

2.1.3 迭代器的实现

1)底层迭代器的实现

哈希表的迭代器是单向迭代器(Forward Iterator)。它的难点在于如何在“断开”的桶之间进行遍历(operator++)。迭代器不仅需要指向当前的节点 _node,还需要持有哈希表对象 _pht 的指针。这是因为当一个桶遍历完后,需要通过哈希表对象找到下一个非空桶 。

iterator实现思路分析:

  • iterator实现的大框架跟list的iterator思路是一致的,用一个类型封装结点的指针,再通过重载运算符实现,迭代器像指针一样访问的行为,要注意的是哈希表的迭代器是单向迭代器。
  • 这里的难点是operator++的实现。iterator中有一个指向结点的指针,如果当前桶下面还有结点, 则结点的指针指向下一个结点即可。如果当前桶走完了,则需要想办法计算找到下一个桶。这里的 难点是反而是结构设计的问题,参考上面的源码,我们可以看到iterator中除了有结点的指针,还 有哈希表对象的指针,这样当前桶走完了,要计算下一个桶就相对容易多了,用key值计算出当前桶位置,依次往后找下一个不为空的桶即可。
  • begin()返回第一个桶中第一个节点指针构造的迭代器,这里end()返回迭代器可以用空表示。
  • unordered_set的iterator不支持修改,我们把unordered_set的第二个模板参数改成const K即可, HashTable<K, const K, SetKeyOfT, Hash> _ht;
  • unordered_map的iterator不支持修改key但是可以修改value,我们把unordered_map的第二个模板参数pair的第一个参数改成const K即可, HashTable<K, pair<K,V>, MapKeyOfT, Hash> _ht;

operator++的逻辑:

  • 如果当前节点的 _next 不为空,直接指向下一个节点。

  • 如果当前桶已经走完(_next 为空),则需要计算当前 Key 的哈希值,找到当前桶的索引。

  • 从当前索引的下一个位置开始,线性探测 _tables,直到找到第一个非空桶,或者走完所有桶 

Self operator++()
{
	// 当前桶没走完
	if (_node->next)
	{
		_node = _node->next;
	}
	// 当前桶没走完,找到下一个桶的第一个节点(哈希表的指针就是在这里用的)
	else
	{
		KeyOfT kot;
		Hash hs;
		// 先算出当前在那个桶
		size_t hashi = hs(kot(_node->_data)) % _pht->_tables.size();
		// 要从当前桶的下一个桶开始找
		++hashi;
		while (hashi < _pht->_tables.size())
		{
			// 找到下一个不为空的桶
			if (_pht->_tables[hashi])
			{
				_node = _pht->_tables[hashi];
				break;
			}
			else
			{
				++hashi;
			}
		}

		// 最后一个桶走完了,要++到end()迭代器位置
		if (hashi == _pht->_tables.size())
		{
			// end()中的_node是空
			_node = nullptr;
		}
	}

	return *this;
}

这里只实现了比较复杂的operator++(),完整代码详见底层迭代器的实现


2)哈希表中封装迭代器
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
	// 友元声明:类模板当友元,要加上模板参数
	// 让迭代器成为哈希表的友元,因为在迭代器中会访问哈希表的私有成员变量_tables;另一种方法是写GetTable函数
	template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
	friend struct HtIterator;

	typedef HashNode<T> Node;
public:
	typedef HtIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
	typedef HtIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;

	Iterator Begin()
	{
		// 找到第一个不为空的桶
		for (size_t i = 0; i < _tables.size(); i++)
		{
			// 如果没有数据就直接return End(),不用再遍历了
			if (_n == 0)
			{
				return End();
			}

			if (_tables[i])
			{
				// 用节点的指针和哈希表的指针构造;this就是哈希表的指针
				return Iterator(_tables[i], this);
			}
		}

		// 没有找到
		return End();
	}

	Iterator End()
	{
		return Iterator(nullptr, this);
	}

	ConstIterator Begin() const
	{
		// 找到第一个不为空的桶
		for (size_t i = 0; i < _tables.size(); i++)
		{
			// 如果没有数据就直接return End(),不用再遍历了
			if (_n == 0)
			{
				return End();
			}

			if (_tables[i])
			{
				// 用节点的指针和哈希表的指针构造;this就是哈希表的指针
				return ConstIterator(_tables[i], this); // 这里的this类型是const HT*,所以HtIterator的构造函数中,哈希表的指针要const
			}
		}

		// 没有找到
		return End();
	}

	ConstIterator End() const
	{
		return ConstIterator(nullptr, this); // 这里的this类型是const HT*,所以HtIterator的构造函数中,哈希表的指针要const
	}
}

2.2 封装哈希表实现unorderedset

unordered_set 的核心是将 Key 既作为 Key 也作为 Value。需要注意的是,Set 中的元素是不可修改的,因此我们传递给 HashTable 的模板参数是 const K


2.3 封装哈希表实现unorderedmap

unordered_map 存储的是键值对。

  • 数据类型pair<const K, V>(Key不可改,Value可改)。
  • Key提取:使用 MapKeyOfT 返回 kv.first


2.3.1 实现operator[ ]

unordered_map[] 运算符非常强大:如果 Key 存在则返回 Value 的引用,不存在则插入默认值并返回引用。这依赖于底层的 Insert 函数返回类型 pair<iterator, bool>

V& operator[](const K& key) {
    // 尝试插入 {key, 默认值}
    pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
    // 无论插入成功还是失败,ret.first 都指向该 Key 所在的节点
    return ret.first->second;
}

2.3.2 完整代码实现

#include"HashTable.h"

namespace My
{
	// 这里的仿函数要放在上层,否则传仿函数传不过去
	// 其实还应该加一个和key比较相等的仿函数,这里没加
	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
		// 内部类(把key取出来)
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};

	public:
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator;

		iterator begin()
		{
			return _ht.Begin();
		}

		iterator end()
		{
			return _ht.End();
		}

		const_iterator begin() const
		{
			return _ht.Begin();
		}

		const_iterator end() const
		{
			return _ht.End();
		}

		pair<iterator, bool> insert(const pair<K,V>& kv)
		{
			return _ht.Insert(kv);
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = insert({ key,V() });
			return ret.first->second;
		}

		iterator find(const K& key)
		{
			return _ht.Find(key);
		}

		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}

	private:
		hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
	};
}

结语

如有不足或改进之处,欢迎大家在评论区积极讨论,后续我也会持续更新C++相关的知识。文章制作不易,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,让我们一起努力,共同进步!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值