目录
[一、 unordered 系列关联式容器介绍](#一、 unordered 系列关联式容器介绍)
[1.1 发展背景](#1.1 发展背景)
[1.2 核心特性对比](#1.2 核心特性对比)
[1.3 性能对比测试](#1.3 性能对比测试)
[1. 直接定址法](#1. 直接定址法)
[2. 除留余数法](#2. 除留余数法)
[2. unordered_set的封装](#2. unordered_set的封装)
[3. unordered_map的封装](#3. unordered_map的封装)
一、 unordered 系列关联式容器介绍
1.1 发展背景
在 C++98 标准中,STL 提供了一系列底层为红黑树结构 的关联式容器(set、map、multiset、multimap),这些容器在查询时的时间复杂度为 O(log₂N),即最差情况下需要比较红黑树的高度次。当数据量非常大时,log₂N 的查询效率仍然不够理想。
为了追求更好的查询性能,C++11标准引入了 4 个unordered系列的关联式容器:
unordered_setunordered_mapunordered_multisetunordered_multimap
他们的查询时的时间复杂度为 O(1)。
这些容器的使用方式与红黑树结构的容器基本类似 ,唯一的区别在于底层实现 ------ 哈希表。
1.2 核心特性对比
| 特性 | 红黑树容器 (set/map) | unordered 容器 |
|---|---|---|
| 底层结构 | 红黑树 | 哈希表 |
| 时间复杂度 | O(logN) | 平均 O (1),最坏 O (N) |
| 有序性 | 有序 | 无序 |
| 迭代器类型 | 双向迭代器 | 前向迭代器 |
| 空间利用率 | 较高 | 较低(负载因子控制) |
| 适用场景 | 需要有序遍历、范围查询 | 高频查找、插入、删除 |
- unordered_map 是 C++ STL 中用于存储键值对(key-value)的关联式容器,支持通过键(key)快速索引到其对应的值(value)。
- 在 unordered_map 中,键(key)用于唯一标识容器中的元素,值(value)是与该键绑定的对象,键和值的数据类型可以不同。
- 底层实现上,unordered_map 不会对元素按任何特定顺序排序。为了实现常数时间复杂度的元素查找,unordered_map 会将哈希值相同的键值对存入同一个哈希桶(bucket)中。
- unordered_map 通过键访问单个元素的性能,通常优于基于红黑树实现的 map;但在元素子集的范围遍历场景中,其迭代效率相对更低。
- 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时,有些数据:

注意:
为了尽可能降低哈希冲突的发生概率,哈希表长度
M最好选一个不接近 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;*/
};
}