【重走C++学习之路】20、unordered_map和unordered_set
目录
一、无序关联式容器
二、unordered_map
2.1 构造函数
2.2 容量操作
2.3 迭代器
2.4 增删查操作
三、unordered_set
3.1 构造函数
3.2 容量操作
3.3 迭代器
3.4 增删查操作
四、基于哈希表实现unordered_map和unordered_set
4.1 改造哈希表
1. 基本结构
2. 迭代器实现
4.2 封装unordered_map和unordered_set
结语
一、无序关联式容器
在 C++ 中,无序关联式容器是一类基于键(Key)快速访问元素的容器,其元素没有预设的顺序(不按键排序),而是通过哈希表(Hash Table)实现存储和检索,具有平均情况下 (O(1)) 的插入、删除和查找效率。
主要特点:
无序性
元素的存储顺序与键的插入顺序或键值大小无关,迭代器遍历的顺序是不确定的(由哈希表内部结构决定),不支持按顺序访问(如有序容器map
的按键排序)。关联式访问
每个元素由键值对(key-value
,如unordered_map
)或单一键(unordered_set
)组成,通过键快速定位元素,属于 “关联式容器”(区别于序列式容器如vector
)。哈希表实现
- 通过哈希函数(Hash Function)将键映射到哈希表的桶(Bucket)中,冲突(不同键映射到同一桶)通过链地址法(桶内用链表或红黑树存储冲突元素)处理。
- C++ 标准库默认使用
std::hash<Key>
作为哈希函数,支持自定义类型时需提供哈希函数和等于谓词(operator==
)。
四种无序关联式容器:
容器 | 描述 |
---|---|
std::unordered_set | 存储 唯一键 的集合,元素无序。 |
std::unordered_map | 存储 唯一键值对 的集合,键唯一,元素无序。 |
std::unordered_multiset | 存储 可重复键 的集合,元素无序。 |
std::unordered_multimap | 存储 可重复键值对 的集合,元素无序。 |
使用场景:
- 需要快速查找、插入、删除:如字典、频率统计(用
unordered_map
统计单词出现次数)。- 不关心元素顺序:若需要按键排序,应使用有序关联容器(
map
/set
,基于红黑树,时间复杂度 O(logN))
二、unordered_map
主要特点:
- unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
- 在unordered_map中,键值通常用于唯一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
- 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序,为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
- unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
- unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
- 它的迭代器是单向迭代器。
2.1 构造函数
常用的几个:
- 构造函数
构造空的unordered_map对象
- 拷贝构造函数
利用unordered_map对象构造新的unordered_map对象
- 迭代器构造
利用迭代器将容器中的某个区间来构造新的unordered_map对象
2.2 容量操作
- empty
判断容器是否为空
- size
返回容器中的元素个数
2.3 迭代器
迭代器的接口都是一致的,需要注意的是这个容器只有正向迭代器(包括const迭代器)
2.4 增删查操作
- operator[]
可以通过key找到value
- find
iterator find ( const key_type& k ),查找的时间复杂度可以达到O(1)
- insert
pair<iterator,bool> insert ( const value_type& val ),往容器中插入键值对
- erase
iterator erase ( const_iterator position ),删除容器中的键值对
- clear
清空容器中的元素
- swap
交换两个容器中的元素
示例:
void test_unordered_map()
{unordered_map<int, int> um;int arr[] = { 4,2,3,1,6,8,9,3 };for (auto e : arr){um.insert(make_pair(e, e));}unordered_map<int, int>::iterator umit = um.begin();while (umit != um.end()){cout << umit->first << ":" << umit->second << endl;++umit;}
}
三、unordered_set
主要特点:
- unordered_set是以无特定顺序存储唯一元素的容器,并且允许根据它们的值快速检索单个元素,是一种K模型。
- 在unordered_set中,元素的值同时是它的key,它唯一地标识它。键值是不可变的,因unordered_set中的元素不能在容器中修改一次 ,但是可以插入和删除它们。
- 在内部,unordered_set中的元素不是按任何特定顺序排序的,而是根据它们的哈希值组织成桶,以允许直接通过它们的值快速访问单个元素,时间复杂度可以达到O(1)。
- unordered_set容器比set容器更快地通过它们的key访问单个元素,尽管它们通常对于通过其元素的子集进行范围迭代的效率较低。
- 容器中的迭代器至少是单向迭代器。
3.1 构造函数
- 构造函数
构造空的unordered_set对象
- 拷贝构造函数
利用unordered_set对象构造新的unordered_set对象
- 迭代器构造
利用迭代器将容器中的某个区间来构造新的unordered_set对象
3.2 容量操作
- empty
判断容器是否为空
- size
返回容器中的元素个数
3.3 迭代器
同 unordered_map一样只有正向迭代器。
3.4 增删查操作
- find
iterator find ( const key_type& k ),查找的时间复杂度可以达到O(1)
insert
pair<iterator,bool> insert ( const value_type& val ),往容器中插入key
erase
iterator erase ( const_iterator position ),删除容器中的key
clear
清空容器中的元素
swap
交换两个容器中的元素
示例:
void test_unordered_set()
{unordered_set<int> us;int arr[] = { 4,2,3,1,6,8,9,3 };for (auto e : arr){us.insert(e);}unordered_set<int>::iterator usit = us.begin();while (usit != us.end()){cout << *usit << " ";++usit;}
}
总结: unordered_map和unordered_set基本上相差不大,就是unordered_map多了一个operator[]的操作,支持通过key找到value的操作。而且与map和set的操作也类似,只是底层在存储结构上有差异。
四、基于哈希表实现unordered_map和unordered_set
这里利用开散列的哈希表来实现unordered_map和unordered_set,有关哈希内容可以去我的上一篇文章查看【重走C++学习之路】19、哈希
4.1 改造哈希表
1. 基本结构
template<class T>
struct HashNode
{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data), _next(nullptr){}
};template<class K, class T, class KOFV, class Hash>
class HashTable
{typedef HashNode<T> Node;private:vector<Node*> _tables;int _num = 0;// 记录表中的数据个数
};
第一个和第二个模板参数跟map和set的是实现类似,为了将key和pair<key,value>封装在一起;第三个模板参数是为了统一取数操作;第四个模板参数是哈希函数。
2. 迭代器实现
template<class K, class T, class Ref, class Ptr, class KOFV, class Hash>
struct __HashTable_Iterator
{typedef __HashTable_Iterator<K, T, Ref, Ptr, KOFV, Hash> Self;typedef HashNode<T> Node;typedef HashTable<K, T, KOFV, Hash> HashTable;Node* _node;HashTable* _ht;__HashTable_Iterator(Node* node, HashTable* ht):_node(node),_ht(ht){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Self& operator++(){if (_node->_next){_node = _node->_next;return *this;}else{KOFV kofv;int index = _ht->HashFunc(kofv(_node->_data)) % _ht->_tables.size();for (size_t i = index + 1; i < _ht->_tables.size(); ++i){if (_ht->_tables[i]){_node = _ht->_tables[i];return *this;}}_node = nullptr;return *this;} }bool operator==(const Self& self) const{return _node == self._node&& _ht == self._ht;}bool operator!=(const Self& self) const{return !this->operator==(self);}
};
3. 哈希表内部修改
template<class K, class T, class KOFV, class Hash>
class HashTable
{typedef HashNode<T> Node;friend struct __HashBucket_Iterator<K, T, T&, T*, KOFV, Hash>;// 为了让迭代器中能够使用HashFunc这个函数public:typedef __HashTable_Iterator<K, T, T&, T*, KOFV, Hash> iterator;iterator begin(){for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i] != nullptr)return iterator(_tables[i], this);// 哈希桶的第一个节点 }return end();// 没有节点返回最后一个迭代器}iterator end(){return iterator(nullptr, this);}
};
4.2 封装unordered_map和unordered_set
template<class K, class V, class Hash = _Hash<K>>
class unordered_map
{struct MapKeyOfValue{const K& operator()(const pair<K, V>& kv){return kv.first;}};typedef HashTable<K, pair<K, V>, MapKeyOfValue, Hash> HashTable;public:// 告诉编译器这只是一个类型typedef typename HashBucket::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> insert(const pair<K, V>& kv){return _ht.Insert(kv);}bool erase(const K& key){return _ht.Erase(key);}iterator find(const K& key){return _ht.Find(key);}V& operator[](const K& key){pair<iterator, bool> ret = insert(make_pair(key, V()));return ret.first->second;}private:HashBucket _ht;
};template<class K, class Hash = _Hash<K>>
class unordered_set
{struct SetKeyOfValue{const K& operator()(const K& key){return key;}};typedef HashTable<K, K, SetKeyOfValue, Hash> HashTable;
public:// 告诉编译器这只是一个类型typedef typename HashBucket::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> insert(const K& kv){return _ht.Insert(kv);}bool erase(const K& key){return _ht.Erase(key);}iterator find(const K& key){return _ht.Find(key);}private:HashBucket _ht;
};
结语
这两个容器应用在后续的项目和做题中用的比较多,可以在O(1)的时间复杂度完成操作,这对效率的提升是巨大的。除此之外,在map和unordered_map这两者之间,基本上后者用的比较多。
下一篇将会介绍位图和布隆过滤器这两个处理海量元素的哈希应用,有兴趣的朋友可以关注一下。