目录
前几期我们学习了红黑树和红黑树的模拟实现,最终使用红黑树封装了map和set。本期开始我们将学习下一个重要的知识点---哈希表,最终使用哈希表封装unordered_map和unordered_set。
unordered_set和unordered_map
在学习哈希之前,我们先了解一下unordered_set和unordered_map,这两个容器也都是关联式容器,我们之前已经学习了map和set。unordered_set和unordered_map与set和map大体上是一样的,但是也有几点不同。
**1.**map和set的底层是用红黑树进行封装实现的,也正因为如此,红黑树是一个搜索二叉树,搜索二叉树的元素我们是按照中序遍历的方法进行遍历的。所以map和set的元素的遍历是有序的,而unordered_set和undered_map的元素的遍历是无序的。
**2.**map和set是双向迭代器,而unordered_set和unordered_map是单项迭代器,通过C++官方文档就可以看出来。
**3.**map和set查找一个元素时,因为底层是红黑树,所以最多查找高度次,所以查找一个元素的时间复杂度为O(logN),而unordered_set和unordered_map底层是哈希,所以查找一个元素的时间复杂度为O(1),为什么是O(1),下面会为大家阐述。
哈希概念
有N个数的集合,这N个数的范围为[1,26],N个数中有重复的数,现在要统计这N个数中,每个数出现的次数,我们该如何进行统计呢?
有一种方法,就是开辟一个大小为27的一维数组,让这N个数的每个数都%27,然后将取模之后得到的数作为数组的下标,对应数组元素的值为这一数字出现的次数,通过这一方法就可以统计得到N个数每个元素出现的次数。这其实就是哈希的一种体现。
哈希表:通过哈希函数,使得关键码和元素位置建立一一映射的表,我们称之为哈希表。
哈希表基本结构
cpp
namespace CloseHash
{
enum Status
{
EMPTY,
DELETE,
EXIST
};
template<class K, class V>
struct HashData
{
private:
pair<K, V> _kv;
Status _st = EMPTY;
};
template<class K,class V>
class HashTable
{
private:
vector<HashData<K, V>> _v;
int _n; //哈希表中有效元素的个数
};
}
哈希冲突
何为哈希冲突呢?前提为我们要用一个一维数组存储数据。
有1组数为1,7,2,8,映射在大小为7的数组中,1映射到下标为1的位置,7也映射到1的位置,但是数组的一个元素中只能存储一个数据,那么1先存入之后,7要存入时这就产生了冲突,我们就称这为哈希冲突。
简单来说,哈希冲突就是不想同的元素,通过哈希映射,映射到了哈希表的同一位置。
如何解决哈希冲突呢?我们使用了闭散列的方法,即为线性探测和二次探测法。什么是线性探测?什么又是二次探测法呢?
线性探测
线性探测就是,当第二个元素与第一个元素产生哈希冲突时,从产生冲突的位置开始,依次往后继续寻找空的位置,寻找到空的位置之后,将产生冲突的元素放在空的位置。代码实现如下。
代码如下。
cpp
//哈希表中元素的插入
bool insert( const pair<K,V>& kv)
{
//先去判断哈希表中是否存在当前要插入的元素,因为哈希表的原理是不排序加去重
HashData<K, V>* ret = find(kv.first);
if (ret != nullptr)
{
return false;
}
if (_v.size() == 0 || _n*10 / _v.size() > 7)
{
int NewSize = _v.size() == 0 ? 10 : _v.size() * 2;
HashTable<K, V> ht;
ht._v.resize(NewSize);
for (int i = 0; i< _v.size(); i++)
{
if (_v[i]._st == EXIST)
{
ht.insert(_v[i]._kv);
}
}
_v.swap(ht._v);
}
HashFunc<K> hf;
int start = hf(kv.first) % _v.size();
int i = 0;
size_t index = start;
while (_v[start]._st == EXIST)
{
++i;
start = index + i;
start% _v.size();
}
_v[start]._kv = kv;
_v[start]._st = EXIST;
++_n;
return true;
}
大家来想一个问题?当哈希表中的元素越来越多时,产生哈希冲突的概率必然也是越来越大的,怎么样去避免这类现象出现呢?
唯一的办法就是不断的去扩容,所以我们引入了平衡因子的概念,就是哈希表中的有效元素个数除以哈希表的容量,注意,这里的容量是size(),即vector中申请并初始化之后的元素的数目。
二次探测
何为二次探测,并不是探测两次,而是2次方。图示如下。
二次探测和线性探测类似。线性探测有一个缺点,就是当相同位置的元素过多时,就会侵占当前位置附近的其他位置,这就导致,本来处在附近位置的元素又去侵占其它元素的位置,这样会降低整个哈希表的查找效率,所以我们引入了二次探测,即插入的元素引起哈希冲突时,并不会在后续的第一个空位置插入冲突的元素,而是会去寻找当前位置+i*i的位置,如果当前位置为空,则插入元素。
代码如下。
cpp
bool insert( const pair<K,V>& kv)
{
//先去判断哈希表中是否存在当前要插入的元素,因为哈希表的原理是不排序加去重
HashData<K, V>* ret = find(kv.first);
if (ret != nullptr)
{
return false;
}
if (_v.size() == 0 || _n*10 / _v.size() > 7)
{
int NewSize = _v.size() == 0 ? 10 : _v.size() * 2;
HashTable<K, V> ht;
ht._v.resize(NewSize);
for (int i = 0; i< _v.size(); i++)
{
if (_v[i]._st == EXIST)
{
ht.insert(_v[i]._kv);
}
}
_v.swap(ht._v);
}
HashFunc<K> hf;
int start = hf(kv.first) % _v.size();
int i = 0;
size_t index = start;
while (_v[start]._st == EXIST)
{
++i;
//start = index + i;
start = index + i * i;
start% _v.size();
}
_v[start]._kv = kv;
_v[start]._st = EXIST;
++_n;
return true;
}
闭散列哈希表整体代码如下。
cpp
#pragma once
#include<iostream>
#include<vector>
#include <string>
using namespace std;
namespace CloseHash
{
enum Status
{
EMPTY,
DELETE,
EXIST
};
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return key;
}
};
template<>
struct HashFunc <string>
{
size_t operator()(const string& string)
{
size_t value = 0;
for (auto e : string)
{
value *= 31;
value += e;
}
return value;
}
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
Status _st = EMPTY;
};
template<class K,class V,class hashFunc=HashFunc<K>>
class HashTable
{
public:
bool erase(const K& key)
{
HashData<K, V>* ret = find(key);
if (ret == nullptr)
{
return false;
}
else
{
ret->_st = DELETE;
_n--;
return true;
}
}
HashData<K,V>* find(const K& key)
{
if (_v.size() == 0)
{
return nullptr;
}
HashFunc<K> hf;
size_t start = hf(key) % _v.size();
int i = 0;
size_t index = start;
while (_v[start]._st != EMPTY)
{
if (_v[start]._kv.first == key && _v[start]._st == EXIST)
{
return &_v[start];
}
++i;
start = index + i;
}
return nullptr;
}
//哈希表中元素的插入
bool insert( const pair<K,V>& kv)
{
//先去判断哈希表中是否存在当前要插入的元素,因为哈希表的原理是不排序加去重
HashData<K, V>* ret = find(kv.first);
if (ret != nullptr)
{
return false;
}
if (_v.size() == 0 || _n*10 / _v.size() > 7)
{
int NewSize = _v.size() == 0 ? 10 : _v.size() * 2;
HashTable<K, V> ht;
ht._v.resize(NewSize);
for (int i = 0; i< _v.size(); i++)
{
if (_v[i]._st == EXIST)
{
ht.insert(_v[i]._kv);
}
}
_v.swap(ht._v);
}
HashFunc<K> hf;
int start = hf(kv.first) % _v.size();
int i = 0;
size_t index = start;
while (_v[start]._st == EXIST)
{
++i;
//start = index + i;
start = index + i * i;
start% _v.size();
}
_v[start]._kv = kv;
_v[start]._st = EXIST;
++_n;
return true;
}
private:
vector<HashData<K,V>> _v;
int _n; //哈希表中有效元素的个数
};
void HashTest()
{
HashTable<int, int> ht;
ht.insert(make_pair(1, 1));
ht.insert(make_pair(2, 1));
ht.insert(make_pair(3, 1));
ht.insert(make_pair(4, 1));
cout << ht.find(1) << endl;
cout << ht.find(5) << endl;
}
}
调试代码,发现运行结果符合预期。
以上便是哈希表中闭散列哈希表的所有内容,下期将为大家带来开散列哈希表的内容。
本期内容到此结束^_^