作者:小 琛 欢迎转载,请标明出处
在之前的存储结构中(搜索树、红黑树),都有其缺点。
例如:在查找一个元素时,必须要经过关键值的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN ),搜索的效率取决于搜索过程中元素的比较次数。这与其本身特质有关,它们毕竟是一种类似遍历的方法查找。
何为理想的搜索方法? 可以不经过任何比较,一次直接从表中得到要搜索的元素。 通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。 关键字:一 一映射
例如:设定一个数组,该数组的每个成员对应不同的值,查找9时,直接去该数组第9个元素查找
常见哈希函数
直接定制法 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况除留余数法 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址对于两个数据元素的关键字j和k,有Hash(j)==Hash(k)。即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。 例如:
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
因此:闭散列哈希表的最大缺陷就是空间利用率低,是一种典型的空间换时间的做法
namespace CLOSE_HASH { // unordered_set<K> ->HashTable<K, K> // unordered_map<K,V> ->HashTable<K, pair<K,V>> enum State { EMPTY, EXITS, DELETE, }; template<class T> struct HashData { T _data; State _state; }; template<class K, class T, class KeyOfT> class HashTable { typedef HashData<T> HashData; public: bool Insert(const T& d) { KeyOfT koft; // 负载因子 = 表中数据/表的大小 衡量哈希表满的程度 // 表越接近满,插入数据越容易冲突,冲突越多,效率越低 // 哈希表并不是满了才增容,开放定制法中,一般负载因子到了0.7左右就开始增容 // 负载因子越小,冲突概率越低,整体效率越高,但是负载因子越小,浪费的空间越大, // 所以负载因子一般取一个折中值。 if (_tables.size() == 0 || _num * 10 / _tables.size() >= 7) { // 1.开2倍大小的新表 // 2.遍历旧表的数据,重新计算在新表中位置 // 3.释放旧表 //vector<HashData> newtables; //size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2; //newtables.resize(newsize); //for (size_t i = 0; i < _tables.size(); ++i) //{ // if (_tables[i]._state == EXITS) // { // // 计算在新表中的位置并处理冲突 // size_t index = koft(_tables[i]._data) % newtables.size(); // while (newtables[index]._state == EXITS) // { // ++index; // if (index == _tables.size()) // { // index = 0; // } // } // newtables[index] = _tables[i]; // } //} //_tables.swap(newtables); HashTable<K, T, KeyOfT> newht; size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2; newht._tables.resize(newsize); for (size_t i = 0; i < _tables.size(); ++i) { if (_tables[i]._state == EXITS) { newht.Insert(_tables[i]._data); } } _tables.swap(newht._tables); } // 线性探测 计算d中的key在表中映射的位置 //size_t index = koft(d) % _tables.size(); //while (_tables[index]._state == EXITS) //{ // if (koft(_tables[index]._data) == koft(d)) // { // return false; // } // ++index; // if (index == _tables.size()) // { // index = 0; // } //} //_tables[index]._data = d; //_tables[index]._state = EXITS; //_num++; // 二次探测 // 计算d中的key在表中映射的位置 size_t start = koft(d) % _tables.size(); size_t index = start; int i = 1; while (_tables[index]._state == EXITS) { if (koft(_tables[index]._data) == koft(d)) { return false; } index = start + i*i; ++i; index %= _tables.size(); } _tables[index]._data = d; _tables[index]._state = EXITS; _num++; return true; } HashData* Find(const K& key) { KeyOfT koft; // 计算d中的key在表中映射的位置 size_t index = key % _tables.size(); while (_tables[index]._state != EMPTY) { if (koft(_tables[index]._data) == key) { if (_tables[index]._state == EXITS) { return &_tables[index]; } else if (_tables[index]._state == DELETE) { return nullptr; } } ++index; if (index == _tables.size()) { index = 0; } } return nullptr; } bool Erase(const K& key) { HashData* ret = Find(key); if (ret) { ret->_state = DELETE; --_num; return true; } else { return false; } } private: vector<HashData> _tables; size_t _num = 0; // 存了几个有效数据 };假设有这样几种情况:
我的现有哈希桶已经填满,后续再继续将数据填充至哈希桶中必会引发哈希冲突。我所插入的元素冲突率很大,某一个链上挂载的数据很多,此时我的搜索效率就会变差。采用增容的方式,我们通常把:“当前插入值的个数 / 哈希桶本身的存储空间大小”称之为负载因子。
一般情况下,我们要保证负载因子的大小<1,因为我们希望哈希查找效率为O(1),当大于1的时候,哈希会进行扩容,通常扩2倍。
当进行扩容的时候,需要将哈希桶内所有的元素移除,重新进行哈希映射,这样原本的冲突元素,会映射到新的位置。
