哈希
一、unordered系列关联式容器
map和set的底层是用红黑树实现的,在最差的情况下也能在高度次查询到节点。但是当节点数量非常多的时候,效率并不理想,所以C++11引入了unorderedmap与unorderedset,能极快的查找到元素节点,但是它们的底层不是用搜索树实现的,所以不能保证有序。
二、哈希原理
2.1 哈希映射
红黑树我们需要进行比较查找才能找到对应节点。而进过哈希映射函数,让key值跟存储位置建立映射关系,那么在查找时通过该函数可以很快找到该元素。
像计数排序就可以看作一个简单的哈希映射,叫做直接定址法,但是得范围集中才行。
如果范围不集中,就可以用除留余数法,我们可以用元素的值去模上容器的大小。这样所有的元素就一定能存入表中。
2.2 哈希冲突
上面的除留余数法可能会导致两个元素要存储在同一个位置。我们把这种情况称为哈希冲突。而解决哈希冲突的方法有两种
2.2.1 闭散列—开放地址法
闭散列的大致方法就是:当映射的地方已经有值了,那么就按规律找其他位置。而查找空位的方法又分为线性探测和二次探测。
【线性探测】
插入:
用除留余数法求出key值的关键码,并将它放到对应的位置上。如果该位置已经存在数据被占用了,那么继续寻找下一个位置,也就是+1的位置,如果+1的位置已经有数据,那么继续+1,直到寻找到下一个空位置为止。
查找:
查找就是取余后往后探索,知道找到空位置就停止,这里要注意如果删除了一个数据,而要查找的元素在删除位置的后边,就会在删除的地方停下来,导致本来存在的元素查找不到。
解决这种情况的方式:
可以再设置一种状态(枚举),将数组中每个数据的状态记录一下,所以就有了存在,空和删除这三种状态。删除的位置状态时删除,查找的时候不会停下。
这里要注意有一种情况是整个闭散列全部都存在或者为删除状态(边插入边删除不会扩容),所以最多循环一圈。
负载因子:
表中的有效数据个数/表的大小,载荷因子不能超过1。为了减小冲突,一般到0.7就会扩容。
字符串哈希:
这里要注意如果key是字符串就不能使用除法取余,所以我们需要一个仿函数把字符串转换成数字。
【二次探测】
我们知道线性探测如果发生了冲突并且冲突连在一起就会引起数据堆积,导致搜索效率降低,为了解决这种情况,就有了二次探测。
二次探测的方法就是以i的2次方去进行探测,如果要找的位置Idx被占,下次找Idx + 1^2,如果再次被占,则找Idx + 2^2,以此类推。
2.2.2 代码实现
// 状态
enum Sta
{
EXIST,
DELETE,
EMPTY,
};
// 数据类型
template <class K, class V>
struct HashData
{
pair<K, V> _kv;
Sta _state = EMPTY;
};
template <class K>
struct HashKey
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
// 字符串哈希
template <>
struct HashKey<string>
{
size_t operator()(const string& key)
{
size_t ans = 0;
for (int i = 0; i < key.size(); i++)
{
ans *= 131;
ans += key[i];
}
return ans;
}
};
// 哈希表结构
template <class K, class V, class GetKey = HashKey<K>>
class HashTable
{
typedef HashData<K, V> data;
public:
HashTable()
: _n(0)
{
_tables.resize(7);
}
bool insert(const pair<K, V>& kv)
{
// 重复
if (find(kv.first)) return false;
// 负载因子
size_t load = _n * 10 / _tables.size();
if (load >= 10)
{
// 出作用域后销毁
HashTable<K, V> newhash;
newhash._tables.resize(2 * _tables.size());
for (auto& e : _tables)
{
if (e._state == EXIST)
{
newhash.insert(e._kv);
}
}
_tables.swap(newhash._tables);
}
GetKey Get;
size_t hashI = Get(kv.first) % _tables.size();
while (_tables[hashI]._state == EXIST)
{
++hashI;
hashI %= _tables.size();
}
_tables[hashI]._kv = kv;
_tables[hashI]._state = EXIST;
++_n;
return true;
}
data* find(const K& key)
{
GetKey Get;
size_t hashI = Get(key) % _tables.size();
size_t startI = hashI;// 最多循环一圈
while (_tables[hashI]._state != EMPTY)
{
if (_tables[hashI]._state == EXIST
&& _tables[hashI]._kv.first == key)
{
return &_tables[hashI];
}
++hashI;
hashI %= _tables.size();
if (hashI == startI) break;
}
return nullptr;
}
bool erase(const K& key)
{
data* node = find(key);
if (node)
{
node->_state = DELETE;
return true;
}
return false;
}
private:
vector<data> _tables;
size_t _n;// 有效数据个数
};
2.2.3 开散列—拉链法
闭散列解决哈希冲突的办法就是抢占别人的位置,而开散列不一样,冲突的元素可以一起在同一个位置。
首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

【增容】
随着插入的数量增加,可能导致一个桶的节点数目非常多,为了应对这种情况,在一定情况下需要增容。一般当负载因子为1的时候扩容。
2.2.4 代码实现
template <class K, class V>
struct HashNode
{
HashNode(const pair<K, V> kv)
: _kv(kv)
, _next(nullptr)
{
}
pair<K, V> _kv;
HashNode<K, V>* _next;
};
template <class K, class V, class GetKey = HashKey<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable()
: _n(0)
{
_tables.resize(10);
}
~HashTable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
bool insert(const pair<K, V>& kv)
{
// 重复
if (find(kv.first))
return false;
// 负载因子为1扩容
if (_tables.size() == _n)
{
vector<Node*> newtable;
newtable.resize(2 * _n);
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t idx = GetKey()(cur->_kv.first) % newtable.size();
cur->_next = newtable[idx];
newtable[idx] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtable);
}
GetKey Get;
size_t hashI = GetKey()(kv.first) % _tables.size();
Node* newnode = new Node(kv);
newnode->_next = _tables[hashI];
_tables[hashI] = newnode;
++_n;
return true;
}
Node* find(const K& key

最低0.47元/天 解锁文章
3073

被折叠的 条评论
为什么被折叠?



