[C++][STL]unordered_set类和unordered_map类
一、unordered系列容器
在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到logN,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到。在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同。
unordered_xxx系列与map和set容器的用法上几乎没有任何区别
他们的区别就是
unordered_xxx系列都是哈希表作为底层的,而map和set是用红黑树作为底层的
unordered_xxx系列不排序,只去重
unordered_xxx系列是单项迭代器
二、unordered_set
如下就是unordered_set的文档
unordered_set是一种容器,它以无特定顺序的方式存储唯一的元素,并允许根据元素的值快速检索各个元素。
在unordered_set中,元素的值同时也是它的键,唯一标识该元素。键是不可变的,因此,unordered_set中的元素一旦放入容器后就不能被修改,不过可以插入和删除。
在内部,unordered_set中的元素不按任何特定顺序排序,而是根据它们的哈希值组织成桶,以便通过它们的值(平均具有恒定的平均时间复杂度)直接快速访问各个元素。
对于通过键访问单个元素,unordered_set容器比set容器更快,尽管对于通过子集范围迭代它们通常效率较低。
容器中的迭代器是单向迭代器。
这些接口其实大差不差
void test1()
{
unordered_set<int> us;
us.insert(7);
us.insert(5);
us.insert(4);
us.insert(3);
us.insert(1);
us.insert(6);
unordered_set<int>::iterator usit = us.begin();
while (usit != us.end())
{
cout << *usit << endl;
usit++;
}
}
我们可以自己试着用一用
三、unordered_map
如下是unordered_map的文档
unordered_map 是关联容器,用于存储由键值和映射值组合形成的元素,并允许根据各个元素的键快速检索各个元素。
在 unordered_map 中,键值通常用于唯一标识元素,而映射值是具有与此键关联的内容的对象。键和映射值的类型可能不同。
在内部,unordered_map中的元素不是根据其键值或映射值按任何特定顺序排序的,而是根据其哈希值组织到桶中,以允许直接通过其键值快速访问各个元素(平均平均时间复杂度保持不变)。
unordered_map 容器通过键访问单个元素的速度比 map 容器更快,尽管它们通常对其元素子集进行范围迭代的效率较低。
unordered_map实现直接访问运算符 (operator[]),它允许使用其键值作为参数直接访问映射值。
容器中的迭代器至少是单向迭代器。
我们可以试着用一用
void test2()
{
unordered_map<string, string> dict;
dict["insert"] = "插入";
dict["sort"] = "排序";
dict["delete"] = "删除";
dict["string"] = "字符串";
dict["insert"] = "xxxxx";
dict.insert(make_pair("iterator", "迭代器"));
unordered_map<string, string>::iterator umit = dict.begin();
while (umit != dict.end())
{
cout << umit->first << ":" << umit->second << endl;
umit++;
}
cout << endl;
}
四、unordered_set与set的比较
如下所示,我们采用如下代码进行比较
void test3()
{
const size_t N = 100000;
unordered_set<int> us;
set<int> s;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; ++i)
{
v.push_back(rand());
//v.push_back(rand()+rand()); //减少重复数据
//v.push_back(i); //有序的时候
}
size_t begin1 = clock();
for (auto e : v)
{
s.insert(e);
}
size_t end1 = clock();
cout << "set insert:" << end1 - begin1 << endl;
size_t begin2 = clock();
for (auto e : v)
{
us.insert(e);
}
size_t end2 = clock();
cout << "unordered_set insert:" << end2 - begin2 << endl;
size_t begin3 = clock();
for (auto e : v)
{
s.find(e);
}
size_t end3 = clock();
cout << "set find:" << end3 - begin3 << endl;
size_t begin4 = clock();
for (auto e : v)
{
us.find(e);
}
size_t end4 = clock();
cout << "unordered_set find:" << end4 - begin4 << endl << endl;
cout <<"插入数据个数:"<< s.size() << endl;
cout <<"插入数据个数:" << us.size() << endl << endl;;
size_t begin5 = clock();
for (auto e : v)
{
s.erase(e);
}
size_t end5 = clock();
cout << "set erase:" << end5 - begin5 << endl;
size_t begin6 = clock();
for (auto e : v)
{
us.erase(e);
}
size_t end6 = clock();
cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
}
可以看到,在无序的数据的时候,unordered_set更占优势一些。
但是我们会发现有很多的重复数据,于是我们可以对随机值+随机值以此减少重复数据
可以看到,还是unordered_set占据优势
在有序的数据时,此时set占据优势
因此,如果数据是无序的,unordered_set更优,如果是有序的,使用set更好
五、哈希与各种查找的比较
1.直接查找
就是我们最常见的暴力查找,他的时间复杂度是O(N)
2.二分
不过我们可以对其进行一定程度的优化,即先排序,这样的画他的时间复杂度就变为了logN,但是增删还是不方便,而且排序也需要时间。
3.平衡树
再后来就是使用红黑树,他的效率都是很优秀的,增删查改都是logN
4.哈希
即存储的值和存储位置建立出一个对应关系,这样的话时间复杂度直接变为了O(1),哈希我们也称作散列,哈希的方式有点类似于计数排序
5.STL中的哈希
STL库中,set类和map类都是红黑树作为底层实现的,与之类似,unordered系列的unordered_set类和unordered_map类,都是通过哈希表作为底层来实现的。
六、哈希表的修改
下面是我们之前写的哈希表
namespace hash_bucket
{
template<class K,class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next = nullptr;
HashNode(const pair<K, V>& kv)
:_kv(kv)
{}
};
template<class K>
struct DefaultHashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct DefaultHashFunc<string>
{
size_t operator()(const string& str)
{
size_t hashi = 0;
for (auto ch : str)
{
hashi = hashi * 131 + ch;
}
return hashi;
}
};
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
typedef HashNode<K,V> Node;
public:
HashTable()
:_n(0)
{
_table.resize(10, nullptr);
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
{
return false;
}
HashFunc hf;
if (_n == _table.size())
{
size_t newSize = _table.size() * 2;
vector<Node*> newTable(newSize, nullptr);
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hf(cur->_kv.first) % newTable.size();
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(newTable);
}
size_t hashi = hf(kv.first) % _table.size();
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return true;
}
void Print()
{
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
printf("[%d]->", i);
while (cur)
{
cout << cur->_kv.first << ":" << cur->_kv.second << "->";
cur = cur->_next;
}
cout << "NULL" << endl;
}
}
Node* Find(const K& key)
{
HashFunc hf;
size_t hashi = hf(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
HashFunc hf;
size_t hashi = hf(key) % _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
if (prev)
{
prev->_next = cur->_next;
}
else
{
_table[hashi] = cur->_next;
}
delete cur;
cur = nullptr;
_n--;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
~HashTable()
{
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
private:
vector<Node*> _table;
size_t _n;
};
}
现在我们需要为了把它改装成unordered系列容器
1.结点
template<class T>
struct HashNode
{
HashNode<T>* _next;
T _data;
HashNode(const T& data)
: _next(nullptr)
, _data(data)
{}
};
对于哈希表的修改与之前我们用红黑树去封装map和set类似。
首先是将结点都改为T类型的,这个T类型对于set而言是K,对于map而言是pair<K,V>
2.迭代器
改造后的哈希表,最重要的功能之一就是支持单向迭代器
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 HashIterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, KeyOfT, Hash> Ht;
typedef HashIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
typedef HashIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
Node* _node;
const Ht* _ht;
HashIterator(Node* node, const Ht* ht)
: _node(node)
, _ht(ht)
{}
HashIterator(const Iterator& it)
: _node(it._node)
, _ht(it._ht)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &(operator*());
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
};
- 这里增加_ht成员变量,这样当一条单链表走到空,可以走到下一个哈希桶的位置,所以需要哈希表的地址
- 这里存在相互引用的问题,所以前置声明哈希表
- const修饰_ht,使const迭代器能够被构造
- 迭代器的拷贝构造函数有两个用途:
- 以普通迭代器拷贝出普通迭代器(普通迭代器调用时)
- 以普通迭代器拷贝出const迭代器(const迭代器调用时)
3.operator++
Self& operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else
{
int flag = 0;
size_t hashi = Hash()(KeyOfT()(_node->_data)) % _ht->_tables.size();
for (size_t i = hashi + 1; i < _ht->_tables.size(); ++i)
{
if (_ht->_tables[i])
{
_node = _ht->_tables[i];
flag = 1;
break;
}
}
if (!flag)
{
_node = nullptr;
}
}
return *this;
}
Self operator++(int)
{
Self tmp = *this;
++*this;
return tmp;
}
- 前置++的思路:
- 下一个结点不为空,则跳到下一位
- 下一个结点为空,则先取模算出哈希地址,再往后探测不为空的哈希桶
- 后置++:复用前置++,返回临时对象
4.本体
1.成员变量和默认成员函数
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 HashIterator;
protected:
typedef HashNode<T> Node;
public:
HashTable()
{
_tables.resize(10);
}
~HashTable()
{
for (auto& cur : _tables)
{
while (cur)
{
Node* del = cur;
cur = cur->_next;
delete del;
}
}
}
protected:
vector<Node*> _tables;
size_t _n = 0;//有效数据个数
};
- 将迭代器声明为友元,使迭代器内部可操作_tables
- 第三个模板参数为KeyOfT(仿函数),用于获取不同数据T的键值key来进行比较
- 第四个模板参数为Hash(仿函数),用于将不同类型key转换为整型来进行取模
2.begin和end
typedef HashIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
typedef HashIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;
iterator begin()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
return iterator(_tables[i], this);
}
}
return iterator(nullptr, this);
}
const_iterator begin() const
{
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
return const_iterator(_tables[i], this);
}
}
return const_iterator(nullptr, this);
}
iterator end()
{
return iterator(nullptr, this);
}
const_iterator end() const
{
return const_iterator(nullptr, this);
}
- begin返回最开始不为空的哈希桶的迭代器,end返回空迭代器
- 构造迭代器需要传入哈希表本身的地址,这里直接传this指针即可
3.Find
iterator Find(const K& key)
{
size_t hashi = Hash()(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (KeyOfT()(cur->_data) == key)
{
return iterator(cur, this);
}
cur = cur->_next;
}
return iterator(nullptr, this);
}
- 返回迭代器
- Hash转整型,KeyOfT获取键值
4.Insert
pair<iterator, bool> Insert(const T& data)
{
KeyOfT kot;
iterator it = Find(kot(data));
if (it._node)//保持key唯一
{
return make_pair(it, false);
}
Hash hash;
if (_n == _tables.size())//负载因子为1时,扩容
{
size_t newsize = _tables.size() * 2;
vector<Node*> newtables(newsize);
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
//将旧表结点重新映射到新表上
size_t hashi = hash(kot(cur->_data)) % newsize;
cur->_next = newtables[hashi];
newtables[hashi] = cur;
//跳回旧表的下一结点
cur = next;
}
}
_tables.swap(newtables);
}
size_t hashi = hash(kot(data)) % _tables.size();
Node* newnode = new Node(data);
//头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return make_pair(iterator(newnode, this), true);
}
- 返回pair,第一个参数为迭代器,第二个参数为布尔值(记录是否插入成功)
5.Erase
bool Erase(const K& key)
{
size_t hashi = Hash()(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (KeyOfT()(cur->_data) == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
5.unordered_set
1 成员变量与仿函数
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
protected:
HashTable<K, K, SetKeyOfT, Hash> _ht;
};
- unordered_set类仿函数,直接返回参数key
- 成员变量的第二个模板参数为K,第三个模板参数为SetKeyOfT
- 模板Hash可以根据特定需要而传手动实现的哈希化函数
2 begin和end
typedef typename HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;
typedef typename HashTable<K, K, SetKeyOfT, Hash>::const_iterator const_iterator;
iterator begin()
{
return _ht.begin();
}
const_iterator begin() const
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
const_iterator end() const
{
return _ht.end();
}
3 find
iterator find(const K& key)
{
return _ht.Find(key);
}
4 insert
pair<iterator, bool> insert(const K& key)
{
return _ht.Insert(key);
}
5 erase
bool erase(const K& key)
{
return _ht.Erase(key);
}
6.unordered_map
1 成员变量与仿函数
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<const K, V>& kv)
{
return kv.first;
}
};
public:
protected:
HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};
- unordered_map类仿函数,返回参数pair的first
- 成员变量的第二个模板参数为pair,第三个模板参数为MapKeyOfT
- 模板Hash可以根据特定需要而传手动实现的哈希化函数
2 begin和end
typedef typename HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
typedef typename HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::const_iterator const_iterator;
iterator begin()
{
return _ht.begin();
}
const_iterator begin() const
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
const_iterator end() const
{
return _ht.end();
}
3 find
iterator find(const K& key)
{
return _ht.Find(key);
}
4 insert
pair<iterator, bool> insert(const pair<const K, V>& kv)
{
return _ht.Insert(kv);
}
5 erase
bool erase(const K& key)
{
return _ht.Erase(key);
}
6 operator[ ]
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}