目录
假如理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(哈希)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
计算方法:关键码(数据)%数组的容量的元素个数(除留余数法)
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比 较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表 (Hash Table)(或者称散列表)
插入579 ,查询任意元素的时间复杂度都是O(1)
当插入1,11,21,31,41,按计算方法取模的结果都是1 ,不同关键码通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞
1. 直接定制法:取关键字的某个线性函数为散列地址:Hash(Key)= 直接把关键码转化哈希地址 优点:简单、均匀缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况
2. 除留余数法:设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函 数:Hash(key) = key% p(p<=m)m是容量(通常直接使用m就行),将关键码转换成哈希地址
解决哈希冲突两种常见的方法是:闭散列和开散列
3.1闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去(思想)
3.1.线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止(实际方法)
- size_t start = hash(kv.first) % _tables.size();
- size_t i = 0;
- size_t index = start;
- // 线性探测
- while (_tables[index]._status == EXIST)
- {
- i++;
- index =start+ i;
- index %= _tables.size();
- }
3.2.二次探测 线性探测的缺陷是产生冲突的数据堆积在一块(效率会变低),这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测是为了避免该问题;不在一次一次加了,加二次方,如果算出来的哈希地址大于容量,就取模就好像又走了一遍。
- size_t start = hash(kv.first) % _tables.size();
- size_t i = 0;
- size_t index = start;
- // 二次探测
- while (_tables[index]._status == EXIST)
- {
- i++;
- index =start + i * i;
- index %= _tables.size();
- }
3.3荷载因子
哈希表什么情况下进行扩容?如何扩容?
哈希表的荷载因子a: a=插入的元素/哈希表的容量;这个值a需限制在0.7-0.8之下,达到就扩容;如果不扩容哈希碰撞的可能性就非常大,效率下降。
- //状态,防止元素被删除后哈希表查询不到一些位置
- enum Status
- {
- EXIST,
- EMPTY,
- DELETE,
- };
-
- template<class T, class V>
- struct HashData
- {
- pair
_kv; - Status _status = EMPTY;
- };
-
- //仿函数,字符串需要特殊处理
- template<class K>
- struct Hash
- // 特化
- template<>
- struct Hash
-
- template<class T, class V,class Func=Hash
> - class HashTable
- {
- public:
- //接口函数
- bool Erase(const T& key)
- HashData
* Find(const T& key) - bool Insert(const pair
& kv) -
- private:
- vector
> _tables; - //插入的元素个数
- size_t _n=0;
- };
4.1.查询
- HashData
* Find(const T& key) - {
- if (_tables.size() == 0)
- {
- return nullptr;
- }
- //可以先不管这个仿函数,就是取里面的值
- Func hash;
- size_t start = hash(key)% _tables.size();
- size_t i = 0;
- size_t index = start;
- while (_tables[index]._status != EMPTY)
- {
- if (_tables[index]._kv.first == key && _tables[index]._status == EXIST)
- {
- return &_tables[index];
- }
- i++;
- index =start + i * i;
- index %= _tables.size();
- }
- return nullptr;
- }
4.2删除
- bool Erase(const T& key)
- {
- HashData
* ret = Find(key); - if (ret == nullptr)
- {
- return false;
- }
- else
- {
- --_n;
- ret->_status = DELETE;
- return true;
- }
- }
- bool Insert(const pair
& kv) - {
- HashData
* ret = Find(kv.first); - if (ret)
- {
- return false;
- }
- // 荷载因子到0.7,就扩容
- // 荷载因子越小,冲突概率越低,效率越高,空间浪费越多
- // 荷载因子越大,冲突概率越高,效率越低,空间浪费越少
- if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
- {
- //扩容
- size_t newHashSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
- HashTable
newHT; - newHT._tables.resize(newHashSize);
- for (size_t i = 0; i < _tables.size(); i++)
- {
- if (_tables[i]._status == EXIST)
- {
- newHT.Insert(_tables[i]._kv);
- }
- }
- _tables.swap(newHT._tables);
- }
-
- Func hash;
- size_t start = hash(kv.first) % _tables.size();
- size_t i = 0;
- size_t index = start;
- // 线性探测 or 二次探测
- while (_tables[index]._status == EXIST)
- {
- i++;
- index =start + i * i;
- //index =start + i;
- index %= _tables.size();
- }
- _tables[index]._kv = kv;
- _tables[index]._status = EXIST;
- ++_n;
- return true;
- }
4.3.1.荷载因子为0.7了,扩容会导致容量变大,取模:key/哈希表容量,那么这个哈希表的映射关系不符合了;只能全部重新插入了
4.3.2字符串需要怎样插入了?
4.3.2.1仿函数及string的特化
- //仿函数
- template<class K>
- struct Hash
- {
- size_t operator()(const K& key)
- {
- return key;
- }
- };
- // 特化
- template<>
- struct Hash
- {
- size_t operator()(const string& s)
- {
- size_t value = 0;
- for (auto e : s)
- {
- // BKDR,减少哈希冲突
- value *= 31;
- value += e;
- }
- return value;
- }
- };