【STL/数据结构】哈希表和unordered系列容器的封装

目录

[一、 unordered 系列关联式容器介绍](#一、 unordered 系列关联式容器介绍)

[1.1 发展背景](#1.1 发展背景)

[1.2 核心特性对比](#1.2 核心特性对比)

[1.3 性能对比测试](#1.3 性能对比测试)

二、哈希表

1.哈希概念

2.哈希函数的实现方法

[1. 直接定址法](#1. 直接定址法)

[2. 除留余数法](#2. 除留余数法)

3、哈希冲突

开放定址法(闭散列)

线性探测

二次探测

链地址法(开散列)

4、装填因子

三、unordered系列容器的封装

1.哈希表

[2. unordered_set的封装](#2. unordered_set的封装)

[3. unordered_map的封装](#3. unordered_map的封装)


一、 unordered 系列关联式容器介绍

1.1 发展背景

在 C++98 标准中,STL 提供了一系列底层为红黑树结构 的关联式容器(setmapmultisetmultimap),这些容器在查询时的时间复杂度为 O(log₂N),即最差情况下需要比较红黑树的高度次。当数据量非常大时,log₂N 的查询效率仍然不够理想。

为了追求更好的查询性能,C++11标准引入了 4 个unordered系列的关联式容器:

  • unordered_set
  • unordered_map
  • unordered_multiset
  • unordered_multimap

他们的查询时的时间复杂度为 O(1)。

这些容器的使用方式与红黑树结构的容器基本类似唯一的区别在于底层实现 ------ 哈希表

1.2 核心特性对比

特性 红黑树容器 (set/map) unordered 容器
底层结构 红黑树 哈希表
时间复杂度 O(logN) 平均 O (1),最坏 O (N)
有序性 有序 无序
迭代器类型 双向迭代器 前向迭代器
空间利用率 较高 较低(负载因子控制)
适用场景 需要有序遍历、范围查询 高频查找、插入、删除
  1. unordered_map 是 C++ STL 中用于存储键值对(key-value)的关联式容器,支持通过键(key)快速索引到其对应的值(value)。
  2. 在 unordered_map 中,键(key)用于唯一标识容器中的元素,值(value)是与该键绑定的对象,键和值的数据类型可以不同。
  3. 底层实现上,unordered_map 不会对元素按任何特定顺序排序。为了实现常数时间复杂度的元素查找,unordered_map 会将哈希值相同的键值对存入同一个哈希桶(bucket)中。
  4. unordered_map 通过键访问单个元素的性能,通常优于基于红黑树实现的 map;但在元素子集的范围遍历场景中,其迭代效率相对更低。
  5. unordered_map 重载了元素访问操作符 operator[],支持直接以键(key)作为入参,访问其对应的值(value)。

1.3 性能对比测试

cpp 复制代码
#include <iostream>
#include <vector>
#include <set>
#include <unordered_set>
#include <time.h>
using namespace std;

void test_performance()
{
    const size_t N = 1000000;  // 100万条数据
    unordered_set<int> us;
    set<int> s;
    vector<int> v;
    v.reserve(N);
    
    srand((unsigned int)time(0));
    for (size_t i = 0; i < N; ++i)
    {
        v.push_back(rand());
    }

    // 插入测试
    size_t begin1 = clock();
    for (auto e : v) s.insert(e);
    size_t end1 = clock();
    cout << "set insert: " << end1 - begin1 << "ms" << endl;

    size_t begin2 = clock();
    for (auto e : v) us.insert(e);
    size_t end2 = clock();
    cout << "unordered_set insert: " << end2 - begin2 << "ms" << endl;

    // 查找测试
    size_t begin3 = clock();
    for (auto e : v) s.find(e);
    size_t end3 = clock();
    cout << "set find: " << end3 - begin3 << "ms" << endl;

    size_t begin4 = clock();
    for (auto e : v) us.find(e);
    size_t end4 = clock();
    cout << "unordered_set find: " << end4 - begin4 << "ms" << endl;

    // 删除测试
    size_t begin5 = clock();
    for (auto e : v) s.erase(e);
    size_t end5 = clock();
    cout << "set erase: " << end5 - begin5 << "ms" << endl;

    size_t begin6 = clock();
    for (auto e : v) us.erase(e);
    size_t end6 = clock();
    cout << "unordered_set erase: " << end6 - begin6 << "ms" << endl;
}

当前是在debug模式下,unordered系列的功能都更优一点,底层还是因为使用了哈希结构

二、哈希表

1.哈希概念

哈希表(Hash Table)也叫散列表,是编程中非常经典的高效数据结构。它的核心逻辑,是通过一套固定规则给数据做「散列映射」,再依托这套规则实现极速查找。这套核心规则,就是我们常说的哈希函数------ 它能**把任意输入的数据,转换成一个固定的索引值,我们就可以根据这个索引,把数据存到数组对应的位置里。**后续查找数据时,直接用同一个哈希函数算出它的索引值,就能一步定位到目标数据,理想状态下,整个查找过程仅需常数时间就能完成。

2.哈希函数的实现方法

理想情况下,哈希函数应该把数据均匀地散列 到哈希表中。为了尽量逼近这一理想状态,前辈们设计出了多种哈希函数构造方法,比如 直接定址法除留余数法乘法散列法全域散列法 等。其中 直接定址法除留余数法 最为常用,下面我们重点介绍这两种。

1. 直接定址法

当数据分布范围比较集中时,直接定址法是首选。比如,有一组数据的取值全部在 0~99 之间,那就可以直接开一个大小为 100 的数组,把数据值当作数组下标来存储。假如这组数据里出现了 3 个 "6",那么数组下标为 6 的位置就存 3。

如果数据的下限不是 0,处理起来也很简单。比如要处理 26 个小写英文字母,它们的 ASCII 码范围是 97~122。这时可以开一个大小为 26 的数组,让每个字母的存储下标 = 该字母的 ASCII 码 − 'a' 的 ASCII 码,就能顺利映射了。

当然,直接定址法的缺点也非常明显:一旦数据范围很大且分布稀疏,就会严重浪费存储空间。


2. 除留余数法

除留余数法(也叫除法散列法),是实际当中最常用的一种哈希函数实现方式。它的核心思想是:**把数据对一个数(通常取哈希表的长度)取模,将得到的余数作为存储位置的下标。假设哈希表的大小为 M,**那么对应的哈希函数就是:

比如,当模值设为11时,有些数据:

注意:

  1. 为了尽可能降低哈希冲突的发生概率,哈希表长度 M 最好选一个不接近 2 的整数次幂的素数

  2. 该方法要求数据元素能够转换为整数,这样才能方便地通过取模运算生成索引下标。

3、哈希冲突

当一个数据经过哈希函数计算后,得到的索引位置已经被另一个数据占用时,这两个数据就无法同时存储在同一位置了。 这种情况,我们就称之为哈希冲突,也叫哈希碰撞。从理论上讲,最理想的状态当然是数据都能均匀散布到哈希表的各个位置,但在实际应用中,哈希冲突几乎是无法完全避免的。因此,必须有一套合适的方案来处理它。

处理哈希冲突的常见方法有:开放定址法、链地址法、再哈希法、位图法等。这里,我们重点介绍最常用的前两种。

开放定址法(闭散列)

开放定址法的思路很简单:一旦发现目标位置已经被占用,就按照某种既定策略,在哈希表中寻找另一个空位,把冲突的数据存进去。常见的探测策略有三种:线性探测、二次探测和双重探测

线性探测

线性探测的做法是,从冲突位置开始,**一个接一个地向后查找,直到遇见第一个空位为止,然后把元素放进去。**如果一路找到表尾都还没找到空位,就折返回表头继续找。

这种方式的缺点是,很容易让冲突的数据在表中扎堆聚集,而且可能挤占其他元素的 "原本位置",从而拖慢整体效率

二次探测

二次探测是针对线性探测的优化。它不再老老实实地一步一步找,而是按照平方序列进行左右跳跃式探测。比如,先探测冲突位置右边 1 个位置,然后是左边 1 个位置,接着右边 4 个位置,左边 4 个位置......以此类推。

这样做,有效地缓解了数据聚集的问题。

双重探测

双重探测的思路更进一步:它额外引入一个哈希函数,用这个函数来计算每次探测的步长,让探测序列更加分散,从而进一步降低冲突堆积的可能。

链地址法(开散列)

链地址法,也叫拉链法。它从根本上改变了数据的存储方式:数据本身不再直接放在哈希表里,而是让哈希表的每个槽位都变成一个指针,指向一个链表。 此时,哈希表的每个位置更像一个 "桶",我们称之为哈希桶

当某个位置不需要存数据时,这个指针就空着;一旦有数据要存在这里,就把它挂到该位置对应的链表尾部。也就是说,挂在同一条链表上的所有元素,彼此之间正是发生了哈希冲突的关系。

链地址法的一大好处是,发生冲突时不会去抢占其他元素的原始位置,因此相对于开放定址法,效率往往更高。

不过,如果某个位置的冲突特别严重,链表被拉得过长,查找效率就会退化,趋近于 O(N)。针对这个问题,可以考虑在链表长度超过一定阈值时,将其转换为一棵红黑树。

4、装填因子

装填因子(也叫负载因子)用来衡量哈希表的装满程度,它的值是:已存入的元素个数 / 哈希表的总容量。装填因子越大,说明表的空间利用率越高,但也意味着哈希冲突出现的概率会明显上升,导致整体操作效率下降。所以,它实际上反映了一种空间和时间的权衡。

在实际工程中,我们通常把装填因子当作触发哈希表扩容的阈值。一旦实际装填因子达到这个事先设定好的值,就会对哈希表进行扩容,以维持较好的性能。

这个阈值的具体大小,取决于采用什么方式处理哈希冲突:如果用开放定址法 ,阈值一般设在 0.7~0.8 之间;如果用链地址法 ,阈值可以更宽松,通常设为 1

三、unordered系列容器的封装

1.哈希表

cpp 复制代码
#pragma once

enum State
{
	EXIST,
	EMPTY,
	DELETE
};


static const int __stl_num_primes = 28;//库里面这样实现的
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
  53,         97,         193,       389,       769,
  1543,       3079,       6151,      12289,     24593,
  49157,      98317,      196613,    393241,    786433,
  1572869,    3145739,    6291469,   12582917,  25165843,
  50331653,   100663319,  201326611, 402653189, 805306457,
  1610612741, 3221225473, 4294967291
};

inline unsigned long __stl_next_prime(unsigned long n)
{
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list + __stl_num_primes;
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last - 1) : *pos;
}


template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;//不管啥类型先强转成size_t
	}
};

template<>//特化一下
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t	hashi = 0;
		for (auto e : key)
		{
			hashi *= 131; //BKDRHS  可以防止重复,abcd badc这样的
			hashi += e;
		}

		return hashi;
	}
};

namespace hash_bucket
{
	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;

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


	template<class K, class T, class KeyOfT, class Hash>
	class HashTable;//前置声明,让Iterator知道有这个类

	template<class K,class T,class Ref,class Ptr,class KeyOfT,class Hash>
	struct HTIterator
	{
		typedef HashNode<T> Node;
		typedef HashTable<K, T, KeyOfT, Hash> HT;
		typedef HTIterator<K, T,Ref,Ptr,KeyOfT, Hash> Self;


		Node* _node;
		const HT* _ht;

		HTIterator(Node* node,const HT* ht)
			:_node(node)
			,_ht(ht)
		{ }

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

		Self& operator++()
		{
			if (_node->_next)
			{
				_node = _node->_next;
			}
			else
			{
				KeyOfT kot;
				Hash hs;

				size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size();
				++hashi;
				while (hashi < _ht->_tables.size())
				{
					if (_ht->_tables[hashi])
					{
						_node = _ht->_tables[hashi];
						break;
					}
					else
					{
						++hashi;
					}
				}

				if (hashi == _ht->_tables.size())
					_node =  nullptr;

			}

			return *this;
		}

		bool operator!=(const Self& s)
		{
			return  _node != s._node;
		}

		bool operator==(const Self& s)
		{
			return  _node == s._node;
		}


	};




	template<class K, class T,class KeyOfT, class Hash  >
	class HashTable
	{
		//友元声明
		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 (int i = 0; i < _tables.size(); i++)
			{
				if (_tables[i])
				{
					return Iterator(_tables[i], this);
				}
			}
			return End();
		}

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

		ConstIterator Begin() const
		{
			for (int i = 0; i < _tables.size(); i++)
			{
				if (_tables[i])
				{
					return ConstIterator(_tables[i], this);
				}
			}
			return End();
		}

		ConstIterator End()const
		{
			return ConstIterator(nullptr, this);
		}

		HashTable(size_t size = __stl_next_prime(0))
			:_tables(size, nullptr)
		{
		}

		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		pair<Iterator,bool> Insert(const T& data)
		{
			KeyOfT kot;
			Iterator it = Find(kot(data));
			if (it!=End())
				return {it,false};
			Hash hs;

			// 负载因子到1,再扩容	
			if (_n == _tables.size())
			{
				//直接把旧节点移到表上
				vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;

						size_t hashi = hs(kot(cur->_data)) % newtables.size();
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;

						cur = next;
					}

					_tables[i] = nullptr;

				}
				_tables.swap(newtables);
			}

			size_t hashi = hs(kot(data)) % _tables.size();
			Node* newnode = new Node(data);

			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;

			return { {newnode,this},false };
		}

		Iterator Find(const K& key)
		{
			KeyOfT kot;
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
					return Iterator(cur,nullptr);

				cur = cur->_next;
			}

			return End();
		}

		bool Erase(const K& key)
		{
			Hash hs;
			KeyOfT kot;
			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					--_n;
					delete cur;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;
		}

	private:
		vector<Node*> _tables; // 指针数组
		size_t _n = 0;
	};
}

