1. unordered_map、unordered_set
1.1 是什么
这两者底层采用了哈希表来实现,在功能上没有map和set的排序功能,因为哈希表本身是不具备二叉搜索树的性质的,但是哈希算法使得unordered_map、unordered_set的搜索效率是比红黑树高的。
1.2 哈希的概念
拿一个书店来举例子,顾客可以在书店中借书,店主需要通过顾客借书的天数来进行相应的收费。那么店主可以在本子上记录下顾客的名字(该本子上按照字母序来记录顾客名),下次顾客再来还书的时候,店主就可以像翻字典一样,在本子上翻到顾客借书的信息。
这种将顾客名字的首字母和顾客信息产生关联的方式就叫做哈希。
也就是说对于任意的信息,我们只要找到该信息和某个值能产生联系,并且在计算机用相应的数据结构表示出这种关系即可。一般在C++中我们会使用vecotr数组来存放哈希表,通过将任意的信息通过算法设计(哈希函数)和vector的下标产生关联,这样在计算机中寻找的时候就很容易了。
1.3 哈希冲突
对于两个信息kv1和kv2,对于这两者如果我们通过哈希函数计算出存储的位置是相同的,这种情况就被称为哈希冲突,一般而言在哈希表中,值和存储位置是多对一的关系。
2 .哈希函数
2.1 直接定值法
比如在统计一个字符串中每个小写字母出现的次数,在这里我们可以使用红黑树的map来解决问题,我们也可以使用一个哈希表来解决,将小写字母 a 到 z 的ASCII码值和数组的下标做映射就可以了,ASCII码为97 的 a 存在数组的第一个位置,以此类推。
这种方法只适合数据量比较小而且数据比较集中的情况,如果数据量非常分散的话,我们就需要把数组开的很大,虽然可以实现目的但是浪费了很多存储空间。
2.2 除留余数法
Hash(key)= key % n (n为vector存储元素个数的大小)
以这种方式存储数据可以比直接定值法节约很多空间,但是会出现哈希碰撞的情况。
3. 哈希碰撞的解决
3.1 闭散列(开放定值法)
在发生哈希冲突时,如果哈希表在还没有满的情况下,我就把当前的数据放到冲突位置的下一个空的位置上去(问哦的位置被占了,我就占用别人的位置)
寻找空的位置我们有两种方式去寻找
3.1.1 线性探测
该哈希表采用除留余数法作为哈希函数,从发生冲突的位置开始,依次向vector的下一个位置探测,直到探测到一个空的位置。
(1)插入
在插入时先用除留余数法算出当前信息与vector中下标的映射关系,如果发生哈希冲突后,就一直向下一个位置探测,直到找到一个空的位置,最后插入该信息。
(2)删除
使用闭散列的结构再删除元素时,我们不能真正物理上删除掉这个元素,因为我们在查找时,会根据哈希函数算出相应的vector下标,在vector中从算出的下标位置一直向后寻找,直到碰到有节点是空为止。所以如果真的把一个值物理删除的话会影响到查找函数的运行。
所以我们对于每一个vector中的节点我们都需要定义三个状态即:EXIST/EMPTY/DELETE
这三种状态分别代表 某元素在vector中是存在的/ 不存在的 / 被删除的。
(3)扩容
这里在哈希表中引入了负载因子的概念,所谓负载因子a = 表中现存元素的个数 / 散列表的长度。
负载因子,代表着表中元素的多少,a越大说明表中元素越多,产生冲突的可能个就越大,空间利用率就越大,反之亦然,a越小,表中元素越小,产生冲突的可能就越小。
对于开放定址法,负载因子一般控制在0.7-0.8左右。
3.1.2 二次探测法
与线性探测法类似,只不过是往n位置的 2 的 i 次方向后探测(i>=0),还是要占用其他元素的空间只不过比线性探测法好一些,还是不能根本的解决问题。
3.2 开散列
开散列(拉链法、哈希桶),第一步还是使用除留余数法的哈希函数求出一个散列地址(在vector中存在哪)发生哈希碰撞后,用一个链表把发生碰撞的元素串起来,vector中个每个位置只要存放链表的头节点的地址就可以了。
对于开散列而言也是需要扩容的,如果不扩容的话,结构上确实没啥问题,但是每一个节点的链表就会变得很长,在JAVA中,其解决方式为如果一个节点上存放的链表长度大于8,那么就把该链表转化为一个红黑树,这样对于较长链表的情况也不用担心效率的问题了。
在C++中我们直接采用扩容的方法就可以了,如果负载因子大于等于1时,我们就需要扩容,这样做的目的是保证每一个节点的链表中的元素只有一个/每个桶中只有一个元素。
4.整体代码
4.1 闭散列
#pragma once
#include<vector>
#include<string>
#include<list>
using namespace std;
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 hash = 0;
for (auto e : key)
{
hash *= 31;
hash += e;
}
return hash;
}
};
namespace open_address//开放定址法(闭散列)在一个空间里去找位置
{
enum Status
{
EMPTY,
EXIST,
DELETE
};//枚举状态,用来标记表中的某个元素是存在还是不存在
template<class K,class V>
struct HashData
{
pair<K, V> _kv;
Status _s;//标记元素的状态
};
//struct HashFuncString
//{
// size_t operator()(const string& key)
// {
// size_t hash = 0;
// for (auto e : key)
// {
// hash *= 31;//避免字母全相同但是顺序不同的情况
// hash += e;
// }
// return hash;
// }
//};
template<class K,class V,class Hash=HashFunc<K>>
class HashTable
{
public:
HashTable()
{
_tables.resize(10);
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
// 负载因子 = 存储数据的个数 / 整个容器的空间
if (_n*10 / _table.size() == 7)//负载因子到0.7就扩容
{
size_t newSize = _tables.size()*2;
HashTable<K, V, Hash> newHT;
newHT._tables.resize(newSize);
//遍历旧表
for (size_ti = 0; i < _tables.size(); i++)
{
if (_tables[i]._s == EXIST)
{
newHT.Insert(_tables[i]._kv);
}
}
_tables.swap(newHT._tables);//扩容复用insert,不需要自己再写一次映射逻辑
}
Hash hf;
//线性探测
size_t hashi = hf(kv.(first)) % _tables.size();
while (_tables[hashi]._s == EXIST)
{
hashi++;//如果想要插入的位置已经有值了那么就往后面一个位置插入
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._s = EXIST;// 找到空的位置插入,并且该位置的状态修改为存在
return true;
}
HashData<K, V>* Find(const K& key)
{
Hash hf;
size_t hashi = hf(kv.first) % _tables.size();
while (_tables[hashi]._s != EMPTY)
{
if (_tables[hashi]._s==EXIST &&
_tables[hashi]._kv.first == key)
{
return &_tables.[hashi];
}
hashi++;//不为空继续往后走
hashi %= _tables.size();
}
return NULL;
}
bool Erase(const K& key)//伪删除法
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_s = DELETE;
--_n;
return true;
}
else
{
return false;
}
}
void Print()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i]._s == EXIST)
{
printf("[%d]->%d\n", i, _tables[i]._kv.first);
}
else if (_tables[i]._s == EMPTY)
{
printf("%d->E\n", i);
}
else
{
printf("[%d]->D\n", i);
}
}
}
private:
vector<HashData> _tables;
size_t _n = 0; //存储关键字的个数
};
}
4.2 开散列
namespace hash_bucket {
template<class K, class V>
struct HashDate {
pair<K, V> _kv;
HashDate<K, V>* _next;
HashDate(const pair<K, V>& kv) :_kv(kv), _next(nullptr) {
}
};
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable {
typedef HashDate<K, V> Node;
public:
HashTable() {
_table.resize(10);
}
HashTable(const HashTable<K, V, HashFunc>& ht) {
//resize到表的容量(vector的size)
_table.resize(ht.capacity());
for (size_t i = 0; i < ht._table.size(); ++i) {
Node* cur = ht._table[i];
while (cur) {
insert(cur->_kv);
cur = cur->_next;
}
}
}
~HashTable() {
for (size_t i = 0; i < _table.size(); ++i) {
Node* cur = _table[i];
while (cur) {
Node* next = cur->_next;
delete cur;
cur = next;
}
}
}
bool insert(const pair<K, V>& kv) {
if (find(kv.first)) {
return false;
}
size_t size = _table.size();
HashFunc hf;
if (_n == size) {
size_t newSize = size * 2;
//建立新标
vector <Node*> newTable;
newTable.resize(newSize);
for (size_t i = 0; i < size; ++i) {
//依次取下来旧表中的结点重新建立映射关系后头插到新表
Node* cur = _table[i];
while (cur) {
Node* next = cur->_next;
size_t hashi = hf(cur->_kv.first) % newSize;
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
//最后交换一下
_table.swap(newTable);
}
size_t hashi = hf(kv.first) % size;
Node* newNode = new Node(kv);
//头插
newNode->_next = _table[hashi];
_table[hashi] = newNode;
++_n;
return true;
}
bool erase(const K& key) {
HashFunc hf;
size_t hashi = hf(key) % _table.size();
Node* prev = nullptr;
Node* cur = _table[hashi];
while (cur) {
if (cur->_kv.first == key) {
//头删
if (prev == nullptr) {
_table[hashi] = cur->_next;
}
//中间位置删除
else {
prev->_next = cur->_next;
}
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
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;
}
size_t size() const {
return _n;
}
size_t capacity() const {
return _table.size();
}
void print() {
for (size_t 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;
}
puts("NULL");
}
cout << endl;
}
private:
//指针数组
vector <Node*> _table;
size_t _n = 0;
};
}