目录
1.什么是哈希
哈希(Hash)是指将任意长度的输入通过哈希函数(Hash Function)转换为固定长度的输出的过程。哈希函数可以将输入数据映射为较短的固定长度的值,通常用一串数字和字母来表示,这串值就被称为哈希值或者哈希码。
哈希函数具有以下特性:
- 输入相同的数据经过哈希函数得到的哈希值是唯一确定的。 即使输入数据只有微小的改动,得到的哈希值也会发生很大的变化。
- 无法从哈希值中逆向推导出原始的输入数据。
- 不同的输入数据得到的哈希值一般是不同的,但由于哈希值的长度是固定的,所以可能会发生不同的输入数据得到相同的哈希值,这种情况被称为哈希冲突。
2.哈希函数
一般来说,哈希函数,只要你想,什么样的哈希函数都可以,但是考虑到实际的存储和哈希冲突,一般都采用主流的哈希函数。
比如,这个大佬写的博客上讲解了很多种的字符串哈希函数:
字符串hash函数
这种哈希函数是针对存储数据是字符串类型的情况。
常见的哈希函数:(hashi是哈希表存储位置的下标,key表示要存储的值的关键字)
- 直接定址法:
哈希函数:取关键字的某个线性函数为散列地址:hashi=A*key+B
优点:简单、均匀
缺点:需要事先知道关键字的大小范围;如果出现最大的数很大且分散,会导致大量空间浪费
适用场景:查找比较小且连续的数- 除留余数法:
哈希函数:设散列表的大小为m,去一个不大于m,但最接近m的质数p作为除数:hashi=key%p(p<=m)
优点:无需知道关键字的大小范围,手动控制空间大小
缺点:关键字不均匀时容易发生哈希冲突;只适用于整型,其他类型使用需要先转成整型
适用场景:关键字分布较为均匀的时候- 平方取中法:
哈希函数:将关键字进行平方运算,取平方结果的中间几位作为哈希值:hashi=MSMethod(key*key)
优点:简单易实现,均匀性较好
缺点:对关键字长度敏感;哈希冲突风险
适用场景:中小规模数据集;整数类型的关键字;哈希冲突处理要求不严格的场景- 折叠法:
哈希函数:其基本原理是将关键字按固定长度分割,然后相邻分段相加得到哈希值。
优点:对于大型关键字适用性强,相对简单易实现
缺点:可能会导致哈希冲突,特别是在分割长度选择不当的情况下;效率可能不如其他更复杂的哈希函数
适用场景:适合处理较大关键字的哈希需求;在对哈希函数的均匀性要求不是特别严格的情况下适用- 随机数法:
哈希函数:通过随机选择一个数作为哈希值:hashi=random()
优点:简单快速,实现简单,不需要复杂的计算;哈希冲突较少,由于随机选择哈希值,冲突风险较低
缺点:不确定性,哈希值是随机生成的,不具有可预测性;哈希表大小限制,需要确保随机数的范围在哈希表大小范围内
适用场景:小规模数据集;无需持久化的临时哈希需求;对哈希值唯一性要求不高- 数学分析法:
哈希函数:通过对关键字进行数学运算得到哈希值
优点:控制性强,设计者可以根据实际需求对哈希函数进行精细调整,以满足特定要求;可预测性,相比随机数法,数学分析法的哈希值具有一定的可预测性
缺点:设计复杂度高,需要设计者对数据特征和哈希表特点有深入理解,才能设计出高效的数学分析法哈希函数;哈希冲突风险,不当的数学分析法设计可能导致哈希冲突的增加
适用场景:定制化需求;对哈希值具有一定可预测性要求;充分理解数据特征的场景(注意:哈希函数设计得越精妙,哈希冲突产生的可能性就越低,但是无法避免哈希冲突)
3.哈希冲突
哈希冲突是多个关键字通过哈希函数映射得到的哈希值相同导致的。
解决哈希冲突常见的两种方法:闭散列和开散列
3.1闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
3.1.1线性探测法
从发生冲突的位置开始,依次向后探测,直到找到一个为空的位置为止。
(1)插入:
假设这个数组是空的,容量为9,哈希函数是除留余数法
此时,有一个key为3的数据要插入,根据除留余数法,hashi=3%9=3,即映射到下标为3的位置
之后,又有一个key为30的数据要插入,根据除留余数法,hashi=30%9=3,即映射到下标为3的位置,可是下标为3的空间已经被key为3的数据给占了,那怎么办呢?——根据线性探测法,从当前映射位置,向后遍历
while(!Empty(hashtable,hashi))//Empty判断hashtable数组下标hashi的位置是否为空,为空返回true,不为空返回false
{
hashi++;
hashi%=9;
}//当循环结束时,就找到了空位置的hashi
当循环结束,key为30的数据就会在当前下标hashi的位置进行存储。
当要寻找某个数据时,也要严格遵守这个操作,即通过哈希函数找到指定下标,如果发生哈希冲突,就往后遍历寻找,直到找到下一个为空的地方(被删除过的位置不算)。
(2)删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,因为在上文,我们说了寻找某个元素的循环终止条件是遇见下一个空,如果所要寻找的值存储在一个被删除的空间后,就会受到影响。
因此,线性探测法采用标记的伪删除法来删除一个元素。
//创建一个枚举变量,包含3种状态,使数组存储的元素和枚举变量进行组合,对数组的每一个空间进行描述
enum State
{
EXIST,//表示该位置已有数据
DELETE,//表示该位置之前有数据,之后被删除
EMPTY//表示该位置没有过数据,为空
}
模拟实现线性探测法(源码):
#include<iostream>
#include<vector>
using namespace std;
//定义枚举常量
enum status
{
EXIST,
DELETE,
EMPTY
};
//哈希结点
template<class k, class v>
struct hashdata
{
pair<k, v> _data;
status _status = EMPTY;
};
//哈希仿函数
template<class k>
struct hashfunc
{
size_t operator()(const k& key)
{
return (size_t)key;
}
};
//使用除留余数法进行映射的哈希表
template<class k, class v, class hash = hashfunc<k>>
class hashtable
{
public:
typedef hashdata<k, v> node;
hashtable(size_t size = 10)
{
_ht.resize(size);
}
node* find(const k& key)
{
hash hs;
size_t hashi = hs(key) % _ht.size();
//在遇见空结点之前一直寻找
while (_ht[hashi]._status != EMPTY)
{
if (_ht[hashi]._data.first == key && _ht[hashi]._status == EXIST)
return &_ht[hashi];
hashi++;
hashi %= _ht.size();
}
//没找到就返回空
return nullptr;
}
bool insert(const pair<k, v>& kv)
{
//如果已经存在,则无法插入
if (find(kv.first))
return false;
//如果负载因子超过0.7要进行扩容
if (_size * 10 / _ht.size() >= 7)
{
//创一个新的哈希表,并扩容为原来的两倍
hashtable<k, v, hash> newht(_ht.size() * 2);
//遍历原哈希表
for (auto& e : _ht)
{
//把旧哈希表的值重新映射到新哈希表上
if (e._status == EXIST)
{
newht.insert(e._data);
}
}
//把两个哈希表交换
_ht.swap(newht._ht);
}
hash hs;
//线性探测
size_t hashi = hs(kv.first) % _ht.size();
//找到删除位置或者空位置停下
while (_ht[hashi]._status == EXIST)
{
hashi++;
hashi %= _ht.size();
}
_ht[hashi]._data = kv;
_ht[hashi]._status = EXIST;
_size++;
return true;
}
bool erase(const k& key)
{
hash hs;
size_t hashi = hs(key) % _ht.size();
while (_ht[hashi]._status != EMPTY)
{
if (_ht[hashi]._data.first == key && _ht[hashi]._status ==EXIST)
{
_ht[hashi]._status = DELETE;
return true;
}
hashi++;
hashi %= _ht.size();
}
return false;
}
private:
vector<node> _ht;
size_t _size = 0;
};
(3)扩容
散列表的载荷因子定义为:α=填入表中的元素个数 / 散列表的长度
α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,表明填入表中的元素越多,产生冲突的可能性越大;反之,α越小,表明填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子α的函数,只是不同处理冲突的方法有不同的函数。
对于闭散列(开放定制法),载荷因子是特别重要的因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中按照指数曲线上升。因此,一些采用闭散列的hash库,如Java的系统库限制了载荷因子为0.75,超过此值将resize散列表。
3.1.2二次探测法
线性探测法的缺陷就是产生冲突的数据堆积在一起,这是因为每次插入的位置都是下一个空位置,所以冲突的数据都是连着插入。
二次探测法在此进行改进,不采取向后挨个挨个遍历式的寻找空位置,当发生哈希冲突时:
hashi=(hashi+(-1)k+1 * i2 ) %m
解释:k=1,2,3,…(探测次数),i=1,2,3,…,每寻找两次,i++,m为表的容量
循环终止:当新的哈希值所在空间的State为EMPTY时停止循环
数组为空,插入key为11的数据,根据除留余数法,11%10=1,得到哈希值为1,hashtable[1].State == EMPTY,插入成功。
接着插入key为1的数据,根据除留余数法,1%10 = 1,得到哈希值为1,hashtable[1].State == EXIST,发生哈希冲突。
根据二次探测法,hashi=(hashi+(-1)1 * 12)%10=(1+1)%10=2,hashtable[2].State == EMPTY,因此key为1的数据插入到哈希表下标为2的空间。
3.2开散列
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合成为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
3.2.1哈希桶的实现(包含模版和迭代器)
#include<iostream>
#include<vector>
#include<utility>
using namespace std;
template<class T>
//对值转化为size_t
struct HashFunc
{
public:
size_t operator()(const T& val)
{
return (size_t)val;
}
};
//对string类型进行特化,特化规则是每个字母下标*133+字母ASCLL
template<>
struct HashFunc<string>
{
public:
size_t operator()(const string& s)
{
const char* str = s.c_str();
unsigned int seed = 131; // 31 131 1313 13131 131313
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return hash;
}
};
//哈希桶结点
template<class K>
struct HashBucketNode
{
HashBucketNode(const K& data)
: _pNext(nullptr), _data(data)
{}
HashBucketNode<K>* _pNext;
K _data;
};
// 为了实现简单,在哈希桶的迭代器类中需要用到hashBucket本身,
template<class K, class V, class KeyOfValue, class HF>
class HashBucket;
// 注意:因为哈希桶在底层是单链表结构,所以哈希桶的迭代器不需要--操作
template <class K, class V, class KeyOfValue, class HF>
struct HBIterator
{
typedef HashBucket<K, V, KeyOfValue, HF> HashBucket;
typedef HashBucketNode<V>* PNode;
typedef HBIterator<K, V, KeyOfValue, HF> Self;
HBIterator(PNode pNode = nullptr, HashBucket* pHt = nullptr)
:_pNode(pNode)
, _pHt(pHt)
{}
//前置++
Self& operator++()
{
// 当前迭代器所指节点后还有节点时直接取其下一个节点
if (_pNode->_pNext)
_pNode = _pNode->_pNext;
else
{
// 找下一个不空的桶,返回该桶中第一个节点
size_t bucketNo = _pHt->HashFunc(KeyOfValue()(_pNode->_data)) + 1;
for (; bucketNo < _pHt->BucketCount(); ++bucketNo)
{
if (_pNode = _pHt->_table[bucketNo])
break;
}
}
return *this;
}
//后置++
Self operator++(int)
{
PNode node = _pNode;
if (_pNode->_pNext)
_pNode = _pNode->_pNext;
else
{
// 找下一个不空的桶,返回该桶中第一个节点
size_t bucketNo = _pHt->HashFunc(KeyOfValue()(_pNode->_data)) + 1;
for (; bucketNo < _pHt->BucketCount(); ++bucketNo)
{
if (_pNode = _pHt->_ht[bucketNo])
break;
}
}
return Self(node, _pHt);
}
V& operator*()
{
return _pNode->_data;
}
V* operator->()
{
return &(_pNode->_data);
}
bool operator==(const Self& it) const
{
return _pNode == it._pNode;
}
bool operator!=(const Self& it) const
{
return !(operator==(it));
}
PNode _pNode;
private: // 当前迭代器关联的节点
HashBucket* _pHt;// 哈希桶--主要是为了找下一个空桶时候方便
};
// 本文所实现的哈希桶中key是唯一的
template<class K, class V, class KeyOfValue, class HF>
class HashBucket
{
public:
typedef KeyOfValue KOF;
typedef HashBucketNode<V> Node;
typedef Node* PNode;
typedef HashBucket<K, V, KOF, HF> Self;
typedef HBIterator<K, V, KOF, HF> iterator;
friend iterator;
public:
HashBucket(size_t capacity = 10)
: _table(GetNextPrime(capacity))//以奇数素组为容量进行扩容
, _size(0)
{}
~HashBucket()
{
Clear();
}
iterator begin()
{
for (int i = 0; i < BucketCount(); i++)
{
if (_table[i] != nullptr)
return iterator(_table[i], this);
}
return iterator(nullptr, this);
}
iterator end()
{
return iterator(nullptr, this);
}
// 哈希桶中的元素不能重复
pair<iterator, bool> Insert(const V& data)
{
//先Find,如果找到了返回结点,没找到返回空,返回空就可以插入
KOF kof;
PNode node = Find(kof(data))._pNode;
//node不为空,返回结点
if (node)
return pair<iterator, bool>(iterator(node, this), false);
//控制负载因子<0.7
if (10 * _size / BucketCount() >= 7)
{
//扩容
CheckCapacity();
}
//映射
size_t hashi = HashFunc(kof(data));
node = new Node(data);
node->_pNext = _table[hashi];
_table[hashi] = node;
_size++;
return pair<iterator, bool>(iterator(node, this), true);
}
// 删除哈希桶中为data的元素(data不会重复)
iterator Erase(iterator& it)
{
//找到了删,没找到返回false
KOF kof;
if (Empty())
return iterator(nullptr, this);
size_t hashi = HashFunc(kof(*it));
PNode cur = _table[hashi];
PNode pre = nullptr;
while (cur)
{
//如果是桶的首节点
if (cur == _table[hashi])
{
_table[hashi] = _table[hashi]->_pNext;
delete cur;
return iterator(_table[hashi], this);
}
if (cur->_data == data)
{
pre->_pNext = cur->_pNext;
delete cur;
return iterator(pre->_pNext, this);
}
pre = cur;
cur = cur->_pNext;
}
return iterator(nullptr, this);
}
//查询操作
iterator Find(const K& key)
{
//Find规则,找到了返回结点,没找到返回空
//映射桶的位置
KOF kof;
size_t hashi = HashFunc(key);
PNode cur = _table[hashi];
while (cur)
{
if (kof(cur->_data) == key)
return iterator(cur, this);
cur = cur->_pNext;
}
return iterator(nullptr, this);
}
//有效元素个数
size_t Size()const
{
return _size;
}
//判空
bool Empty()const
{
return 0 == _size;
}
void Clear()
{
for (int i = 0; i < BucketCount(); i++)
{
PNode cur = _table[i];
while (cur)
{
_table[i] = cur->_pNext;
delete cur;
cur = _table[i];
}
}
}
size_t BucketCount()const
{
return _table.capacity();
}
void Swap(Self& ht)
{
_table.swap(ht._table);
swap(_size, ht._size);
}
size_t BucketSize(const K& key)
{
PNode cur = Find(key);
if (cur == nullptr)
return 0;
size_t hashi = HashFunc(key);
cur = _table[hashi];
size_t count = 0;
while (cur)
{
count++;
cur = cur->_pNext;
}
return count;
}
size_t Count(const K& key)
{
KOF kof;
size_t hashi = HashFunc(key);
PNode cur = _table[hashi];
size_t count = 0;
while (cur)
{
if (kof(cur->_data) == key)
count++;
cur = cur->_pNext;
}
return count;
}
private:
size_t HashFunc(const K& key)
{
return HF()(key) % _table.capacity();
}
void CheckCapacity()
{
//开新桶
KOF kof;
Self newht(BucketCount());
//进行重新连接
for (int i = 0; i < BucketCount(); i++)
{
PNode cur = _table[i];
while (cur)
{
//重新映射
size_t hashi = kof(cur->_data) % newht.BucketCount();
//原桶头结点指向下一个
_table[i] = cur->_pNext;
//当前结点映射链接到新桶上
cur->_pNext = newht._table[hashi];
newht._table[hashi] = cur;
//cur继续遍历原桶
cur = _table[i];
}
}
//交换
Swap(newht);
}
//以质数集合为容量进行扩容
size_t GetNextPrime(size_t capacity)
{
const size_t arr[] = { 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, 4294967291ul
};
for (int i = 0; i < sizeof(arr) / sizeof(size_t); i++)
{
if (capacity < arr[i])
return arr[i];
}
}
private:
vector<PNode> _table;
size_t _size; // 哈希表中有效元素的个数
};
4.unordered_map和unordered_set
unordered_map和unordered_set是C++11标准引入的,属于C++标准库中的无序关联容器(Unordered Containers)。它们提供了基于哈希表的数据存储结构,用于实现快速的查找、插入和删除操作。
4.1unordered_map的模拟实现
//底层的哈希表用的是上文实现的哈希桶结构
template<class K, class V, class HF = HashFunc<K>>//V对于map来说是第二种类型
class unordered_map
{
struct MapOfValue
{
const K& operator()(const pair<K, V>& data)
{
return data.first;
}
};
// 通过key获取value的操作
typedef HashBucket<K, pair<K, V>, MapOfValue, HF> HT;
public:
//对于HT和iterator,pair<K,V>是他们的V,而pair的second是map的V
typedef typename HT::iterator iterator;
public:
unordered_map(HT ht = HT())
:_ht(ht)
{}
iterator begin() { return _ht.begin(); }
iterator end() { return _ht.end(); }
// capacity
size_t size()const { return _ht.size(); }
bool empty()const { return _ht.empty(); }
// Acess
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(pair<K, V>(key, V()));
return (*(ret.first)).second;
}
const V& operator[](const K& key)const
{
pair<iterator, bool> ret = _ht.Insert(pair<K, V>(key, V()));
return (*(ret.first)).second;
}
// lookup
iterator find(const K& key) { return _ht.Find(key); }
size_t count(const K& key) { return _ht.Count(key); }
// modify
pair<iterator, bool> insert(const pair<K, V>& value)
{
return _ht.Insert(value);
}
iterator erase(iterator position)
{
return _ht.Erase(position);
}
// bucket
size_t bucket_count() { return _ht.BucketCount(); }
size_t bucket_size(const K& key) { return _ht.BucketSize(key); }
private:
HT _ht;
};
4.2unordered_set的模拟实现
//底层的哈希表用的是上文实现的哈希桶结构
template<class K, class HF = HashFunc<K>>
class unordered_set
{
// 通过key获取value的操作
struct SetOfValue
{
const K& operator()(const K& data)
{
return data;
}
};
typedef HashBucket<K, K, SetOfValue, HF> HT;
public:
typename typedef HT::iterator iterator;
public:
unordered_set() : _ht()
{}
iterator begin() { return _ht.begin(); }
iterator end() { return _ht.end(); }
// capacity
size_t size()const { return _ht.size(); }
bool empty()const { return _ht.empty(); }
// lookup
iterator find(const K& key) { return _ht.Find(key); }
size_t count(const K& key) { return _ht.Count(key); }
// modify
pair<iterator, bool> insert(const K& valye)
{
return _ht.Insert(valye);
}
iterator erase(iterator position)
{
return _ht.Erase(position);
}
// bucket
size_t bucket_count() { return _ht.BucketCount(); }
size_t bucket_size(const K& key) { return _ht.BucketSize(key); }
private:
HT _ht;
};