目录
认识哈希表:
哈希冲突:
除留余数法--(常用)
平方取中法--(了解)
折叠法--(了解)
随机数法--(了解)
泛型编程:
闭散列:
线性探测:
二次探测:
扩容:
查找:
插入:
删除:
开散列:
扩容:
查找:
插入:
删除:
迭代器:
全部代码:
认识哈希表:
哈希表是一种数据结构,也称散列表,主要用于查找,且使用很频繁,可见它的效率相比其他用于查找的数据结构,肯定有优势。之前学习的顺序表和平衡二叉搜索树,查找的时间复杂度为O(n)和O(logn),它们两都需要通过key值一一比较不断缩小查找范围,进而查找到所需数据。而哈希表的优势在于无需比较,只需通过某种函数(哈希函数)计算关键码,通过映射关系可直接找到数据,近似O(1)的时间复杂度。
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功
我们将它们放入哈希表key值如何取呢?一般数据是size_t或者能强转成size_t类型的key值直接取该数据,对于无法强转的例如string,则通过哈希函数转换,key值也不是直接当作下标存入哈希表内,不然如果一个数据过大,就要开很大的数组, 一般我们需要将 数据%数组大小来得到对应的下标,这种转换也是哈希函数, 举个例子:数据集合{1,7,6,4,5,9};
哈希冲突:
还是上面的例子,这时我们有一个数据75要进入哈希表,通过哈希函数计算key值,75 % 10 = 5,但是发现下标为5的位置已经有数据,这就出现了不同的数据对应的相同的下标,这就是所谓的哈希冲突,又称哈希碰撞。
注意哈希冲突是不能完全解决的,只能缓解,鸽巢定理可证,所有的字符串,肯定比size_t的最大值要大,因为不限长度字符串几乎是无限的,所以鸽子比巢多,肯定会有巢有两只以上的鸽子。
缓解哈希冲突主要从两个方面:
1.,第一个方面就是对哈希函数入手,上面这个例子的哈希函数,是用key对数组大小取余,这是直接定址法,这种哈希函数出现哈希冲突的概率还是不小的,先来看看哈希函数的设计原则:
哈希函数的定义域必须包括需要存储的全部关键码,而如果哈希表允许有m个地址时,其值
域必须在0到m-1之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单
再来看看几种常见的哈希函数:
除留余数法--(常用)
假设哈希表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
平方取中法--(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
折叠法--(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按哈希表表长,取后几位作为散列地址。
随机数法--(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中
random为随机数函数。
2.第二个方面就是对哈希表的存储结构入手,想必大家见过最多的哈希表结构就是顺序表+链表,其实哈希表也可以单纯用顺序表实现,两种不同的底层结构在于它们如何应对哈希冲突,C++的STL库中使用的是顺序表+链表的方式,没错这种方式的效率是更优的,但是单纯用顺序表的结构也是值得学习的,接下来的内容我会分别介绍并模拟实现这两种哈希表的底层结构。
泛型编程:
在模拟实现中,我的my_unordered_set和my_unordered_map封装了一个哈希表HashTable,但set里面存的是一个数据K,而set里面存的是pair<K,T>,HashTable里面只接受一个data,这就导致了如果是set,data就是K类型,如果是map,data就是pair<K,V>,但我们只有一个哈希表,该怎么解决这种矛盾呢?
仿函数:我们可以分别在set和map里创建一个类,在类里重载运算符(),然后在set中的()重载中直接返回K,在map中的()重载中返回pair中的K,也就是pair中的first,然后将这个类传给HashTable,在HashTable中使用data前就调用这个类的括号来取里面的数据:
set:
map:
在HashTable中的使用:(哈希地址的计算中就用到了)
HashFunc和上面讲的一样,主要作用是如果key为其他不是size_t的类型将它们强转成size_t和若为string则通过哈希函数转化成size_t,这种方法就是泛型编程的一种。
闭散列:
闭散列,又称开放定址法,也就是上面提到的单纯使用顺序表的方法来实现哈希表,它应对哈希冲突的方法是如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,那么如何找到“下一个”空位置呢?
线性探测:
回到最开始的例子,我们需要插入75,通过哈希函数计算下标为5,但下标为5的位置已经被占用。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
所以最后我们能找到8下标位置为空位置,然后进行插入数据。
注意往后探测过程中不能超过数组长度,所以我们每向后走一次就需要%数组长度,以保证当超过数组长度时,下标能回到数组开头。
如何判断这个位置是否为空,只需将每个位置里面存入一个状态值(枚举类型),总共有三个状态:EMPTY,EXIST,DELETE,分别表示空,存在数据,之前有数据但被删除
这部分代码只需理解线性探测思路就行,其他不理解的地方,需结合全部代码。
二次探测:
虽然线性探测能解决哈希冲突,但可以发现这样冲突的数据大部分都聚在了一起,不离散,如图:
为了避免这种情况,线性探测时hashi每次向后走1步,我们采用二次探测,也就是每次向后走i的平方步,每次i++,也就是依次走1,4,9,16,25......步,实现很简单,这里不演示。
扩容:
闭散列的扩容不是满了才扩容,我们先引入一个概念:负载因子,负载因子 = 存在的元素/数组的容量,简单来说负载因子就是占用率,当负载因子>=0.7的时候我们才进行扩容。
扩容思路:
我们可以直接开一个新的hash表,将新表的大小设为旧表的2倍,再将旧表的元素一个个插入到新表,最后用swap函数交换新旧表。
这样写的好处:不必销毁新表,因为新表是局部对象,函数结束后自动销毁了。
查找:
通过key查找某个节点:
先通过key用哈希函数算出对应哈希地址,再从哈希地址开始往后线性探测,找到后返回节点:
插入:
分析一下插入,当插入一个数时该如何做呢?
1.先用查找函数判断能否找到,若找到了,代表原哈希表里有,直接返回false。
2.用负载因子判断是否需要扩容,需要就进行扩容。
3.通过key和哈希函数,算出哈希地址。
4.哈希地址上有值就往后线性探测。
删除:
因为之前,我们在每个节点上都设置了三种状态:EMPTY,EXIST,DELETE,所以现在删除一个数就非常简单了:
只需先通过哈希函数和线性探测找到该节点,再将该节点的状态改为DELETE即可。
开散列:
开散列也就是C++STL库哈希表实现方法,说明它相比闭散列还是有一定的优越性的,开散列应对哈希冲突的方法就是在冲突数据下面用链表进行连接。
扩容:
开散列的扩容条件就是_n == 数组大小的时候:
相比闭散列的扩容方法,开散列只要扩容条件不同,其他差不多,只有旧表中每个桶的数据要依次头插到新表对应的哈希地址。
查找:
如果对应的哈希地址里面有数据,就沿着该地址的链表遍历,找到即可。
插入:
1.先用查找函数判断能否找到,若找到了,代表原哈希表里有,直接返回false。
2.判断是否需要扩容,需要就进行扩容。
3.通过key和哈希函数,算出哈希地址。
4.在该哈希地址的链表处进行头插。
删除:
1.先通过查找函数找到key对应的哈希地址
2.遍历该哈希地址的链表。
3.找到后连接该节点的上一个和下一个节点。
4.注意如果是头删要特殊处理,因为上一个节点为空。
迭代器:
迭代器功能都比较简单,这里我只讲++的思路,其他功能可以到文章最后看全部代码。
++:
1.到一个哈希地址时要先判断存不存在冲突数据,也就是链表。
2.若有冲突数据,直接走向链表的next即可。
3.链表走到尾部,就需要从这个链表的哈希地址开始往后线性探测,直到找到下一个有有效数据的哈希地址或者哈希表走完。
全部代码:
my_ordered_set.h
#include"HashTable.h"template<class K,class Hash = HashFunc<K>>
class my_unordered_map
{struct SetKeyOfT{const K& operator()(const K& key){return key;}};typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::const_iterator const_iterator;public: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);}iterator find(const K& key){return _ht.Find(key);}bool erase(const K& key){return _ht.Erase(key);}private:hash_bucket::HashTable<K,const K,SetKeyOfT,Hash> _ht;
};
my_unordered_map.h
#include"HashTable.h"
template<class K, class T,class Hash = HashFunc<K>>
class my_unordered_set
{struct MapKeyOfT{const K& operator()(const pair<K,T>& kv){return kv.first;}};typedef typename hash_bucket::HashTable<K, pair<K,T>, MapKeyOfT, Hash>::iterator iterator;typedef typename hash_bucket::HashTable<K, pair<K,T>, MapKeyOfT, Hash>::const_iterator const_iterator;public: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);}iterator find(const K& key){return _ht.Find(key);}bool erase(const K& key){return _ht.Erase(key);}T& operator[](const K& key){pair<iterator, bool> ret = _ht.Insert(make_pair(key, T()));return ret->iterator->second;}
private:hash_bucket::HashTable<K, pair<K,T>, MapKeyOfT, Hash> _ht;
};
HashTable.h
#pragma once
#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& key){size_t hash = 0;for (auto ch : key){hash *= 131;hash += ch;}return hash;}
};namespace open_address
{enum State{EMPTY,EXIST,DELETE};template<class K, class V>struct HashData{pair<K, V> _kv;State _state = EMPTY;};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;//_n / _table.size() >= 0.7//不采用这种判断方法是因为左右类型不同,所以将左右都*10if (_n * 10 / _tables.size() >= 7){HashTable<K, V, Hash> newHT;newHT._tables.resize(_tables.size() * 2);// 旧表重新计算负载到新表for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);//交换新旧表}Hash hs;size_t hashi = hs(kv.first) % _tables.size();//hs为仿函数,将key转为size_t,因为kv.first 可能为string // 线性探测while (_tables[hashi]._state == EXIST)//_tables为顺序表{++hashi;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}HashData<K, V>* Find(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size();// 线性探测while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state == 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 == nullptr){return false;}else{ret->_state = DELETE;--_n;return true;}}private:vector<HashData<K, V>> _tables;size_t _n = 0; // 有效数据个数};
}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 KeyOfT,class Hash = HashFunc<T>>class HashTable{typedef HashNode<T> Node;public:template<class Ptr,class Ref>struct _HTIterator//迭代器,设置为内部类{typedef _HTIterator Self;typedef HashNode<T> Node;Node* _node;const HashTable* _pht;_HTIterator(Node* node,const HashTable* pht):_node(node),_pht(pht){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator!=(const Self& s){return _node != s._node;}Self& operator++(){if (_node->next)//当前桶没走完,直接找下一个节点{return _node->next;}else{Hash ha;KeyOfT kot;size_t i = ha(kot(_node->_data)) % _pht->_table.size();i++;for (;i < _pht->_table.size();i++){if (_pht->_table[i])break;}//循环结束后要么找到一个有数据的桶,要么整个表走完了if (i == _table.size())//表走完了{_node = nullptr;}else//找到了有数据的桶{_node = _pht->_table[i];}}return this;}};typedef _HTIterator<T*, T&> iterator;typedef _HTIterator<const T*, const T&> const_iterator;iterator end(){return iterator(nullptr, this);}iterator begin(){size_t i = 0;for (i = 0;i < _table.size();i++){Node* cur = _table[i];if (cur){return iterator(cur, this);}}}const_iterator end() const{return const_terator(nullptr, this);}const_iterator begin() const{size_t i = 0;for (i = 0;i < _table.size();i++){Node* cur = _table[i];if (cur){return const_iterator(cur, this);}}}pair<iterator,bool> Insert(const T& data){HashFunc hs;KeyOfT kot;iterator it = Find(kot(data));if (it!=end())//如果表中原来有{return make_pair(it, false);}if (_n == _table.size())//扩容{vector<Node*> newtable(2 * _table.size(), nullptr);for (int i = 0;i < _table.size();i++){Node* cur = _table[i];while (cur){Node* next = cur->next;size_t hashi = hs(kot(data)) % newtable._table.size();//头插cur->next = newtable[hashi];newtable[hashi] = cur;cur = next;}_table[i] = nullptr;}//新旧表交换_table.swap(newtable._table);}size_t hashi = hs(kot(data)) % _table.size();Node* newnode = new HashNode(data);//头插newnode->next = _table[hashi];_table[hashi] = newnode;_n++;return make_pair(interator(newnode,this),true);}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();}bool Erase(const K& key){Hash hs;KeyOfT kot;size_t hashi = hs(key) % _table.size();Node* prev = nullptr;Node* cur = _table[hashi];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;}private:vector<Node*> _table;size_t _n;};
}