


4. unordered_map的元素访问


注意:unordered_map中key是不能重复的,因此count函数的返回值最大为1


#include
#include
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(rand()); // N比较大时,重复值比较多
//v.push_back(rand()+i); // 重复值相对少
v.push_back(i); // 没有重复,有序
}
size_t begin1 = clock();
for (auto e : v)
{
s.insert(e);
}
size_t end1 = clock();
cout << "set insert:" << end1 - begin1 << endl;
size_t begin2 = clock();
for (auto e : v)
{
us.insert(e);
}
size_t end2 = clock();
cout << "unordered_set insert:" << end2 - begin2 << endl;
size_t begin3 = clock();
for (auto e : v)
{
s.find(e);
}
size_t end3 = clock();
cout << "set find:" << end3 - begin3 << endl;
size_t begin4 = clock();
for (auto e : v)
{
us.find(e);
}
size_t end4 = clock();
cout << "unordered_set find:" << end4 - begin4 << endl << endl;
cout << "插入数据个数:" << s.size() << endl;
cout << "插入数据个数:" << us.size() << endl << endl;
size_t begin5 = clock();
for (auto e : v)
{
s.erase(e);
}
size_t end5 = clock();
cout << "set erase:" << end5 - begin5 << endl;
size_t begin6 = clock();
for (auto e : v)
{
us.erase(e);
}
size_t end6 = clock();
cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
return 0;
}
根据测试结果可以得出以下结论:
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O( N N N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一 一映射的关系,那么在查找时通过该函数可以很快找到该元素。
映射的关系:
当向该结构中:
插入元素
搜索元素
删除元素
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

对于两个数据元素的关键字 k i k_i ki和 k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) ==Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
发生哈希冲突该如何处理呢?
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
解决哈希冲突两种常见的方法是:闭散列和开散列
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
通过哈希函数获取待插入元素在哈希表中的位置
通过哈希函数获取待插入元素在哈希表中的位置 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素

