哈希表与哈希桶的模拟实现
unordered系列关联式容器介绍
在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到$log_2 N$,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好 的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个 unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是 其底层结构不同,例如, unordered_multimap和unordered_multiset
==但是unordered系列的底层是哈希==!——其实是应该叫做hash_map,hash_set会比较合理的!但是之所以叫做unordered是为了在功能上进行区分!==因为map与set的底层是二叉搜索树!是有序的!但是以哈希为底层的unordered_map和unordered_set是无序的!所以这就是为什么叫unordered!==
==有一些区别就是unordered系列的容器不支持反向迭代器!==
除了这两点之外unordered系列的容器和由二叉树作为底层的容器几乎没有区别!
那么究竟为什么需要unordered系列的容器呢?
==我们看如下的测试==
#include <iostream>
#include <unordered_set>
#include <set>
#include <vector>
using namespace std;
int main()
{
const size_t N = 100000;
unordered_set<int> us;
set<int> s;
vector<int> v;
v.reserve(N);
srand(time(0));
for(size_t i = 0; i < N; ++i)
{
v.push_back(i);//大量数据不同
//v.push_back(rand());//大量数据相同
//v.push_back(rand()+i);//部分数据相同
}
size_t begin = clock();
for(size_t i = 0; i < N; ++i)
{
us.insert(v[i]);
}
size_t end = clock();
cout << "unordered_set: " << end - begin << endl;
begin = clock();
for(size_t i = 0; i < N; ++i)
{
s.insert(v[i]);
}
end = clock();
cout << "set: " << end - begin << endl;
size_t begin2 = clock();
for(auto e:v)
{
s.find(e);
}
size_t end2 = clock();
cout << "set find:" << end2 - begin2 << endl;
size_t begin3 = clock();
for(auto e:v)
{
us.find(e);
}
size_t end3 = clock();
cout << "unordered_set find :" << end3 - begin3 << endl;
size_t begin4 = clock();
for(auto& e:v)
{
s.erase(e);
}
size_t end4 = clock();
cout << "set: erase :" << end4 - begin4 << endl;
size_t begin5 = clock();
for(auto& e:v)
{
us.erase(e);
}
size_t end5 = clock();
cout << "unordered_set: erase :" << end5 - begin5 << endl;
return 0;
}
insert性能对比
在大量的重复数据的情况下!
在大量不重复的数据的情况下
我们发现了在大批量的有重复的进行插入的时候!unordered系列容器快了好几倍!——rand只能产生3w多个随机值!所以就会造成大量的重复!
如果是不同的的数据进行插入,那么set就会比会比unordered系列的快一点
find效率对比!
==而且我们发现unordered系列的查找非常的快!这是由unordered的底层哈希决定的!无论是大量的数据重复还是大量的数据不重复!unordered系列的find都更加的快!==
erase效率对比
在有大量的数据不同的情况下
在部分或者少量数据相同的情况下
在大量数据相同的情况下!
在数据量重复的情况比较多的时候我们会发现unordered系列会更加的占优势!在大量不重复的时候非unordered版本会更好一点!不过也不一定,在不同的编译器下面实现的可能会有不同的结果!
==但是我们综上我们可以发现,unordered系列的find效率极高!无论是什么场景!unordered系列的find性能都是高于非unordered系列的!所以如果对于查找性能要求比较高的!我们可以使用unordered版本!==
哈希的概念
unordered系列的容器的底层是哈希表!
什么是哈希?哈希就是一种映射!==我们一般叫做哈希映射==——key值跟存储位置建立一种关联关系!
闭散列——开放定址法
思路就是——映射的位置已经有存在值了,就按照一定的规则找到其他的位置
规则1——线性探测——如果这个位置已经发生了冲突了,那么就往下面继续找!直到找到空为止!
关于哈希表数据应该如何删除
==所以我们既不应该将数据置空!也不应该以位置空为结束条件!——我们应该弄一个状态标识符来标记每一个位置的状态!==
状态标识符——应该有三种状态
- 该位置为空
- 该位置不为空,但是已经删除
- 该位置不为空,没有删除!
==遇到状态为删除的是不能停下来的!要遇到状态为空的的才能停下来!==
闭散列的代码实现——线性探测的实现
哈希数据类
namespace MySTL
{
enum State
{
EMPTY,
EXIST,
DELETE
};//设定三种状态
template<class K,class V>
struct HashData
{
std::pair<K, V> _kv;
State _state;
};
}
哈希数据类的构造
namespace MySTL
{
enum State
{
EMPTY,
EXIST,
DELETE
};
template<class K,class V>
struct HashData
{
HashData(const std::pair<K,V>& kv = std::pair<K,V>())
:_kv(kv)
,_state(EMPTY)
{}
std::pair<K, V> _kv;
State _state;
};
}
哈希类的成员变量
#pragma once
#include<iostream>
#include<vector>
namespace MySTL
{
template<class K, class V>
class Hashtable
{
typedef HashData<K,V> Data;
private:
std::vector<Data> _table;
size_t _n;//有效元素个数
};
}
构造函数
#pragma once
#include<iostream>
#include<vector>
namespace MySTL
{
template<class K, class V>
class Hashtable
{
typedef HashData<K,V> Data;
public:
Hashtable(size_t size = 10)//开始就要给表一个值!防止出现空表导致插入出问题!
:_table(size)
,_n(0)
{}
private:
std::vector<Data> _table;
size_t _n;//有效元素个数
};
}
Insert
#pragma once
#include<iostream>
#include<vector>
namespace MySTL
{
template<class K, class V>
class Hashtable
{
typedef HashData<K,V> Data;
public:
bool Insert(const std::pair<K,V>& kv)
{
//大于标定的负载因 子就需要进行扩容!
//不要使用if(_n/_table.size() > 0.7)因为_n是size_t,_table.size()是size_t
//_n/_table.size()的结果是不会是一个浮点数的!
if(_n*10/_table.size() >= 7)
{
//方法一
/*
size_t newsize = _table.size() * 2;
std::vector<Data> newtable(newsize);//创建一个新的表!
for(auto& e:_table)//一个个的拷贝!
{
if(e._state != EXIST)//只有存在的才需要拷贝!一定要判断!
continue;
size_t hashi = e._kv.first%newsize;
while(newtable[hashi]._state == EXIST)
{
++hashi;
if(hashi == newsize)
hashi = 0;
}
newtable[hashi] = e;
}
_table.swap(newtable);
*/
//方法二
Hashtable<K,V> NEWHT(_table.size()*2);
for(auto& e:_table)
{
if(e._state == EXIST)
NEWHT.Insert(e._kv);
}
//我们可以直接创建一个新的哈希表!
//然后调用新的哈希表的插入函数!
//复用代码!因为新表的负载因子一定是不会超过0.7的!
//所以不会再次进行扩容!
//那么就会调用下面的代码!
//这样就不用再写一遍插入的代码了!
//然后交换一下就好了!
_table.swap(NEWHT._table);
}
size_t hashi = kv.first% _table.size();//先算要映射的位置!
//要模上表的大小,因为可能会超出表的大小!
//而不是capacity,因为capacity是表的容量,可能会比表的大小大!
while(_table[hashi]._state == EXIST)//循环结束的条件就是遇到空,或者遇到删除!
{
if(_table[hashi]._kv.first == kv.first)//如果已经存在
return false;
//线性探测就是++
++hashi;
if(hashi == _table.size())
hashi = 0;
}
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
++_n;//有效元素个数加一!
}
private:
std::vector<Data> _table;
size_t _n;//有效元素个数
};
}
负载因子
==insert的时候!是完全不用担心是满的!为什么?==
哈希表如何进行扩容
哈希表的扩容能不能直接单纯的将数据拷贝下来呢?——答案是否定的!
为什么呢?——因为==进行了扩容之后表的大小已经发生了改变!映射关系也已经发生了改变!==
==我们必须重新计算映射关系!==
扩容的时候,不要去使用reserve或者reszie,因为这样子我们相当于是在原表上进行处理!——会很麻烦!
因为如果我们重新算映射关系的时候,将还没计算映射关系的值给覆盖了!那就很不妙了!
==所以最好是弄一个新表!然后重新映射完后交换一下==
Find
#pragma once
#include<iostream>
#include<vector>
namespace MySTL
{
template<class K, class V>
class Hashtable
{
typedef HashData<K,V> Data;
public:
Data* Find(const K& key)
{
//查找和插入的逻辑是一致的!就是线性探测的过程!
size_t hashi = key%_table.size();
size_t starti = hashi;
while(_table[hashi]._state != EMPTY)//找到空才能停
{
if(_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)//要判断是否存在!而不是删除!
return &_table[hashi];
++hashi;
if(hashi == _table.size())
hashi = 0;
if(starti == hashi)//极端场景下面不存在空!全是存在或者删除!
{
break;
}
}
return nullptr;
}
private:
std::vector<Data> _table;
size_t _n;//有效元素个数
};
}
==我们使用Data* 作为返回值有一个好处!——就是可以方便进行复用!==
erase
#pragma once
#include<iostream>
#include<vector>
namespace MySTL
{
template<class K, class V>
class Hashtable
{
typedef HashData<K,V> Data;
public:
bool erase(const K& key)
{
Data* ret = Find(key);//直接复用Find我们就可以不用自己去写查找逻辑了!
if(ret == nullptr)
return false;
else
{
ret->_state = DELETE;//这种就是伪删除!
--_n;//减去有效数据次数
return true;
}
}
private:
std::vector<Data> _table;
size_t _n;//有效元素个数
};
}
双重映射——哈希函数
上面的映射一个问题!——那就是只能映射整形!
我们想要插入一个string类型的时候!就会发生报错!
那这时候应该怎么办?——答案就是双重映射!
==先将string映射为整形!然后再对整形取模!==——这个算法要是一个稳定的算法!让一个字符串尽可能的对应一个唯一的数字!
例如:我们可以去字符串的第一个字母(char类型的变量)——但是这样子有很大的概率就会发生冲突!因为只要第一个字母相同那么就会发生冲突!
所以我们就必须==写一个哈希函数——一个仿函数!==用来进行转换!
这个哈希函数的的作用就是将key转换为一个整形类型!
namespace MySTL
{
enum State
{
EMPTY,
EXIST,
DELETE
};
template<class K,class V>
struct HashData
{
HashData(const std::pair<K,V>& kv = std::pair<K,V>())
:_kv(kv)
,_state(EMPTY)
{}
std::pair<K, V> _kv;
State _state;
};
//写一个哈希函数!
template<class K>
struct HashFunc//这就是我们默认的哈希函数!但是这个哈希函数是不能将string类转换为size_t的!
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//多加一个模板参数!——仿函数!
template<class K, class V,class Hash = HashFunc<K>>
class Hashtable
{
typedef HashData<K,V> Data;
public:
Hashtable(size_t size = 10)
:_table(size)
,_n(0)
{}
bool Insert(const std::pair<K,V>& kv)
{
if(_n*10/_table.size() >= 7)
{
Hashtable<K,V,Hash> NEWHT(_table.size()*2);
for(auto& e:_table)
{
if(e._state == EXIST)
NEWHT.Insert(e._kv);
}
_table.swap(NEWHT._table);
}
Hash hf;
size_t hashi = hf(kv.first) % _table.size();
while(_table[hashi]._state == EXIST)
{
if(_table[hashi]._kv.first == kv.first)
return false;
++hashi;
if(hashi == _table.size())
hashi = 0;
}
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
++_n;
return true;
}
Data* Find(const K& key)
{
Hash hf;
size_t hashi = hf(key)%_table.size();
while(_table[hashi]._state != EMPTY)
{
if(_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)
return &_table[hashi];
++hashi;
if(hashi == _table.size())
hashi = 0;
}
return nullptr;
}
bool erase(const K& key)
{
Data* ret = Find(key);
if(ret == nullptr)
return false;
else
{
ret->_state = DELETE;
--_n;
return true;
}
}
private:
std::vector<Data> _table;
size_t _n;//有效元素个数
};
}
//将string类转换为size_t的一个哈希函数!
namespace MySTL
{
struct HashFuncstring
{
size_t operator()(const std::string& key)
{
size_t hash = 0;
for(auto& e:key)
{
hash = hash*131 + e;//不一定要使用这个算法,也可以自己写一个算法
}
return hash;
}
}
//或者我们也可以将模版进行偏特化!
//将上面的写的hash仿函数偏特化一下!
template<>
struct HashFunc<std::string>
{
size_t operator()(const std::string& key)
{
size_t hash = 0;
for(auto& e:key)
{
hash = hash*131 + e;
}
return hash;
}
};
}
namespace MySTL
{
void testHash()
{
std::vector<std::string> fruit = {"apple","banana","orange","apple","banana","apple"};
//使用我们自己写的这个hash函数!
Hashtable<std::string,int,HashFuncstring> HT2;
for(auto& e:fruit)
{
HashData<std::string,int>* ret = HT2.Find(e);
if(ret == nullptr)
HT2.Insert(std::make_pair(e,1));
else
++(ret->_kv.second);
}
}
//我们要针对这个类型写一个特定的仿函数!
//哈希的用法就是这样的!
}
我们可以看到我们成功的统计了水果的数量!
==不过我们哈希表一般要么是使用整形作为key,要么就是字符串作为key!其实很少用自定义类型作为key==
这个哈希表是不用写拷贝构造,赋值重装和析构的!对于自定义类型,哈希会自己去调用vector的拷贝/构造/赋值重装!
开散列——哈希桶介绍
相比哈希表(由闭散列实现!)——闭散列无论如何,占用别的数据的位置是不可避免的!
但是由开散列实现的哈希桶就没有这个问题!
==但是哈希桶有一个问题!那就是如果大量的数据发生了冲突!甚至最极端的情况!所有数据都冲突了!(就是全都挂在了同一个的单链表上!)那么就会从哈希桶退化为单链表!==
==一般哈希桶都是使用单链表作为它的子结构!==
这就是为什么unordered系列的容器只支持单向迭代器!——而不支持双向迭代器!
哈希桶的实现
哈希桶节点类
namespace MySTL
{
template<class K,class V>
struct HashNode
{
HashNode(const std::pair<K,V> kv = std::pair<K,V>())
:_kv(kv)
,_next(nullptr)
{}
std::pair<K,V> _kv;
HashNode<K,V>* _next;
};
}
哈希桶
成员变量
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};//默认哈希函数!
template<class K,class V,class Hash = HashFunc<K>>
class HashBucket
{
typedef HashNode<K, V> Node;
public:
HashBucket()
:_n(0)
,_Bucket(10, nullptr)
{}
private:
std::vector<Node> _Bucket;
size_t _n;
};
}
==如果不想自己写链表可以去使用STL库里面的list或者forward_list这两个容器==
构造函数
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
template<class K>
struct Hash
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<class K,class V,class Hash = HashFunc<K>>
class HashBucket
{
typedef HashNode<K, V> Node;
public:
HashBucket(size_t n = 10)
:_n(0)
{
_Bucket.resize(n);
}
private:
std::vector<Node> _Bucket;
size_t _n;
};
}
析构函数
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<class K,class V,class Hash = HashFunc<K>>
class HashBucket
{
typedef HashNode<K, V> Node;
public:
~HashBucket()
{
for(auto cur : _Bucket)
{
while(cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
}
}
private:
std::vector<Node*> _Bucket;
size_t _n;
};
}
==闭散列是不需要写析构函数的!但是开散列是需要的!如果不写析构函数!因为_Bucket只会释放每个桶的头节点!而不会释放每个桶的所有节点!会造成存泄露!==
Find
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<class K,class V,class Hash = HashFunc<K>>
class HashBucket
{
typedef HashNode<K, V> Node;
public:
Node* Find(const K& key)
{
size_t hashi = Hash()(key) % _Bucket.size();
Node cur = _Bucket[hashi];
while(cur)
{
if(cur->_kv.first == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
private:
std::vector<Node> _Bucket;
size_t _n;
};
}
Insert
#include <iostream>
#include <vector>
namespace MySTL
{
template<class K,class V,class Hash = HashFunc<K>>
class HashBucket
{
typedef HashNode<K, V> Node;
public:
bool Insert(const std::pair<K,V> &kv)
{
if(Find(kv.first))
return false;
//还要考虑扩容!要考虑一下负载因子
//闭散列的负载因子是不会大于一的,但是开散列的负载因子是可以大于一的,因为理论上开散列可以无限挂
//一般的负载因子控制在1
if(_n == _Bucket.size())//我们这边就简单写死
{
//方法一
//像闭散列一样创建一个新的哈希表,然后把旧的哈希表的元素全部插入到新的哈希表中
//然后交换一下根节点就结束!
//这样子的前提是要写析构函数保证旧的哈希表的元素都能被释放!
//因为vector只会只会去释放每个位置的根节点元素!但是根节点后面的元素都不会被释放!
//HashBucket<K,V,Hash> newHT;
//newHT._Bucket.resize(_Bucket.size()*2);
//for(auto cur :_Bucket)
//{
// while(cur)
// {
// newHT.Insert(cur->_kv);
// cur = cur->_next;
// }
//}
//_Bucket.swap(newHT._Bucket);
//方法二,将旧表的节点全部拿出来,然后重新插入到新表中
std::vector<Node*> newBucket;
newBucket.resize(_Bucket.size()*2, nullptr);
for(int i = 0;i< _Bucket.size();i++)
{
Node* cur = _Bucket[i];
while(cur)
{
Node* next = cur->_next;
size_t hashi = Hash()(cur->_kv.first) % newBucket.size();
//cur是新插入的节点!要进行头插!变成新的头结点
//nweBucket[hashi]是原来的头结点!要变成新的头结点的next
cur->_next = newBucket[hashi];
//不用担心第一次是空的,即使第一次是nullptr,也不会出错!
newBucket[hashi] = cur;//变成新的头结点
cur = next;
}
_Bucket[i] = nullptr;
}
_Bucket.swap(newBucket);//交换两个表
}
size_t hashi = Hash()(kv.first) % _Bucket.size();
//要放在扩容的后面!否则会导致扩容之后用的是扩容前的映射!
//哈希桶和哈希表不一样!哈希桶不用担心哈希冲突,因为哈希桶的每个位置都是一个链表
//直接进行头插
//不用判断开始是不是空!
Node* newhead = new Node(kv);
newhead->_next = _Bucket[hashi];
_Bucket[hashi] = newhead;
++_n;
return true;
}
private:
std::vector<Node*> _Bucket;
size_t _n;
};
}
**闭散列的负载因子不能大于一!**但是==开散列可以!==因为理论上来说,开散列是可以无限挂下去的!
==但是一般我们会将负载因子控制在1左右!==
在STL库的unordered系列的容器中给我们提供了一个接口!
==这个接口允许我们控制负载因子的大小!==
开链表如何进行扩容
if(_n == _Bucket.size())//我们这边就简单写死
{
//方法一
//像闭散列一样创建一个新的哈希表,然后把旧的哈希表的元素全部插入到新的哈希表中
//然后交换一下根节点就结束!
//这样子的前提是要写析构函数保证旧的哈希表的元素都能被释放!
//因为vector只会只会去释放每个位置的根节点元素!但是根节点后面的元素都不会被释放!
HashBucket<K,V,Hash> newHT;
newHT._Bucket.resize(_Bucket.size()*2);
for(auto cur :_Bucket)
{
while(cur)
{
newHT.Insert(cur->_kv);
cur = cur->_next;
}
}
_Bucket.swap(newHT._Bucket);
}
==但是这种方法扩容的消耗十分的大!因为我们要去new新的节点!然后再调用析构函数去释放所有旧表的节点!==
==所以我们可以想办法利用一下旧的节点!——但是绝不是像上面的一样!直接将新表指向桶!这样子旧导致一个桶被指向连词,如果不将旧表置空还会导致节点析构两次!==
if(_n == _Bucket.size())//我们这边就简单写死
{
//方法二,将旧表的节点全部拿出来,然后重新插入到新表中
std::vector<Node*> newBucket;
newBucket.resize(_Bucket.size()*2, nullptr);
for(int i = 0;i< _Bucket.size();i++)
{
Node* cur = _Bucket[i];
while(cur)
{
Node* next = cur->_next;
size_t hashi = Hash()(cur->_kv.first) % newBucket.size();
//cur是新插入的节点!要进行头插!变成新的头结点
//nweBucket[hashi]是原来的头结点!要变成新的头结点的next
cur->_next = newBucket[hashi];
//不用担心第一次是空的,即使第一次是nullptr,也不会出错!
newBucket[hashi] = cur;//变成新的头结点
cur = next;
}
_Bucket[i] = nullptr;
}
_Bucket.swap(newBucket);
}
==这样我们既避免了重新new新的节点!也避免了去调用哈希表的析构函数!==
关于表扩容的大小
有一个说法,就是如果表的大小是一个素数,那么就可以在取模的时候让数据更加的分散
而在STL库的早期实现中也确实采用了这个说法
==我们可以看到因为每一次扩容如果要算下一个素数很麻烦!所以STL库干脆给了一个素数表!然后通过lower_bound每次去找到比n更大的素数然后返回,而到最后如果到最大值了,就不再进行扩容!而是返回最大值!==
inline unsigned long __stl_next_prime(unsigned long n)
{
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291
};//是一种
for(int i = 0;i<__stl_num_primes;i++)
{
if(__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return __stl_prime_list[__stl_num_primes-1];//如果已经最大了就返回最后的值!
}
==也不用担心到了最大了但是不扩容的问题!因为最大了有42亿9千万多个数据!即使我们按最小的字节来计算也就是42亿9千万多个字节——也就是 4G==——而如果每个数据是整形(4字节)那么就是16G的数据!——而且不止有这个这个16G的数据还有每个节点还有一个指针!(我们按32位来计算)——那么就是**(4+4)*4 = 32G**的数据!那么光是节点的数据就是非常的恐怖了,何况还有挂节点的数组的大小
所以几乎不用担心,哈希表可能会怎么大!
#include <iostream>
#include <vector>
namespace MySTL
{
template<class K,class V,class Hash = HashFunc<K>>
class HashBucket
{
typedef HashNode<K, V> Node;
public:
bool Insert(const std::pair<K,V> &kv)
{
if(Find(kv.first))
return false;
size_t hashi = Hash()(kv.first) % _Bucket.size();
if(_n == _Bucket.size())//我们这边就简单写死
{
std::vector<Node*> newBucket;
//然后就简单的修一下一下扩容
newBucket.resize( __stl_next_prime(_Bucket.size()), nullptr);
for(int i = 0;i< _Bucket.size();i++)
{
Node* cur = _Bucket[i];
while(cur)
{
//.....
}
}
_Bucket.swap(newBucket);//交换两个表
}
//......
}
private:
std::vector<Node*> _Bucket;
size_t _n;
};
}
如果实在害怕链表挂的太长了!情况过于极端!还可以在超过一定长度之后将链表改成红黑树!我们既然可以挂单链表,自然也是可以挂红黑树的!(像是java的处理就是在链表长度超过8就将链表改成红黑树!)这样子就是最极端的情况就是退化为log2(N)
拷贝构造
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<class K,class V,class Hash = HashFunc<K>>
class HashBucket
{
typedef HashNode<K, V> Node;
public:
HashBucket(const HashBucket<K,V,Hash>& ht)//拷贝构造
{
_Bucket.resize(ht._Bucket.size());
for(size_t i = 0;i < ht._Bucket.size();++i)
{
Node* cur = ht._Bucket[i];
while(cur)//不断插入的这个链表的节点!
{
Insert(cur->_kv);
cur = cur->_next;
}
}
}
private:
std::vector<Node*> _Bucket;
size_t _n;
};
}
Erase
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
template<class K,class V,class Hash = HashFunc<K>>
class HashBucket
{
typedef HashNode<K, V> Node;
public:
bool Erase(const K& key)
{
//开散列不能像闭散列一样Find然后删除!因为这是一个单链表!要找到前一个节点才能删除!
size_t hashi = Hash()(key) % _Bucket.size();
Node* cur = _Bucket[hashi];
Node* curprev = nullptr;
while(cur)
{
//找到后开始准备删除
if(cur->_kv.first == key)
{
if(cur == _Bucket[hashi]) //如果删除的是头结点!
{
_Bucket[hashi] = cur->_next;
}
else
{
curprev->_next = cur->_next;
}
delete cur;
_n--;
return true;
}
cur = cur->_next;
curprev = cur;
}
return false;
}
private:
std::vector<Node*> _Bucket;
size_t _n;
};
}
12