哈希
- 一,哈希算法
- 1.什么是哈希
- 2.哈希产生的原因
- 3.常见哈希算法
- 4.闭散列( 哈希表)
- 1.线性探测
- 2.二次探测
- 5.开散列(哈希桶)
- 1.开散列插入
- 2.开散列扩容
- 二,代码实现
- 1.哈希表
- 2.哈希桶
- 1.迭代器的实现
- 2.底层容器的选择
- 3.重要接口
- 三,利用哈希桶封装unordered_map&set
- 1.unordered_set
- 2.unordered_map
一,哈希算法
1.什么是哈希
哈希算法是一种将输入数据(或称为“消息”)转换为固定长度的散列值(哈希值)的算法。它广泛应用于数据存储、加密、数据完整性检查等领域。
讲通俗点就是将分散的数据重新映射,使其重新分布在某些区间,方便储存和统计。
2.哈希产生的原因
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素
时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
O(
l
o
g
2
N
log_2 N
log2N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
本质哈希就是通过时间换空间的方式进行更快的访问。
3.常见哈希算法
1.通过取模加密的方式使得那些距离很分散的值的填入目标区间
如下数据:
2 3 6 19 20 10004 8
这些数据中大部分分布在20之内,但有一个超过一万,那我们难道开结构为10000的数组吗?答案是否定的。
如果通过取余的方式进行重新映射
则通过10个空间可以全部映射
2.哈希冲突
那么如果两个数据同时映射到一个位置怎么办?这就被称为哈希冲突。
解决哈希冲突常见有两种方式:闭散列和开散列
4.闭散列( 哈希表)
1.线性探测
- 插入
如果在某映射位置上发生冲突,那么可以选择在此之后,依次向后探测,直到寻找到下一个空位置为止。我们要控制负载因子,负载因子指的是插入数据与空间的比值,当负载因子到达一定时选择扩容,减少冲突。
如果我们在对以上例子插入一个4则可以变成这样
再插入一个14继续探测
- 删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素10004,如果直接删除掉,4查找起来可能会受影
响。因此线性探测采用标记的伪删除法来删除一个元素。
2.二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法
为:
H
i
H_i
Hi = (
H
0
H_0
H0 +
i
2
i^2
i2 )% m, 或者:
H
i
H_i
Hi = (
H
0
H_0
H0 -
i
2
i^2
i2 )% m。其中:i =
1,2,3…,
H
0
H_0
H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
5.开散列(哈希桶)
1.开散列插入
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
如图解决冲突
2.开散列扩容
为了保持每个桶里的数量不过多,桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。
二,代码实现
1.哈希表
1.哈希表的底层我们可以选择一个节点类型的vector数组
vector<HashData<K, V>> _table;
size_t _n = 0;
这里节点数我们暂且使用key_value模型,方便后续测试
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
state _state = EMPTY;
};
我们给每个节点定义一种状态,有存在,空,删除三种状态,方便在线性探测时区分
enum state
{
EMPTY,
EXIST,
DELETE
};
2.数据插入
在线性探索之前我们需要先解决一个问题,如果数据不是整型怎么办?这里我们借助仿函数
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 *= 131;
hash += e;
}
return hash;
}
};
如果是整型我们将其强转为无符号整型,避免负数的出现,若遇到字符串我们使用一个特化,将其使用一种哈希算法*131+=的方法,方法不唯一,经过这样处理之后,基本映射之后不会出现冲突。
插入函数
bool insert(const pair<K, V>& kv)
首先检查该数据是否插入过,进行查重
if (find(kv.first))
return false;
然后检查负载因子是否过载,为了将表的效率最大化,负载因子不能大不能小,如果过大则需要频繁线性探测,如果太小,则造成空间浪费,这里我们将其设置为0.7,如果大于等于0.7,则进行扩容,重新映射
//_n/_table.size() >= 0.7 变形
if ( _n * 10 / _table.size() >= 7)
{
reseve(_table.size() * 2);
}
扩容逻辑
void reseve(int newszie)
{
HashTable<K, V> newtable;
newtable._table.resize(newszie);
for (size_t i = 0; i < _table.size(); ++i)
{
if (_table[i]._state == EXIST
{
newtable.insert(_table[i]._kv);
}
}
_table.swap(newtable._table);
}
这里我们使用现代写法,创建一张新表,将原表数据一一插入,随后交换新旧表。
插入及线性探测
size_t hashi = hs(kv.first) % _table.size();
//线性探索
while (_table[hashi]._state == EXIST)
{
++hashi;
hashi %= _table.size();
}
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
++_n;
return true;
使用取模操作,映射下标,如果该位置存在则继续向后探测,否则进行插入。
3.查找
HashData<K, V>* find(const K& key)
{
HashFunc<K> hs;
size_t hashi = hs(key) % _table.size();
while (_table[hashi]._state != EMPTY)
{
if (_table[hashi]._state == EXIST &&
hs(key) == hs(_table[hashi]._kv.first))
{
return &_table[hashi];
}
++hashi;
hashi %= _table.size();
}
return nullptr;
}
为了防止删除的数据空缺影响搜索探测,遍历条件改为不为空就探索,而识别条件需要同时满足存在和相等。
简单测试
void test1()
{
int ar[11] = { 2,4,1,10,9,7,11,8,13,0,11 };
HashTable<int, int> ht;
for (auto e : ar)
{
ht.insert({ e,e });
}
cout << ht.size();
cout << endl;
cout << ht.find(2)->_kv.second << endl;
cout << ht.find(10) << endl;
cout << ht.find(13) << endl;
cout << ht.find(19) << endl;
}
插入和查重均成功
2.哈希桶
由于STL里unorldered_map,unorldered_set都选择哈希桶,所以这里着重强调哈希桶,并完成迭代器的封装
1.迭代器的实现
template<class K, class T,class Ptr,class Ref, class KeyOfT, class Hash>
struct Hash_Iterator
由于我选择的是友元类的形式,所以这里模板参数传6个,k,T,keyof是map,set在封装重要选择,ptr,ref是迭代器便于生成const,非const类型的前缀,hash是在内部结构,需要遍历的逻辑
- 提前重命名一下
typedef HashNode<T> node;
typedef Hash_Iterator<K,T,Ptr,Ref,KeyOfT,Hash> Self;
- 迭代器的构造
由于在遍历时需要找到,下一个位置,也许要跳着表找,所以还需要将哈希表的指针传入
node* _node;
const HashTable<K,T, KeyOfT,Hash>* _hst;
Hash_Iterator(node* node,const HashTable<K, T, KeyOfT, Hash>* hst)
:_node(node)
,_hst(hst)
{}
- ++遍历
Self& operator++()
{
//桶里有元素
if (_node->_next)
{
_node = _node->_next;
}
//桶里无元素,需要寻找下一个桶
else
{
KeyOfT kot;
Hash hs;
size_t hashi = hs(kot(_node->_data)) % _hst->_table.size();
++hashi;
for(;hashi < _hst->_table.size();++hashi)
{
if (_hst->_table[hashi])
break;
}
if (hashi == _hst->_table.size())
_node = nullptr;
else
_node = _hst->_table[hashi];
}
return *this;
}
如果桶里有下一个节点那么直接迭代即可,否则需要利用哈希表,重新计算下一个位置
- 访问
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
2.底层容器的选择
这里我们可以选择vector<List>或vector<node*>如果使用LIst作为实例数据则封装迭代时稍微有些麻烦,所以这里直接选择原生指针,作为桶内逻辑
private:
vector<node*> _table;
size_t _n;
这里的哈希节点我们统一使用T,方便后续map,set封装
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data),
_next(nullptr)
{}
};
3.重要接口
- 默认构造
HashTable()
{
_table.resize(10, nullptr);
_n = 0;
}
- 析构函数
~HashTable()
{
for (size_t hashi = 0; hashi < _table.size(); hashi++)
{
node* cur = _table[hashi];
while (cur)
{
node* next = cur->_next;
delete cur;
cur = next;
}
_table[hashi] = nullptr;
}
}
先通过表进行遍历,如果空位中的哈希桶含有元素便进行链式访问释放。
- 插入
pair<Iterator, bool> Insert(const T& data)
返回值包括插入后或插入位置的迭代器以及布尔类型的结果
1.防止重复插入
Iterator it = Find(kot(data));
if (it != End())
return make_pair(it, false);
2.扩容
if (_n == _table.size())
{
//重新建立新表,将原来数据挪动
vector<node*> newtable;
newtable.resize(_n * 2);
for (size_t hashi = 0; hashi < _table.size(); hashi++)
{
node* cur = _table[hashi];
while (cur)
{
node* next = cur->_next;
size_t hashi = hs(kot(cur->_data)) % newtable.size();
cur->_next = newtable[hashi];
newtable[hashi] = cur;
cur = next;
}
_table[hashi] = nullptr;
}
_table.swap(_newtable);
}
这里扩容不同于哈希表的线性结构,由于哈希桶中有链式结构,如果重新建表太麻烦了,所以我们决定挪动数据
3.插入新数据
size_t hashi = hs(kot(data)) % _table.size();
node * newnode = new node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return make_pair(Iterator(newnode,this),true);
计算哈希下标,进行对应位置头插,将新数据的next指向原哈希下标,然后将该数据赋给该位置
- 删除
bool Erase(const K& key)
{
Hash hs;
KeyOfT kot;
size_t hashi = hs(key) % _table.size();
node* cur = _table[hashi];
node* prev = nullptr;
while (cur)
{
if (kot(cur->_data) == key)
{
if (prev == nullptr)
{
_table[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
首先计算映射下标,然后考虑是否桶内第一个值是否为key,否则遍历
若为值,则直接将头数据转让给下一个
如果不是key,则记录当前prev,然后遍历,直到找到后,prev->_next = cur->_next
- 查找
Iterator Find(const K& key)
{
Hash hs;
KeyOfT kot;
size_t hashi = hs(key) % _table.size();
node* cur = _table[hashi];
while (cur)
{
if (kot(cur->_data) == key)
return Iterator(cur,this);
cur = cur->_next;
}
return End();
}
三,利用哈希桶封装unordered_map&set
相比于map,set来说,unordered_map&unordered_set更强调无需,搜索速度更快,由于映射则查找特定元素仅需O(1)的时间复杂度
1.unordered_set
template<class K, class Hash = HashFunc<K>>
class unordered_set
定义: unordered_set 是一个不重复元素的集合。它只存储键,不存储值。
特点:
- 所有元素都是唯一的,不允许重复。
- 不保证顺序,基于哈希函数来存储元素。
- 支持快速的查找、插入和删除,平均时间复杂度是 O(1)。
首先构造一个仿函数,便于在底层数据中取到key
struct setkeyofT
{
const K& operator()(const K& key)
{
return key;
}
};
成员变量构建
private:
HashBucket::HashTable<K, K, setkeyofT, Hash> _ht;
ps:这里Hashbucket是命名作用域
提前取出并重命名迭代器
typedef typename HashBucket::HashTable<K, K, setkeyofT, Hash>::Iterator iterator;
typedef typename HashBucket::HashTable<K, K, setkeyofT, Hash>::ConstIterator const_iterator;
ps:typename是为让编译器进入底层取出迭代器,如果不加,编译器不会擅自进入未实例化底层的
接下来就只需接入各个模块
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
const_iterator begin() const
{
return _ht.Begin();
}
const_iterator end() const
{
return _ht.End();
}
pair< iterator,bool> insert(const K& key)
{
return _ht.Insert(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
2.unordered_map
同上,贴出代码
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
public:
struct mapkeyofset
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
typedef typename HashBucket::HashTable<K, pair<K,V>, mapkeyofset, Hash>::Iterator iterator;
typedef typename HashBucket::HashTable<K, pair<K,V>, mapkeyofset, Hash>::ConstIterator const_iterator;
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
const_iterator begin()const
{
return _ht.Begin();
}
const_iterator end()const
{
return _ht.End();
}
pair<iterator,bool> insert(const pair<K, V>& data)
{
return _ht.Insert(data);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
private:
HashBucket::HashTable<K, pair<K,V>, mapkeyofset,Hash> _ht;
};