2. unordered_set的封装

cpp 复制代码
#pragma once
#include"HashTable.h"

namespace ncs
{
	template<class K,class Hash = HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

	public:

		typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT,Hash>::Iterator iterator;
		typedef typename hash_bucket::HashTable<K,const K, SetKeyOfT, Hash>::ConstIterator const_iterator;

		using iterator = typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator;
		using const_iterator = typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator;
		//using  小区别

		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 K& key)
		{
			return _ht.Insert(key);
		}



	private:
		hash_bucket::HashTable<K,const K, SetKeyOfT, Hash> _ht;
	};

	void Print(const unordered_set<int>& set)
	{
		unordered_set<int>::const_iterator it = set.begin();
		while (it != set.end())
		{
			//*it += 1;
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

	void test_unordered_set()
	{
		unordered_set<int> set;
		set.Insert(1);
		set.Insert(2);
		set.Insert(3);

		//for (auto e : set)
		//{
		//	cout << e << " ";

		//}
		//cout << endl;
		Print(set);
	};

3. unordered_map的封装

cpp 复制代码
#pragma once
#include"HashTable.h"

namespace ncs
{
	template<class K,class V, class Hash = HashFunc<K>>
	class unordered_map
	{
		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;
		}


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



	void test_unordered_map()
	{
		//unordered_map<string, string> map;
		//map.Insert({ "a","1" });
		//map.Insert({ "b","2" });
		//map.Insert({ "c","3" });

		unordered_map<string, string> dict;
		dict.Insert({ "string", "字符串" });
		dict.Insert({ "left", "左边" });

		dict["sort"];

		dict["left"] = "左边+剩余";
		dict["right"] = "左边+剩余";


		unordered_map<string, string>::iterator it = dict.begin();
		while (it != dict.end())
		{


			cout << it->first << ":" << it->second << endl;
			++it;
		}
		cout << endl;
	/*	for (auto e : dict)
		{
			cout << e.first <<" "<<e.second<<" ";

		}
		cout << endl;*/

	};


}
相关推荐
Brilliantwxx8 小时前
【C++】初步认识STL(3)
开发语言·c++·笔记·算法
charlie1145141918 小时前
通用GUI编程技术——图形渲染实战(四十)——深度缓冲与3D变换:从平面到立体
开发语言·c++·平面·3d·图形渲染·win32
啊吧怪不啊吧8 小时前
C++之基于正倒排索引的Boost搜索引擎项目日志+server代码及详解
c++·搜索引擎·项目
小张同学8248 小时前
-RAG检索增强生成让智能体拥有企业级专属知识库
开发语言·python·架构·pycharm
DevilSeagull8 小时前
Rust 枚举(enum)深度解析:从定义到 Option 的安全之道
开发语言·后端·安全·rust·github
Ulyanov8 小时前
《现代 Python 桌面应用架构实战:PySide6 + QML 从入门到工程化》:实时时钟与数据驱动 UI —— 从“事件回调”到“状态绑定”的范式跃迁
开发语言·python·qt·ui·架构·交互
AI进化营-智能译站8 小时前
ROS2 C++开发系列06:变量、数据类型与IO实战
java·开发语言·c++·ai
HABuo10 小时前
【linux(四)】套接字编程--基于UDP协议的客户端服务端
linux·服务器·c++·网络协议·ubuntu·udp·centos
阿里嘎多学长15 小时前
2026-04-30 GitHub 热点项目精选
开发语言·程序员·github·代码托管