引言
在之前的博客中,我们已经实现了底层为红黑树结构的的一系列关联式容器,在查询时效率可以达到,在最差的情况下也需要比较红黑树的高度次,当树中的节点非常多的时候,查询的效率也不理想。因此在C++11中,STL又提供了四个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本上是类似的,只是其底层结构不同。
Unordered_map
- unordered_map是存储<key,value>键值对的关联式容器,其允许通过key快速地索引到与其对应的value。
- 在unordered_map中,键值通常用于唯一地标识元素,而映射值是一个对象,其内容与此键关联,键和映射值得类型可能不同。
- 在内部,unordered_map没有对<key,value>按照任何特定的顺序排序,为了能在常数范围内找到key多对应的value,unordered——map将相同哈希值的键放在同一哈希桶中。
- unordered_map容器通过key访问单个元素要比map快,但是它通常在遍历元素子集的范围迭代方面效率比较低。
- unordered_map实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
- 它的迭代器至少是向前迭代器。
Unordered_set
- unoredered_set不再以键值对的形式存储数据,而是直接存储数据的值。
- 容器内部存储的各个元素的值都互不相等,且不能被修改。
- 不会对内部存储的数据进行排序(与unordered_map一样,归结于其底层结构)
对于unordered_set容器中不以键值对的形式存储数据,我们也可以认为其存储的都是键和值相等的键值对,只不过为了节省存储空间,其在实际存储中只选择存储每个键值对的值。
哈希
unordered系列的关联式容器的效率之所以比较高是因为其底层使用了哈希结构,所以我们先来介绍一下哈希相关的概念。
哈希概念
在以往的顺序结构及平衡树中,各个元素的关键码与其存储的位置并没有对应的关系,因此我们在其中查找一个数据时往往需要进行多次的关键码的比较,这样就导致效率的低下,由此我们考虑是否可以设计出一种存储结构,其可以不经过任何的比较,一次便可以从结构中找到我们所需要的元素呢?通过某种函数(hashFunc哈希函数)使得元素的存储位置与它的关键码之间能够建立起一个一一对应的关系,那么在查找的时候便可以通过该函数的转换很快找到该元素。
- 插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并进行存放。
- 搜索元素:对元素的关键码进行同样的计算,把求得得函数值当作元素得存储位置,在结构中按此位置取元素进行比较,若关键码相等,则比较成功。
上述方式即为哈希(散列)方法,哈希方法中使用的函数称为哈希函数(散列)函数,构造出来的结构称为哈希表(或者称为散列表)。
如我们打算将1,3,2,14,37,25这几个数字放入哈希表中,首先我们将哈希函数设置为%; 表示的是哈希表的容量大小。我们假设其为10;那么经过计算上述元素的存放位置如下:
哈希冲突
如上如所示,走到上一步我们已经在哈希表中放入了数据, 但是倘若此时我们还需要在表中插入一个新得数据44,该怎么插入呢?插入到哪一个位置呢?此时就引出了一个新的问题:哈希冲突(或称哈希碰撞)。
哈希冲突:不同的关键字经过相同的哈希函数的计算后得出相同的哈希地址,这种现象称为哈希冲突。把具有不同关键码而有着相同的哈希地址的数据元素称为“同义词”。
那么为什么会出现上述的哈希冲突呢?出现了哈希冲突该如何解决呢?
哈希函数
引起哈希冲突的一个原因可能是哈希函数的设计不够合理
哈希函数的设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0—m-1之间。
- 哈希函数计算出来的地址能均匀分布在整个空间中。
- 哈斯函数的设计应该比较简单,
下面介绍几个常用的哈希函数
1、直接定址法:
取关键字的某个线性函数作为散列地址:Hash(key)=A*key+B;
其特点是简单均匀,缺点是需要事先知道关键字的分布情况。适用于查找比较下且连续的情况。
2、除留余数法:
设散列表中允许的地址数为m,那么就取一个不大于m,但是最接近或者等于m的质数p作为除数,将余数作为该关键码的地址。
闭散列和开散列
解决哈希冲突的另外两种常见的方法是闭散列和开散列
闭散列
闭散列也叫开放定址法,当发生哈希冲突的时候,如果当前哈希表还没有被填满,即哈希表中还有空位置的情况下,可以将当前发生冲突的关键码存放在放生冲突的位置的“下一个”位置上去。那么此时新的问题又来了,该如何在表中找到下一个空位置呢?
这里有两种方法:线性探测和二次探测。
线性探测
从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止。
- 对于插入的情况来说:首先通过哈希函数获取到该元素在哈希表中的位置,如果当前的位置上没有元素,则直接插入,如果已经有了插入的元素则发生哈希冲突,从当前位置向后探测直到直到下一个空位置,插入,如上面的例子,我们需要插入44时,按哈希函数的计算,44的地址应该在哈希表的4处,但是该地址已经有了元素14,所以向后寻找下一个空的位置,找到了下标为6的地址,将44存储到该地址。
- 对于线性探测的删除来说,不能随便删除掉表中的元素,如果直接删除掉已有的元素,则会直接影响到其他元素的搜索。比如删除掉元素4,44的查找可能会受到影响。那么该如何解决这个问题呢?我们采用标记的伪删除法来删除一个元素。即对哈希表中的每一个空间都给个标记,分别用三种标记代表当前位置的三种状态:位置空、已经有元素和元素已经删除。
线性探测的优点:实现非常简单
线性探测的缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据的堆积,即本该不属于该位置的关键码因为哈希冲突占据了该位置,使得本该属于该位置的关键码需要找寻其他空位置,随着在表中插入数据的不断增大,会产生“堆积”,使得寻找某关键码的位置时需要进行多次比较,导致搜索的效率降低。
二次探测
上述的线性探测的缺陷是产生冲突的数据容易堆积在一起,为了优化这个问题,提出了另一种二次探测的方法,当当前位置发生哈希冲突时,改变寻找下一个空位置的策略: 下一个位置为当前冲突位置后偏移量为(1,2,3...)的二次方地址处。
扩容问题(负载因子)
散列表的负载因子定义为
a是散列表装满程度的标志因子,由于表长是定值,因此当插入表中的元素越多时a就越大,产生哈希冲突的可能性就越大,反之,产生冲突的可能性就越小。实际上,散列表的平均查找长度是负载因子a的函数,只是不同处理冲突的方法有不同的函数。
对于开放定址法,负载因子是一个很重要的因素,应该严格控制在0.7-.8以下。查表时的CPU缓存不命中按照指数曲线上升。
开散列
开散列又叫链地址法(开链法),首先对于关键码集合用散列函数计算散列地址,对具有相同的地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表连接起来,各链表的头节点存储在哈希表中。
总结
应用链地址法处理溢出的情况时需要增设链接指针,看起来似乎增加了存储开销,但是事实上由于开地址法必须保持大量的空闲空间以确保搜索的效率,而表项所占的空间又比指针大很多,所以使用链地址法比开放空间法更节省存储空间。
哈希表(闭散列)的实现
哈希表底层
为了简便哈希表底层我们直接用库里的vector,再定义一个变量用来标记哈希表中存放的有效元素的个数
std::vector<HashData> _tables;
size_t _num = 0;
表中节点
在前面的闭散列提到为了方便哈希表的删除我们需要对每个地址进行标记,当前位置是否是空状态,是否为存在元素状态,是否为删除过元素的状态。对于哈希表中的每一个元素的节点,其中不仅要存当前元素的值还需要存储该节点的状态。
enum State//当前位置的状态
{
EMPTY,
EXITS,
DELETE,
};
template<class T>
struct HashData
{
T _data;
State _state;
};
插入
那么该如何插入呢?
- 首先先通过哈希函数计算出该元素在哈希表中的存储位置
- 接着判断当前节点的状态,如果为空或者删除状态则直接插入,否则判断当前结点的元素是否与插入的元素相同,不相同则向后寻找下一个空或删除状态的节点再插入。
那么一个新的的问题就出现了:如果我们在哈希表中存储的是整型,那么通过哈希函数计算出它的存储位置是可以的,但是倘若我们要存储的是一个字符或者字符串呢?此时字符或者字符串该怎么通过哈希函数计算出它的存储位置呢?所以这里我们还需要仿函数来配合,通过重载operator()来实现对不同类型的数据的返回以方便计算其存储位置。
KeyOfT koft;//仿函数
bool Insert(const T& d)
{
//插入
size_t index = koft(d) % _tables.size();
HashData cur = _tables[index];
while (cur._state == EXITS)
{
if (koft(cur._data) == koft(d))
{
return false;
}
cur = _tables[++index];
if (index == _tables.size())//如果从当前位置走到尾还没有空位置,则从头开始
{
cur = _tables[0];
}
}
cur._data = d;
cur._state = EXITS;
++_num;
return true;
}
现在我们考虑表满了的如何给哈希表扩容的情况,事实上哈希表一般不是满了才扩容,开放定址法中,一般负载因子达到0.7左右就开始扩容。
扩容的思路是:增容一个原来两倍空间的新表,再将旧表中的数据重新根据哈希函数映射到新表,再释放旧表的空间。
方法1、
if (_tables.size() == 0 || _num * 10 / _tables.size() >= 7)
{
std::vector<HashData>newtables;
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
newtables.resize(newsize);
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._state == EXITS)
{
size_t index = koft(_tables[i]._data) % newtables.size();
while (newtables[index]._state == EXITS)
{
++index;
if (index == _tables.size())
{
index = 0;
}
}
newtables[index] = _tables[i];
}
}
_tables.swap(newtables);
方法2、开好空间后循环旧表,将旧表中的元素不断插入新表
HashTable<K,T,KeyOFT> newht;
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
newht._tables.resize(newsize);
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._state == EXIT)
{
newht.Insert(_tables[i]);
}
}
_tables.swap(newtables);
查找
哈希表的查找类似于插入,需要先利用哈希函数计算出元素本该存储的位置 ,判断当前所在位置的节点状态,直到找到。
HashData* Find(const K& key)
{
KeyOfT koft;
size_t index = koft(key) % _tables.size();
while (_tables[index]._state != EMPTY)
{
if (koft(_tables[index]._data) == koft(key))
{
if (_tables[index]._state == EXITS)
{
return &_tables[index];
}
else if (_tables[index]._state == DELETE)
{
return nullptr;
}
}
++index;
if (index == _tables.size())
{
index = 0;
}
}
return nullptr;
}
删除
有了查找之后,删除就比较简单了,找到该元素,将存储该元素的结点的状态置于删除状态,有效个数减一即可。
bool Erase(const K& key)
{
HashData* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
--_num;
return true;
}
else
{
return false;
}
}
完整代码
namespace CLOSED_HASH
{
enum State//当前位置的状态
{
EMPTY,
EXITS,
DELETE,
};
template<class T>
struct HashData
{
T _data;
State _state;
};
/*template<class K>
struct SetKeyOfT
{
const K& operator()( const K& key)
{
return key;
}
}; */
template<class K, class T,class KeyOfT>
class HashTable
{
public:
KeyOfT koft;
typedef HashData<T> HashData;
bool Insert(const T& d)
{
//负载因子:表中数据/表的大小,衡量哈希表满的程度
// 表越接近满,插入的数据越容易冲突,冲突越多,效率越低
//计算d中的key在表中的位置
//哈希表一般不是满了才增容,开放定址法中,一般负载因子到了0.7左右才增容
if (_tables.size() == 0 || _num * 10 / _tables.size() >= 7)
{
//增容开一个2倍 的空间,将旧表中的数据重新映射到新表,释放旧表的空间
//传统的思路室先开空间,再进行resize初始化,遍历旧表的数据重新计算其在新表的位置,释放旧表
//下是新方法1
std::vector<HashData>newtables;
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
newtables.resize(newsize);
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._state == EXITS)
{
size_t index = koft(_tables[i]._data) % newtables.size();
while (newtables[index]._state == EXITS)
{
++index;
if (index == _tables.size())
{
index = 0;
}
}
newtables[index] = _tables[i];
}
}
_tables.swap(newtables);
//方法二
/*HashTable<K,T,KeyOFT> newht;
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
newht._tables.resize(newsize);
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._state == EXIT)
{
newht.Insert(_tables[i]);
}
}
_tables.swap(newtables);*/
}
//插入
size_t index = koft(d) % _tables.size();
HashData cur = _tables[index];
while (cur._state == EXITS)
{
if (koft(cur._data) == koft(d))
{
return false;
}
cur = _tables[++index];
if (index == _tables.size())
{
cur = _tables[0];
}
}
cur._data = d;
cur._state = EXITS;
++_num;
return true;
}
HashData* Find(const K& key)
{
KeyOfT koft;
size_t index = koft(key) % _tables.size();
while (_tables[index]._state != EMPTY)
{
if (koft(_tables[index]._data) == koft(key))
{
if (_tables[index]._state == EXITS)
{
return &_tables[index];
}
else if (_tables[index]._state == DELETE)
{
return nullptr;
}
}
++index;
if (index == _tables.size())
{
index = 0;
}
}
return nullptr;
}
bool Erase(const K& key)
{
HashData* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
--_num;
return true;
}
else
{
return false;
}
}
private:
std::vector<HashData> _tables;
size_t _num = 0;
};
/*void TestHashTable()
{
HashTable<int, int, SetKeyOfT<int>> ht;
ht.Insert(4);
ht.Insert(14);
ht.Insert(24);
ht.Insert(34);
ht.Insert(44);
ht.Insert(54);
ht.Insert(64);
ht.Insert(74);
ht.Insert(84);
}*/
}
哈希桶的实现
完整代码
namespace OPEN_HASH
{
template<class K>
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
template<class T>
struct HashNode
{
HashNode<T>* _next;
T _data;
HashNode(const T& data)
:_next(nullptr)
,_data(data)
{}
};
template<class K, class T, class KeyOfT, class Hash>//前置声明,不然迭代器中的HashTable向前找找不到HashTable
class HashTable;
//迭代器
template<class K,class T,class KeyOfT,class Hash>
struct __HashTableIterator
{
typedef HashNode<T> Node;
typedef __HashTableIterator<K, T,KeyOfT,Hash> Self;
typedef HashTable<K, T, KeyOfT, Hash> HT;
Node* _node;
HT* _pht;
__HashTableIterator(Node* node,HT* pht)//传Hashtable的指针
:_node(node)
,_pht(pht)
{}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &(_node->_data);
}
Self& operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else
{
//找下一个桶,通过当前桶的最后一个值的大小反推该桶的位置
KeyOfT koft;
size_t i = _pht->HashFunc(koft(_node->_data)) % _pht->_tables.size();
for (; i < _pht->_tables.size(); ++i)
{
Node* cur = _pht->_tables[i];
if (cur)
{
_node = cur;
return *this;
}
}
_node = nullptr;
}
return *this;
}
bool operator!=(const Self& s)
{
return _node == s._node;
}
};
//仿函数
template<class K>
struct _Hash
{
const K& operator()(const K& key)
{
return key;
}
};
template<>
struct _Hash<std::string>//特化,为了不用调_HashString也能默认返回string不用仿函数
{
size_t operator()(const std::string& key)
{
size_t hash = 0;
for (size_t i = 0; i < key.size(); ++i)
{
hash *= 131;
hash += key[i];
}
return hash;
}
};
/*struct _HashString
{
size_t operator()(const std::string& key)
{
size_t hash = 0;
for (size_t i = 0; i < key.size(); ++i)
{
hash *= 131;
hash += key[i];
}
return hash;
}
};*/
template<class K, class T, class KeyOfT,class Hash>//Hash把数据转化为可以取模的整形
class HashTable
{
typedef HashNode<T> Node;
public:
friend struct __HashTableIterator<K, T, KeyOfT, Hash>;
typedef __HashTableIterator<K, T, KeyOfT,Hash> iterator;
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);
}
//析构函数
~HashTable()
{
Clear();//vector不用清,自己会调用自己的析构函数
}
void Clear()
{
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;
}
}
//帮助不能取模的数据取模
size_t HashFunc(const K& key)
{
Hash hash;
return hash(key);
}
//查找
Node* Find(const K& key)
{
KeyOfT koft;
size_t index = HashFunc(koft(key)) % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (koft(cur->_data) == koft(key))
{
return cur;
}
else
{
cur = cur->_next;
}
}
return nullptr;
}
//删除
bool Erase(const K& key)
{
KeyOfT koft;
size_t index = HashFunc(koft(key)) % _tables.size();
Node* prev = nullptr;//想要删除得找到前一个结点
Node* cur = _tables[index];
while (cur)
{
//删除
if (koft(cur->_data) == koft(key))
{
if (prev == nullptr)
{
//表示要删除的值在第一个点
_tables[index] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
//插入
std::pair<iterator,bool> Insert(const T& data)
{
KeyOfT koft;
//一般开散列把负载因子控制到1,根据负载因子来扩容
if (_tables.size() == _num)
{
//扩容
std::vector<Node*> newtables;
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
newtables.reserve(newsize);
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
//拿出旧表中的每一个数据计算它在新表中的位置,放入新表
size_t index = HashFunc(koft(cur->_data)) % newtables.size();
cur->_next = newtables[index];
newtables[index] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtables);
}
//计算该数据在表中的映射位置
if (_tables.size() != 0)
{
size_t index =HashFunc(koft(data)) % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (koft(cur->_data) == koft(data))
{
return make_pair<iterator(cur, this), false>;
}
else
{
cur = cur->_next;
}
}
//没找到,准备头插(尾插也行)
Node* newnode = new Node(data);
newnode->_next = _tables[index];
_tables[index] = newnode;
++_num;
return make_pair<iterator(newnode,this), true>;
}
}
private:
std::vector<Node*> _tables;
size_t _num = 0;//记录表中存储数据的个数
};
/*void TestHashTable()
{
HashTable<int, int, SetKeyOfT<int>,_Hash> ht;
ht.Insert(4);
ht.Insert(14);
ht.Insert(24);
ht.Insert(5);
ht.Insert(15);
ht.Insert(25);
ht.Insert(6);
ht.Insert(16);
ht.Insert(32);
ht.Insert(78);
ht.Insert(36);
}
void TestHashTable2()
{
HashTable<std::string, std::string, SetKeyOfT<std::string>,_HashString> ht;
ht.Insert("sort");
ht.Insert("string");
ht.Insert("hello");
ht.Insert("world");
}*/
}
Unorder_map
#pragma once
#include<iostream>
#include"HashTable.h"
using namespace std;
using namespace OPEN_HASH;
namespace lrk2
{
template<class K,class V,class Hash= _Hash<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K,V>& kv)
{
return kv.first;
}
};
public:
typedef typename HashTable<K, pair<K,V>, MapKeyOfT, Hash>::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);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair<key, V()>);
return ret.first->second;
}
private:
HashTable<K, pair<K, V>, MapKeyOfT,Hash> _ht;
};
void test_unodered_set()
{
unordered_map<string,string> dict;
dict.Insert(make_pair("sort","排序"));
dict.Insert(make_pair("left", "左边"));
dict.Insert(make_pair("right", "右边"));
dict.Insert(make_pair("string", "字符串"));
dict.Insert(make_pair("hello", "你哈"));
unordered_map<string,string>::iterator it = dict.begin();
/*while (it != s.end())
{
cout << *it << " ";
++it;
}*/
cout << it->first << "-"<<it->second<<" ";
cout << endl;
}
}
Unorder_set
#pragma once
#include<iostream>
#include"HashTable.h"
using namespace std;
using namespace OPEN_HASH;
namespace lrk1
{
template<class K,class Hash=_Hash<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename HashTable<K, K, SetKeyOfT, Hash>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
pair<iterator, bool> Insert(const K& k)
{
return _ht.Insert(k);
}
private:
HashTable<K, K, SetKeyOfT,Hash> _ht;
};
void test_unodered_set()
{
unordered_set<int> s;
s.Insert(1);
s.Insert(5);
s.Insert(4);
s.Insert(2);
s.Insert(6);
unordered_set<int>::iterator it = s.begin();
/*while (it != s.end())
{
cout << *it << " ";
++it;
}*/
cout << *it << " ";
cout << endl;
}
}