前言:由于STL容器库提供的这两个容器的用法和其他容器差不多,用法方面我只会提一嘴,中心 放在模拟实现上
1. unordered_map
unordered_map
是一个关联容器,用于存储键值对(key-value
)的集合。它的键(key
)是唯一的,每个键对应一个值(value
)。unordered_map
的查找、插入和删除操作的平均时间复杂度为 O(1)。
特点
-
无序性:元素存储的顺序与插入顺序无关,由哈希函数决定。
-
唯一键:每个键在容器中只能出现一次。
-
高效查找:通过哈希表实现,查找效率高。
适用场景
-
需要快速查找、插入和删除键值对的场景。
-
键需要唯一且无序的场景。
参考代码
#include<iostream>
#include<unordered_map>
using namespace std;
int main()
{
unordered_map<int, string> umap;
umap.insert({ 1, "Apple" });
umap[2] = "Banana"; // 另一种插入方式
for (auto& pair : umap)
{
cout << pair.first << ": " << pair.second << endl;
}
if (umap.find(1) != umap.end())
{
cout << umap[1]; // 输出 "Apple"
}
umap.erase(1); // 删除键为 1 的元素
for (auto& pair : umap)
{
cout << pair.first << ": " << pair.second << endl;
}
return 0;
}
2.unordered_set
unordered_set
是一个关联容器,用于存储唯一的键值集合。它与 unordered_map
类似,但只存储键(没有值)。unordered_set
的查找、插入和删除操作的平均时间复杂度也为 O(1)。
特点
-
无序性:元素存储的顺序与插入顺序无关。
-
唯一性:每个键只能出现一次。
-
高效查找:通过哈希表实现,查找效率高。
适用场景
-
需要快速判断某个元素是否存在的场景。
-
需要存储唯一元素的场景。
参考代码:
#include <iostream>
#include <unordered_set>
int main() {
std::unordered_set<int> uset;
uset.insert(1);
uset.insert(3);
uset.insert(2);
// 遍历
for (const auto& elem : uset)
{
std::cout << elem << " " ;
}
std::cout << std::endl;
// 查找
if (uset.count(2)) {
std::cout << "2 exists in the set" << std::endl;
}
// 遍历
for (const auto& elem : uset) {
std::cout << elem << " ";
}
// 删除
uset.erase(2);
// 遍历
for (const auto& elem : uset)
{
std::cout << elem << " ";
}
std::cout << std::endl;
std::cout << std::endl;
return 0;
}
3. unordered_map
和 unordered_set
的区别
特性 | unordered_map | unordered_set |
---|---|---|
存储内容 | 键值对(key-value ) | 唯一的键值 |
插入操作 | 插入键值对 | 插入单个值 |
查找操作 | 通过键查找对应的值 | 判断某个值是否存在 |
适用场景 | 需要存储键值对的场景 | 需要存储唯一值的场景 |
4. 性能注意事项
-
哈希冲突:如果哈希函数设计不好,可能会导致哈希冲突,降低性能。
-
负载因子:
unordered_map
和unordered_set
有一个负载因子(load factor
),表示哈希表中元素数量与桶数量的比例。当负载因子过高时,容器会自动重新哈希(rehash
),这会增加时间开销。 -
自定义哈希函数:如果默认的哈希函数不满足需求,可以自定义哈希函数。
5.总结
-
unordered_map
:适用于需要存储键值对的场景,查找、插入和删除效率高。 -
unordered_set
:适用于需要存储唯一值的场景,判断元素是否存在效率高。
两者都基于哈希表实现,因此在需要高效查找的场景下非常有用。
6.二者的模拟实现
6.1 改造hstable.h文件
由于二者都是基于哈希结构来实现的,所以我们可以将上节课的哈希桶的内容拿出来稍加改造,加入迭代器,并重载一下++,*,->等运算符,实现一下begin(),end()函数等等,要包括iterator和const_iterator,这里直接给出改造后的代码,读者可以根据需要选择性阅读!
这是改造后的hstable文件(原哈希桶文件)
#pragma once
#include<iostream>
#include<vector>
using namespace std;
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;
}
};
template<>
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t hashi = 0;
for (auto ch : key)
{
hashi *= 131;
hashi += ch;
}
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;
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;
}
}
// 所有桶都走完了,nullptr去做end()
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;
}
};
//pair<const K, V>
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
// 友元声明 hash_bucket::HashTable<K, pair<const K, V>, MapKetofT, Hash>::Iterator iterator;
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++)
{
if (_tables[i])
{
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)
{
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) //这里说的有问题 就是const的
{
KeyOfT kot;
Iterator it = Find(kot(data)); //大写
if (it != End())
{
//说明原来的哈希表已经有这个数据了,插入失败
return { it,false };
}
Hash hs;
// 负载因子到1,再扩容
if (_n == _tables.size())
{
// 也可以,但是扩容新开辟节点,释放旧节点,有点浪费
/*HashTable<K, V> newHT(__stl_next_prime(_tables.size() + 1));
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
newHT.Insert(cur->_kv);
cur = cur->_next;
}
}
_tables.swap(newHT._tables);*/
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];
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;
//
// HTIterator(Node* node, const HT* ht)
// :_node(node)
/// , _ht(ht)
//
//return { {newnode,this},true };
return make_pair(Iterator(newnode, this), true);
}
Iterator Find(const K& key)
{
KeyOfT kot;
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi]; //_tables[hashi]可以理解为链表的第一个数
while (cur)
{
if (kot(cur->_data) == key)
{
return Iterator(cur, nullptr);
}
cur = cur->_next;
}
return End();
}
bool Erase(const K& key)
{
KeyOfT kot;
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
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;
};
}
6.2 编写unorderedmap.h文件
由于我们在模拟实现一个容器,那()运算符是必不可少的,这里我们对数据的操作都是要对键进行操作的,所以我们运算符重载一下Operator(),取键值
struct MapKetofT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
然后我们把begin(),end(),insert()等函数封装一下,
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) //pait<string, string>&
{
return _ht.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert({ key,V() });
return ret.first->second;
}
最后是汇总代码以及测试文件:
#pragma once
#include"hstable.h"
#include<string>
namespace rens
{
template<class K, class V, class Hash = HashFunc<K>>
class um
{
struct MapKetofT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKetofT, Hash>::Iterator iterator;
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKetofT, 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) //pait<string, string>&
{
return _ht.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert({ key,V() });
return ret.first->second;
}
private:
hash_bucket::HashTable<K, pair<const K, V>, MapKetofT, Hash> _ht; //写错了
};
}
void test_unordered_map()
{
rens::um<string, string> dict;
dict.insert({ "string"," " });
dict.insert({ "left", "" });
dict["sort"];
dict["left"] = "左边,剩余";
dict["right"] = "右边";
rens::um<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
}
6.3 编写Unorderedset.h文件
基本逻辑思路是一样的,这里不在赘述,这里有一点注意的是,我们这里SetKeyOfT模板,可以不写,但是由于我们是将他与hstable文件以及unorderedmap.h文件一起编译的,为了照顾unorderedmap,我们这里加一下
参考代码:
#pragma once
#include"hstable.h"
namespace rens
{
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;
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>& s)
{
unordered_set<int>::const_iterator it = s.begin();
while (it != s.end())
{
//*it = 1;
cout << *it << " ";
++it;
}
cout << endl;
}
void test_unordered_set()
{
unordered_set<int> us;
us.insert(5);
us.insert(1);
us.insert(3);
us.insert(5);
us.insert(315);
us.insert(5312);
us.insert(333);
for (auto e : us)
{
cout << e << " ";
}
cout << endl;
unordered_set<int>::iterator it = us.begin();
while (it != us.end())
{
// *it = 1;
cout << *it << " ";
++it;
}
cout << endl;
print(us);
}
}
6.4 测试一下
#include"unorderedset.h"
#include"unorderedmap.h"
int main()
{
//test_unordered_map();
rens::test_unordered_set();
return 0;
}
到这里,我们对这两个容器的模拟实现就结束了,下一篇文章中我们将会讲解c++11的一些新的特性与相关知识!