哈希概念
unordered系列的关联式容器(如unordered_map unordered_set) 之所以效率比较高,是因为其底层使用了哈希结构
顺序结构以及平衡树
中,元素关键码与其存储位置之间没有对应的关系,因此在
查找一个元素
时,必须要经过关键码的多次比较
理想的搜索方法:可以
不经过任何比较
,
一次
直接从表中得到要搜索的元素
哈希/散列:关键值与另一个值,建立一个关联关系 是一种思想
哈希表/散列表:关键字和存储位置建立一个关联关系
哈希
(
散列
)
方法:通过某种函数(hashFunc)
使元素的存储位置与它的关键值之间能够建立
一一映射的关系,那么在查找时
通过该函数
可以
很快找到该元素
哈希方法中:使用的转换函数称作
哈希(散列)函数
,构造出来的结构
称
为
哈希表
(Hash Table)(or
散列表
)
向该结构中:
插入元素:
根据待插入元素的
关键码
,以此
函数计算出
该元素的
存储位置
并按此位置进行存放
搜索元素:
根据待插入元素的
关键码
,以此
函数计算出
该元素的
存储位置,
在结构中按此位置取元素比较,若关键码相等,则搜索成功
![](https://i-blog.csdnimg.cn/blog_migrate/1471b57aeda2afd5fbb847bebd1d05d4.png)
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
如若再插入99,则会发生哈希冲突
哈希冲突
不同关键字
通过相同哈希函数计算出
相同的哈希地址
,该种现象称为哈希冲突
或哈希碰撞
。
引起哈希冲突的一个原因可能是:
哈希函数设计不够合理
常见哈希函数
1. 直接定址法--(常用)
Hash
(
Key
)
= A*Key + B
使用场景:关键字范围集中且数据量不大的情况
缺点:若是值很分散,那么此方法会导致空间开很大,浪费
关键字与存储位置是一对一的关系,不存在哈希冲突
2. 除留余数法--(常用)
设散列表中允许的
地址数为
m
,取一个不大于
m
,但最接近或者等于
m
的质数
p
作为除数
Hash(key) = key% p(p<=m)
关键字可以分散,数据量可以很大
关键字与存储位置是多对一的关系,存在哈希冲突
3.
平方取中法
--(
了解
)
假设关键字为
1234
,对它平方就是
1522756
,抽取中间的
3
位
227
作为哈希地址;
再比如关键字为
4321
,对它平方就是
18671041
,抽取中间的
3
位
671(
或
710)
作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
4.
折叠法
--(
了解
)
折叠法是将关键字从左到右分割成位数相等的几部分
(
最后一部分位数可以短些
)
,然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5.
随机数法
--(
了解
)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即
H(key) = random(key),
其中
random为随机数函数
通常应用于关键字长度不等时采用此法
总之,哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
哈希冲突解决
两种方法:闭散列和开散列
闭散列(开放定址法)
当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去
寻找下一个空位置:hashi为元素使用哈希函数映射出的地址
1. 线性探测 hashi+i (i>=0)
2. 二次探测 hashi+i^2 (i>=0)
线性探测:
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
插入:通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突
则使用线性探测找到下一个空位置,插入新元素
![](https://i-blog.csdnimg.cn/blog_migrate/c4b54d38210fe552f81487ec0c91c285.png)
删除 :采用伪删除法
采用闭散列处理哈希冲突时,
不能随便物理删除
哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索
。比如删除元素
4
,如果直接删除掉,则下标为4的空间为空,当查找44时,直接就是找到了一个空位置,查找结束,找不到,但实际上元素44是应该要被搜索成功的
响。因此
线性探测采用标记的
伪删除法
来删除一个元素
。
删除状态的意义:1 再插入,这个位置可以覆盖值
2 防止后面冲突的值,出现找不到的情况,遇到删除状态,还是继续往后找
enum Status//标记存储值的状态
{
EMPTY,//空
EXIST,//存在
DELETE//删除
};
哈希表设计代码(开放定址法/闭散列解决哈希冲突)
#pragma once
#include<string>
#include<vector>
#include<iostream>
using namespace std;
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 *= 31;
hash += e;
}
return hash;
}
};
namespace open_address//开放定址法/闭散列
{
enum Status//标记存储值的状态
{
EMPTY,//空
EXIST,//存在
DELETE//删除
};
template<class K,class V>
struct HashData
{
pair<K, V> _kv;
Status _s;//状态
};
template<class K,class V,class Hash = HashFunc<K>>
class HashTable
{
public:
HashTable()
{
_tables.resize(10);
}
bool Insert(const pair<K, V>& kv)
{
//检查是否已经存在
if (Find(kv.first))
return false;
//检查是否需要扩容
// 负载因子0.7就扩容
if (_n * 10 / _tables.size() == 7)
{
size_t newSize = _tables.size() * 2;
HashTable<K, V, Hash> newHT;
newHT._tables.resize(newSize);
//遍历旧表
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i]._s == EXIST)
newHT.Insert(_tables[i]._kv);
}
_tables.swap(newHT._tables);
}
//插入
Hash hf;
size_t hashi = hf(kv.first) % _tables.size();
while (_tables[hashi]._s == EXIST)
{
++hashi;
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._s = EXIST;
++_n;
return true;
}
HashData<K, V>* Find(const K& key)//若是一次性找不到,则一直找到空就结束
{
Hash hf;
size_t hashi = hf(key) % _tables.size();
while (_tables[hashi]._s !=EMPTY)
{
if (_tables[hashi]._s == EXIST && _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
hashi++;
hashi %= _tables.size();
}
return nullptr;
}
// 伪删除法
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_s = DELETE;
--_n;
return true;
}
else
{
return false;
}
}
void Print()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i]._s == EXIST)
{
cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;
}
else if (_tables[i]._s == EMPTY)
{
printf("[%d]->\n", i);
}
else
{
printf("[%d]->D\n", i);
}
}
cout << endl;
}
private:
vector<HashData<K,V>> _tables;
size_t _n = 0;// 存储的关键字的个数
};
}
哈希表的扩容问题:
负载因子:存储关键字个数/空间大小,当负载因子为0.7时就扩容
开散列(链地址法/开链法/拉链法)
首先对关键码集合用散列函数
计算散列地址
,具有
相同地址的关键码归于
同一子集合,每一个子集合称为
一个桶
,各个桶中的元素通过一个
单链表链接
起来
开散列中每个桶中放的都是发生哈希冲突的元素
哈希表(开散列解决哈希冲突)模拟实现unordered_map 与unordered_set
HashTable.h
#pragma once
#include<string>
#include<vector>
#include<iostream>
using namespace std;
template<class K>
struct HashFunc // 哈希函数采用除留余数法,被模的key必须要为整形才可以处理
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct HashFunc<string>//将字符串映射成一个整型 (若key为字符串)
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key)
{
hash *= 31;
hash += e;
}
return hash;
}
};
namespace hash_bucket//开散列/拉链法
{
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{}
};
// 前置声明
template<class K, class T, class Hash, class KeyOfT>
class HashTable;
template<class K, class T, class Ref,class Ptr,class Hash, class KeyOfT>
struct __HTIterator
{
typedef HashNode<T> Node;
typedef __HTIterator<K, T, Ref, Ptr, Hash, KeyOfT> Self;
Node* _node;
const HashTable<K, T, Hash, KeyOfT>* _pht;//权限可以平移/缩小,不能放大
size_t _hashi;
__HTIterator(Node*node, HashTable<K, T, Hash, KeyOfT>* pht, size_t hashi)
:_node(node)
,_pht(pht)
,_hashi(hashi)
{}
//有普通的this( HashTable的this)指针与const修饰的this指针,走更适配的
__HTIterator(Node* node, const HashTable<K, T, Hash, KeyOfT>* pht, size_t hashi)
:_node(node)
, _pht(pht)
, _hashi(hashi)
{}
Self& operator++()
{
if (_node->_next)//当前桶没走完,还有节点,走到下一个节点
{
_node = _node->_next;
}
else//当前桶已走完,找下一个桶的起始节点
{
++_hashi;
while (_hashi < _pht->_tables.size())
{
if (_pht->_tables[_hashi])
{
_node = _pht->_tables[_hashi];
break;
}
++_hashi;
}
if (_hashi == _pht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
};
// unordered_set -> Hashtable<K, K>
// unordered_map -> Hashtable<K, pair<K, V>>
template<class K,class T,class Hash,class KeyOfT>
class HashTable
{
typedef HashNode<T> Node;
template<class K, class T, class Ref, class Ptr, class Hash, class KeyOfT>
friend struct __HTIterator;
public:
typedef __HTIterator<K, T, T&, T*, Hash, KeyOfT> iterator;
typedef __HTIterator<K, T, const T&, const T*, Hash, KeyOfT> const_iterator;
iterator begin()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])
{
return iterator(_tables[i], this, i);
}
}
return end();
}
iterator end()
{
return iterator(nullptr,this,-1);
}
const_iterator begin()const
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])
{
return const_iterator(_tables[i], this, i);
}
}
return end();
}
const_iterator end()const
{
return const_iterator(nullptr,this,-1);
}
HashTable()
{
_tables.resize(10);
}
~HashTable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
pair<iterator, bool> Insert(const T& data)
{
//检查是否已经存在
Hash hf;//将关键字转成/映射成一个整型 (data可以为int,可以为string)
KeyOfT kot;//取出data中的关键字,data若为Key则就是它本身,data若为pair,则取得pair的第一个元素
iterator it = Find(kot(data));
if (it != end())
return make_pair(it, false);
//检查是否需要扩容
// 负载因子最大到1
if (_n ==_tables.size())
{
vector<Node*> newTables;
newTables.resize(_tables.size() * 2, nullptr);
//遍历旧表
for (size_t i = 0; i < _tables.size(); i++)
{
//挪动映射至新表
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hf(kot(cur->_data)) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = hf(kot(data)) % _tables.size();
Node* newnode = new Node(data);
//头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return make_pair(iterator(newnode,this,hashi), true);
}
iterator Find(const K& key)
{
Hash hf;
KeyOfT kot;
size_t hashi = hf(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur,this,hashi);
}
cur = cur->_next;
}
return end();
}
bool Erase(const K& key)
{
Hash hf;
KeyOfT kot;
size_t hashi = hf(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
if (prev == nullptr)//第一个就是要找的
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _tables;
size_t _n = 0;
};
}
MyUnorderedMap.h
#pragma once
#include"HashTable.h"
namespace djx
{
template<class K,class V,class Hash = HashFunc<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)//获取关键字
{
return kv.first;
}
};
public:
typedef typename hash_bucket::HashTable<K, pair<const K, V>, Hash, MapKeyOfT>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
const V& operator[](const K& key) const
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
iterator find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
hash_bucket::HashTable<K, pair<const K, V>, Hash, MapKeyOfT> _ht;
};
}
MyUnorderedSet.h
#pragma once
#include"HashTable.h"
namespace djx
{
template<class K,class Hash = HashFunc<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
// 对类模板取内嵌类型,加typename告诉编译器这里是类型
typedef typename hash_bucket::HashTable<K, K, Hash, SetKeyOfT>::const_iterator iterator;
typedef typename hash_bucket::HashTable<K, K, Hash, SetKeyOfT>::const_iterator const_iterator;
const_iterator begin() const
{
return _ht.begin();
}
const_iterator end() const
{
return _ht.end();
}
pair<const_iterator, bool> insert(const K& key)
{
auto ret = _ht.Insert(key);
return make_pair(const_iterator(ret.first._node, ret.first._pht, ret.first._hashi), ret.second);
}
iterator find(const K& key)
{
auto ret = _ht.Find(key);
return const_iterator(ret._node, ret._pht, ret._hashi);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
hash_bucket::HashTable<K, K, Hash, SetKeyOfT> _ht;
};
}
测试:
void test_set()
{
djx::unordered_set<int> us;
us.insert(4);
us.insert(19);
us.insert(62);
us.insert(3);
djx::unordered_set<int>::iterator it = us.begin();
while (it != us.end())
{
cout << *it << " ";
++it;
}
cout << endl;
auto e = us.find(19);
cout << *e << endl;
us.erase(19);
for (auto e : us)
{
cout << e << " ";
}
cout << endl;
}
void test_map()
{
djx::unordered_map<string, string> dict;
dict.insert(make_pair("sort", ""));
dict.insert(make_pair("string", ""));
dict.insert(make_pair("insert", ""));
for (auto& kv : dict)
{
//kv.first += 'x';不可以修改K
kv.second += 'x';
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
string arr[] = { "苹果","葡萄","葡萄","甜瓜"};
djx::unordered_map<string, int> count_map;
for (auto& e : arr)
{
count_map[e]++;
}
for (auto& kv : count_map)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
count_map.erase("苹果");
}
一些代码设计的细节: