目录
三、封装 unordered_set 和 unordered_map
一、前言
续,之前再说set和map的时候,说的C++中的关联式容器还有另外的两种,分别为unordered_set和unordered_map,他们之间的区别呢,就是底层实现上,set和map的底层使用红黑树实现的,而unordered_set 和unordered_map的底层使用哈希表实现的,在前面几篇文章中呢,我也对红黑树的个别功能进行了模拟实现,而且给出了封装set和map的大致架子,今天我打算对哈希进行一个探究,也可以对hashtable进行实现,也可以用我们自己模拟实现的哈希对 unordered_set 和 unordered_map进行封装。希望呢,各位看到本篇文章的大佬可以指出我文章中的一些错误。
二、hash
1、哈希的原理
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log_2 N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。这样就可以让存入的数据随机分散到哈希表中,也可以用相同的方法取出元素
2、哈希冲突
不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
哈希冲突是很难解决的,所以,就要在存储的时候去解决冲突带来的影响:就有了闭散列,和开散列两种常见方法
3、闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
对于“下一个”空位置,该怎么寻找,又有多种方式,其中比较常用的有:线性探测和二次探测
(1)线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
那么这样的话,再插入元素的时候就可以通过哈希函数算出的地址去插入,如果发生了哈希冲突,就使用线性探测去寻找下一个空位置,再插入,再插入的话,类似的如果当前地址被占有,就再去找下一个空位置,去插入。
但是呢,这样就出现了一个新的问题,设想一个场景:一个位置发生了哈希冲突,使用线性探测找到下一个空位置离哈希函数算出的地址之间有一段距离,在这 两个地址之间,有一个数据被删除了,那在查找冲突的那个数据的时候,怎么判断他是否存在?
所以,还需要在每一个位置设置一个标记位,标记为为空,或者被删除的时候,是可以插入的,在查找的时候,这个位置为空就不要再查找了,这个表中没有要查找的数据,如果这个位置是被删除的话,那就要继续往后查找,这样就解决了刚才的问题。
(2)二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:每间隔i^2(i = 1,2,3,4……)去探测,相同的方法去查找,
(3)对闭散列进行模拟实现(使用线性探测)
namespace close_hash
{
enum State { EMPTY, EXIST, DELETE };
template<class K, class V>
class HashTable
{
struct Elem
{
pair<K, V> _val;
State _state;
//Elem(const pair<K, V>& val = pair<K, V>(), State state = State()) : _val(val), _state(state){}
};
public:
HashTable(size_t capacity = 10)
: _ht(capacity), _size(0), _totalSize(capacity)
{
/*for (size_t i = 0; i < capacity; ++i)
_ht[i]._state = EMPTY;*/
}
// 插入
bool Insert(const pair<K, V>& val) {
//如果表中已有这个元素,那就不再进行插入
if (Find(val.first) != (size_t)-1) return false;
//如果hashtable中的元素达到了一定量,就要让其进行扩容
if (_size * 10 / _totalSize >= 7) {
//进行扩容
size_t newsize = _totalSize * 2;
HashTable<K, V> newht(newsize);
for (auto elem : _ht) {
newht.Insert(elem._val);
}
Swap(newht);
}
size_t hashi = HashFunc(val.first);
if (_ht[hashi]._state == EMPTY || _ht[hashi]._state == DELETE) {
_ht[hashi]._val = val;
_ht[hashi]._state = EXIST;
++_size;
return true;
}
else {
int i = 1;
while (_ht[HashFunc(hashi + i)]._state == EXIST) {
++i;
}
_ht[HashFunc(hashi + i)]._val = val;
_ht[HashFunc(hashi + i)]._state = EXIST;
++_size;
return true;
}
}
// 查找
size_t Find(const K& key) {
size_t hashi = HashFunc(key);
size_t tag = hashi;
while (_ht[hashi]._state != EMPTY && _ht[hashi]._val.first != key) {
hashi = HashFunc(++hashi);
if (hashi == tag) {
return -1;
}
}
if (_ht[hashi]._state == EMPTY) return -1;
return hashi;
}
// 删除
bool Erase(const K& key) {
size_t hashi = Find(key);
if (hashi != (size_t)-1) {
_ht[hashi]._state = DELETE;
_size--;
return true;
}
return false;
}
size_t Size()const
{
return _size;
}
bool Empty() const
{
return _size == 0;
}
void Swap(HashTable<K, V>& ht)
{
swap(_size, ht._size);
swap(_totalSize, ht._totalSize);
_ht.swap(ht._ht);
}
private:
size_t HashFunc(const K& key)
{
return key % _ht.capacity();
}
private:
vector<Elem> _ht;
size_t _size;
size_t _totalSize; // 哈希表中的所有元素:有效和已删除, 扩容时候要用到
};
}
4、开散列
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
开散列的实现:
KeyOfV
注意:因为后续要对哈希进行封装,我们使用仿函数KeyOfV来接收要存储一些复杂类型的V,来获取其中可以充当key的元素值
//open_hash.hpp
namespace ltx
{
template<class T>
struct HashFunc
{
public:
size_t operator()(const T& val)
{
return val;
}
};
template<>
struct HashFunc<string>
{
public:
size_t operator()(const string& s)
{
const char* str = s.c_str();
unsigned int seed = 131; // 31 131 1313 13131 131313
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return hash;
}
};
template<class V>
struct HashBucketNode
{
HashBucketNode(const V& data)
: _pNext(nullptr), _data(data)
{}
HashBucketNode<V>* _pNext;
V _data;
};
template<class K, class V, class KeyOfV, class HF>
class HashBucket;
//迭代器
template<class K, class V, class KeyOfV, class HF>
struct HashIterator {
typedef HashBucketNode<V> Node;
typedef HashBucket<K, V, KeyOfV, HF> hashtable;
typedef HashIterator<K, V, KeyOfV, HF> Self;
Node* _node;
hashtable* _hash;
HashIterator(Node* node, hashtable* hash):_node(node), _hash(hash) {}
Self& operator++() {
if (_node->_pNext) {
_node = _node->_pNext;
return *this;
}
else {
KeyOfV kov;
HF hf;
size_t hashi = hf(kov(_node->_data)) % _hash->capacity() + 1;
while (hashi < _hash->capacity()) {
while (hashi < _hash->capacity() && !_hash->_table[hashi]) {
hashi = hashi + 1;
}
if (hashi == _hash->capacity()) break;
_node = _hash->_table[hashi];
return *this;
}
_node = nullptr;
return *this;
}
}
V& operator*() {
return _node->_data;
}
V* operator->() {
return &_node->_data;
}
bool operator==(const Self& s)const {
if (_node == nullptr && s._node == nullptr)
return true;
return _node == s._node;
}
bool operator!=(const Self& s)const {
return !(*this == s);
}
};
// 本文所实现的哈希桶中key是唯一的
template<class K, class V, class KeyOfV, class HF>
class HashBucket
{
typedef HashBucketNode<V> Node;
typedef Node* pNode;
typedef HashBucket<K, V, KeyOfV, HF> Self;
public:
typedef HashIterator<K, V, KeyOfV, HF> iterator;
template<class K, class V, class KeyOfV, class HF>
friend struct HashIterator;
HashBucket(size_t capacity = 10)
: _table(capacity)
, _size(0)
, _capacity(capacity)
{}
HF hf;
KeyOfV kov;
~HashBucket()
{
clear();
}
iterator begin() {
int i = 0;
for (i = 0; i < capacity(); i++) {
if (_table[i]) return iterator(_table[i], this);
}
return end();
}
iterator end() {
return iterator(nullptr, this);
}
// 哈希桶中的元素不能重复
pair<iterator, bool> insert(const V& data) {
Node* node = find(data);
if (node) return make_pair(iterator(node,this), true);
//如果哈希表中的结点达到一定的数量,则要进行扩容
if (size() == capacity()) {
Self newtable(capacity() * 2);
swap(newtable);
_size = newtable._size;
for (int i = 0; i < newtable.capacity(); ++i) {
while (newtable._table[i]) {
Node* cur = newtable._table[i];
newtable._table[i] = cur->_pNext;
cur->_pNext = nullptr;
//将cur插入到新的哈希表中:
size_t hashi = hashFunc(cur->_data);
if (!_table[hashi])
_table[hashi] = cur;
else {
cur->_pNext = _table[hashi];
_table[hashi] = cur;
}
}
}
}
size_t hashi = hashFunc(data);
Node* cur = _table[hashi];
if (!cur) {
_table[hashi] = new Node(data);
_size++;
return make_pair(iterator(_table[hashi],this),true);
}
while (cur->_pNext) cur = cur->_pNext;
cur->_pNext = new Node(data);
_size++;
return make_pair(iterator(cur->_pNext,this), true);
}
// 删除哈希桶中为data的元素(data不会重复)
iterator& erase(iterator& it) {
size_t hashi = hashFunc(*it);
if (kov(_table[hashi]->_data) == kov(*it)) {
++it;
Node* next = _table[hashi]->_pNext;
delete _table[hashi];
_table[hashi] = next;
_size--;
return it;
}
Node* cur = _table[hashi];
while (cur->_pNext) {
if (kov(cur->_pNext->_data) == kov(*it)) {
++it;
Node* next = cur->_pNext->_pNext;
delete cur->_pNext;
cur->_pNext = next;
_size--;
return it;
}
cur = cur->_pNext;
}
return it;
}
Node* find(const V& data) {
size_t hashi = hashFunc(data);
Node* cur = _table[hashi];
while (cur) {
if (kov(cur->_data) == kov(data)) {
return cur;
}
cur = cur->_pNext;
}
return cur;
}
size_t size()const
{
return _size;
}
bool empty()const
{
return 0 == _size;
}
void clear() {
iterator it = begin();
while (it != end()) {
erase(it);
}
}
size_t bucketCount()const
{
return _table.capacity();
}
size_t capacity()const {
return _capacity;
}
void swap(Self& ht)
{
_table.swap(ht._table);
std::swap(_size, ht._size);
std::swap(_capacity, ht._capacity);
}
private:
size_t hashFunc(const V& data)
{
return hf(kov(data)) % _table.capacity();
}
//void CheckCapacity();
private:
vector<Node*> _table;
size_t _size; // 哈希表中有效元素的个数
size_t _capacity;
};
}
开散列与闭散列比较
应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:
由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子 a <=0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。
三、封装 unordered_set 和 unordered_map
1、unordered_set
//_unordered_set.hpp
#pragma once
#include "open_hash.hpp"
namespace ltx
{
// unordered_set中存储的是K类型,HF哈希函数类型
// unordered_set在实现时,只需将hashbucket中的接口重新封装即可
template<class K, class HF = HashFunc<K>>
class unordered_set
{
// 通过key获取value的操作
struct KeyOfValue
{
const K& operator()(const K& data)
{
return data;
}
};
typedef HashBucket<K, K, KeyOfValue, HF> HT;
public:
typedef typename HT::iterator iterator;
public:
unordered_set() : _ht()
{}
iterator begin() { return _ht.begin(); }
iterator end() { return _ht.end(); }
// capacity
size_t size()const { return _ht.size(); }
bool empty()const { return _ht.empty(); }
// lookup
iterator find(const K& key) { return _ht.find(key); }
//size_t count(const K& key) { return _ht.Count(key); }
// modify
pair<iterator, bool> insert(const K& valye)
{
return _ht.insert(valye);
}
iterator erase(iterator position)
{
return _ht.erase(position);
}
// bucket
size_t bucket_count() { return _ht.BucketCount(); }
size_t bucket_size(const K& key) { return _ht.BucketSize(key); }
private:
HT _ht;
};
}
2、unordered_map
//_unordered_map.hpp
#pragma once
#include "open_hash.hpp"
namespace ltx
{
// unordered_map中存储的是pair<K, V>的键值对,K为key的类型,V为value的类型,HF哈希函数类型
// unordered_map在实现时,只需将hashbucket中的接口重新封装即可
template<class K, class V, class HF = HashFunc<V>>
class unordered_map
{
// 通过key获取value的操作
struct KeyOfValue
{
const K& operator()(const pair<K, V>& data)
{
return data.first;
}
};
typedef HashBucket<K, pair<K, V>, KeyOfValue, HF> HT;
public:
typename typedef HT::iterator iterator;
public:
unordered_map() : _ht()
{}
iterator begin() { return _ht.begin(); }
iterator end() { return _ht.end(); }
// capacity
size_t size()const { return _ht.size(); }
bool empty()const { return _ht.empty(); }
///
// Acess
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.InsertUnique(pair<K, V>(key, V()));
return ret.fisrt->second;
}
const V& operator[](const K& key)const {
pair<iterator, bool> ret = _ht.InsertUnique(pair<K, V>(key, V()));
return ret.fisrt->second;
}
//
// lookup
iterator find(const K& key) { return _ht.find(key); }
//size_t count(const K& key) { return _ht.Count(key); }
/
// modify
pair<iterator, bool> insert(const pair<K, V>& valye)
{
return _ht.insert(valye);
}
iterator erase(iterator position)
{
return _ht.erase(position);
}
// bucket
//size_t bucket_count() { return _ht.bucketCount(); }
size_t bucket_size(const K& key) { return _ht.bucketSize(key); }
private:
HT _ht;
};
}