我们将数据插入到有限的空间,那么空间中的元素越多,插入元素时产生冲突的概率也就越大,冲突多次后插入哈希表的元素,在查找时的效率必然也会降低。介于此,哈希表当中引入了负载因子(载荷因子):
负载因子 = 表中有效数据个数 / 空间的大小
但负载因子越小,也就意味着空间的利用率越低,此时大量的空间实际上都被浪费了。对于闭散列(开放定址法)来说,负载因子是特别重要的因素,一般控制在0.7~0.8以下,超过0.8会导致在查表时CPU缓存不命中(cache missing)按照指数曲线上升。
因此,一些采用开放定址法的hash库,如JAVA的系统库限制了负载因子为0.75,当超过该值时,会对哈希表进行增容。
对于2.1中如果要插入44,产生冲突,使用解决后的情况为:

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任 何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在 搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出 必须考虑增容。
因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
线性探测的优点:实现非常简单。
线性探测的缺点:一旦发生冲突,所有的冲突连在一起,容易产生数据“堆积”,即不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要多次比较(踩踏效应),导致搜索效率降低。
注意: 在将原哈希表的数据插入到新哈希表的过程中,不能只是简单的将原哈希表中的数据对应的挪到新哈希表中,而是需要根据新哈希表的大小重新计算每个数据在新哈希表中的位置,然后再进行插入。
将键值对插入哈希表的具体步骤如下:
我们可以用枚举定义这三个状态
// 状态
enum Status
{
EMPTY, // 空
EXIST, // 存在
DELETE // 删除
};
闭散列的哈希表中的每个位置存储的结构,应该包括所给数据和该位置的当前状态。
//哈希表每个位置存储的结构
template<class K, class V>
struct HashData
{
pair<K, V> _kv; // 键值对
Status _status = EMPTY; // 状态
};
而为了在插入元素时好计算当前哈希表的负载因子,我们还应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容。
//哈希表
template<class K, class V>
class HashTable
{
public:
//...
private:
vector<HashData<K, V>> _tables; //哈希表
size_t _n = 0; //哈希表中的有效元素个数
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv; // 键值对
Status _status; // 状态
};
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
// 特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key)
{
hash *= 31; // BKDR
hash += e;
}
cout << key << ":" << hash << endl;
return hash;
}
};
// 插入方法
bool Insert(const pair<K, V>& kv)
{
// 1、查看哈希表中是否存在该键值的键值对
if (Find(kv.first))// 哈希表中已经存在该键值的键值对(不允许数据冗余)
return false;
//2、判断是否需要调整哈希表的大小
if (_tables.size() == 0)
_tables.resize(10);
else if (_n * 10 / _tables.size() == 7)// 负载因子大于0.7需要增容
{
// 2倍扩容
size_t newSize = _tables.size() * 2;
//a、创建一个新的哈希表,新哈希表的大小设置为原哈希表的2倍
HashTable<K, V, HashFun> newHT;
newHT._tables.resize(newSize);
//b、遍历旧表,将原哈希表当中的数据插入到新哈希表
for (size_t i = 0; i < _tables.size(); i++)
{
// 如果_tables[i]的位置有数据就进行再次映射
if (_tables[i]._status == EXIST)
{
newHT.Insert(_tables[i]._kv);
}
// c、与旧表进行交换
_tables.swap(newHT._tables);
}
}// 扩容 end...
HashFun hf; // 对于int来说是直接用值来比较,对于string类型使用BKDR方法来比较
// 3、将键值对插入哈希表
// a、通过哈希函数计算哈希地址,线性探测
size_t hashi = hf(kv.first) % _tables.size(); // 除数不能是capacity
size_t index = hashi, i = 1;
//b、找到一个状态为EMPTY或DELETE的位置
while (_tables[hashi]._status == EXIST)
{
index = hashi + i; // 线性探测
index = hashi + i * i; // 二次探测
hashi %= _tables.size(); // 防止下标超出哈希表范围
i++;
}
//c、将数据插入该位置,并将该位置的状态设置为EXIST
_tables[hashi]._kv = kv;
_tables[hashi]._status = EXIST;
//4、哈希表中的有效元素个数++
++_n;
return true;
}
在哈希表中查找数据的步骤如下:
// 查找方法
HashData<K, V>* Find(const K& key)
{
if (_tables.size() == 0)
return nullptr;
HashFun hf;
// 计算位置
size_t hashi = hf(key) % _tables.size();
size_t index = hashi, i = 1;
// 不为空就一直找
while (_tables[hashi]._status != EMPTY)
{
//若该位置的状态为EXIST,并且key值匹配,则查找成功
if (_tables[hashi]._status == EXIST
&& _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
index = hashi + i; // 线性探测
// index = hashi + i * i; // 二次探测
hashi %= _tables.size(); // //防止下标超出哈希表范围
++i;
}
// 找不到的情况
return nullptr;
}
在哈希表中删除数据的步骤如下:
// 伪删除法
bool Erase(const K& key)
{
//1、查看哈希表中是否存在该键值的键值对
HashData<K, V>* res = Find(key);
if (res)
{
//2、若存在,则将该键值对所在位置的状态改为DELETE即可
res->_status = DELETE;
--_n; //3、哈希表中的有效元素个数减一
return true; // 删除成功
}
return false; // 删除失败
}

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。
闭散列的开放定址法,负载因子不能超过1,一般建议控制在[0.0, 0.7]之间。
开散列的哈希桶,负载因子可以超过1,一般建议控制在[0.0, 1.0]之间。
哈希桶的极端情况就是,所有元素全部产生冲突,最终都放到了同一个哈希桶中,此时该哈希表增删查改的效率就退化成了 O ( N ) O ( N ) O(N)


为了避免出现这种极端情况,当桶当中的元素个数超过一定长度,有些地方就会选择将该桶中的单链表结构换成红黑树结构,比如在JAVA中比较新一点的版本中,当桶当中的数据个数超过8时,就会将该桶当中的单链表结构换成红黑树结构,而当该桶当中的数据个数减少到8或8以下时,又会将该桶当中的红黑树结构换回单链表结构。
但有些地方也会选择不做此处理,因为随着哈希表中数据的增多,该哈希表的负载因子也会逐渐增大,最终会触发哈希表的增容条件,此时该哈希表当中的数据会全部重新插入到另一个空间更大的哈希表,此时同一个桶当中冲突的数据个数也会减少,因此不做处理问题也不大。
template<class K,class V>
struct HashData
{
HashData<K, V>* _next;
pair<K, V> _kv;
// 构造
HashData(const pair<K,V> &kv)
:_kv(kv)
,_next(nullptr)
{}
};
与闭散列的哈希表不同的是,在实现开散列的哈希表时,我们不用为哈希表中的每个位置设置一个状态字段,因为在开散列的哈希表中,我们将哈希地址相同的元素都放到了同一个哈希桶中,并不需要经过探测寻找所谓的“下一个位置”。
哈希表的开散列实现方式,在插入数据时也需要根据负载因子判断是否需要增容,所以我们也应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容。
//哈希表
template<class K, class V>
class HashTable
{
public:
//...
private:
vector<Node*> _table; //哈希表
size_t _n = 0; //哈希表中的有效元素个数
};
向哈希表中插入数据的步骤如下:

bool Insert(const pair<K, V>& kv)
{
// 1、查看哈希表中是否存在该键值的键值对
if (Find(kv))
return false;
Hash hf;
// 2、判断是否需要调整哈希表的大小
if (_n == _tables.size())// 哈希表的大小为0,或负载因子超过1
{
//增容
//a、创建一个新的哈希表,新哈希表的大小设置为原哈希表的2倍(若哈希表大小为0,则将哈希表的初始大小设置为10)
vector<Node*> newTables;
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
newTables.resize(newsize, nullptr);
// b、将原哈希表当中的结点插入到新哈希表
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])// 桶不为空
{
Node* cur = _tables[i]; //将该桶的结点取完为止
while (cur)
{
Node* next = cur->_next; //记录cur的下一个结点
size_t index = hf(cur->_kv.first) % newTables.size(); // 通过哈希函数计算出对应的哈希桶编号index cur->_next = newTables[index];
newTables[index] = cur; // 将该结点头插到新哈希表中编号为index的哈希桶中
cur = next; // 取原哈希表中该桶的下一个结点
}
_tables[i] = nullptr; // 该桶取完后将该桶置空
}
}
}
size_t hashi = hf(kv.first) % _tables.size();
Node* newnode = new Node(kv);
// 头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
在哈希表中查找数据的步骤如下:
Node* Find(const K& key)
{
Hash hf; // 通过仿函数
if (_tables.size() == 0) // 哈希表大小为0,查找失败
return nullptr;
size_t hashi = hf(key) % _tables.size(); // 通过哈希函数计算出对应的哈希桶编号
// 遍历哈希桶
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
在哈希表中删除数据的步骤如下:
bool Erase(const K& key)
{
Hash hf;
//1、通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)
size_t hashi = hf(key) % _tables.size();
//2、在编号为index的哈希桶中寻找待删除结点
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
//3、若找到了待删除结点,则删除该结点
if (cur->_kv.first == key)
{
if (prev == nullptr) //待删除结点是哈希桶中的第一个结点
{
_tables[hashi] = cur->_next; // 将第一个结点从该哈希桶中移除
}
else // 待删除结点不是哈希桶的第一个结点
{
prev->_next = cur->_next; // 将该结点从哈希桶中移除
}
delete cur;
--_n; // 4、删除结点后,将哈希表中的有效元素个数减一
return true;
}
// 继续往后找
prev = cur;
cur = cur->_next;
}
return false;
}
#pragma once
#include
#include
#include
#include
#include
using namespace std;
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
// 特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key)
{
hash *= 31; // BKDR
hash += e;
}
cout << key << ":" << hash << endl;
return hash;
}
};
namespace lsl_open_address
{
// 状态
enum Status
{
EMPTY, // 空
EXIST, // 存在
DELETE // 删除
};
//哈希表每个位置存储的结构
template<class K, class V>
struct HashData
{
pair<K, V> _kv; // 键值对
Status _status = EMPTY; // 状态
};
template<class K, class V, class HashFun = HashFunc<K>>
class HashTable
{
public:
HashTable()
{
_tables.resize(10);
}
// 插入方法
bool Insert(const pair<K, V>& kv)
{
// 1、查看哈希表中是否存在该键值的键值对
if (Find(kv.first))// 哈希表中已经存在该键值的键值对(不允许数据冗余)
return false;
//2、判断是否需要调整哈希表的大小
if (_tables.size() == 0)
_tables.resize(10);
else if (_n * 10 / _tables.size() == 7)// 负载因子大于0.7需要增容
{
// 2倍扩容
size_t newSize = _tables.size() * 2;
//a、创建一个新的哈希表,新哈希表的大小设置为原哈希表的2倍
HashTable<K, V, HashFun> newHT;
newHT._tables.resize(newSize);
//b、遍历旧表,将原哈希表当中的数据插入到新哈希表
for (size_t i = 0; i < _tables.size(); i++)
{
// 如果_tables[i]的位置有数据就进行再次映射
if (_tables[i]._status == EXIST)
{
newHT.Insert(_tables[i]._kv);
}
// c、与旧表进行交换
_tables.swap(newHT._tables);
}
}// 扩容 end...
HashFun hf; // 对于int来说是直接用值来比较,对于string类型使用BKDR方法来比较
// 3、将键值对插入哈希表
// a、通过哈希函数计算哈希地址,线性探测
size_t hashi = hf(kv.first) % _tables.size(); // 除数不能是capacity
size_t index = hashi, i = 1;
//b、找到一个状态为EMPTY或DELETE的位置
while (_tables[hashi]._status == EXIST)
{
index = hashi + i; // 线性探测
index = hashi + i * i; // 二次探测
hashi %= _tables.size(); // 防止下标超出哈希表范围
i++;
}
//c、将数据插入该位置,并将该位置的状态设置为EXIST
_tables[hashi]._kv = kv;
_tables[hashi]._status = EXIST;
//4、哈希表中的有效元素个数++
++_n;
return true;
}
// 查找方法
HashData<K, V>* Find(const K& key)
{
if (_tables.size() == 0)
return nullptr;
HashFun hf;
// 计算位置
size_t hashi = hf(key) % _tables.size();
size_t index = hashi, i = 1;
// 不为空就一直找
while (_tables[hashi]._status != EMPTY)
{
//若该位置的状态为EXIST,并且key值匹配,则查找成功
if (_tables[hashi]._status == EXIST
&& _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
index = hashi + i; // 线性探测
// index = hashi + i * i; // 二次探测
hashi %= _tables.size(); // //防止下标超出哈希表范围
++i;
}
// 找不到的情况
return nullptr;
}
// 伪删除法
bool Erase(const K& key)
{
//1、查看哈希表中是否存在该键值的键值对
HashData<K, V>* res = Find(key);
if (res)
{
//2、若存在,则将该键值对所在位置的状态改为DELETE即可
res->_status = DELETE;
--_n; //3、哈希表中的有效元素个数减一
return true; // 删除成功
}
return false; // 删除失败
}
void Print()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i]._status == EXIST)
{
// printf("[%d]->%d\n", i, _tables[i].first);
cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;
}
else if (_tables[i]._status == EMPTY)
{
printf("[%d]->\n", i);
}
else
{
printf("[%d]->E\n", i);
}
}
cout << endl;
}
private:
vector<HashData<K, V>> _tables;
size_t _n = 0;// 存储的关键字的个数
};
}
namespace lsl_hash_bucket
{
template<class K,class V>
struct HashData
{
HashData<K, V>* _next;
pair<K, V> _kv;
// 构造
HashData(const pair<K,V> &kv)
:_kv(kv)
,_next(nullptr)
{}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashData<K, V> Node;
public:
HashTable()
{
_tables.resize(10);
}
~HashTable()
{
for (int i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
Node* Find(const K& key)
{
Hash hf; // 通过仿函数
if (_tables.size() == 0) // 哈希表大小为0,查找失败
return nullptr;
size_t hashi = hf(key) % _tables.size(); // 通过哈希函数计算出对应的哈希桶编号
// 遍历哈希桶
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
bool Insert(const pair<K, V>& kv)
{
// 1、查看哈希表中是否存在该键值的键值对
if (Find(kv))
return false;
Hash hf;
// 2、判断是否需要调整哈希表的大小
if (_n == _tables.size())// 哈希表的大小为0,或负载因子超过1
{
//增容
//a、创建一个新的哈希表,新哈希表的大小设置为原哈希表的2倍(若哈希表大小为0,则将哈希表的初始大小设置为10)
vector<Node*> newTables;
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
newTables.resize(newsize, nullptr);
// b、将原哈希表当中的结点插入到新哈希表
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])// 桶不为空
{
Node* cur = _tables[i]; //将该桶的结点取完为止
while (cur)
{
Node* next = cur->_next; //记录cur的下一个结点
size_t index = hf(cur->_kv.first) % newTables.size(); // 通过哈希函数计算出对应的哈希桶编号index cur->_next = newTables[index];
newTables[index] = cur; // 将该结点头插到新哈希表中编号为index的哈希桶中
cur = next; // 取原哈希表中该桶的下一个结点
}
_tables[i] = nullptr; // 该桶取完后将该桶置空
}
}
}
size_t hashi = hf(kv.first) % _tables.size();
Node* newnode = new Node(kv);
// 头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
bool Erase(const K& key)
{
Hash hf;
//1、通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)
size_t hashi = hf(key) % _tables.size();
//2、在编号为index的哈希桶中寻找待删除结点
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
//3、若找到了待删除结点,则删除该结点
if (hf(cur->_kv.first) == key)
{
if (prev == nullptr) //待删除结点是哈希桶中的第一个结点
{
_tables[hashi] = cur->_next; // 将第一个结点从该哈希桶中移除
}
else // 待删除结点不是哈希桶的第一个结点
{
prev->_next = cur->_next; // 将该结点从哈希桶中移除
}
delete cur;
--_n; // 4、删除结点后,将哈希表中的有效元素个数减一
return true;
}
// 继续往后找
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _tables;
size_t _n = 0;
};
}
unordered_set是K模型的容器,而unordered_map是KV模型的容器
template<class K, class T>
class HashTable
unordered_set容器,那么传入哈希表的模板参数就是key和key。template<class K>
class unordered_set
{
public:
//...
private:
HashTable<K, K> _ht; //传入底层哈希表的是K和K
};
unordered_map容器,那么传入哈希表的模板参数就是key以及key和value构成的键值对。template<class K, class V>
class unordered_map
{
public:
//...
private:
HashTable<K, pair<K, V>> _ht; //传入底层哈希表的是K以及K和V构成的键值对
};
而哈希结点的模板参数也应该由原来的K、V变为T:
更改模板参数后,哈希结点的定义如下:
//哈希结点的定义
template<class K, class T>
struct HashData
{
HashData<T>* _next;
T _data;
// 构造
HashData(const T& data)
:_data(data)
, _next(nullptr)
{}
};
template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
struct __HTIterator
{
typedef HashNode<T> Node; //哈希结点的类型
typedef HashTable<K, T, KeyOfT, HashFunc> HT; //哈希表的类型
typedef __HTIterator<K, T, KeyOfT, HashFunc> Self; //正向迭代器的类型
Node* _node; //结点指针
HT* _pht; //哈希表的地址
// 构造
__HTIterator(Node* node,HT* ht)
:_node(node)
,_pht(ht)
{}
};
T& operator*()
{
//返回哈希结点中数据的引用
return _node->_data;
}
T* operator->()
{
//返回哈希结点中数据的地址
return &_node->_data;
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._node;
}
//前置++
Self& operator++()
{
if (_node->_next) //该结点不是当前哈希桶中的最后一个结点
{
_node = _node->_next; //++后变为当前哈希桶中的下一个结点
}
else //该结点是当前哈希桶中的最后一个结点
{
KeyOfT kot;
HashFunc hf;
size_t index = hf(kot(_node->_data)) % _pht->_table.size(); //通过哈希函数计算出当前所处哈希桶编号index(除数不能是capacity)
index++; //从下一个位置开始找一个非空的哈希桶
while (index < _pht->_table.size()) //直到将整个哈希表找完
{
if (_pht->_table[index]) //当前哈希桶非空
{
_node = _pht->_table[index]; //++后变为当前哈希桶中的第一个结点
return *this;
}
index++; //当前哈希桶为空桶,找下一个哈希桶
}
_node = nullptr; //哈希表中已经没有空桶了,++后变为nullptr
}
return *this;
}
#pragma once
#include
#include
#include
using namespace std;
// 使用素数
size_t GetNextPrime(size_t prime)
{
const int PRIMECOUNT = 28;
static const size_t primeList[PRIMECOUNT] =
{
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul,805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
size_t i = 0;
for (; i < PRIMECOUNT; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}
template<class K>
struct Hash
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
// 特化
template<>
struct Hash<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key)
{
hash *= 31; // BKDR
hash += e;
}
cout << key << ":" << hash << endl;
return hash;
}
};
namespace lsl_open_address
{
// 状态
enum Status
{
EMPTY, // 空
EXIST, // 存在
DELETE // 删除
};
//哈希表每个位置存储的结构
template<class K, class V>
struct HashData
{
pair<K, V> _kv; // 键值对
Status _status = EMPTY; // 状态
};
template<class K, class V, class HashFun = Hash<K>>
class HashTable
{
public:
HashTable()
{
_tables.resize(10);
}
// 插入方法
bool Insert(const pair<K, V>& kv)
{
// 1、查看哈希表中是否存在该键值的键值对
if (Find(kv.first))// 哈希表中已经存在该键值的键值对(不允许数据冗余)
return false;
//2、判断是否需要调整哈希表的大小
if (_tables.size() == 0)
_tables.resize(10);
else if (_n * 10 / _tables.size() == 7)// 负载因子大于0.7需要增容
{
// 2倍扩容
size_t newSize = _tables.size() * 2;
//a、创建一个新的哈希表,新哈希表的大小设置为原哈希表的2倍
HashTable<K, V, HashFun> newHT;
newHT._tables.resize(newSize);
//b、遍历旧表,将原哈希表当中的数据插入到新哈希表
for (size_t i = 0; i < _tables.size(); i++)
{
// 如果_tables[i]的位置有数据就进行再次映射
if (_tables[i]._status == EXIST)
{
newHT.Insert(_tables[i]._kv);
}
// c、与旧表进行交换
_tables.swap(newHT._tables);
}
}// 扩容 end...
HashFun hf; // 对于int来说是直接用值来比较,对于string类型使用BKDR方法来比较
// 3、将键值对插入哈希表
// a、通过哈希函数计算哈希地址,线性探测
size_t hashi = hf(kv.first) % _tables.size(); // 除数不能是capacity
size_t index = hashi, i = 1;
//b、找到一个状态为EMPTY或DELETE的位置
while (_tables[hashi]._status == EXIST)
{
index = hashi + i; // 线性探测
index = hashi + i * i; // 二次探测
hashi %= _tables.size(); // 防止下标超出哈希表范围
i++;
}
//c、将数据插入该位置,并将该位置的状态设置为EXIST
_tables[hashi]._kv = kv;
_tables[hashi]._status = EXIST;
//4、哈希表中的有效元素个数++
++_n;
return true;
}
// 查找方法
HashData<K, V>* Find(const K& key)
{
if (_tables.size() == 0)
return nullptr;
HashFun hf;
// 计算位置
size_t hashi = hf(key) % _tables.size();
size_t index = hashi, i = 1;
// 不为空就一直找
while (_tables[hashi]._status != EMPTY)
{
//若该位置的状态为EXIST,并且key值匹配,则查找成功
if (_tables[hashi]._status == EXIST
&& _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
index = hashi + i; // 线性探测
// index = hashi + i * i; // 二次探测
hashi %= _tables.size(); // //防止下标超出哈希表范围
++i;
}
// 找不到的情况
return nullptr;
}
// 伪删除法
bool Erase(const K& key)
{
//1、查看哈希表中是否存在该键值的键值对
HashData<K, V>* res = Find(key);
if (res)
{
//2、若存在,则将该键值对所在位置的状态改为DELETE即可
res->_status = DELETE;
--_n; //3、哈希表中的有效元素个数减一
return true; // 删除成功
}
return false; // 删除失败
}
void Print()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i]._status == EXIST)
{
// printf("[%d]->%d\n", i, _tables[i].first);
cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;
}
else if (_tables[i]._status == EMPTY)
{
printf("[%d]->\n", i);
}
else
{
printf("[%d]->E\n", i);
}
}
cout << endl;
}
private:
vector<HashData<K, V>> _tables;
size_t _n = 0;// 存储的关键字的个数
};
}
namespace lsl_hash_bucket
{
template<class T>
struct HashNode
{
HashNode<T>* _next;
T _data;
// 构造
HashNode(const T& data)
:_data(data)
, _next(nullptr)
{}
};
// 前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct __HTIterator
{
typedef HashNode<T> Node; // 哈希节点的类型
typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self; // 迭代器的类型
const HashTable<K, T, KeyOfT, Hash>* _pht; // 哈希表的地址
Node* _node; // 节点指针
size_t _hashi;
// 构造
__HTIterator(Node* node, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
:_node(node)
, _pht(pht)
, _hashi(hashi)
{}
__HTIterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
:_node(node)
, _pht(pht)
, _hashi(hashi)
{}
Ref operator*()
{
//返回哈希结点中数据的引用
return _node->_data;
}
Ptr operator->()
{
//返回哈希结点中数据的地址
return &_node->_data;
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._node;
}
//前置++
Self& operator++()
{
if (_node->_next)
{
// 当前桶还有节点,走到下一个节点
_node = _node->_next;
}
else
{
// 当前桶已经走完了,找下一个桶开始
++_hashi;
while (_hashi < _pht->_tables.size())
{
if (_pht->_tables[_hashi])
{
_node = _pht->_tables[_hashi];
break;
}
++_hashi;
}
if (_hashi == _pht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
};
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
typedef HashNode<T> Node;
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
friend struct __HTIterator;
public:
typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;
iterator begin()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])
{
return iterator(_tables[i], this, i);
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this, -1);
}
const_iterator begin() const
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])
{
return const_iterator(_tables[i], this, i);
}
}
return end();
}
const_iterator end() const
{
return const_iterator(nullptr, this, -1);
}
HashTable()
{
_tables.resize(GetNextPrime(0));
}
~HashTable()
{
//将哈希表当中的结点一个个释放
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])
{
Node* cur = _tables[i];
while (cur) //将该桶的结点取完为止
{
Node* next = cur->_next; //记录下一个结点
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
}
//赋值运算符重载函数
HashTable& operator=(HashTable ht)
{
//交换哈希表中两个成员变量的数据
_tables.swap(ht._table);
swap(_n, ht._n);
return *this;
}
iterator Find(const K& key)
{
Hash hf;
KeyOfT kot;
size_t hashi = hf(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur, this, hashi);
}
cur = cur->_next;
}
return end();
}
pair<iterator, bool> Insert(const T& data)
{
Hash hf;
KeyOfT kot;
iterator it = Find(kot(data));
if (it != end())
return make_pair(it, false);
// 负载因子最大到1
if (_n == _tables.size())
{
vector<Node*> newTables;
newTables.resize(_tables.size() * 2, nullptr);
// 遍历旧表
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
// 挪动到映射的新表
size_t hashi = hf(kot(cur->_data)) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = hf(kot(data)) % _tables.size();
Node* newnode = new Node(data);
// 头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return make_pair(iterator(newnode, this, hashi), true);
}
bool Erase(const K& key)
{
Hash hf; // hf是要算的hash值,这里采用BKDR
KeyOfT kot; // kot是要怎么取值,比如set是直接就是key,map是要取pair的first
//1、通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)
size_t hashi = hf(kot(key)) % _tables.size();
//2、在编号为index的哈希桶中寻找待删除结点
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
//3、若找到了待删除结点,则删除该结点
if (kot(cur->_data) == key)
{
if (prev == nullptr) //待删除结点是哈希桶中的第一个结点
{
_tables[hashi] = cur->_next; // 将第一个结点从该哈希桶中移除
}
else // 待删除结点不是哈希桶的第一个结点
{
prev->_next = cur->_next; // 将该结点从哈希桶中移除
}
delete cur;
--_n; // 4、删除结点后,将哈希表中的有效元素个数减一
return true;
}
// 继续往后找
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _tables;
size_t _n = 0;
};
}
#include"HashTable.h"
namespace lsl
{
template<class K, class Hash = Hash<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
//现在没有实例化,没办法到HashTable里面找iterator,所以typename就是告诉编译器这里是一个类型,实例化以后再去取
typedef typename lsl_hash_bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;
typedef typename lsl_hash_bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator const_iterator;
const_iterator begin() const
{
return _ht.begin();
}
const_iterator end() const
{
return _ht.end();
}
iterator find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
pair<const_iterator, bool> insert(const K& key)
{
auto ret = _ht.Insert(key);
return pair<const_iterator, bool>(const_iterator(ret.first._node, ret.first._pht, ret.first._hashi), ret.second);
}
private:
lsl_hash_bucket::HashTable<K, K, SetKeyOfT, Hash> _ht;
};
}
#include"HashTable.h"
namespace lsl
{
template<class K, class V, class Hash = Hash<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename lsl_hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
const V& operator[](const K& key) const
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
iterator find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
lsl_hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};
}
// 使用素数
size_t GetNextPrime(size_t prime)
{
const int PRIMECOUNT = 28;
static const size_t primeList[PRIMECOUNT] =
{
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul,805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
size_t i = 0;
for (; i < PRIMECOUNT; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}

所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
映射x的时候:
x在数组的第几个整形呢? i = x / 32x在数组的第几个位呢? i = x % 32在左移的时候不是方向,不管是大端机还是小端机,左移是向高位移动

// 将x比特位置1
void set(size_t x)
{
// 计算第几个整形
size_t i = x / 32;
// 计算第几个位
size_t j = x % 32;
// 将第j位处理成1其他位不变
_bits[i] |= (1 << j);
}


// 将x比特位置0
void reset(size_t x)
{
// 计算第几个整形
size_t i = x / 32;
// 计算第几个位
size_t j = x % 32;
// 将第j位处理成0其他位不变
_bits[i] &= ~(1 << j);
}

// 检测位图中x是否为1
bool test(size_t x)
{
// 计算第几个整形
size_t i = x / 32;
// 计算第几个位
size_t j = x % 32;
// 检测第j位是否为1
return _bits[i] & (1 << j);
}
#pragma once
#include
#include
using namespace std;
namespace lsl
{
template<size_t N>
class bitset
{
public:
// 构造
bitset()
{
// _bits.resize((N >> 5) + 1, 0); // 可以这样写,但是要注意优先级
_bits.resize(N / 32 + 1, 0);
}
// 将x比特位置1
void set(size_t x)
{
// 计算第几个整形
// size_t i = x >> 5; // 也可以这样写
size_t i = x / 32;
// 计算第几个位
size_t j = x % 32;
// 将第j位处理成1其他位不变
_bits[i] |= (1 << j);
}
// 将x比特位置0
void reset(size_t x)
{
// 计算第几个整形
size_t i = x / 32;
// 计算第几个位
size_t j = x % 32;
// 将第j位处理成0其他位不变
_bits[i] &= ~(1 << j);
}
// 检测位图中x是否为1
bool test(size_t x)
{
// 计算第几个整形
size_t i = x / 32;
// 计算第几个位
size_t j = x % 32;
// 检测第j位是否为1
return _bits[i] & (1 << j);
}
private:
vector<int> _bits;
};
}



我们这里可以大概估算一下,如果使用3个哈希函数,即k的值为3,值我们取0.7,那么 m m m 和 n n n 的关系大概是 m = 4 × n m = 4 × n m=4×n,也就是布隆过滤器的长度应该是插入元素个数的4倍。
首先,布隆过滤器可以实现为一个模板类,因为插入布隆过滤器的元素不仅仅是字符串,也可以是其他类型的数据,只有调用者能够提供对应的哈希函数将该类型的数据转换成整型即可,但一般情况下布隆过滤器都是用来处理字符串的,所以这里可以将模板参数K的缺省类型设置为string。
布隆过滤器中的成员一般也就是一个位图,我们可以在布隆过滤器这里设置一个非类型模板参数N,用于让调用者指定位图的长度。
//布隆过滤器
template<size_t N, class K = string, class HashFunc1 = BKDRHash, class HashFunc2 = APHash, class HashFunc3 = DJBHash>
class BloomFilter
{
public:
//...
private:
lsl::bitset<N> _bs;
};
实例化布隆过滤器时需要调用者提供三个哈希函数,由于布隆过滤器一般处理的是字符串类型的数据,因此这里我们可以默认提供几个将字符串转换成整型的哈希函数。
这里选取将字符串转换成整型的哈希函数,是经过测试后综合评分最高的BKDRHash、APHash和DJBHash,这三种哈希算法在多种场景下产生哈希冲突的概率是最小的。
此时本来这三种哈希函数单独使用时产生冲突的概率就比较小,现在要让它们同时产生冲突概率就更小了。
代码:
struct BKDRHash
{
size_t operator()(const string& s)
{
size_t value = 0;
for (auto ch : s)
{
value = value * 131 + ch;
}
return value;
}
};
struct APHash
{
size_t operator()(const string& s)
{
size_t value = 0;
for (size_t i = 0; i < s.size(); i++)
{
if ((i & 1) == 0)
{
value ^= ((value << 7) ^ s[i] ^ (value >> 3));
}
else
{
value ^= (~((value << 11) ^ s[i] ^ (value >> 5)));
}
}
return value;
}
};
struct DJBHash
{
size_t operator()(const string& s)
{
if (s.empty())
return 0;
size_t value = 5381;
for (auto ch : s)
{
value += (value << 5) + ch;
}
return value;
}
};
void Set(const K& key)
{
//计算出key对应的三个位
size_t hash1 = HashFunc1()(key) % N;
size_t hash2 = HashFunc2()(key) % N;
size_t hash3 = HashFunc3()(key) % N;
// 将位图中的这三个位设置成1
_bs.set(hash1);
_bs.set(hash2);
_bs.set(hash3);
}
检测时,需要通过三个哈希函数分别计算出该元素对应的三个比特位,然后判断位图中的这三个比特位是否被设置为1。
bool Test(const T& key)
{
//依次判断key对应的三个位是否被设置
size_t hash1 = HashFunc1()(key) % N;
if (_bs.test(hash1) == false)
return false;
size_t hash2 = HashFunc2()(key) % N;
if (_bs.test(hash1) == false)
return false;
size_t hash3 = HashFunc3()(key) % N;
if (_bs.test(hash1) == false)
return false;
return true; // 可能存在,但是可能存在误判
}
布隆过滤器一般不支持删除操作:
如何让布隆过滤器支持删除?
要让布隆过滤器支持删除,必须要做到以下两点:
可是布隆过滤器最终还是没有提供删除的接口,因为使用布隆过滤器本来就是要节省空间和提高效率的。在删除时需要遍历文件或磁盘中确认待删除元素确实存在,而文件IO和磁盘IO的速度相对内存来说是很慢的,并且为位图中的每个比特位额外设置一个计数器,就需要多用原位图几倍的存储空间,这个代价也是不小的。

比如当我们首次访问某个网站时需要用手机号注册账号,而用户的各种数据实际都是存储在数据库当中的,也就是磁盘上面。
当我们用手机号注册账号时,系统就需要判断你填入的手机号是否已经注册过,如果注册过则会提示用户注册失败。
但这种情况下系统不可能直接去遍历磁盘当中的用户数据,判断该手机号是否被注册过,因为磁盘IO是很慢的,这会降低用户的体验。
这种情况下就可以使用布隆过滤器,将所有注册过的手机号全部添加到布隆过滤器当中,当我们需要用手机号注册账号时,就可以直接去布隆过滤器当中进行查找。

代码实现:
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
if (_bs1.test(x) == false && _bs2.test(x) == false) // 00
{
_bs2.set(x); // _bs1不需要动 _ba2设置成1
}
else // if (_bs1.test(x) == false && _bs2.test(x) == true) // 01
{
_bs1.set(x); // 1
_bs2.reset(x); // 0
}
}
void Print()
{
for (size_t i = 0; i < N; i++)
{
if (_bs1.test(i) == false && _bs2.test(i) == true) // 01 -->出现一次
{
cout << "1->" << i << endl;
}
else if (_bs1.test(i) == true && _bs2.test(i) == false) //10 -->出现两次以上
{
cout << "2->" << i << endl;
}
}
cout << endl;
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
各自映射到一个位图,一个值在两个位图都存在,则是交集
方案一:(一个位图需要512M内存)
一个整数要表示四种状态也是只需要两个位就够了,此时当我们读取到重复的整数时,就可以让其对应的两个位按照00→01→10→11的顺序进行变化,最后状态是01或10的整数就是出现次数不超过2次的整数。
代码实现:
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
if (_bs1.test(x) == false && _bs2.test(x) == false) // 00
{
_bs2.set(x); // _bs1不需要动 _ba2设置成1
}
else if (_bs1.test(x) == false && _bs2.test(x) == true) // 01
{
_bs1.set(x); // 1
_bs2.reset(x); // 0
}
else if (_bs1.test(x) == true && _bs2.test(x) == false) // 10
{
_bs1.set(x); // 1
_bs2.set(x); // 1
}
}
void Print()
{
for (size_t i = 0; i < N; i++)
{
if (_bs1.test(i) == false && _bs2.test(i) == true) // 01 -->出现一次
{
cout << "1->" << i << endl;
}
else if (_bs1.test(i) == true && _bs2.test(i) == false) //10 -->出现两次
{
cout << "2->" << i << endl;
}
}
cout << endl;
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
允许存在一些误判,那么我们就可以用布隆过滤器
先读取其中一个文件当中的query,将其全部映射到一个布隆过滤器当中。
然后读取另一个文件当中的query,依次判断每个query是否在布隆过滤器当中,如果在则是交集,不在则不是交集。

现在要求给出精确算法,那么就不能使用布隆过滤器了,此时需要用到哈希切分。
假设平均每个query为20字节,那么100亿个query就是200G,由于我们只有1G内存,这里可以考虑将一个文件切分成400个小文件。
这里我们将这两个文件分别叫做A文件和B文件,此时我们将A文件切分成了A0~A399共400个小文件,将B文件切分成了B0~B399共400个小文件。
在切分时需要选择一个哈希函数进行哈希切分,以切分A文件为例,切分时依次遍历A文件当中的每个query,通过哈希函数将每个query转换成一个整型 i ( 0 < = i < = 399 ) i (0 <= i <= 399) i(0<=i<=399),然后将这个query写入到小文件Ai当中。对于B文件也是同样的道理,但切分A文件和B文件时必须采用的是同一个哈希函数。

由于切分A文件和B文件时采用的是同一个哈希函数,因此A文件与B文件中相同的query计算出的 i ii 值都是相同的,最终就会分别进入到Ai和Bi文件中,这也是哈希切分的意义。
我们就只需要分别找出A0与B0的交集、A1与B1的交集、…、A399与B399的交集,最终将这些交集和起来就是A文件和B文件的交集。

那各个小文件之间又应该如何找交集呢?
将这些小文件看作一个个的哈希桶,将大文件中的query通过哈希函数映射到这些哈希桶中,如果是相同的query,则会产生哈希冲突进入到同一个小文件中。
经过哈希切分后得到的这些小文件,理论上就能够加载到内存当中了,如果个别小文件仍然太大那可以对其再进行一次哈希切分,总之让最后切分出来的小文件能够加载到内存。

在Linux下我们可以使用此命令来完成:
sort log_txt | uniq -c | sort -nrk1,1 | head -K