哈希
unordered系列关联式容器
unordered_set
和set接口和功能几乎一样。
unordered_map
1.unordered_map是储存<key,value>键值对的关联式容器,允许通过key快速索引与其对应的value。 2.在unordered_map中,键值用于唯一的标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。 3.在内部,unordered_map没有对<key,value>按照特定的舒徐排序,为了在常熟范围内找到key所对应的value,unordered_map将相同的键值放在了相同的桶中。 4.unordered_map容器通过key访问单个元素比map块,但它在遍历元素自己的范围迭代方面效率低。 5.unordered_map实现了直接访问操作符(operator[])。它允许key作为参数直接访问value。 6.它的迭代器至少是向前迭代器。
unordered_map接口
1.unordered_map构造
函数声明 | 功能介绍 |
---|---|
unordered_map | 构造不同格式的unordered_map对象 |
2.unordered_map容量
函数声明 | 功能 |
---|---|
bool empty()const | 检测为空 |
size_t size()const | 获取有效元素的个数 |
3.unordered_map迭代器
函数声明 | 功能 |
---|---|
begin | 返回第一个元素的迭代器 |
end | 返回最后一个元素的下一个位置的迭代器 |
cbegin | const begin()const |
cend | const end()const |
4.unordered_map的元素访问
函数声明 | 功能 |
---|---|
operator[] | 返回key对应的value,没有固定的默认值(取决于key) |
*注意:该函数实际上调用哈希桶的插入操作,参数key与v()构造一个默认值往底层哈希桶插入 | |
插入成功,返回v() 失败返回key对应的value的值* |
5.unordered_map查询
函数声明 | 功能 |
---|---|
iterator find(const K& key) | 返回key在哈希桶中的位置 |
size_t count(const K& key) | 返回哈希桶中有关key的键值对的个数 |
unordered_map count返回的最大值为1,原因:不能重复 |
6.unordered_map的修改
函数声明 | 功能 |
---|---|
insert | 插入键值对 |
erase | 删除键值对 |
void clear() | 清空有效元素的个数 |
void swap(unordered_map&) | 交换两个容器中的元素(指针交换) |
7.unordered_map操作
函数声明 | 功能 |
---|---|
size_t bucket count()const | 返回哈希桶中的总个数 |
size_t bucket size(size_t n)const | 返回n号桶有效元素的总个数 |
size_tbucket(const K& key) | 返回元素key所在的桶号 |
比较map和unordered_map的效率
#include<iostream>
#include<vector>
#include<algorithm>
#include<unordered_map>
#include<map>
#include<time.h>
using namespace std;
void test_map()
{
const int n = 100000;
srand(time(0));
vector<int> v;
v.reserve(n);
for (int i = 0; i < n; i++)
{
v.push_back(rand()+i);//伪随机只有3.5w左右
}
//测试插入的速度
map<int, int> p;
unordered_map<int, int> up;
clock_t start1 = clock();
for (auto e : v)
{
p.insert(make_pair(e, e));
}
clock_t end1 = clock();
clock_t start2 = clock();
for (auto e : v)
{
up.insert(make_pair(e, e));
}
clock_t end2 = clock();
//查找速度
clock_t start3 = clock();
for (auto e : v)
{
p.find(e);
}
clock_t end3 = clock();
clock_t start4 = clock();
for (auto e : v)
{
up.find(e);
}
clock_t end4 = clock();
//删除速度
clock_t start5 = clock();
for (auto e : v)
{
p.erase(e);
}
clock_t end5 = clock();
clock_t start6 = clock();
for (auto e : v)
{
up.erase(e);
}
clock_t end6 = clock();
cout << "插入速度:" << endl << "map:" << end1 - start1 << endl << "unordered_map:" << end2 - start2 << endl;
cout << "find速度:" << endl << "map:" << end3 - start3 << endl << "unordered_map:" << end4 - start4 << endl;
cout << "erase速度:" << endl << "map:" << end5 - start5 << endl << "unordered_map:" << end6 - start6 << endl;
}
int main()
{
test_map();
return 0;
}
底层结构
unordered系列之所以效率比较高,是因为底层使用了哈希结构。
哈希概念
顺序结构及平衡树中,元素关键码与其存储位置间没有对应关系,因此,在查找一个元素时,必须多次经过关键码的比较。顺序查找的时间复杂度为O(N).平衡树中为树的高度,即O(logN),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到的要搜索的元素。 如果构造一种存储结构,通过某种函数(hashfunc)使元素的存储位置与他的该案件吗之间建立一一映射关系,那么在查找时通过该函数可以很快找到该元素。
该结构中:
插入元素 根据待插入元素的关键码,以此函数计算该元素的储存位置并按此位置进行存放。
存放元素 对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
该方式为哈希(散列)方法,哈希方法中使用的函数转化称为哈希(散列)函数,构造出来的结构称为哈希表(散列表)
int a[] = {1,7,4,6,5,9}; hashi(key)= key % capicity;
用这种方法,几乎不用做多次关键码比较,搜索速度特别快。 但是我要insert(17)?,会出现哈希冲突的问题。
哈希冲突
概念
不同关键字通过相同的哈希数计算相同的哈希地址,这种现象称为哈希冲突。 把具有相同哈希地址的数据元素称为”同义词“
哈希函数
能够触发哈希冲突,可能是哈希函数设计不够合理。 设计原则 1.哈希函数的定义域必须包括需要储存的全部关键码,而如果散列表允许有m个地址,其值域必须到[0,m-1]之间 2.哈希函数算出来的地址能均匀的分布在整个空间中。 3.哈希函数应该比较简单。
常见哈希函数 1.直接定址法 取关键字的某个线性函数为散列地址:Hash(key) = A*key+B 优点:简单,均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况
2.除留余数法 设散列表中允许的地址数为m,取一个不大于m,但接近或者等于m的质数作为除数,按照哈希函数:Hash(key) = key % p( p <= m ),将关键码换成哈希函数。
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。
哈希冲突的解决
闭散列 和 开散列
闭散列
闭散列:开放地址法,发生哈希冲突的时候,如果哈希表未装满,说明在哈希表中还有空位置,那么可以把key存放到冲突位置的”下一个“空位置中去。
线性探测 从发生冲突的位置开始,一次向后探测,直到寻找到下一个空位置为止。
思考: 哈希表啥时候可以进行扩容? 荷载因子: a = 填入表中的元素/散列表的长度 a在[0.7,0.8]为佳。
线性探测 | 特性 |
---|---|
优点 | 实现非常简单 |
缺点 | 一旦发生哈希冲突,却数据量较大,容易引发”堵塞“,导致搜索效率降低 |
二次探测 为避免数据过于集中导致的”堵塞“,使用二次探测可以避免该问题。 在线性探测中i = 1,在二次探测中i的值可以随机
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次,因此只要表中有一半的空位置,既不会存在表装满的问题。在搜索时可以不考虑表装满的情况,但插入时必须保证a<=0.5
开散列的最大缺陷就是空间利用率比较低。这也是哈希缺陷 相关代码: 初版,能用但不是目前最优秀的表
#include<iostream> #include<vector> #include<string> using namespace std; enum State { EMPTY, EXSIST, DELETE }; template< class K,class V> struct HashData { pair<K, V> _kv; State _state = EMPTY;//获取某个桶的状态,给个缺省值。 }; //仿函数,完成对内置类型和string的支持 template<class K> struct DefaultHash { size_t operator()(const K& key) { return (size_t)key; } }; //模板特化,支持string template<> struct DefaultHash<string> { //使用BKDR算法 size_t operator()(const string& key) { size_t hash = 0; for (auto ch : key) { hash = hash * 131 + ch; } return hash; } }; template<class K,class V,class Hashfunc = DefaultHash<K>> class HashTable { typedef HashData<K, V> Data; public: bool insert(const pair<K, V>& kv) { //hash的扩容必须处理,一般情况下,hash是绝对不会被存满的。 //就像二叉搜索树一样,欸一个关键字因子 // Hashfunc hf; //去重 if (find(kv.first))//存在就不要再插入了。 return false; //首先判读size()是不是为0? //判断关键字因子是不是超过了70% if (_table.size() == 0 || _n * 10 / _table.size() >= 7)//这里次序不能颠倒,否则会报错 { size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2; //一旦更新,个元素相对于hashtable的位置就会出错,需要单独调整 //现代写法:通过船舰一个新的hashtable,通过调用insert函数,此时不会走if语句 //编译器跑完以后,交换指针即可。 HashTable<K, V> hashtable; hashtable._table.resize(newsize); // for (auto& e : _table) { if(e._state == EXSIST) hashtable.insert(e._kv); } hashtable._table.swap(_table);//交换指针,因为hashtable是临时对象,生命周期结束后,会自己析构。 } size_t starti = hf(kv.first);//获取key //将key%=size()获取映射在hashtable中的位置 starti %= _table.size(); size_t hashi = starti; //二次查找、扫描 int i = 1;//采取的是闭散列法,挪到下一个位置上 while (_table[hashi]._state == EXSIST)//当桶的状态为EXIST时,不能存放数据没。 { hashi = hashi + i; } //赋值 _table[hashi]._kv = kv; //更改状态 _table[hashi]._state = EXSIST; _n++; return true; } Data* find(const K& key)//找到返回指针,找不到返回nullptr { if (_table.size() == 0) { return nullptr; } //为空的时候就停止,empty和delete继续 //闭散列的存储方法,所找的数据都应该在[hashi,_table.end()]内,为空就结束了 Hashfunc hf; size_t starti = hf(key); starti %= _table.size(); size_t hashi = starti; int i = 1; while (_table[hashi]._state != EMPTY) { if (_table[hashi]._state == EXSIST && hf(_table[hashi]._kv.first) == hf(key)) //删除是伪删除法,本质是数据的覆盖,所以要state 和 值双重排定 { return &_table[hashi]; } hashi = hashi + i; hashi %= _table.size();//防止hashi越界。 } return nullptr; } bool erase(const K& key) { //为空,就不要删除了 if (_table.size() == 0) return false; //找到元素 Data* ret = find(key); if (ret)//存在 { ret->_state = DELETE; _n--;//关键字因子个数要处理,因为exist变化了。 return true; } else { return false; } } private: vector<Data> _table; size_t _n = 0; //关键字因子的个数 };
开散列
概念
开散列法又叫做链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址空间的关键码归于同一自己和,每个子集合成为一个桶,各个桶中的元素通过一个单链表连起来,各链表的头节点储存在哈希表中。
开散列增容:
桶数量是一定数量的,随着新元素的插入,每个桶中的元素不断增多,极端情况下,可能导致一个同种链表节点非常多,会影响哈希表的性能,所以条件合理的情况下,要对哈希表进行扩容。
扩容条件:每个哈希桶中刚好挂一个节点,继续插入元素时,每一次都会发生哈希冲突。因此,在元素个数刚好等于桶的个数时,可以进行扩容。
相关代码:
#include<iostream>
#include<vector>
#include<string>
using namespace std;
enum State
{
EMPTY,
EXSIST,
DELETE
};
template< class K,class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;//获取某个桶的状态,给个缺省值。
};
//仿函数,完成对内置类型和string的支持
template<class K>
struct DefaultHash
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//模板特化,支持string
template<>
struct DefaultHash<string>
{
//使用BKDR算法
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto ch : key)
{
hash = hash * 131 + ch;
}
return hash;
}
};
template<class K,class V,class Hashfunc = DefaultHash<K>>
class HashTable
{
typedef HashData<K, V> Data;
public:
bool insert(const pair<K, V>& kv)
{
//hash的扩容必须处理,一般情况下,hash是绝对不会被存满的。
//就像二叉搜索树一样,欸一个关键字因子
//
Hashfunc hf;
//去重
if (find(kv.first))//存在就不要再插入了。
return false;
//首先判读size()是不是为0?
//判断关键字因子是不是超过了70%
if (_table.size() == 0 || _n * 10 / _table.size() >= 7)//这里次序不能颠倒,否则会报错
{
size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
//一旦更新,个元素相对于hashtable的位置就会出错,需要单独调整
//现代写法:通过船舰一个新的hashtable,通过调用insert函数,此时不会走if语句
//编译器跑完以后,交换指针即可。
HashTable<K, V> hashtable;
hashtable._table.resize(newsize);
//
for (auto& e : _table)
{
if(e._state == EXSIST)
hashtable.insert(e._kv);
}
hashtable._table.swap(_table);//交换指针,因为hashtable是临时对象,生命周期结束后,会自己析构。
}
size_t starti = hf(kv.first);//获取key
//将key%=size()获取映射在hashtable中的位置
starti %= _table.size();
size_t hashi = starti;
//二次查找、扫描
int i = 1;//采取的是闭散列法,挪到下一个位置上
while (_table[hashi]._state == EXSIST)//当桶的状态为EXIST时,不能存放数据没。
{
hashi = hashi + i;
}
//赋值
_table[hashi]._kv = kv;
//更改状态
_table[hashi]._state = EXSIST;
_n++;
return true;
}
Data* find(const K& key)//找到返回指针,找不到返回nullptr
{
if (_table.size() == 0)
{
return nullptr;
}
//为空的时候就停止,empty和delete继续
//闭散列的存储方法,所找的数据都应该在[hashi,_table.end()]内,为空就结束了
Hashfunc hf;
size_t starti = hf(key);
starti %= _table.size();
size_t hashi = starti;
int i = 1;
while (_table[hashi]._state != EMPTY)
{
if (_table[hashi]._state == EXSIST && hf(_table[hashi]._kv.first) == hf(key))
//删除是伪删除法,本质是数据的覆盖,所以要state 和 值双重排定
{
return &_table[hashi];
}
hashi = hashi + i;
hashi %= _table.size();//防止hashi越界。
}
return nullptr;
}
bool erase(const K& key)
{
//为空,就不要删除了
if (_table.size() == 0)
return false;
//找到元素
Data* ret = find(key);
if (ret)//存在
{
ret->_state = DELETE;
_n--;//关键字因子个数要处理,因为exist变化了。
return true;
}
else
{
return false;
}
}
private:
vector<Data> _table;
size_t _n = 0; //关键字因子的个数
};
namespace Bucket
{
template<class K,class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
//构造函数
HashNode(const pair<K, V>& kv)
:_kv(kv)
,_next(nullptr)
{}
};
template<class K>
struct DefaultHash
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct DefaultHash<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto ch : key)
{
hash += ch * 131;
}
return hash;
}
};
template<class K,class V>
class HashTable
{
typedef HashNode<K, V> Node;
public:
~HashTable()
{
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
}
}
bool insert(const pair<K, V>& kv)
{
if (find(kv.first))
return false;
//扩容否?
if (_n == _table.size())
{
size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
//重新映射
HashTable<K, V> newHT;
newHT.resize(newSize,nullptr);
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
newHT.insert(cur->_kv);
cur = cur->_next;
}
}
newHT._table.swap(_table);//需要析构函数
//原因:vector自己调用析构函数,但是Node节点却不会自己析构,因为其是内置类型的。
}
size_t starti = kv.first;
starti %= _table.size();
size_t hashi = starti;
Node* newnode = new Node(kv);
//头插法链接
newnode->_next = _table[hashi];
_table[hashi] = newnode;
_n++;
}
Node* find(const K& key)
{
if (_table.size() == 0)
{
return nullptr;
}
size_t starti = key;
size_t hashi = starti %= _table.size();
Node* cur = _table[hashi];//找到key所在的哪个桶
while (cur)
{
if (cur->_kv.first == key)
{
return true;
}
cur = cur->_next;
}
return false;
}
bool erase(const K& key)
{
if (_table.size() == 0)
return false;
size_t starti = key;
size_t hashi = starti %= _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (prev == nullptr)//头节点
{
_table[hashi] = cur->_next;
delete cur;
return true;
}
else//非头结点
{
prev->_next = cur->_next;
delete cur;
return true;
}
//更新
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _table;
size_t _n = 0;
};
}
哈希的模拟实现
类似于map和set的封装 template<class K,class T,class KeyOfT(获取key),class hashFunc(获取hashi,映射)>
hash.h
#pragma once
#include<iostream>
#include<vector>
#include<string>
using namespace std;
template<class K>
struct DefaultHash
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct DefaultHash<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto ch : key)
{
hash = hash*131 + ch;
}
return hash;
}
};
namespace Bucket
{
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
//构造函数
HashNode(const T& data)
:_data(data)
, _next(nullptr)
{}
};
//set
//template<class K,class K,class KeyOfT,class hashfunc>
/*map
template<class K,class V,class KeyOfT,class hashFunc>*/
//必须前置声明HashTable
template<class K, class T, class KeyOfT, class hashFunc>
class HashTable;
template<class K, class T, class KeyOfT, class hashFunc>
class __HTIterator
{
typedef HashNode<T> Node;
typedef __HTIterator<K, T, KeyOfT, hashFunc> Self;
public:
Node* _node;//迭代器返回的是节点
HashTable<K, T, KeyOfT, hashFunc>* _pht;//表的指针
//进行迭代器的处理
__HTIterator(Node* node, HashTable<K, T, KeyOfT, hashFunc>* pht)
:_node(node)
, _pht(pht)
{}
//前置++
Self& operator++()
{
if (_node->_next)//下一个不为空
{
_node = _node->_next;
}
else//为空
{
//确定_node的位置
KeyOfT kot;
hashFunc hf;
size_t hashi = hf(kot(_node->_data));//转成key->hashi
hashi %= _pht->_table.size();//找到映射位置
++hashi;
for (; hashi < _pht->_table.size(); hashi++)
{
if (_pht->_table[hashi])//找到
{
_node = _pht->_table[hashi];
break;
}
}
if (hashi == _pht->_table.size())//没找到
{
_node = nullptr;
}
}
return *this;
}
Self operator++(int)
{
Self tmp(*this);
++(this);
return tmp;
}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
bool operator==(const Self& s)const
{
return _node == s._node;
}
bool operator!=(const Self& s)const
{
return _node != s._node;
}
};
template<class K, class T, class KeyOfT, class hashFunc>
class HashTable
{
template<class K, class T, class KeyOfT, class hashFunc>
friend class __HTIterator;
typedef HashNode<T> Node;
public:
typedef __HTIterator<K, T, KeyOfT, hashFunc> iterator;
//iterator 有两个参数 _node _pht
iterator begin()
{
//找到第一个元素
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i])
{
return iterator(_table[i], this);//单参数才支持隐士类型的转化
}
}
//找不到
return end();
}
iterator end()
{
return iterator(nullptr, this);//单参数才支持隐士类型的转化
}
public:
~HashTable()
{
size_t i = 0;
for (; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
bool Insert(const T& data)
{
KeyOfT kot;//用于获取key的值
hashFunc hf;//哈希函数,用于获取对应的hash
if (Find(kot(data)))
return false;
//扩容否?
if (_n == _table.size())
{
size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
//重新映射
/*HashTable<K, V> newHT;
newHT.resize(newSize,nullptr);*/
vector<Node*> newHT;
newHT.resize(newSize, nullptr);
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
/*newHT.Insert(cur->_kv);
cur = cur->_next;*/
//优化写法,获取原表的节点,直接链接到newHT
Node* next = cur->_next;//手动记录下一个cur的下一个节点
size_t starti = hf(kot(cur->_data));//获取到key以后,再获取key的映射hash
size_t hashi = starti %= newSize;//获取映射的hashi
//链接,头插法。
cur->_next = newHT[hashi];
newHT[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
newHT.swap(_table);//需要析构函数
//原因:vector自己调用析构函数,但是Node节点却不会自己析构,因为其是内置类型的。
}
size_t starti = kot(data);
starti %= _table.size();
size_t hashi = hf(starti);
Node* newnode = new Node(data);
//头插法链接
newnode->_next = _table[hashi];
_table[hashi] = newnode;
_n++;
return true;
}
Node* Find(const K& key)
{
hashFunc hf;
KeyOfT kot;
if (_table.size() == 0)
{
return nullptr;
}
size_t starti = hf(key);
size_t hashi = starti %= _table.size();
Node* cur = _table[hashi];//找到key所在的哪个桶
while (cur)
{
if (kot(cur->_data) == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
hashFunc hf;
if (_table.size() == 0)
return false;
size_t starti = hf(key);//获取hash,传递的是否类型是不知道的
size_t hashi = starti %= _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (prev == nullptr)//头节点
{
_table[hashi] = cur->_next;
delete cur;
return true;
}
else//非头结点
{
prev->_next = cur->_next;
delete cur;
return true;
}
//更新
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _table;
size_t _n = 0;
};
}
unordered_set
#include"Hash.h"
namespace bit
{
template<class K,class hashFunc = DefaultHash<K>>
class set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename Bucket::HashTable<K, K, SetKeyOfT, hashFunc>::iterator iterator;//去类里面的内嵌类型,需要typename
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
bool insert(const K& key)
{
return _ht.Insert(key);
}
private:
Bucket::HashTable<K, K,SetKeyOfT,hashFunc> _ht;
};
}
unordered_map
#include"Hash.h"
namespace bit
{
template<class K,class V,class hashFunc = DefaultHash<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename Bucket::HashTable<K, pair<K, V>, MapKeyOfT, hashFunc>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
_ht.end();
}
bool insert(const pair<K,V>& kv)
{
_ht.Insert(kv);
}
private:
Bucket::HashTable<K, pair<K,V>, MapKeyOfT> _ht;
};
}