1 概述
开放定址法也叫闭散列,是解决哈希冲突的一种方法,当发生哈希冲突之后,如果哈希表没有被装满(正常情况哈希表不会被装满的),那就向后移动,寻找一个没有元素的地址,然后插入。下面介绍移动寻找合适地址的方法:线性探测。
2 线性探测
线性探测: 当发生哈希冲突时,依次向后探测,直到寻找到下一个空位置为止。
2.1 插入
通过代码来实现插入:
- 首先定义哈希表单个数据的结构体:
enum State { EMPTY, EXIST, DELETE }; template<class K, class V> struct Hash { pair<K, V> _kv; State _state = EMPTY; };
- 引入一个状态值的含义是,当删除一个元素以后,如果没有状态值,无法赋予被删除元素的地址处一个此处被删除的标识(赋予0,-1,都无法合理的标识此处被删除,没有元素),且状态值也可以标识无元素地址处,使得迭代时能够选择性的跳过DELETE和EMPTY处。
- 创建哈希表:
template<class K, class V> class hashTable { public: bool Insert(const pair<K, V>& val) { } private: vector<Hash<K, V>> _tables; int _size = 0; };
- 实现插入:
template<class K, class V> class hashTable { public: bool Insert(const pair<K, V>& val) { //需要注意的是,一定是除余size,而不是capacity size_t hashi = val.first % _tables.size(); size_t j = 1; size_t index = hashi; //如果发生了哈希冲突,也就是此处地址上存在元素,那就向后探测 while (_tables[index]._state == EXIST) { index = hashi + j;//每次都是向后探测一位,j++. index = index % _tables.size();//保证了向后探测时如果到了空间末尾,那就从空间开 //头处继续探测,保证了能够探测到一个合适的位置 j++; } //此时index处不存在元素: _tables[index]._kv = val; _tables[index]._state = EXIST; _size++; return true; } private: vector<Hash<K, V>> _tables; int _size = 0; };
写完上面代码会发现以下问题:
- _tables没有初始化,没有开辟空间,所以后面的插入元素也不可能完成。
- 如果空间用完了,需要进行扩容
- 效率问题,哈希表最重要的问题就是哈希冲突,使用线性探测法可以发现:哈希表存储数据的空间存储数据时越密集,发生哈希冲突的概率就越大,整个哈希表的效率就会越低,这是我们不愿意看见的,解决这个问题的话,就需要引入载荷因子(负载因子)。
将代码进行补充:
template<class K, class V> class hashTable { public: bool Insert(const pair<K, V>& val) { //负载因子超过0.7就扩容 if (_tables.size() == 0 || _size * 10 / _tables.size() >= 7) { size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2; hashTable<K, V> newTables; newTables._tables.resize(newSize); //扩容以后由于size的改变,部分元素的位置也会改变,所以需要重新计算位置并插入 //将原数组中的数组重新插入新数组 if (_tables.size()) { int i = 0; while (_tables[i]._state == EXIST) { newTables.Insert(_tables[i]._kv); i++; } } //重新插入新数组也可以这么实现 /*for (auto& data : _tables) { if (data._state == EXIST) { newTables.Insert(data._kv); } }*/ _tables.swap(newTables._tables); } //需要注意的是,一定是除余size,而不是capacity size_t hashi = val.first % _tables.size(); size_t j = 1; size_t index = hashi; //如果发生了哈希冲突,也就是此处地址上存在元素,那就向后探测 while (_tables[index]._state == EXIST) { index = hashi + j;//每次都是向后探测一位,j++. index = index % _tables.size();//保证了向后探测时如果到了空间末尾,那就从空间开 //头处继续探测,保证了能够探测到一个合适的位置 j++; } //此时index处不存在元素: _tables[index]._kv = val; _tables[index]._state = EXIST; _size++; return true; } private: vector<Hash<K, V>> _tables; int _size = 0; };
2.2 查找
Hash<K, V>* Find(const K& key) { //如果数组为空就没必要查找了 if (_tables.size() == 0) { return nullptr; } size_t hashi = key % _tables.size(); size_t i = 1; size_t index = hashi; //之前考虑过如果数组没有装满,元素之间肯定有很多的空隙会为EMPTY, //如果这么写的话会不会明明目标位置就在后面,但是早早停下了,但是 //仔细一想,插入的时候,就是向后探测,只要遇到EMPTY就插入了,所以 //绝对不会出现这种情况。 while (_tables[index]._state != EMPTY) { if (_tables[index]._kv.first == key && _tables[index]._state == EXIST) { return &_tables[index]; } index = hashi + i; index %= _tables.size(); i++; //查找了一圈还是没有找到,那就是没有这个元素 if (i == _tables.size()) { break; } } return nullptr; }
2.3 删除
bool Erase(const K& key) { Hash<K, V>* Eindex = Find(key); if (Eindex) { //注意的是Eindex是一个Hash类型的指针,而不是下标 Eindex->_state = DELETE; _size--; } else { return false; } return true; }
2.6 完整代码
#pragma once #include <iostream> #include <vector> using namespace std; enum State { EMPTY, EXIST, DELETE }; template<class K, class V> struct Hash { pair<K, V> _kv; State _state = EMPTY; }; template<class K, class V> class hashTable { public: bool Insert(const pair<K, V>& val) { //负载因子超过0.7就扩容 if (_tables.size() == 0 || _size * 10 / _tables.size() >= 7 ) { size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2; hashTable<K, V> newTables; newTables._tables.resize(newSize); //将原数组中的数组重新插入新数组 /*for (auto& data : _tables) { if (data._state == EXIST) { newTables.Insert(data._kv); } }*/ if (_tables.size()) { int i = 0; while (_tables[i]._state == EXIST) { newTables.Insert(_tables[i]._kv); i++; } } _tables.swap(newTables._tables); } //插入val size_t hashi = val.first % _tables.size(); size_t j = 1; size_t index = hashi; while (_tables[index]._state == EXIST) { index = hashi + j; index = index % _tables.size(); j++; } _tables[index]._kv = val; _tables[index]._state = EXIST; _size++; return true; } Hash<K, V>* Find(const K& key) { if (_tables.size() == 0) { return nullptr; } size_t hashi = key % _tables.size(); size_t i = 1; size_t index = hashi; while (_tables[index]._state != EMPTY) { if (_tables[index]._kv.first == key && _tables[index]._state == EXIST) { return &_tables[index]; } index = hashi + i; index %= _tables.size(); i++; if (i == _tables.size()) { break; } } return nullptr; } bool Erase(const K& key) { Hash<K, V>* Eindex = Find(key); if (Eindex) { Eindex->_state = DELETE; _size--; } else { return false; } return true; } private: vector<Hash<K, V>> _tables; int _size = 0; };
2.5 线性探测的优缺点
- 线性探测优点:实现非常简单,
- 线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
低。
可以通过二次探测缓解。
3. 二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题.
二次探测是向后探测是一次向后移动两个单位,这能够缓解很多哈希冲突都发生在邻近的一片位置时效率低下的问题。
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
😄 创作不易,你的点赞和关注都是对我莫大的鼓励,再次感谢您的观看😄