1.哈希概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。搜索的效率取决于搜索过程中元素的比较次数,因此顺序结构中查找的时间复杂度O(N),平衡树中查找的时间复杂度为树的高度O ( l o g N ) 。
而最理想的搜索方法是,可以不经过任何比较,一次直接从表中得到要搜索的元素,即查找的时间复杂度为O(1)。
如果构造一种存储结构,该结构能够通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时就能通过该函数很快找到该元素。
向该结构当中插入和搜索元素的过程如下:
插入元素: 根据待插入元素的关键码,用此函数计算出该元素的存储位置,并将元素存放到此位置。
搜索元素: 对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素进行比较,若关键码相等,则搜索成功。
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
该方式即为哈希(散列)方法, 哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(散列表)。
2.哈希冲突
不同关键字通过相同哈希函数计算出相同的哈希地址,这种现象称为哈希冲突或哈希碰撞。我们把关键码不同而具有相同哈希地址的数据元素称为“同义词”。那么发生哈希冲突该如何处理呢?
3.哈希函数
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
哈希函数的定义域必须包括需要存储的全部关键码
如果散列表允许有m个地址时,其值域必须在0到m-1之间,哈希函数计算出来的地址能均匀分布在整个空间中.
哈希函数应该比较简单
4.常见哈希函数
1. 直接定址法
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B优点:简单、均匀
缺点:需要事先知道关键字的分布情况使用场景:适合查找比较小且连续的情况
2. 除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数(也可以直接取m),按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
5.哈希冲突解决
解决哈希冲突两种常见的方法是:闭散列和开散列
5.1 闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
5.1.1方法一:线性探测
当发生哈希冲突时,从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止。
Hi=(H0+i)%m ( i = 1 , 2 , 3 , . . . ) (i=1,2,3,...)(i=1,2,3,...)
H0:通过哈希函数对元素的关键码进行计算得到的位置。
Hi:冲突元素通过线性探测后得到的存放位置。
m:表的大小。
比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入:通过哈希函数获取待插入元素在哈希表中的位置如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。
删除:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素
思考:哈希表什么情况下进行扩容?如何扩容?
一般使用0.7作为负载因子,如下是线性探测的实现:
pragma once#include<iostream>
#include<vector>
#include<string>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& s){size_t hash = 0;for (auto e : s){hash += e;hash *= 131;}return hash;}
};namespace openaddresss {enum State{EMPTY,EXIST,DELETE //查找过程中遇到空,或者找到了才结束};template<class K, class V>struct HashData{pair<K, V> _data;State _state = EMPTY; //HashData需要存在默认构造函数 HashData(pair<K, V> data = pair<K, V>()):_data(data){}};template<class K, class V, class Hash = HashFunc<K>>class HashTable{typedef HashData<K, V> Node;public:HashTable(){_hsTable.resize(10);_n = 0;}//查找HashData<K, V>* find(const K& key){Hash hs;int len = _hsTable.size();int hashi = hs(key) % len;while (_hsTable[hashi]._state != EMPTY) //查找的时候,如果这个值刚刚被删除,待查找的值可能在后面,此时依旧循环{if (_hsTable[hashi]._state == EXIST && key == _hsTable[hashi]._data.first){return &(_hsTable[hashi]);}hashi++;hashi %= len;}return nullptr;}//插入 如果映射位置冲突, ++hashi.hashi%lenbool insert(pair<K, V> data){Hash hs;K key = data.first;//先查找是否存在,存在即返回falseif (find(key) != nullptr){return false;}//扩容if ((double)_n / (double)_hsTable.size() > 0.7){HashTable<K, V, Hash> newTable;newTable._hsTable.resize(2 * _hsTable.size());//重新映射for (size_t i = 0; i < _hsTable.size(); i++){if (_hsTable[i]._state == EXIST){newTable.insert(_hsTable[i]._data);}}swap(newTable._hsTable, _hsTable);}int len = _hsTable.size();//插入int hashi = hs(key) % len;while (_hsTable[hashi]._state == EXIST){hashi++;hashi %= len;}// 已经在vector中 resize了,直接设置即可,无需new元素插入_hsTable[hashi]._data = data;_hsTable[hashi]._state = EXIST;_n++;return true;}bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){_n--;ret->_state = DELETE;return true;}else{return false;}}private:vector<Node> _hsTable; //hash表size_t _n; //hash表的有效数据个数};
}
线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。
5.1.2 二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题找下一个空位置的方法为:
Hi=(H0+i²)%m ( i = 1 , 2 , 3 , . . . ) (i=1,2,3,...)(i=1,2,3,...)
采用二次探测为产生哈希冲突的数据寻找下一个位置,相比线性探测而言,采用二次探测的哈希表中元素的分布会相对稀疏一些,不容易导致数据堆积。和线性探测一样,采用二次探测也需要关注哈希表的负载因子。
研究表明:当表的长度为质数且表装载因子a不超过0.5时,5.新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
因此,闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
5.2 开散列
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
下面是开散列的实现:
#pragma once#include<iostream>
#include<vector>
#include<string>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& s){size_t hash = 0;for (auto e : s){hash += e;hash *= 131;}return hash;}
};//哈希表的开散列 插入中直接头插
namespace HashBucket
{template<class K, class V>struct HashNode{HashNode<K, V>* _next; pair<K, V> _kv; HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}};template<class K, class V, class Hash = HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public: //增删查改HashTable(){_table.resize(5);}~HashTable(){for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}}Node* find(const K& key){Hash hs;int n = _table.size();//将key 映射进去int hashi = hs(key) % n;Node* cur = _table[hashi];while (cur){if (key == cur->_kv.first){return cur;}cur = cur->_next;}return nullptr;}bool insert(const pair<K, V>& kv){if (find(kv.first))return false;Hash hs;// 负载因子到1就扩容if (_n == _table.size()){//增容时,可以在素数数组中找到下一个素数作为哈希表增容后的大小,使得增容后的值的分布更均匀vector<Node*> newTables(_table.size() * 2, nullptr);for (size_t i = 0; i < _table.size(); i++){// 取出旧表中节点,重新计算挂到新表桶中Node* cur = _table[i];while (cur){Node* next = cur->_next;// 头插到新表size_t hashi = hs(cur->_kv.first) % newTables.size();cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newTables);}size_t hashi = hs(kv.first) % _table.size();Node* newnode = new Node(kv);// 头插newnode->_next = _table[hashi];_table[hashi] = newnode;++_n;return true;}bool Erase(const K& key){Hash hs;size_t hashi = hs(key) % _table.size();Node* prev = nullptr;Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){// 删除if (prev){prev->_next = cur->_next;}else{_table[hashi] = cur->_next;}delete cur;--_n;return true;}prev = cur;cur = cur->_next;}return false;}void Some(){size_t bucketSize = 0;size_t maxBucketLen = 0;size_t sum = 0;double averageBucketLen = 0;for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];if (cur){++bucketSize;}size_t bucketLen = 0;while (cur){++bucketLen;cur = cur->_next;}sum += bucketLen;if (bucketLen > maxBucketLen){maxBucketLen = bucketLen;}}averageBucketLen = (double)sum / (double)bucketSize;printf("load factor:%lf\n", (double)_n / _table.size());printf("all bucketSize:%d\n", _table.size());printf("bucketSize:%d\n", bucketSize);printf("maxBucketLen:%d\n", maxBucketLen);printf("averageBucketLen:%lf\n\n", averageBucketLen);}private:size_t _n = 0; //默认负载因子为1;vector<Node*> _table;};
}
6.用一个哈希表同时封装出unordered_map和unordered_set
哈希表模板参数的控制
unordered_set是K模型的容器,而unordered_map是KV模型的容器。
要想只用一份哈希表代码同时封装出K模型和KV模型的容器,我们必定要对哈希表的模板参数进行控制。为了与原哈希表的模板参数进行区分,这里将哈希表的第二个模板参数的名字改为T。
template<class K, class T>
class HashTable
如果上层使用的是unordered_set容器,那么传入哈希表的模板参数就是key和key。
template<class K>
class unordered_set
{
public://...
private:HashTable<K, K> _ht; //传入底层哈希表的是K和K
};
但如果上层使用的是unordered_map容器,那么传入哈希表的模板参数就是key以及key和value构成的键值对。
template<class K, class V>
class unordered_map
{
public://...
private:HashTable<K, pair<K, V>> _ht; //传入底层哈希表的是K以及K和V构成的键值对
};
更改模板参数后,哈希结点的定义如下:
//哈希结点的定义
template<class T>
struct HashNode
{T _data;HashNode<T>* _next;//构造函数HashNode(const T& data):_data(data), _next(nullptr){}
};
仿函数获取key值
由于我们在哈希结点当中存储的数据类型是T,这个T可能就是一个键值,也可能是一个键值对,对于底层的哈希表来说,它并不知道哈希结点当中存储的数据究竟是什么类型,因此需要由上层容器提供一个仿函数,用于获取T类型数据当中的键值。
因此,unordered_set和unordered_map容器都需要向底层哈希表提供一个仿函数,该仿函数返回键值对当中的键值。
string类型无法取模
template<class K>
struct Hash
{size_t operator()(const K& key) //返回键值key{return key;}
};
//string类型的特化
template<>
struct Hash<string>
{size_t operator()(const string& s) //BKDRHash算法{size_t value = 0;for (auto ch : s){value = value * 131 + ch;}return value;}
};
迭代器的封装
迭代器通常是对Node*的封装,然而我们在迭代器++时候,需要查找下一个桶的地址,因此我们还需要有hash表的初始地址。
template<class K, class T, class KeyOfValue, class Hash >// HashFunc<K>> 默认参数最好在声明中给出,这里map和set给出
class HashTable;// 迭代器
template <class K, class T, class KeyOfValue, class HF>
struct HBIterator
{// hash的迭代器是什么 想想给什么成员可以实现operator* ++ !=// 一个节点的指针,还需要表的初始地址,这样我们就可以算出它是第一个桶typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfValue, HF> HT; //相互引用typedef HBIterator<K, T, KeyOfValue, HF> self;Node* _cur; //链表和树的迭代器都只包含了一个Node *HT* _ht; //hash表需要++,故而还需要当前表的地址 HBIterator(Node* cur, HT* ht):_cur(cur),_ht(ht){}// 运算符的重载T& operator*(){return _cur->_kv;}self& operator++(){if (_cur->_next){_cur = _cur->_next;}KeyOfValue kov;HF hf;//找到下一个桶size_t i = hf(kov(_cur->_kv)) % _ht->_table.size(); //分清楚是表的大小,还是表中数组大小i++; //i要加到下一个,从下一个桶开始查找size_t j = 0;for (j = i; j < _ht->_table.size(); j++){if (_ht->_table[j]){_cur = _ht->_table[j];break;}}if (j == _ht->_table.size()){_cur = nullptr;}return *this;}bool operator!=(const self& s){return s._cur != this->_cur;}};
解决以上几个问题,就可以实现unordered_map和unordered_set.
hashTable.h:
#pragma once// 封装改成可以实现underoredmap 和underoredset的hash表
#include<iostream>
#include<vector>
#include<string>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& s){size_t hash = 0;for (auto e : s){hash += e;hash *= 131;}return hash;}
};//哈希表的开散列
namespace HashBucket
{// HashNode存储T, 哈希表存的是key还是map是未知的template<class T>struct HashNode{HashNode<T>* _next =nullptr; //需要指向下一个节点的指针T _kv; //需要存储值HashNode(const T & kv):_kv(kv){}};// 在迭代器中需要用到HashTable本身,相互引用加前置声明 template<class K, class T, class KeyOfValue, class Hash >// HashFunc<K>> 默认参数最好在声明中给出,这里map和set给出class HashTable;// 迭代器 template <class K, class T, class KeyOfValue, class HF>struct HBIterator{// hash的迭代器是什么 想想给什么成员可以实现operator* ++ !=// 一个节点的指针,还需要表的初始地址,这样我们就可以算出它是第一个桶typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfValue, HF> HT; //相互引用typedef HBIterator<K, T, KeyOfValue, HF> self;Node* _cur; //链表和树的迭代器都只包含了一个Node *HT* _ht; //hash表需要++,故而还需要当前表的地址 HBIterator(Node* cur, HT* ht):_cur(cur),_ht(ht){}// 运算符的重载T& operator*(){return _cur->_kv;}self& operator++(){if (_cur->_next){_cur = _cur->_next;}KeyOfValue kov;HF hf;//找到下一个桶size_t i = hf(kov(_cur->_kv)) % _ht->_table.size(); //分清楚是表的大小,还是表中数组大小i++; //i要加到下一个size_t j = 0;for (j = i; j < _ht->_table.size(); j++){if (_ht->_table[j]){_cur = _ht->_table[j];break;}}if (j == _ht->_table.size()){_cur = nullptr;}return *this;}bool operator!=(const self& s){return s._cur != this->_cur;}};//template<class K,class T, class KeyOfValue, class Hash>class HashTable{public: //定义友元类 记住它的类型和定义方式template <class K, class T, class KeyOfValue, class HF>friend struct HBIterator;typedef HBIterator<K, T, KeyOfValue, Hash> iterator;private:typedef HashNode<T> Node;public: //增删查改HashTable(){_table.resize(5);}~HashTable(){for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}}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); //用空指针和this对象来构造迭代器}size_t size() const{return _n; //返回hash表中多少元素,与vector中不同}iterator find(const K& key){KeyOfValue kov;Hash hs;int n = _table.size();//将key 映射进去int hashi = hs(key) % n;Node* cur = _table[hashi];while (cur){if (key == kov(cur->_kv)){return iterator(cur, this);}cur = cur->_next;}return iterator(nullptr, this);}pair<iterator, bool> insert(const T& kv){KeyOfValue kov;Node* tmp = find(kov(kv))._cur;if (tmp){return make_pair(iterator(tmp,this),false);}Hash hs;// 负载因子到1就扩容if (_n == _table.size()){vector<Node*> newTables(_table.size() * 2, nullptr);for (size_t i = 0; i < _table.size(); i++){// 取出旧表中节点,重新计算挂到新表桶中Node* cur = _table[i]; //旧表中每个节点的头while (cur){Node* next = cur->_next;// 头插到新表size_t hashi = hs(kov(cur->_kv)) % newTables.size();cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newTables);}size_t hashi = hs(kov(kv)) % _table.size();Node* newnode = new Node(kv);// 头插newnode->_next = _table[hashi];_table[hashi] = newnode;++_n;return make_pair(iterator(newnode, this), true);}iterator Erase(const K& key){Hash hs;KeyOfValue kov;size_t hashi = hs(key) % _table.size();Node* prev = nullptr;Node* cur = _table[hashi];while (cur){if (kov(cur->_kv) == key){iterator ret = iterator(cur->_next, this);// 删除if (prev){prev->_next = cur->_next;}else{_table[hashi] = cur->_next;}delete cur;--_n;return ret;}prev = cur;cur = cur->_next;}return end();}void Some(){size_t bucketSize = 0;size_t maxBucketLen = 0;size_t sum = 0;double averageBucketLen = 0;for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];if (cur){++bucketSize;}size_t bucketLen = 0;while (cur){++bucketLen;cur = cur->_next;}sum += bucketLen;if (bucketLen > maxBucketLen){maxBucketLen = bucketLen;}}averageBucketLen = (double)sum / (double)bucketSize;printf("load factor:%lf\n", (double)_n / _table.size());printf("all bucketSize:%d\n", _table.size());printf("bucketSize:%d\n", bucketSize);printf("maxBucketLen:%d\n", maxBucketLen);printf("averageBucketLen:%lf\n\n", averageBucketLen);}private:size_t _n = 0; //默认负载因子为1;vector<Node*> _table;};}
unordered_map.h:
#pragma once#include"HashTable.h"namespace xwy
{// unordered_map中存储的是pair<K, V>的键值对,K为key的类型,V为value的类型,HF哈希函数类型// unordered_map在实现时,只需将hashtable中的接口重新封装即可template<class K, class V, class HF = HashFunc<K>> //缺省函数 此处给出最好class unordered_map{// 通过key获取value的操作struct KeyOfValue{const K& operator()(const pair<K, V>& data){return data.first;}};public:typedef typename HashBucket::HashTable<K,pair<K,V>,KeyOfValue,HF>::iterator iterator;iterator begin() { return _ht.begin(); }iterator end() { return _ht.end(); }// capacitysize_t size()const { return _ht.size(); }bool empty()const { return _ht.size() == 0; }// AcessV& operator[](const K& key){pair<iterator, bool> ret = insert(make_pair(key, V()));iterator it = ret.first;return (*it).second;}const V& operator[](const K& key)const{pair<iterator, bool> ret= insert(make_pair(key, V()));iterator it = ret.first;return (*it).second;}iterator find(const K& key) { return _ht.find(key); }size_t count(const K& key) { if (find(key))return 1;return 0; } pair<iterator, bool> insert(const pair<const K, V>& value){return _ht.insert(value);}iterator erase(iterator position){K key = (*position).first;return _ht.Erase(key);}private:HashBucket::HashTable<K, pair<K, V>, KeyOfValue, HF> _ht; //关键是成员变量是什么,怎么写,std命令空间已经展开};void test(){unordered_map<int, int> m;m.insert({ 1,1 });m.insert({ 3,3 });m.insert({ 2,2 });auto it = m.begin();cout << endl;cout << "迭代器打印:";while (it != m.end()){cout << (*it).second << " ";++it;}cout << endl;cout << "[]打印:";for (size_t i = 1; i <=m.size(); i++){cout << m[i] << endl;}}
}
unorderedset.h:
#pragma once#include"HashTable.h"namespace xwy
{template<class K, class HF = HashFunc<K>> //缺省函数 此处给出最好class unordered_set{// 通过key获取value的操作struct KeyOfValue{const K& operator()(const K& data){return data;}};public:typedef typename HashBucket::HashTable<K,K, KeyOfValue, HF>::iterator iterator;iterator begin() { return _ht.begin(); }iterator end() { return _ht.end(); }// capacitysize_t size()const { return _ht.size(); }bool empty()const { return _ht.size() == 0; }iterator find(const K& key) { return _ht.find(key); }size_t count(const K& key){if (find(key))return 1;return 0;}pair<iterator, bool> insert(const K& value){return _ht.insert(value);}iterator erase(iterator position){K key = *position;return _ht.Erase(key);}private:HashBucket::HashTable<K, K, KeyOfValue, HF> _ht; //关键是成员变量是什么,怎么写,std命令空间已经展开};void test2(){unordered_set<int> m;m.insert(1);m.insert(3);m.insert(2);auto it = m.begin();cout << endl;while (it != m.end()){cout << *it << " ";++it;}cout << endl;//cout << "删除第一个元素后:";auto eit = m.find(1);m.erase(eit);it = m.begin();while (it != m.end()){cout << *it << " ";++it;}}
}