在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到logN,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同
unordered_map在线文档说明
注意:
unordered_set在线文档说明
注:在功能上和map、set是一样的区别在于,这两个容器遍历出来不是有序的,他们是单向迭代器
void test_op()
{
set<int> s;
unordered_set<int> us;
const int n = 100000000; //改变n的值
vector<int> v;
srand(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();
size_t begin2 = clock();
for (auto e : v)
{
us.insert(e);
}
size_t end2 = clock();
cout << "set insert:" << end1 - begin1 << endl;
cout << "unordered_set insert:" << end2 - begin2 << endl;
cout << "=====================" << endl;
size_t begin3 = clock();
for (auto e : v)
{
s.find(e);
}
size_t end3 = clock();
size_t begin4 = clock();
for (auto e : v)
{
us.find(e);
}
size_t end4 = clock();
cout << "set find:" << end3 - begin3 << endl;
cout << "unordered_set find:" << end4 - begin4 << endl;
cout << "=====================" << endl;
size_t begin5 = clock();
for (auto e : v)
{
s.erase(e);
}
size_t end5 = clock();
size_t begin6 = clock();
for (auto e : v)
{
us.erase(e);
}
size_t end6 = clock();
cout << "set erase:" << end5 - begin5 << endl;
cout << "unordered_set erase:" << end6 - begin6 << endl;
}
注:unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。
总结:
注: 当存储的数据没有特定说明需要排序时,一定要用unordered系列的关联式容器;当需要存储的序列为有序时,应该选用ordered系列的关联式容器
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( logn),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
对于两个数据元素的关键字 ki和kj (i != j),有ki !=kj ,但有:Hash( ki) == Hash(kj ),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
平方取中法
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
通常应用于关键字长度不等时采用此法
数学分析法
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个”空位置中去。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入:
通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
删除:
线性探测的实现:
namespace close_hash
{
enum State
{
EMPTY,
DELETE,
EXIST,
};
template<class K,class T>
struct HashData
{
pair<K, T> _hd;
State _s = EMPTY;
};
template<class K>
struct HashFun
{
size_t operator()(const K& key)
{
return key;
}
};
template<>
struct HashFun<string>
{
//BKDR Hash思想
size_t operator()(const string& key)
{
size_t n = 0;
for (auto& e : key)
{
n *= 131;
n += e;
}
return n;
}
};
template<class K,class T,class Hash=HashFun<K>>
class HashTable
{
public:
HashData<K, T>* find(const pair<K, T>& kv)
{
if (_ht.size() == 0)
return nullptr;
Hash hs;
size_t begin = hs(kv.first) % _ht.size();
size_t i = 0;
while (_ht[begin]._s != EMPTY)
{
if (_ht[begin]._hd == kv && _ht[begin]._s != DELETE)
return &_ht[begin];
else
{
i++;
begin += i;
begin %= _ht.size();
}
}
return nullptr;
}
bool erase(const pair<K, T>& kv)
{
HashData<K, T>* phd=find(kv);
if (phd == nullptr)
return false;
else
{
//伪删除法
phd->_s = DELETE;
_n--;
return true;
}
}
bool Insert(const pair<K, T>& kv)
{
if (find(kv))
return false;
if (_ht.size() == 0 || _n * 10 / _ht.size() >= 7)
{
size_t newsize = _ht.size() == 0 ? 10 : _ht.size() * 2;
HashTable<K,T,Hash> nht;
nht._ht.resize(newsize);
for (const auto& e : _ht)
{
if(e._s==EXIST)
nht.Insert(e._hd);
}
_ht.swap(nht._ht);
}
Hash hs;
size_t begin = hs(kv.first) % _ht.size();
size_t i = 0;
while (_ht[begin]._s == EXIST)
{
i++;
begin += i;
begin %= _ht.size();
}
_ht[begin]._hd = kv;
_ht[begin]._s = EXIST;
_n++;
return true;
}
private:
vector<HashData<K, T>> _ht;
size_t _n=0; //存储有效数据的个数
};
}
注意:
线性探测优点:实现非常简单
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:Hi = ( H0+i^2 )% m,或者:Hi = (H0 -i^2 )% m。其中:i = 1,2,3…,H0 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
二次探测的实现:
namespace close_hash
{
enum State
{
EMPTY,
DELETE,
EXIST,
};
template<class K,class T>
struct HashData
{
pair<K, T> _hd;
State _s = EMPTY;
};
template<class K>
struct HashFun
{
size_t operator()(const K& key)
{
return key;
}
};
template<>
struct HashFun<string>
{
//BKDR Hash思想
size_t operator()(const string& key)
{
size_t n = 0;
for (auto& e : key)
{
n *= 131;
n += e;
}
return n;
}
};
template<class K,class T,class Hash=HashFun<K>>
class HashTable
{
public:
HashData<K, T>* find(const pair<K, T>& kv)
{
if (_ht.size() == 0)
return nullptr;
Hash hs;
size_t begin = hs(kv.first) % _ht.size();
size_t i = 0;
while (_ht[begin]._s != EMPTY)
{
if (_ht[begin]._hd == kv && _ht[begin]._s != DELETE)
return &_ht[begin];
else
{
i++;
i = i * i;
begin += i;
begin %= _ht.size();
}
}
return nullptr;
}
bool erase(const pair<K, T>& kv)
{
HashData<K, T>* phd=find(kv);
if (phd == nullptr)
return false;
else
{
//伪删除法
phd->_s = DELETE;
_n--;
return true;
}
}
bool Insert(const pair<K, T>& kv)
{
if (find(kv))
return false;
//if (_ht.size() == 0 || _n * 10 / _ht.size() >= 7)
if (_ht.size() == 0 || _n * 10 / _ht.size() >= 5)
{
size_t newsize = _ht.size() == 0 ? 10 : _ht.size() * 2;
HashTable<K,T,Hash> nht;
nht._ht.resize(newsize);
for (const auto& e : _ht)
{
if(e._s==EXIST)
nht.Insert(e._hd);
}
_ht.swap(nht._ht);
}
Hash hs;
size_t begin = hs(kv.first) % _ht.size();
size_t i = 0;
while (_ht[begin]._s == EXIST)
{
i++;
i = i * i;
begin += i;
begin %= _ht.size();
}
_ht[begin]._hd = kv;
_ht[begin]._s = EXIST;
_n++;
return true;
}
private:
vector<HashData<K, T>> _ht;
size_t _n=0; //存储有效数据的个数
};
}
相比线性探测的好处,如果一个位置有很多值映射,冲突剧烈,那么他们存储是相对会比较分散,不会引发一片一片的冲突
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
开散列中每个桶中放的都是发生哈希冲突的元素
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。
补充:
const int PRIMECOUNT = 28;
const size_t primeList[PRIMECOUNT] = {
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
size_t GetNextPrime(size_t prime) {
size_t i = 0;
for(; i < PRIMECOUNT; ++i)
{
if(primeList[i] > primeList[i])
return primeList[i];
}
return primeList[i];
}
字符串哈希算法
应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。
补充:
#pragma once
#include
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return key;
}
};
// 特化
template<>
struct HashFunc < string >
{
size_t operator()(const string& key)
{
// BKDR Hash思想
size_t hash = 0;
for (size_t i = 0; i < key.size(); ++i)
{
hash *= 131;
hash += key[i];
}
return hash;
}
};
namespace bucket_hash
{
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{}
};
size_t GetNextPrime(size_t prime)
{
const int PRIMECOUNT = 28;
static const size_t primeList[PRIMECOUNT] =
{
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
size_t i = 0;
for (; i < PRIMECOUNT; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
// 拷贝 和 赋值 需要自己实现桶的拷贝
~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;
}
_n;
}
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false;
}
Hash hf;
// 素数
size_t index = hf(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[index];
while (cur)
{
if (cur->_kv.first == key)
{
// 1、cur是头结点
// 2、非头节点
if (prev == nullptr)
{
_tables[index] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
Node* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hf;
size_t index = hf(key) % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
else
{
cur = cur->_next;
}
}
return nullptr;
}
bool Insert(const pair<K, V>& kv)
{
Hash hf;
//当负载因子到1时,进行扩容
if (_n == _tables.size())
{
//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
size_t newSize = GetNextPrime(_tables.size());
//HashTable newHT;
vector<Node*> newtables;
newtables.resize(newSize, nullptr);
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t index = hf(cur->_kv.first) % newSize;
cur->_next = newtables[index];
newtables[index] = cur;
cur = next;
}
_tables[i] = nullptr;
}
newtables.swap(_tables);
}
size_t index = hf(kv.first) % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (cur->_kv.first == kv.first)
{
return false;
}
else
{
cur = cur->_next;
}
}
Node* newnode = new Node(kv);
newnode->_next = _tables[index];
_tables[index] = newnode;
++_n;
return true;
}
private:
vector<Node*> _tables;
size_t _n = 0; // 存储多少有效数据
};
}
注意:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include
using namespace std;
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return key;
}
};
// 特化
template<>
struct HashFunc < string >
{
size_t operator()(const string& key)
{
// BKDR Hash思想
size_t hash = 0;
for (size_t i = 0; i < key.size(); ++i)
{
hash *= 131;
hash += key[i];
}
return hash;
}
};
namespace bucket_hash
{
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data)
, _next(nullptr)
{}
};
size_t GetNextPrime(size_t prime)
{
const int PRIMECOUNT = 28;
static const size_t primeList[PRIMECOUNT] =
{
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
size_t i = 0;
for (; i < PRIMECOUNT; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}
template<class K, class T, class Hash, class KeyOfT>
class HashTable;
template<class K, class T, class Hash, class KeyOfT>
struct HTIterator
{
typedef HashNode<T> Node;
typedef HTIterator<K, T, Hash, KeyOfT> Self;
typedef HashTable<K, T, Hash, KeyOfT> HT;
HTIterator(Node* node, HT* ht)
:_node(node)
,_ht(ht)
{}
Node* _node;
HT* _ht;
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s)const
{
return _node != s._node;
}
bool operator ==(const Self& s)const
{
return _node == s._node;
}
Self operator++()
{
if (_node->_next) // 在当前桶迭代
{
_node = _node->_next;
}
else // 找下一个桶
{
KeyOfT kot;
const K& key = kot(_node->_data);
Hash hf;
size_t index = hf(key) % _ht->_tables.size();
++index;
_node = nullptr;
while (index < _ht->_tables.size())
{
if (_ht->_tables[index])
{
_node = _ht->_tables[index];
break;
}
else
{
++index;
}
}
// 后面没有桶了
if (index == _ht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
};
template<class K, class T, class Hash , class KeyOfT>
class HashTable
{
typedef HashNode<T> Node;
public:
typedef HTIterator<K, T, Hash, KeyOfT> iterator;
// 拷贝 和 赋值 需要自己实现桶的拷贝
friend struct HTIterator<K, T, Hash, KeyOfT>;
HashTable() = default;
iterator end()
{
return iterator(nullptr, this);
}
iterator begin()
{
for (size_t i = 0;i < _tables.size();i++)
{
if (_tables[i])
return iterator(_tables[i], this);
}
return end();
}
HashTable(const HashTable<K, T, Hash, KeyOfT>& ht)
{
_tables.resize(ht._tables.size(),nullptr);
for (size_t index = 0;index < ht._tables.size();index++)
{
if (ht._tables[index])
{
Node* cur = ht._tables[index];
while (cur)
{
Node* next = cur->_next;
Node* newnode = new Node(cur->_data);
newnode->_next = _tables[index];
_tables[index] = newnode;
cur = next;
}
}
}
_n = ht._n;
}
HashTable<K, T, Hash, KeyOfT>& operator=(HashTable<K, T, Hash, KeyOfT> ht)
{
_tables.swap(ht._tables);
::swap(_n, ht._n);
return *this;
}
~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;
}
_n;
}
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false;
}
Hash hf;
// 素数
size_t index = hf(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[index];
KeyOfT kot;
while (cur)
{
if (kot(cur->_data) == key)
{
// 1、cur是头结点
// 2、非头节点
if (prev == nullptr)
{
_tables[index] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
Node* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hf;
KeyOfT kot;
size_t index = hf(key) % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (kot(cur->_data) == key)
{
return cur;
}
else
{
cur = cur->_next;
}
}
return nullptr;
}
pair<iterator, bool> Insert(const T& data)
{
Hash hf;
KeyOfT kot;
//当负载因子到1时,进行扩容
if (_n == _tables.size())
{
//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
size_t newSize = GetNextPrime(_tables.size());
//HashTable newHT;
vector<Node*> newtables;
newtables.resize(newSize, nullptr);
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
const K& key = kot(cur->_data);
size_t index = hf(key) % newSize;
cur->_next = newtables[index];
newtables[index] = cur;
cur = next;
}
_tables[i] = nullptr;
}
newtables.swap(_tables);
}
const K& key = kot(data);
size_t index = hf(key) % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (kot(cur->_data) == kot(data))
{
return make_pair(iterator(cur, this), false);
}
else
{
cur = cur->_next;
}
}
Node* newnode = new Node(data);
newnode->_next = _tables[index];
_tables[index] = newnode;
++_n;
return make_pair(iterator(newnode, this), true);
}
private:
vector<Node*> _tables;
size_t _n = 0; // 存储多少有效数据
};
}
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include"HashTable.h"
namespace lc
{
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)const
{
return key;
}
};
typedef bucket_hash::HashTable<K, K, HashFunc<K>, SetKeyOfT> HST;
public:
typedef typename HST::iterator iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
bool erase(const K& key)
{
return _t.Erase(key);
}
pair<iterator,bool> insert(const K& key)
{
return _t.Insert(key);
}
private:
HST _t;
};
}
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include"HashTable.h"
namespace lc
{
template<class K,class V,class Hash= HashFunc<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<const K,V>& kv)const
{
return kv.first;
}
};
typedef bucket_hash::HashTable<K, pair<const K,V>, HashFunc<K>, MapKeyOfT> HST;
public:
typedef typename HST::iterator iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
pair<iterator,bool> insert(const pair<const K, V>& kv)
{
return _t.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
bool erase(const K& key)
{
return _t.Erase(key);
}
private:
HST _t;
};
}