unordered系列的关联式容器在前面博客:unordered系列 中讲到了,这里我就讲一下:
1)底层的结构——哈希结构和哈希冲突
2)哈希冲突的解决方法——闭散列和开散列
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较,顺序查找时间复杂度为O(N),平衡树中为树的高度,搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间可以建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中插入元素时,根据待插元素的关键码计算出存储位置并进行存放。
搜索元素时,对待查元素的关键码进行同样的计算,再去相应位置查看是否存在。
以上方式就是哈希(散列)方法,哈希方法中使用的的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置:hash(key)=key%capacity;
capacity为存储元素底层空间总大小。
用该方法进行搜索不必进行多次关键码的比较,因此搜索速度比较快。
那么,此时如果要插入44,会出现什么问题?这里就遇到哈希冲突。
后面会讲到解决哈希冲突方法。
对于两个数据的关键字k_i和k_j,若有k_i!=k_j,但Hash(k_i)==Hash(k_j),即:不同的关键字通过相同的哈希函数计算出相同的哈希地址,该现象就称哈希冲突或哈希碰撞。
并把具有相同哈希地址的数据元素称为“同义词”。
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。哈希函数的设计原则:
常见的解决方法有两种:闭散列和开散列
闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装载,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置的下一个空位子去。
闭散列的插入:
1)通过哈希函数获取待插入元素在哈希表中的位置
2)如果该位置上没有元素则直接插入新元素;如果该位置上已经有元素,则使用线性探测找到下一个空位置再插入新元素。
bool Insert(const pair& kv)
{
size_t index = kv.first % _table.size();
while (_table[index]._state == EXIST)
{
if (_table[index]._kv.first == kv.first)
{
return true;
}
//线性探测解决哈希冲突问题
++index;
if (index == _table.size())
index = 0;
}
//若是该位置没值,直接插入
_table[index]._kv = kv;
_table[index]._state = EXIST;
_size++;
return true;
}
删除步骤:
采用闭散列处理哈希冲突时,不能随便删除,否则会影响其他元素的查找。 比如删除4,如果直接删除,则在查找44时会直接判断不存在。因此线性探测采用标记的伪删除来删除元素。
//哈希的每个空间都给一个标记
//EMPTY 为空,EXIST 为存在,DELETE 为删除
enum State{EMPTY,EXIST,DELETE};
bool Erase(const K& key)
{
HashNode* node = Find(ket);
if (node != nullptr)
{
node->_state = DELETE;
--_size;
return true;
}
else
{
return false;
}
}
查找:
用相同的哈希函数计算待查找元素的位置,若该位置状态为空即EMPTY 则不存在;若该位置非空但不是待查元素,则继续向后查找直至下一个状态为空的位置停止。
HashNode* Find(const K& key)
{
size_t index = key%_table.size();
while (_table[index]._state != EMPTY)
{
if (_table[index]._kv.first == key && _table[index] == EXIST)
{
return &_table[index];
}
++index;
if (index == _table.size())
index = 0;
}
return nullptr;
}
增容:
void CheckCapacity()
{
//load factor 等于0.7开始增容,负载因子越小效率越高,空间浪费越多
if (_tabele.size() == 0 || (_size * 10) / _table.size() == 7)
{//增容
size_t newcapacity = _table.size() == 0 ? 10 : _table.size() * 2;
HashTable _newht(newcapacity);
//旧表的数据重新计算在新表中的位置
for (size_t i = 0; i < _table.size(); ++i)
{
if (_table[i]._state == EXIST)
_newht.Insert(_table[i]._kv);
}
//交换之后,旧表更新为newht,newht为本函数中的局部对象,
//出了该函数了作用域之后会自动调用析构函数释放旧表资源
_table.swap(_newht._table);
}
}
线性探测的优点:实现简单
线性探测的缺点:一旦所有的哈希冲突连在一起,就容易产生数据“堆积”,即不同的关键码占据了其他数据原本可利用的空位,使得寻找某个关键码的位置需要多次进行比较,导致搜索效率降低。
那么怎么解决呢?下面就提供了解决方法——二次探测
二次探测找下一个空位置的具体做法是,先通过下面函数计算
,其中H_0是通过散列函数Hash(x)对元素的关键码key进行计算得到的位置,m是表的大小。
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5, 如果超出必须考虑增容。
闭散列的最大缺陷:空间利用率比较低,这也是哈希的缺陷。
#pragma once
#include
#include
using namespace std;
enum State
{
EMPTY,
DELETE,
EXIST
};
template
struct HashNode
{
pair _kv;
State _state;
};
template
class HashTable
{
typedef HashNode HashNode;
public:
HashTable(size_t N = 10)
{
_table.resize(N);
for (size_t i = 0; i < _table.size(); ++i)
{
_table[i]._state = EMPTY;
}
_size = 0;
}
void CheckCapacity()
{
//load factor 等于0.7开始增容,负载因子越小效率越高,空间浪费越多
if (_tabele.size() == 0 || (_size * 10) / _table.size() == 7)
{//增容
size_t newcapacity = _table.size() == 0 ? 10 : _table.size() * 2;
HashTable _newht(newcapacity);
//旧表的数据重新计算在新表中的位置
for (size_t i = 0; i < _table.size(); ++i)
{
if (_table[i]._state == EXIST)
_newht.Insert(_table[i]._kv);
}
//交换之后,旧表更新为newht,newht为本函数中的局部对象,
//出了该函数了作用域之后会自动调用析构函数释放旧表资源
_table.swap(_newht._table);
}
}
bool Insert(const pair& kv)
{
size_t index = kv.first % _table.size();
while (_table[index]._state == EXIST)
{
if (_table[index]._kv.first == kv.first)
{
return true;
}
//线性探测解决哈希冲突问题
++index;
if (index == _table.size())
index = 0;
}
//若是该位置没值,直接插入
_table[index]._kv = kv;
_table[index]._state = EXIST;
_size++;
return true;
}
HashNode* Find(const K& key)
{
size_t index = key%_table.size();
while (_table[index]._state != EMPTY)
{
if (_table[index]._kv.first == key && _table[index] == EXIST)
{
return &_table[index];
}
++index;
if (index == _table.size())
index = 0;
}
return nullptr;
}
bool Erase(const K& key)
{
HashNode* node = Find(ket);
if (node != nullptr)
{
node->_state = DELETE;
--_size;
return true;
}
else
{
return false;
}
}
private:
//HashNode* _table;
//size_t _size;//有效数据个数
//size_t _capacity;//哈希表的空间大小
vector _table;
size_t _size;//哈希表中有效数据的个数
};
void TestHashTable()
{
HashTable ht;
int a[] = { 3, 2, 5, 1 };
for (auto& e : a)
{
ht.Insert(make_pair(e, e));
}
}
开散列在下篇博客中:哈希冲突解决方法之开散列 中会讲到。大家看完博客记得关注博主哦~