哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,通过这种方式,可以不经过任何比较,一次直接从表中得到要搜索的元素,即查找的时间复杂度为 O(1)。
这个映射函数叫做哈希(散列)函数,存放记录的数组叫做哈希(散列)表。
最典型的例子是计数排序,将要排序的数组每个元素和新开辟的数组的下标进行映射。
向哈希表中插入和搜索元素的过程如下:
先用哈希函数将被要插入或查找的键值转化为数组的一个索引。
在理想情况下,不同的键都能转化为不同的索引值。当然,这只是理想情况,所以我们需要面对两个或者多个键都会散列到相同的索引值的情况。这就是哈希冲突的问题。
取关键字的某个线性函数作为散列地址:hash(key)=A*key + B
优点:简单、均匀
缺点:实现需要知道关键字的分布情况,并且只适合查找比较小,且连续分布的情况
适用场景:查找字符串中,第一次出现的单词:构建一个数组 hash[ch-‘a’] 即为对应的地址
不适用场景:给一批数据, 1 5 8 100000 像这数据跨度大,数据元素不连续,很容易造成空间浪费
设散列表中允许的地址数为m,通常是取一个不大于m,但是最接近或者等于m的质数num,作为除数,按照哈希函数进行计算hash(key)= key%num, 将关键码转换成哈希地址
优点:使用场景广泛,不受限制。
缺点:存在哈希冲突,需要解决哈希冲突,哈希冲突越多,效率下降越厉害。
hash(key)=key*key 然后取函数返回值的中间的几位,作为哈希地址
比如 25^2 = 625 取中间的一位 2 作为哈希地址
比较适合不知道关键字的分布,而位数又不是很大的情况
将关键字从左到右分割成位数相等的几部分(最后一部分可以短些),然后将这几部分叠加求和,并且按照散列表长度,取最后几位作为散列地址
适用于不知道关键字分布,关键字位数比较多的情况
选取一个随机函数,取关键字的随机函数值,作为它的哈希地址,hash(key) = random(key),random为随机函数
通常用于关键字长度不等的情况
通过实现分析关键字,来获取哈希地址
比如用每个人的手机号码充当关键字,如果采用前三位作为哈希地址,那么冲突的概率是非常大的。如果采用的是中间3位那么冲突的概率要小很多
常用于处理关键字位数比较大的情况,且事前知道关键字的分布和关键字的若干位的分布情况
不同关键字通过相同哈希函数计算出相同的哈希映射地址,这种现象称为哈希冲突或哈希碰撞。
解决哈希冲突通常有两种方法:闭散列(开放地址发)和开散列(链地址法)。
闭散列也叫做开放地址法,当发生哈希冲突的时候,如果哈希表未被填满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置的"下一个"空位置中去,寻找下一个空位置的方法有线性探测法和二次探测法。
从发生冲突的位置开始,依次向后探测,直到寻找到下一个位置为止
优点:实现非常简单
缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据"堆积",即不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要进行多次比较,导致搜索效率降低。
随着哈希表中数据的增多,产生哈希冲突的可能性也随着增加。
我们将数据插入到有限的空间,那么空间中的元素越多,插入元素时产生冲突的概率也就越大,冲突多次后插入哈希表的元素,在查找时的效率必然也会降低。介于此,哈希表当中引入了负载因子(载荷因子):
散列表的载荷因子定义为 α = 填入表中的元素 / 散列表的长度
α是散列表装满程度的标志因子,α越大表明装入表中的元素越多,产生冲突的可能性也就越大,反之填入表中的元素越少,冲突可能性越低,空间利用率也就越低
闭散列:一般将载荷因子控制在 0.7-0.8以下,超过0.8查表时候的缓存不中率会按照指数曲线上升(哈希可能性冲突越大),因此一般hash库中,都将α设置在0.8以下。 闭散列,千万不能为满,否则在插入的时候会陷入死循环
开散列/哈希桶:一般将载荷因子控制在1。超过1,那么链表就挂得越长,效率也就越低
线性探测的缺陷是产生哈希冲突,容易导致冲突的数据堆积在一起,这是因为线性探测是逐个的找下一个空位置.
二次探测为了缓解这种问题(不是解决),对下一个空位置的查找进行了改进(跳跃式查找):
POS = (H+i2)%m
其中:i=1、2、3…
H是发生哈希冲突的位置
m是哈希表的大小
插入操作比较简单:通过哈希函数插入元素在哈希表中的位置,如果发生了哈希冲突,则使用线性探测或二次探测寻找下一个空位置插入元素。
但是删除操作比较麻烦,采用闭散列处理哈希冲突时,不能随便删除哈希表中已有的元素,如果直接删除元素,会影响其他元素的搜索(比如原来下标为40的元素因为前面删除了一个元素下标变成了39)。
因此线性探测采用标记的伪删除法来删除下一个元素。
为了减少冲突,哈希表的大小最好是素数。为了能够获取每次增容后的大小,将需要用到的素数序列提前用一个数组存储起来,当我们需要增容时就从该数组当中进行获取。
namespace CloseHash//闭散列
{
enum State
{
EMPTY,
EXIT,
DELETE
};
template<class K,class V>
struct HashData
{
pair<K, V> _kv;
State _state=EMPTY;//节点的状态默认为空
};
template<class K>
struct Hash
{
size_t operator()(const K& key)
{
return key;
}
};
template<>
struct Hash<string>
{
size_t operator()(const string& s)
{
size_t value = 0;
for (auto ch : s)
{
value += ch;
value *= 131;
}
return value;
}
};
template<class K, class V,class HashFunc=Hash<K>>
struct HashTable
{
public:
size_t GetNextPrime(size_t prime)
{
static 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, 429496729ul
};
size_t i = 0;
for (; i < PRIMECOUNT; ++i) {
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}
bool Insert(const pair<K, V>& kv)
{
HashData<K, V>* ret = Find(kv.first);
if (ret) //哈希表中已经存在该键值的键值对(不允许数据冗余)
{
return false; //插入失败
}
//进行扩容检测
if (_n == 0 || (_n / _table.size() * 10 > 7))//当前个数为0或者载荷因子超过了,则进行扩容
{
//size_t newsize = _size == 0 ? 10 : 2 * _tables.size();//初始化给10,后续扩容两倍
//选取素数
size_t newsize = GetNextPrime(_table.size());
//扩容之后,需要重新计算元素的位置
HashTable<K, V, HashFunc> newHT;
newHT._table.resize(newsize);
for (auto& e : _table)
{
if (e._state == EXIT)
newHT.Insert(e._kv);
}
_table.swap(newHT._table);//进行交换
}
HashFunc hf;
size_t start= hf(kv.first) % _table.size();
size_t index = start;
//探测后面的位置,线性探测或二次探测
size_t i = 1;
while (_table[index]._state == EXIT)
{
index =start+i;
index %= _table.size();
++i;
}
_table[index]._kv = kv;
_table[index]._state = EXIT;
++_n;
return true;
}
HashData<K,V>* Find(const K& key)
{
if (_table.size() == 0) return nullptr;
HashFunc hf;
size_t start = hf(key) % _table.size();
size_t index = start;
size_t i = 1;
while (_table[index]._state != EMPTY)
{
if (_table[index]._state== EXIT
&&_table[index]._kv.first == key)
{
return &_table[index];
}
index = start + i;
index %= _table.size();
++i;
}
return nullptr;
}
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret == nullptr)
{
return false;
}
else
{
ret->_state = DELETE;
return true;
}
}
private:
vector<HashData<K,V>> _table;
size_t _n = 0;
};
}
开散列又名哈希桶/开链法,首先对关键码集合采用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表串联起来,各个链表的头节点存储在哈希表中
与闭散列不同,开散列需要负载因子达到1的时候才进行扩容。
因为在理想的情况下每个桶下面只有一个节点。哈希桶的载荷因子控制在1,当大于1的时候就进行扩容,这样平均下来,每个桶下面只有一个节点;
与闭散列进行比较: 看起来哈希桶之中存储节点的指针开销比较大,其实不然。闭散列的负载因子需要保证小于0.7,来确保有足够的空间降低哈希冲突的概率,而表项的空间消耗远远高于指针所占的空间效率,因此哈希桶更能节省空间。
namespace OpenHash//开散列
{
template<class K,class V>
struct HashNode
{
HashNode<K, V>*_next;
pair<K, V>_kv;
HashNode(const pair<K, V>& kv)
:_next(nullptr)
,_kv(kv)
{}
};
template<class K>
struct Hash
{
size_t operator()(const K& key)
{
return key;
}
};
template<>
struct Hash<string>
{
size_t operator()(const string& s)
{
size_t value = 0;
for (auto ch : s)
{
value += ch;
value *= 131;
}
return value;
}
};
template<class K, class V, class HashFunc = Hash<K>>
struct HashTable
{
public:
typedef HashNode<K, V> Node;
Node* Find(const K& key)
{
HashFunc hf;
if (_table.size() == 0) return nullptr;
size_t index = hf(key) % _table.size();
Node* cur = _table[index];
//在桶中进行查找
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
else
{
cur = cur->_next;
}
}
return nullptr;
}
bool Insert(const pair<K, V>& kv)
{
HashFunc hf;
if (Find(kv.first)) return false;//列表里已经存在kv
//负载因子到1时进行增容
if (_n == _table.size())
{
vector<Node*>newtable;
size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
newtable.resize(newSize);
//遍历旧表中的节点,重新计算映射位置,挂到新表中
for (size_t i = 0; i < _table.size(); ++i)
{
if (_table[i])
{
Node* cur = _table[i];
while (cur)
{
//记录原来cur后面的节点
Node* next = cur->_next;
size_t index = hf(cur->_kv.first) % newtable.size();
//头插
cur->_next = _table[index];
_table[index] = cur;
cur = next;
}
_table[i] = nullptr;
}
}
_table.swap(newtable);
}
size_t index = hf(kv.first) % _table.size();
Node* newnode = new Node(kv);
newnode->_next = _table[index];
_table[index] = newnode;
++_n;
}
bool Erase(const K& key)
{
HashFunc hf;
size_t index = hf(key) % _table.size();
Node* prev = nullptr;
Node* cur = _table[index];
//在桶中找到对应的节点进行删除
while (cur)
{
if (cur->_kv.first == key)
{
//头结点的情况
if (_table[index] == cur)
{
_table[index] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
--_n;
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
//要删的节点不存在
return false;
}
private:
vector<Node*> _table;
size_t _n=0;//有效数据的个数
};
}
资料参考:
哈希表、哈希桶的实现
哈希表底层探索
数据结构之(一)Hash(散列)
哈希之开散列,闭散列