总言
主要介绍哈希运用于unordered系列的上层封装框架与相关运用:位图、布隆过滤器、哈希切割。
文章目录
- 总言
- 0、思维导图
- 3、封装
- 3.1、基础封装
- 3.1.1、框架结构
- 3.1.2、Inset 1.0
- 3.2、引入迭代器
- 3.2.1、在迭代器中
- 3.2.2、在哈希表中
- 3.2.3、在unordered上层
- 3.2.4、Insert 2.0 、operator[]
- 3.2.5、验证
- 3.3、整体
- 3.3.1、HashTable
- 3.3.2、unordered_set
- 3.3.3、unordered_map
- 4、应用
- 4.1、位图
- 4.1.1、基本概念介绍
- 4.1.2、常用接口相关实现
- 4.1.2.1、如何构造一个位图?
- 4.1.2.2、set、reset、test
- 4.1.2.3、相关验证
- 4.1.3、扩展运用·海量数据处理
- 4.1.3.1、题一
- 4.1.3.2、题二
- 4.1.3.3、题三
- 4.2、布隆过滤器
- 4.2.1、基本概念介绍
- 4.2.2、相关接口实现
- 4.2.2.1、布隆过滤器的基本框架及哈希函数
- 4.2.2.2、set、test
- 4.2.2.3、相关验证
- 4.2.3、扩展运用:海量数据处理
- 4.2.3.1、问题一:布隆过滤器删除
- 4.2.3.2、问题二:哈希切割
0、思维导图
3、封装
3.1、基础封装
3.1.1、框架结构
类似于红黑树与map、set,我们同样使用一个哈希表,设置不同传入参数,实例化出unordered_map、unordered_set。因此需要修改HashTable的第二参数,同时新增一个模板参数,用于unordered_map、unordered_set的传参。
1)、在哈希表中
template<class T>
struct HashNode
{
T _data;//当前结点存储值
HashNode<T>* _next;//指向下一个结点的指针
HashNode(const T& data)//构造
:_data(data)
,_next(nullptr)
{}
};
template<class K, class T, class Hash, class KeyOfT>
class HashTable
{
typedef HashNode<T> Node;
public:
//……
//其它函数
private:
vector<Node*> _table;
size_t _size = 0;
};
2)、在unordered上层
namespace myunorderedset
{
template<class K,class Hash=HashFunc<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
private:
HashBucket::HashTable<K, K, Hash, SetKeyOfT> _ht;
};
}
namespace myunorderedmap
{
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K,V>& kv)
{
return kv.first;
}
};
//……
//其它实现
private:
HashBucket::HashTable<K, pair<K, V>, Hash, MapKeyOfT> _ht;
};
}
3.1.2、Inset 1.0
1)、在哈希表中
Insert、Erase、Find中需要改动如下:实际只增加KeyOfT部分。
Hash HsTrans;
KeyOfT Kot;
size_t hashi = HsTrans(Kot(data)) % _table.size();
,Kot是为了根据unordered_map、unordered_set获取相应Key值,而HsTrans是为了解决取模问题。
bool Insert(const T& data)
{
Hash HsTrans;
KeyOfT Kot;
if (Find(Kot(data)))//若此处使用Find函数,需要注意其参数类型,需要转换
return false;
//0、扩容检查
CheckCapacity();
//1、计算哈希地址
size_t hashi = HsTrans(Kot(data)) % _table.size();
//2、插入值,修改链接关系
Node* newnode = new Node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_size;
return true;
}
Node* Find(const K& key)
{
if (_table.size() == 0)//表中无元素
return nullptr;
Hash HsTrans;
KeyOfT Kot;
size_t hashi = HsTrans(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (Kot(cur->_data) == key)
return cur;
cur = cur->_next;
}
return nullptr;//找不到的情况
}
bool Erase(const K& key)
{
Node* ret = Find(key);
if (ret)//该目标值存在
{
Hash HsTrans;
size_t hashi = HsTrans(key) % _table.size();
Node* prev = nullptr;
Node* cur = _table[hashi];
while (cur != ret)
{
prev = cur;
cur = cur->_next;
}
if (prev)//非头删
prev->_next = cur->_next;
else//头删
_table[hashi] = cur->_next;
delete cur;
--_size;
return true;
}
return false;
}
2)、在unordered上层
bool insert(const K& key)
{
return _ht.Insert(key);
}
bool insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
事实上后续还需要对其返回值做修改。
3.2、引入迭代器
3.2.1、在迭代器中
1)、基本框架搭建
说明:哈希表中,迭代器的遍历需要依次遍历每个哈希桶,又在每个哈希桶中依次遍历其结点,因此我们可以为迭代器设置两个成员,分别对应单个结点以及整个哈希表。需要注意的是,哈希表的迭代器是单向迭代器,只支持operator++
、不支持operator--
。
//前置声明
template<class K, class T, class Hash, class KeyOfT>
class HashTable;
template<class K,class T,class Hash,class KeyOfT>
struct __Hashiterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, Hash, KeyOfT> HT;
Node* _node;//指向哈希桶中单个结点的指针
HT* _pht;//指向哈希表的指针
__Hashiterator(Node* node, HT* pht)//构造
:_node(node)
,_pht(pht)
{}
};
2)、operator*、operator->
T& operator*()
{
return _node->_data;//返回对应节点的存储值
}
T* operator->()
{
return &(_node->_data);//返回对应节点的存储值的地址
}
3)、operator==、operator!=
bool operator==(const Iterator& it) const
{
return it._node == _node;
}
bool operator!=(const Iterator& it) const
{
return it._node != _node;
}
4)、operator++
Iterator& operator++()
{
if (_node->_next) // 当前桶中节点迭代
{
_node = _node->_next;
}
else //需要寻找哈希表中下一个桶
{
Hash HsTrans;
KeyOfT Kot;
size_t hashi = HsTrans(Kot(_node->_data)) % _pht->_table.size();
++hashi;//到下一个哈希桶位置
for (; hashi < _pht->_table.size(); ++hashi)
{
if (_pht->_table[hashi])//当前迭代后的哈希桶中存在有效值
{
_node = _pht->_table[hashi];
break;
}
//如果当前++hashi后获取到的下一个桶不存储有效值,则再次往后迭代寻找哈希桶
}
if (hashi == _pht->_table.size())//若将哈希表都遍历完成,则说明已经没有存储有效数据的哈希桶了
{
_node = nullptr;
}
}
return *this;
}
3.2.2、在哈希表中
1)、基本说明
1、begin返回的是哈希桶中首个有效节点,因此需要遍历哈希表,找存储有效节点的哈希桶。end返回最后一个节点的下一个位置,因此我们使用一个空指针即可。
2、关于迭代器的构造,根据其参数Node* node, HT* pht
,我们要传入节点指针以及哈希表指针,这里指向哈希表的指针我们可以使用this指针。
iterator begin()
{
for (size_t i = 0; i < _table.size(); ++i)
{
if (_talbe[i])
return iterator(_table[i], this);
}
return end();
}
iteraotr end()
{
return iteraotr(nullptr, this);
}
3.2.3、在unordered上层
在set中:
typedef typename HashBucket::HashTable<K, K, Hash, SetKeyOfT>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
在map中:
typedef typename HashBucket::HashTable<K, pair<K,V>, Hash, MapKeyOfT>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
3.2.4、Insert 2.0 、operator[]
1)、对Insert、Find等返回值做修改
pair<iterator,bool> Insert(const T& data)
{
Hash HsTrans;
KeyOfT Kot;
//去重
iterator ret = Find(Kot(data));
if (ret != end())
return make_pair(ret, false);
//0、扩容检查
CheckCapacity();
//1、计算哈希地址
size_t hashi = HsTrans(Kot(data)) % _table.size();
//2、插入值,修改链接关系
Node* newnode = new Node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_size;
return make_pair(iterator(newnode, this), true);
}
iterator Find(const K& key)
{
if (_table.size() == 0)//表中无元素
return end();
Hash HsTrans;
KeyOfT Kot;
size_t hashi = HsTrans(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (Kot(cur->_data) == key)
return iterator(cur,this);
cur = cur->_next;
}
return end();//找不到的情况
}
在unordered_set中:
pair<iterator, bool> insert(const K& key)
{
return _ht.Insert(key);
}
在unordered_map中:
pair<iterator,bool> insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
2)、unordered_map::operator[]
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
3.2.5、验证
1)、演示一
void test06()
{
myunorderedset::unordered_set<int> set;
set.insert(3);
set.insert(7);
set.insert(9);
set.insert(2);
set.insert(5);
myunorderedset::unordered_set<int>::iterator It = set.begin();
while (It != set.end())
{
cout << *It << " ";
++It;
}
cout << endl;
myunorderedmap::unordered_map<string, string> dict;
dict.insert(make_pair("comprehend", "v.理解"));
dict.insert(make_pair("model", "n.模型"));
dict.insert(make_pair("poisonous", "adj.有毒的"));
myunorderedmap::unordered_map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
cout << it->first << " : " << it->second << endl;
++it;
}
cout << endl;
}
2)、演示二
void test07()
{
//次数统计
string arr[] = { "晴","多云","晴","阴","小雨","多云","多云","阴","晴","小雨","大雨","阴","多云","晴" };
myunorderedmap::unordered_map<string, int>countMap;
for (auto& str : arr)//直接借助范围for遍历
{
countMap[str]++;
}
//范围for
for (const auto& kv : countMap)
{
cout << kv.first << ": " << kv.second << endl;
}
}
3)、小结
一个类型K,去做set和unordered_set的模板参数,有什么要求?
1、set:①要求K类型的对象能比较大小,或者显示提供能够比较的仿函数
2、unordered_set:①要求K类型的对象可以转换为整形取模,或者提供转成整形的仿函数;②要求K类型的对象能支持等于==
比较,或者显示提供能支持等于==
比较的仿函数
3.3、整体
3.3.1、HashTable
#pragma once
#include<iostream>
#include<vector>
#include<utility>
using namespace std;
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;//能够强制类型转换为size_t的。
}
};
template<>
struct HashFunc<string>//方法二
{
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
namespace HashBucket
{
template<class T>
struct HashNode
{
T _data;//当前结点存储值
HashNode<T>* _next;//指向下一个结点的指针
HashNode(const T& data)//构造
:_data(data)
,_next(nullptr)
{}
};
//前置声明
template<class K, class T, class Hash, class KeyOfT>
class HashTable;
template<class K,class T,class Hash,class KeyOfT>
struct __HashIterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, Hash, KeyOfT> HT;
typedef __HashIterator<K, T, Hash, KeyOfT> Iterator;
Node* _node;//指向哈希桶中单个结点的指针
HT* _pht;//指向哈希表的指针
__HashIterator(Node* node, HT* pht)//构造
:_node(node)
,_pht(pht)
{}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &(_node->_data);
}
bool operator==(const Iterator& it) const
{
return it._node == _node;
}
bool operator!=(const Iterator& it) const
{
return it._node != _node;
}
Iterator& operator++()
{
if (_node->_next) // 当前桶中节点迭代
{
_node = _node->_next;
}
else //需要寻找哈希表中下一个桶
{
Hash HsTrans;
KeyOfT Kot;
size_t hashi = HsTrans(Kot(_node->_data)) % _pht->_table.size();
++hashi;//到下一个哈希桶位置
for (; hashi < _pht->_table.size(); ++hashi)
{
if (_pht->_table[hashi])//当前迭代后的哈希桶中存在有效值
{
_node = _pht->_table[hashi];
break;
}
//如果当前++hashi后获取到的下一个桶不存储有效值,则再次往后迭代寻找哈希桶
}
if (hashi == _pht->_table.size())//若将哈希表都遍历完成,则说明已经没有存储有效数据的哈希桶了
{
_node = nullptr;
}
}
return *this;
}
};
template<class K, class T, class Hash, class KeyOfT>
class HashTable
{
typedef HashNode<T> Node;
template<class K, class T, class Hash, class KeyOfT>
friend struct __HashIterator;
public:
typedef __HashIterator<K, T, Hash, KeyOfT> iterator;
iterator begin()
{
for (size_t i = 0; i < _table.size(); ++i)
{
if (_table[i])
return iterator(_table[i], this);
}
return end();
}
iterator end()
{
return iterator(nullptr, this);
}
~HashTable()
{
for (size_t i = 0; i < _table.size(); ++i)
{
Node* cur = _table[i];
Node* next = nullptr;
while (cur)
{
next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
void CheckCapacity()
{
if (_size == _table.capacity())
{
vector<Node*> newtable;//新建一个哈希表
newtable.resize(__stl_next_prime(_table.size()), nullptr);//resize重新设置容量空间
Hash HsTrans;
KeyOfT Kot;
for (size_t i = 0; i < _table.size(); ++i)//遍历原表,在新表中重新建立映射关系:此处我们直接使用原表已经存在的结点,修改链接关系即可
{
Node* cur = _table[i];
while (cur)//若当前桶中存在有效结点:依次取当前桶中每个结点,让其重新链接到新表中
{
Node* next = cur->_next;
size_t hashi = HsTrans(Kot(cur->_data)) % newtable.size();//获取结点在新表中的哈希地址
cur->_next = newtable[hashi];
newtable[hashi] = cur;
cur = next;//在原哈希桶迭代依次修改节点,直到当前桶为空
}
_table[i] = nullptr;
}
//交换
_table.swap(newtable);
}
}
inline size_t __stl_next_prime(size_t n)
{
static const size_t __stl_num_primes = 28;
static const size_t __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
};
//遍历上述的素数集合,设当前_table中元素个数为n,
//则下次resize从新规定空间时,我们只需要找首个大于n的素数即可。
for (size_t i = 0; i < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return -1;
}
pair<iterator,bool> Insert(const T& data)
{
Hash HsTrans;
KeyOfT Kot;
//去重
iterator ret = Find(Kot(data));
if (ret != end())
return make_pair(ret, false);
//0、扩容检查
CheckCapacity();
//1、计算哈希地址
size_t hashi = HsTrans(Kot(data)) % _table.size();
//2、插入值,修改链接关系
Node* newnode = new Node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_size;
return make_pair(iterator(newnode, this), true);
}
iterator Find(const K& key)
{
if (_table.size() == 0)//表中无元素
return end();
Hash HsTrans;
KeyOfT Kot;
size_t hashi = HsTrans(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (Kot(cur->_data) == key)
return iterator(cur,this);
cur = cur->_next;
}
return end();//找不到的情况
}
bool Erase(const K& key)
{
Node* ret = Find(key);
if (ret)//该目标值存在
{
Hash HsTrans;
size_t hashi = HsTrans(key) % _table.size();
Node* prev = nullptr;
Node* cur = _table[hashi];
while (cur != ret)
{
prev = cur;
cur = cur->_next;
}
if (prev)//非头删
prev->_next = cur->_next;
else//头删
_table[hashi] = cur->_next;
delete cur;
--_size;
return true;
}
return false;
}
//统计哈希表中数据个数
size_t Size()
{
return _size;
}
//统计哈希表中表的长度
size_t TableSize()
{
return _table.size();
}
//统计哈希表中桶的个数
size_t BucketNum()
{
size_t count = 0;
for (size_t i = 0; i < _table.size(); ++i)
{
if (_table[i])
count++;
}
return count;
}
//寻找最长桶的长度
size_t MaxBucketLength()
{
size_t length = 0;
for (size_t i = 0; i < _table.size(); ++i)
{
Node* cur = _table[i];
size_t curlen = 0;
while (cur)
{
++curlen;
cur = cur->_next;
}
if (curlen > 0)//用于查看有数据的桶其上挂有多少个结点
printf("[%d]号桶长度:%d\n", i, curlen);
if (curlen > length)
length = curlen;
}
return length;
}
private:
vector<Node*> _table;
size_t _size = 0;
};
}
3.3.2、unordered_set
#pragma once
#include"HashTable_3.h"
namespace myunorderedset
{
template<class K,class Hash=HashFunc<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename HashBucket::HashTable<K, K, Hash, SetKeyOfT>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
pair<iterator, bool> insert(const K& key)
{
return _ht.Insert(key);
}
private:
HashBucket::HashTable<K, K, Hash, SetKeyOfT> _ht;
};
}
3.3.3、unordered_map
#pragma once
#include"HashTable_3.h"
namespace myunorderedmap
{
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K,V>& kv)
{
return kv.first;
}
};
public:
typedef typename HashBucket::HashTable<K, pair<K,V>, Hash, MapKeyOfT>::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;
}
private:
HashBucket::HashTable<K, pair<K, V>, Hash, MapKeyOfT> _ht;
};
}
4、应用
4.1、位图
4.1.1、基本概念介绍
1)、位图引入
问题:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
解决方案:
1、使用搜索树或哈希表是否能解决?
分析:首先我们先分析数据大小,若直接将40亿个无符号整型存储入内存中,需要多大空间?
1G = 1024MB = 1024 * 1024KB = 1024 * 1024 * 1024Byte;
1G = 2^30Byte ≈ 1X10^9 (10亿) Byte
40亿无符号整形,设其一共40*4=160Byte,故需要的内存空间为16G。
在内存空间有限的情况下,上述大小的数据无法存储下,更别说搜索树、哈希表的结构中除了存储基础数据,还有其它成员。因此上述方法并不适用于这类海量数据处理。
2、使用外排序+二分查找是否能解决?
分析:外排序将数据存储在磁盘上,可以解决内存中存放不下的问题,但在磁盘上使用二分查找相对来说效率慢,支持起来不方便。
3、除了上述方法,是否还有其他解决办法?
分析:实际上我们只需要判断出一个数是否存在集合中,不一定需要真实将该数据存储起来,有一个能标记其状态信息的方式即可。即我们将要介绍的位图。
2)、位图介绍
位图: 就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
1、比特位映射标记值:上述问题中,我们需要判断某一数据是否在给定的整形数据中,结果只有在或者不在,刚好对应两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,比如,二进制比特位为1代表存在,0代表不存在。
2、数据所需内存大小:在统计时,可以采用直接定址法,整形数据32位下范围为
0
0
0 ~
2
32
2^{32}
232,那么我们最多只用开辟
2
32
2^{32}
232bit空间位置(原因:32位下整形数据不会超过这个范围),类似于哈希函数对给定的数据集合进行一一映射,则有:
2^32 = 4294967296 bit
4294967296÷8÷1024÷1024÷1024=0.5G
即我们最多只用到0.5G的内存空间。
PS:实际上SQL中也提供了位图:bitset
3)、位图优缺点介绍
优点:速度快、节约空间
缺点:相对局限,只能映射整形数据
4.1.2、常用接口相关实现
4.1.2.1、如何构造一个位图?
template<size_t N>
class bitset
{
bitset()
{
_bits.resize(N / 8 + 1, 0));
}
//……
private:
vector<char> _bits;
};
细节说明:
1、如何按位开辟空间?
根据上述位图使用分析,我们是以bit位来判断数据是否存在的,最大只需要开辟
2
32
2^{32}
232bit位的空间。但实际内存中,没有以单位bit来衡量的类型大小,最小的类型单位char也要8bit。因此,对开辟的内存空间我们需要做一定处理:
2、非类型模板参数size_t N
,此处设置N是根据实际需求开辟对应大小的比特位。但根据1可知,实际vector类型为char,因此resize重新开辟空间时需要做一定转换,这里最后+ 1
是因为通常情况下/属于向下取整,假设
0
<
N
<
8
0<N<8
0<N<8;则
N
/
8
=
0
N/8=0
N/8=0。为了防止此类情况,可以牺牲一点内存资源,多开辟一个char大小的空间。
bitset()
{
_bits.resize(N / 8 + 1, 0);//此处不能使用sizeof(char),因为其计算单位为字节
}
4.1.2.2、set、reset、test
这里我们只实现位图中相对重要的接口:
set:将映射的比特位设置为1
reset:将映射的比特位设置为0
test:检测位图中目标值是否存在
1)、set:如何将某一比特位设置为1?
step1:直接定址法获取给定数值对应哈希地址。 需要思考问题:由于开辟的vector每个数据空间是char类型大小,因此需要判断给定数据是在第几个char的第几个比特位上。
方法:
/ 8 :获取给定数据是在第几个char
% 8 :获取给定数据是在该char中第几个比特位
step2:将对应的bit位设置为存在状态(这里我们默认1为存在)。
方法:需要用于位移操作符、按位或。
step3:相关实现如下:
void set(size_t val)
{
size_t i = val / 8;
size_t j = val % 8;
_bits[i] |= (1 << j);//这里使用的是或等
}
2)、reset:如何将某一比特位恢复为0?
有了上述基础,对reset的实现其思想类似。①找对应的比特位;②需要思考如何将对应比特位设置为0,且不改变其它比特位的值?
设左移3,则0000 0001<<3 得0000 1000,按位取反得,1111 0111;
接下来按位与,则全为1才是1,即可保障其它位不变,0位必为0.
相关实现如下:
void reset(size_t val)
{
size_t i = val / 8;
size_t j = val % 8;
_bits[i] &= ~(1 << j);//与等
}
3)、test:如何判断某一比特位上的值?
方法类似。
若原先该比特位为1,则按位与1后得1,表示位图中存在该数据;
若原先该比特位为0,则按位与1后得0,表示位图中不存在该数据;
相关实现:
bool test(size_t val)
{
size_t i = val / 8;
size_t j = val % 8;
return _bits[i] & (1 << j);
}
4.1.2.3、相关验证
1)、测试set、reset、test
void test08()
{
mybitset::bitset<100> bs1;
bs1.set(6);
bs1.set(12);
bs1.set(24);
cout << bs1.test(6) << endl;
cout << bs1.test(12) << endl;
cout << bs1.test(24) << endl;
bs1.reset(6);
bs1.reset(12);
bs1.reset(24);
cout << bs1.test(6) << endl;
cout << bs1.test(12) << endl;
cout << bs1.test(24) << endl;
}
2)、测试给定数值最大范围(2^32,32位下)空间
假设我们需要开辟最大的bit位空间(32位下即
2
32
2^{32}
232),那么非类型模板参数size_t N
该如何传参?
如下述,我们可以传递-1,也可以使用十六进制的0XFFFFFF。对于-1,由于N的类型位size_t,故会转换为无符号整形。
bitset<-1> bs1;
bitset<0xffffffff> bs2;
之前我们计算得4294967296÷8÷1024÷1024÷1024=0.5G
,可通过任务管理器观察是否开辟了0.5G的内存空间。
4.1.3、扩展运用·海量数据处理
4.1.3.1、题一
1)、问题与分析
问题: 给定100亿个整数,设计算法找到只出现一次的整数?
分析:
1、这也是海量数据处理问题,需要考虑到内存空间有限,限制了哈希、搜索树的使用。
2、我们学习了位图,这里可以考虑到它的运用。在之前的模拟实现中实际类似于K模型,只要求判断一个值在或不在,此题中的场景类似于KV模型,要确定对应K值实际出现几次(V)。
3、分析题目,我们可将其分类处理:出现0次、出现1次、出现2次及2次以上。由于一个比特位只能代表两种情况,因此,我们可以使用两个比特位(可表示四种情况)来表示上述分类。
4、按照3的方式,我们需要重新调整和修改bitset的内部结构。且实际SQL中也有bitset,我们不可能直接对其修改,那就意味着要完完全全重新构建,可相比于直接重建,复用已有资源是更优选择。一种方式是,使用两个位图,利用位图中相同的比特位,表示上述情况分类。
2)、相关实现与验证
template<size_t N>
class twobitset
{
public:
//需求:给定的val值,设置其出现状况
void set(size_t val)
{
bool ret1 = _bits1.test(val);
bool ret2 = _bits2.test(val);
if (ret1 == false && ret2 == false)
{ //原先为00:此时val值进来,需要置为01,表示出现一次
_bits2.set(val);
}
else if (ret1 == false && ret2 == true)
{//原先为01:此时val值进来,需要设置为10,表示出现两次及两次以上
_bits1.set(val);
_bits2.reset(val);
}
}
//用于检测某一值是否只出现一次,若是则将其打印出来
void print_once_num()
{
for (size_t i = 0; i < N; ++i)
{
if (_bits1.test(i) == false && _bits2.test(i) == true)
{
cout << i << " ";
}
}
}
private:
bitset<N> _bits1;
bitset<N> _bits2;
};
代码验证:
void test10()
{
vector<size_t> v;
int n = 20;
v.reserve(n);
srand(time(0));
for (int i = 0; i < n; ++i)
v.push_back(rand() % 15 + 1);
cout << "size:" << v.size() << endl;;
for (const auto& e : v)
cout << e << " ";
cout << endl;
mybitset::twobitset<20> bs1;
for (const auto& e : v)
{
bs1.set(e);
}
cout << "只出现一次的数:";
bs1.print_once_num();
cout << endl;
}
4.1.3.2、题二
1)、问题与分析
问题: 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
方法如下:
我们仍旧可以使用位图来解决,但需要注意在查找交集前需要都文件内部数据去重。判断结果时,可以分别查找两个文件中是否存在该值true&&true
,或者对两个位图进行按位与 1&1=1
。
4.1.3.3、题三
1)、问题与分析
问题: 位图应用变形。1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数。
分析:此题大体方法和之前题一一致,只是需要的状态情况不同。00表示出现1次,01表示出现 1次,10表示出现2次,11表示3次及三次以上。
//需求:给定的val值,设置其出现状况
void set(size_t val)
{
bool ret1 = _bits1.test(val);
bool ret2 = _bits2.test(val);
if (ret1 == false && ret2 == false)
{ //原先为00:此时val值进来,需要置为01,表示出现一次
_bits2.set(val);
}
else if (ret1 == false && ret2 == true)
{//原先为01:此时val值进来,需要设置为10,表示出现两次
_bits1.set(val);
_bits2.reset(val);
}
else if (ret1 == true && ret2 = false)
{//原先为10:此时val值进来,需要设置为11,表示出现三次及三次以上
_bits1.set(val);
_bits2.set(val);
}
}
4.2、布隆过滤器
4.2.1、基本概念介绍
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
场景分析: 对于字符串等类型判断其在集合中是否存在?
说明: 使用一个哈希函数将其映射到一个位置并进行标记,如下,存在误判行为。
当前获取结果为存在:该判断不准确,可能会因为哈希冲突导致误判
当前获取结果为不存在:该判断准确
上述情况如何改进?
如下,我们可以使用多个哈希函数进行映射,以降低误判率。但这种误判率的降低不是可无限扩大的,哈希函数使用的越多,映射位置就越多,那么空间消耗也越多。
因此,如何选择哈希函数的个数和布隆过滤器的长度就是其中一个需要探讨的问题。相关链接
4.2.2、相关接口实现
4.2.2.1、布隆过滤器的基本框架及哈希函数
1)、关于哈希函数的选择问题说明
根据上述公式描述,假设我们选取3个哈希函数,即
K
=
3
K=3
K=3,则插入
N
N
N个元素,所需开辟的布隆过滤器长度为
M
=
K
×
N
l
n
2
≈
3
×
N
0.7
≈
4.29
N
M=\frac{K×N}{ln2}≈\frac{3×N}{0.7}≈ 4.29N
M=ln2K×N≈0.73×N≈4.29N ,我们取
M
=
5
N
M=5N
M=5N。
关于字符串哈希函数的选择如下:
//三个哈希函数
struct HashBKDR
{
// BKDR
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
struct HashAP
{
// BKDR
size_t operator()(const string& key)
{
size_t hash = 0;
for (size_t i = 0; i < key.size(); i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ key[i] ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ key[i] ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashDJB
{
// BKDR
size_t operator()(const string& key)
{
size_t hash = 5381;
for (auto ch : key)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
2)、基础结构框架
template<size_t N, class K=string,
class Hash1= HashBKDR, class Hash2= HashAP, class Hash3= HashDJB>
class BloomFilter
{
public:
private:
const static size_t _ratio = 5;//const修饰的静态成员变量可赋缺省参数
bitset<_ratio*N> _bits;
};
上述实现中,我们直接在BloomFilter
类中开辟了bitset<_ratio*N> _bits
。若数据量过大时可能存在内存空间不够的问题,因此可以使用下述方法:直接在堆上申请空间。
template<size_t N, class K=string,
class Hash1= HashBKDR, class Hash2= HashAP, class Hash3= HashDJB>
class BloomFilter
{
public:
private:
const static size_t _ratio = 5;//const修饰的静态成员变量可赋缺省参数
bitset<_ratio* N>* _bits = new bitset<_ratio* N>;
//bitset<_ratio*N> _bits;
};
4.2.2.2、set、test
1)、对set的实现
set的任务主要是在布隆过滤器中对给定的数值进行设置。因为我们上述规定使用三个哈希函数,因此需要分别用这三个哈希函数获取映射地址,并将该位置进行标记。
void set(const K& val)
{
size_t hash1 = Hash1()(val) % (_ratio * N);
_bits->set(hash1);
size_t hash2 = Hash2()(val) % (_ratio * N);
_bits->set(hash2);
size_t hash3 = Hash3()(val) % (_ratio * N);
_bits->set(hash3);
}
2)、对test的实现
test的任务则是判断给定值是否存在于布隆过滤器中。根据之前内容,当我们分别在三个哈希函数映射的地址查询元素时,若得到的结果为true,存在误判行为,若结果为false,则表示确切结果。
bool test(const K& val)
{
size_t hash1 = Hash1()(val) % (_ratio * N);
if (!_bits->test(hash1))//非真:准确结果
return false;
size_t hash2 = Hash2()(val) % (_ratio * N);
if (!_bits->test(hash2))
return false;
size_t hash3 = Hash3()(val) % (_ratio * N);
if (!_bits->test(hash3))
return false;
return true;//所有哈希函数验证结果都为真,这里存在误判行为
}
4.2.2.3、相关验证
1)、验证一
void test11()
{
BloomFilter<10> bf;
string arr1[] = { "防风", "苏叶", "钩吻", "泽兰", "连翘", "桔梗", "当归", "防风", "钩吻", "防风", "连翘" };
for (auto& str : arr1)
{
bf.set(str);
}
for (auto& str : arr1)
{
cout << str << ":" << bf.test(str) << " ";
}
cout << endl << endl;
string arr2[] = { "防风01", "苏叶", "钩吻03", "泽兰", "连翘05", "桔梗", "当归07", "防风", "钩吻09", "防风", "连翘11" };
for (auto& str : arr2)
{
cout << str << ":" << bf.test(str) << endl;
}
}
2)、验证二
相关代码:
void test12()
{
srand(time(0));
const size_t N = 100000;
BloomFilter<N> bf;//布隆过滤器
cout << sizeof(bf) << endl;
//——————————————————————————————————————
//对照组:给一段字符串,其只是末尾有区别
std::vector<std::string> v1;
std::string url = "https://www.csdn.net/?spm=1001.2014.3001.4476";
for (size_t i = 0; i < N; ++i)
{
v1.push_back(url + std::to_string(1234 + i));
}
for (auto& str : v1)
{
bf.set(str);
}
//——————————————————————————————————————
//——————————————————————————————————————
//实验组一:与对照组相似的字符串。目的:测试相似字符串在布隆过滤器中的误判率
std::vector<std::string> v2;
for (size_t i = 0; i < N; ++i)
{
std::string url = "https://www.csdn.net/?spm=1001.2014.3001.4476";
url += std::to_string(rand() + i);
v2.push_back(url);
}
size_t n2 = 0;
for (auto& str : v2)
{
if (bf.test(str))
{
++n2;
}
}
cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;
//——————————————————————————————————————
//——————————————————————————————————————
//实验组二:与对照组完全区别很大的字符串。目的:测试不相似字符在布隆过滤器中的误判率
std::vector<std::string> v3;
for (size_t i = 0; i < N; ++i)
{
string url = "zhihu.com";
url += std::to_string(rand() + i);
v3.push_back(url);
}
size_t n3 = 0;
for (auto& str : v3)
{
if (bf.test(str))
{
++n3;
}
}
cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
//——————————————————————————————————————
}
结果一:可以看到相似字符串误判率相对高一些。
结果二:调节布隆过滤器长度和插入元素数量的比值,误判率也会随之改变。
4.2.3、扩展运用:海量数据处理
4.2.3.1、问题一:布隆过滤器删除
1)、问题:布隆过滤器支持删除吗?
回答: 通常情况下,布隆过滤器不支持删除。因为当存在哈希冲突时,删除某一比特位的数据,可能会导致其它数据查找失效,造成误判。
问题: 如果要强行支持删除元素的操作呢?
回答: 一种支持删除的方法是,将布隆过滤器中的每个比特位扩展成一个小的计数器(用于统计k个哈希函数计算出的哈希地址处存储的数据量)。比如:插入元素时,给k个计数器加一,删除元素时,给k个计数器减一。但这样一来,布隆过滤器为了支持删除操作,多占用几倍存储空间,其优势也被削弱。
4.2.3.2、问题二:哈希切割
问题描述:给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。
近似:上述布隆过滤器即可解决。
精确:哈希切分。