哈希(解决哈希冲突,封装map/set,哈希的应用{位图/布隆过滤器})
文章目录
- 如何解决哈希冲突:
- ①闭散列:也叫开放定址法,如果发送哈希冲突了,就去找下一个空位置
- ②开散列 -- 拉链法/哈希桶
- 封装map和set
- 哈希表的代码
- 封装的unordered_set
- 封装的unordered_map
- 哈希的应用
- 位图
- 实现位图:
- 布隆过滤器
- 实现布隆过滤器
- 布隆过滤器扩展
一.哈希(按某种规律将某个值存储到固定位置)
除留余数法 key % len
直接映射一定是有哈希碰撞/哈希冲突
如何解决哈希冲突:
①闭散列:也叫开放定址法,如果发送哈希冲突了,就去找下一个空位置
针对的是值相对连续的做法,如果说值特别的散,那么效率会很低
方法一:线性探测,从发送冲突的位置开始,依次向后探测,直到找到下一个空位置为止
(图书馆抢位置,别人如果枪了我的,我就去抢别人的,强盗逻辑)
构建哈希表:
一个vector,一个记录有效个数的变量n
如果vector的最后一个数据冲突则返回到vector的第一个位置继续找空位
删除元素后查找遇到空时回暂停,如何解决这种问题呢?加标记,每个数据给一个枚举状态值,这样删数据只需要将其状态值修改
查找时遇到空EMPTY就停止了,遇到删除DELETE还要继续找
负载因子定义 α = 填入表中的元素个数 / 散列表的长度
α越大,冲突的概率越大,效率低,但是浪费少
α越小,冲突的概率越小,效率高,但是浪费严重
接口:
【 插入 】算插入位置,膜vector的size而不是capacity ,扩容的写法(α>0.7)
扩容时:直接new一个新的table,将原table中元素插入其中,交换指针即可,复用之前insert
重复插入的处理(通过Find接口)
插入的数据不一定是整数,可能是string…用仿函数的方式来解决,取模的地方就用仿函数处理一下
各种字符串Hash函数-博客园
注意:
vector的reserve以后为什么不能cin>>v[i],因为方括号自带检查,reserve只是开空间并未初始化,所以要用resize(默认初始化为0)
方法二:二次探测
引入二次探测背景:连续位置的冲突值比较多的话,引发踩踏洪水效应,按照方法一找邻居的话比较容易冲突
#pragma once
#include<vector>
#include<iostream>
#include<cstdio>using namespace std;template<class K>
struct Hash
{size_t operator()(const K& key){return key;}
};//特化版本
//传string要将它转化成整数,这边先使用BKDR的方法
template<>
struct Hash <string>
{size_t operator()(const string& s){//BKDRsize_t value = 0;for (auto ch : s){value *= 31;value += ch;}return value;}
};//闭散列的方式
namespace CloseHash {enum Status {EXIST,EMPTY,DELETE};template<class K,class V>struct HashData{//保存一对键值对pair<K, V> _kv;//保存此数据的状态Status _status = EMPTY;};template<class K, class V,class HashFunc = Hash<K>>class HashTable{public:bool Erase(const K& key){HashData<K, V>* res = Find(key);if (key){//找到的情况就可以进行删除//只需要将状态置Delete即可,有效个数减1--_n;res->_status = DELETE;return true;}else{//没找到就返回falsereturn false;}}HashData<K, V>* Find(const K& k){if (_tables.size() == 0)return nullptr;HashFunc hf;//算起始点,往后线性探测/二次探测查找待插入位置size_t start = hf(k) % _tables.size();size_t index = start;size_t i = 0;//做的是一个连续的查找,不能中间是断的//也就是说遇到空就会停止while (_tables[index]._status != EMPTY){if (_tables[index]._kv.first == k && _tables[index]._status == EXIST){return &_tables[index];}++i;index = start + i;//防止越界index %= _tables.size();}return nullptr;}//Insert是插入一对键值对bool Insert(const pair<K,V>& kv){//如果本身就有这个元素的话,就避免重复插入HashData<K, V>* res = Find(kv.first);if (res){return false;}//负载因子到0.7就扩容//负载因子越小,冲突的概率就越低,效率就越高,但是空间浪费多//负载因子越大,冲突的概率就越高,效率就越低,但是空间浪费少//没有元素时扩容,负载因子到时扩容if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7){size_t newSize = _tables.size()== 0 ? 10 : _tables.size() * 2;HashTable<K, V, HashFunc> newHT;newHT._tables.resize(newSize);for (size_t i = 0; i < _tables.size(); i++){newHT.Insert(_tables[i]._kv);}_tables.swap(newHT._tables);}HashFunc hf;size_t start = hf(kv.first) % _tables.size();size_t i = 0;size_t index = start;while (_tables[index]._status == EXIST){++i;index = start + i;//防止下标出界index %= _tables.size();}//找到位置以后_tables[index]._kv = kv;_tables[index]._status = EXIST;++_n;return true;}private://用一个vector来存储Hash元素,n表示tables中有效元素的个数vector<HashData<K, V>> _tables;size_t _n = 0;};void HashTableTest(){HashTable<int, int> ht;int a[] = { 2, 12, 22, 32, 42, 52, 62 };for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(72, 72));ht.Insert(make_pair(32, 32));ht.Insert(make_pair(-1, -1));ht.Insert(make_pair(-999, -999));Hash<int> hs;cout << hs(9) << endl;cout << hs(-9) << endl;cout << ht.Find(12) << endl;ht.Erase(12);cout << ht.Find(12) << endl;}void HashTableTest2(){HashTable<string, string> ht;ht.Insert(make_pair("sort", "排序"));ht.Insert(make_pair("string", "字符串"));}
}
②开散列 – 拉链法/哈希桶
是闭散列/开放定址法的优化:闭散列不同位置冲突数据会互相影响,相互位置的冲突会争抢位置,互相影响效率
开散列相邻位置冲突,不会再争抢位置,不会互相影响
控制负载因子 – 负载因子到了就扩容
极端场景防止一个桶冲突太多,链表太长:当一个桶长度超过一定长以后,转换为红黑树(Java中链表的长度超过了8会变成红黑树)
例子:
表长度100
极端场景:
1 .存了50个值,40个值是冲突的,挂再一个桶中
2 .存储了10000个值,平均每个桶长度100,阶段场景有些桶可能有上千个值
【插入】
扩容:这种方法中的扩容最好不要复用自己的Insert,因为会new新的节点,这样是没有必要的,可以将原表中的节点一个个链下来,
注意是一个个,而不是一个桶(因为桶中的元素映射到了新表中可能对应新的位置)
table[i]的节点都被拿走了,最好要把它置空一下
newtable是我们的table想要的,把他们的指针再交换一下
【删除】
namespace LinkHash
{template<class K,class V>struct HashNode{pair<K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<K,V>& kv):_kv(kv),_next(nullptr){}};template<class K,class V,class HashFunc = Hash<K>>class HashTable{typedef HashNode<K, V> Node;public:bool Erase(const K& key){//表空就不用删除if (_tables.empty()){return false;}HashFunc hf;size_t index = hf(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[index];while (cur){if (cur->_kv.first == key){if (prev == nullptr){//头删_tables[index] = cur->_next;}else{//中间删除prev->_next = cur->_next;}--_n;delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}Node* Find(const K& key){//表为空直接返回falseif (_tables.empty()){return nullptr;}HashFunc hf;size_t index = hf(key) % _tables.size();//给一个当前位置的指针,进行遍历查找Node* cur = _tables[index];while (cur){if (cur->_kv.first == key){return cur;}else{cur = cur->_next;}}//没找到return nullptr;}bool Insert(const pair<K, V>& kv){//避免插入相同元素Node* ret = Find(kv.first);if (ret)return false;HashFunc hf;//负载因子等于1的时候扩容if (_n == _tables.size()){size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<Node*> newTables;newTables.resize(newSize);//将旧表中的节点转移到新表//一个一个Node插入新表因为将一个桶直接链过去桶内的元素在新表的映射关系可能会发生改变for (size_t i = 0;i<_tables.size();i++){//_tables[i]实际上存的就是每个节点的指针Node* cur = _tables[i];//如果当前节点不是空while (cur){Node* next = cur->_next;size_t index = hf(cur->_kv.first) % newTables.size();//头插cur->_next = newTables[index];newTables[index] = cur;cur = next;}//旧表的所有元素进入新表后,将旧表的节点清空_tables[i] = nullptr;}_tables.swap(newTables);}//计算位置并插入新节点size_t index = hf(kv.first) % _tables.size();Node* newNode = new Node(kv);newNode->_next = _tables[index];_tables[index] = newNode;++_n;return true;}private://实际上是个指针数组vector<Node*> _tables;size_t _n = 0;};void TestHashTable(){int a[] = { 4, 24, 14,7,37,27,57,67,34,14,54 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(84, 84));}
}
封装map和set
面试题:
一个类型要去做map和set的key有什么要求;做unordered_map和unordered_set的key又有什么要求?
map的key :Key类型对象支持小于比较或者传递一个小于仿函数即可
unordered_map的key:a .能支持取模或者支持转换成取模的无符号整数 b .支持key比较相等或者相等的仿函数
什么时候需要自己写仿函数
1.类型不支持比较大小
2.支持比较大小,但是不是你想要的
unordered_map什么时候需要自己实现仿函数
1 .想要的类型不支持相等(equal)的比较
2 .自定义类型不满足自己需求的类型(举例 Date和Date*)
map是如何只支持小于比较,从而完成大于和相等
map只要求key能够比较大小,比它小往左边走,比它大往右边走,不大不小就是相等
map只需要实现一个小于即可,大于(仿函数换一下位置即可)
多一层keyofT封装,通过key找value
统一适配
这样set找的就是它的K,map的话找的是它pair中的first
哈希表支持迭代器
互相引用的问题(迭代器引用哈希表,哈希表引用迭代器),需要加一个前置申明,说明是一个类模板
私有的问题,用到哈希表中的tables 加一个有元
迭代器不需要实现–,–需要用到双向链表
map的方括号
在的值可以返回对应的value,可以充当查找的功能
直接写的话是插入+修改
重写拷贝构造
生成默认构造的前提是没有其他构造,我们在写拷贝构造后需要自己手动写默认构造
拷贝构造是一个特殊的默认构造函数
C++11语法显示地去生成默认构造 ()=default
哈希表的大小最好是素数比较好,通过查看源码我们知道,用了一个素数表
哈希表的代码
#pragma once
#include<vector>
#include<iostream>
#include<cstdio>using namespace std;template<class K>
struct Hash
{size_t operator()(const K& key){return key;}
};//特化版本
//传string要将它转化成整数,这边先使用BKDR的方法
template<>
struct Hash <string>
{size_t operator()(const string& s){//BKDRsize_t value = 0;for (auto ch : s){value *= 31;value += ch;}return value;}
};//闭散列的方式
namespace CloseHash {enum Status {EXIST,EMPTY,DELETE};template<class K,class V>struct HashData{//保存一对键值对pair<K, V> _kv;//保存此数据的状态Status _status = EMPTY;};template<class K, class V,class HashFunc = Hash<K>>class HashTable{public:bool Erase(const K& key){HashData<K, V>* res = Find(key);if (key){//找到的情况就可以进行删除//只需要将状态置Delete即可,有效个数减1--_n;res->_status = DELETE;return true;}else{//没找到就返回falsereturn false;}}HashData<K, V>* Find(const K& k){if (_tables.size() == 0)return nullptr;HashFunc hf;//算起始点,往后线性探测/二次探测查找待插入位置size_t start = hf(k) % _tables.size();size_t index = start;size_t i = 0;//做的是一个连续的查找,不能中间是断的//也就是说遇到空就会停止while (_tables[index]._status != EMPTY){if (_tables[index]._kv.first == k && _tables[index]._status == EXIST){return &_tables[index];}++i;index = start + i;//防止越界index %= _tables.size();}return nullptr;}//Insert是插入一对键值对bool Insert(const pair<K,V>& kv){//如果本身就有这个元素的话,就避免重复插入HashData<K, V>* res = Find(kv.first);if (res){return false;}//负载因子到0.7就扩容//负载因子越小,冲突的概率就越低,效率就越高,但是空间浪费多//负载因子越大,冲突的概率就越高,效率就越低,但是空间浪费少//没有元素时扩容,负载因子到时扩容if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7){size_t newSize = _tables.size()== 0 ? 10 : _tables.size() * 2;HashTable<K, V, HashFunc> newHT;newHT._tables.resize(newSize);for (size_t i = 0; i < _tables.size(); i++){newHT.Insert(_tables[i]._kv);}_tables.swap(newHT._tables);}HashFunc hf;size_t start = hf(kv.first) % _tables.size();size_t i = 0;size_t index = start;while (_tables[index]._status == EXIST){++i;index = start + i;//防止下标出界index %= _tables.size();}//找到位置以后_tables[index]._kv = kv;_tables[index]._status = EXIST;++_n;return true;}private://用一个vector来存储Hash元素,n表示tables中有效元素的个数vector<HashData<K, V>> _tables;size_t _n = 0;};void HashTableTest(){HashTable<int, int> ht;int a[] = { 2, 12, 22, 32, 42, 52, 62 };for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(72, 72));ht.Insert(make_pair(32, 32));ht.Insert(make_pair(-1, -1));ht.Insert(make_pair(-999, -999));Hash<int> hs;cout << hs(9) << endl;cout << hs(-9) << endl;cout << ht.Find(12) << endl;ht.Erase(12);cout << ht.Find(12) << endl;}void HashTableTest2(){HashTable<string, string> ht;ht.Insert(make_pair("sort", "排序"));ht.Insert(make_pair("string", "字符串"));}
}namespace LinkHash
{template<class T>struct HashNode{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}};template<class K, class T, class KeyOfT, class HashFunc>class HashTable;//迭代器想要复用HashTable里的东西要先在前面申明template<class K,class T,class Ref,class Ptr ,class KeyOfT,class HashFunc>struct __HTIterator{typedef HashNode<T> Node;typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, HashFunc> Self;//哈希表的这个迭代器里有指向HashTable的指针,还有一个每个桶的指针,又这两部分组成//首先定义这两个表指针Node* _node;HashTable<K, T, KeyOfT, HashFunc>* _pht;__HTIterator(Node* node,HashTable<K,T,KeyOfT,HashFunc>* pht):_node(node),_pht(pht){}//解引用Ref operator*(){return _node->_data;}//指针变量的提取,重载后取数据的地址Ptr operator->(){return &_node->_data;}//这个是前置++Self& operator++(){if (_node->_next){//就在当前桶中的情况_node = _node->_next;}else{//不在当前桶中,去下一个桶寻找KeyOfT kot;HashFunc hf;size_t index = hf(kot(_node->_data)) % _pht->_tables.size();//先往后走一个桶++index;//找下一个不为空的桶while (index < _pht->_tables.size()){if (_pht->_tables[index]){break;}else{index++;}}//表走完了,还没找到下一个数据对应的桶if (index == _pht->_tables.size()){_node = nullptr;}else{//迭代器指向对应桶的第一个元素_node = _pht->_tables[index];}}return *this;}bool operator==(const Self& s)const{return _node == s._node;}bool operator!=(const Self& s)const{return _node != s._node;;}};template<class K,class T,class KeyOfT,class HashFunc>class HashTable{typedef HashNode<T> Node;template<class K, class T, class Ref, class Ptr, class KeyOfT, class HashFunc>friend struct __HTIterator;public:typedef __HTIterator< K, T, T&, T*, KeyOfT, HashFunc> iterator;iterator begin(){//返回的是第一个位置对应的迭代器for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i]){return iterator(_tables[i], this);}}return end();}iterator end(){return iterator(nullptr, this);}bool Erase(const K& key){//表空就不用删除if (_tables.empty()){return false;}HashFunc hf;size_t index = hf(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[index];KeyOfT kot;while (cur){if (KeyOfT(cur->_data) == key){if (prev == nullptr){//头删_tables[index] = cur->_next;}else{//中间删除prev->_next = cur->_next;}--_n;delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}Node* Find(const K& key){KeyOfT kot;//表为空直接返回falseif (_tables.empty()){return nullptr;}HashFunc hf;size_t index = hf(key) % _tables.size();//给一个当前位置的指针,进行遍历查找Node* cur = _tables[index];while (cur){if (kot(cur->_data) == key){return cur;}else{cur = cur->_next;}}//没找到return nullptr;}bool Insert(const T& data){KeyOfT kot;//避免插入相同元素Node* ret = Find(kot(data));if (ret)return false;HashFunc hf;//负载因子等于1的时候扩容if (_n == _tables.size()){size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<Node*> newTables;newTables.resize(newSize);//将旧表中的节点转移到新表//一个一个Node插入新表因为将一个桶直接链过去桶内的元素在新表的映射关系可能会发生改变for (size_t i = 0;i<_tables.size();i++){//_tables[i]实际上存的就是每个节点的指针Node* cur = _tables[i];//如果当前节点不是空while (cur){Node* next = cur->_next;size_t index = hf(kot(cur->_data)) % newTables.size();//头插cur->_next = newTables[index];newTables[index] = cur;cur = next;}//旧表的所有元素进入新表后,将旧表的节点清空_tables[i] = nullptr;}_tables.swap(newTables);}//计算位置并插入新节点size_t index = hf(kot(data)) % _tables.size();Node* newNode = new Node(data);newNode->_next = _tables[index];_tables[index] = newNode;++_n;return true;}private://实际上是个指针数组vector<Node*> _tables;size_t _n = 0;};void TestHashTable(){/*int a[] = { 4, 24, 14,7,37,27,57,67,34,14,54 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(84, 84));*/}
}
封装的unordered_set
#pragma once
#include"HashTable.h"namespace zkx
{template<class K,class hash = Hash<K>>class unordered_set{struct SetKeyofT{const K& operator()(const K& key){return key;}};public://typename创建后再初始化,先是一个申明typedef typename LinkHash::HashTable<K, K, SetKeyofT, hash>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}bool insert(const K& key){return _ht.Insert(key);}private:LinkHash::HashTable<K,K,SetKeyofT,hash> _ht;};void test_unordered_set(){unordered_set<int> us;us.insert(1);us.insert(2);us.insert(3);us.insert(4);unordered_set<int>::iterator it = us.begin();while (it != us.end()){cout << *it << " ";++it;}cout << endl;}
}
封装的unordered_map
#pragma once
#include"HashTable.h"namespace zkx
{template<class K,class V,class hash =Hash<V>>class unordered_map{struct MapKeyofT{const K& operator()(const pair<K, V>& kv){return kv.first;}};public:typedef typename LinkHash::HashTable<K, pair<K, V>, MapKeyofT, hash>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}bool insert(const pair<K,V>& kv){return _ht.Insert(kv);}bool erase(const pair<K, V>& kv){return _ht.Erase(kv.first);}private:LinkHash::HashTable<K, pair<K, V>, MapKeyofT, hash> _ht;};void test_unordered_map(){unordered_map<string, string> um;um.insert(make_pair("string","字符串"));um.insert(make_pair("umbrella", "雨伞"));unordered_map<string, string>::iterator it = um.begin();while (it != um.end()){cout << it->first << ":" << it->second << endl;++it;}cout << endl;}}
哈希的应用
位图
1 .面试题:55:46
给40亿个无符号整数,没排过序。给一个无符号整数们如何快速判断一个数是否在这40亿个数中【腾讯】
如果是排好序的,2^30 = 10亿左右,那么最多只要查找32次
40亿个整数要占用多少空间?
160亿个Byte , 1G = 1024 * 1024 * 1024 Byte 约等于10亿字节
所以大概16G
红黑树和哈希表在此基础上还要有自己的一些额外损耗,故不考虑
这道题最佳解法是位图
40亿个整数,每个数一个比特位来存,40亿个比特位,那就是5亿个字节,
又因为1G 约等于 10 亿个字节,所以40亿比特位就大概是0.5G也就是500M
因为存的是整数,有41亿多的数,所以我们开42亿个空间
fstring fscanf 读进位图,这是最优最快的做法
实现位图:
接口
[set]算出在第几个整数,除8即可,算出i;要算出在该整数中的第几个比特位,模8即可,算出j(看图)
将某比特位置1:或等1左移j位(右边是低位左边是高位情况)
[reset]把某个位变成0,其他位不能影响。除了那个位是0,其他位是1,与一下,也就是1左移j位再取反
!逻辑取反,~是按位取反
[test]单纯的算一下第j位是0还是1呢,单纯与而不是与等不修改值,只是判断
最后返回让它隐式类型转换成bool值
库里有位图,可以直接用
开空间的时候最大可以开0xffffff,也可以开vector<-1>这样,因为把有符号整数看成无符号整数
就是最大,补码也就是全1
哈希表的时间复杂度为什么是O(1),怎么算出来的?
有n个桶,平均每个桶挂一个节点,那么就是O(1)
#pragma once
#include<vector>
#include<cstdio>
#include<iostream>
using namespace std;namespace zkx
{template<size_t N>class bitset{public:bitset(){_bits.resize(N / 8 + 1,0);}//将某一位置成1void set(size_t x){//i计算第几个char,j计算第几个位//这种写法是假设右边是低位,左边是高位,也就是小端模式size_t i = x / 8;size_t j = x % 8;_bits[i] |= (1 << j);}//将某一位置成0void reset(size_t x){size_t i = x / 8;size_t j = x % 8;_bits[i] &= (~(1 << j));}//检测某一位是0或1bool test(size_t x){size_t i = x / 8;size_t j = x % 8;return _bits[i] & (1 << j);}private:vector<char> _bits;};void test_bits(){/*bitset<100> bs;bs.set(5);bs.set(4);bs.set(10);bs.set(20);cout << bs.test(5) << endl;cout << bs.test(4) << endl;cout << bs.test(10) << endl;cout << bs.test(20) << endl;cout << bs.test(21) << endl;cout << bs.test(6) << endl << endl;*/bitset<0xffffffff> bs;}
}
题1:
给定100亿个整数,设计算法找出只出现一次的数
一个整数4字节,100亿整数也就是400亿个byte,也就是40G内存
三种状态①出现0次②出现1次③出现2次及以上
思路一:以前是一个位表示一个值,现在我们使用两个位表示一个值,这样重写有点烦
思路二:用两个位图,第一个位图的第i位与第二个位图的第i位合起来作两个比特位,简单写一下代码,
最后把bs1等于0,bs2等于1的找出来即可
#pragma once
#include<vector>
#include<cstdio>
#include<iostream>
using namespace std;namespace zkx
{template<size_t N>class bitset{public:bitset(){_bits.resize(N / 8 + 1,0);}//将某一位置成1void set(size_t x){//i计算第几个char,j计算第几个位//这种写法是假设右边是低位,左边是高位,也就是小端模式size_t i = x / 8;size_t j = x % 8;_bits[i] |= (1 << j);}//将某一位置成0void reset(size_t x){size_t i = x / 8;size_t j = x % 8;_bits[i] &= (~(1 << j));}//检测某一位是0或1bool test(size_t x){size_t i = x / 8;size_t j = x % 8;return _bits[i] & (1 << j);}private:vector<char> _bits;};void test_bits(){/*bitset<100> bs;bs.set(5);bs.set(4);bs.set(10);bs.set(20);cout << bs.test(5) << endl;cout << bs.test(4) << endl;cout << bs.test(10) << endl;cout << bs.test(20) << endl;cout << bs.test(21) << endl;cout << bs.test(6) << endl << endl;*/bitset<0xffffffff> bs;}template<size_t N>class TwoBitSet{public://用来计算只出现过一次的数,二进制也就是01//记录三种状态 1.出现0次 2.出现1次 3.出现2次及以上void Set(size_t x){//00->01if (!_bs1.test(x) && !_bs2.test(x)){_bs2.set(x);}else if (!_bs1.test(x) && _bs2.test(x))//01 -> 10{_bs1.set(x);_bs2.reset(x);}//到了10就两次及其以上了不用考虑了}void PrintNumberEqual_One(){for (size_t i = 0; i < N; ++i){if (!_bs1.test(i) && _bs2.test(i)){cout << i << endl;}}}private:zkx::bitset<N> _bs1;zkx::bitset<N> _bs2;};void TestTwoBitSet(){int a[] = { 99,0,4,50,33,44,2,5,99,0,50,99,50,2 };TwoBitSet<100> bs;for (auto e : a){bs.Set(e);}bs.PrintNumberEqual_One();}
}
题2:给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集
无论是100亿个整数还是1000亿个整数存到位图里都只需要500M
思路一:一个文件中的整数,set到一个位图,读取第二个文件的整数判断在不在位图,在的话就是交集,不在
的话就不是交集。
缺陷:交集会把重复值找出来,多次出现,最后的结果要想办法去重一下,刚开始就去重其实也是可以的
思路二:一个文件中的整数,set到一个位图bs1,另一个文件中整数,set到另一个位图bs2
a,遍历bs2中值,看在不在bs1,在就是交集
b,bs1中的值依次跟bs2中的值与一下,再去看与完是1的位置值就是交集
题3:位图应用变形:一个文件有100亿个int,1G内存,设计算法找到出现次数不超过两次的所有整数
四种情况①出现0次②出现1次③出现2次④出现3次及以上
只要找出情况2和情况3即可
如果题目改为不超过5次的位图,我们就需要3个位图
布隆过滤器
(布隆提出来的,故得此名)
位图:相比于红黑树,哈希表,节省空间,效率高
有局限性:只能处理整数
对于字符串,自定义类型对象,可以使用布隆过滤器
一个字符串转化成对应的一个整数
一定是会存在误判的,并且”在“的时候才会存在误判,”不在“的情况是准确的
实现布隆过滤器
首先需要一个位图(可以用库里的,也可以用自己写的)
每个值映射三个位置(不一定是三个),这样就不会容易冲突,只要有一个位置是0那就是”不在“,三个位置都为”1“
那就是都在
开布隆过滤器的N开多少?怎么样开比较合适?如何选择哈希函数个数和布隆过滤器的长度
知乎相关文章
k为哈希函数个数,m为布隆过滤器长度
k = m/n*ln2
非类型模板参数都是常量
测试一下误判率
相似字符串和不相似字符串其实不怎么影响错误率,X因子影响比较大
布隆过滤器到底如何减少误判?增加X因子的大小(跟调负载因子类似)
映射个数三个,五个即可,不建议更多
布隆过滤器不能保证没有误判
#pragma once#include<string>
#include<time.h>
#include<bitset>
#include<cstdio>
#include<iostream>using namespace std;//三种计算哈希的方法
struct BKDRHash
{size_t operator()(const string& s){// BKDRsize_t value = 0;for (auto ch : s){value *= 31;value += ch;}return value;}
};struct APHash
{size_t operator()(const string& s){size_t hash = 0;for (long i = 0; i < s.size(); i++){if ((i & 1) == 0){hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));}}return hash;}
};struct DJBHash
{size_t operator()(const string& s){size_t hash = 5381;for (auto ch : s){hash += (hash << 5) + ch;}return hash;}
};template<size_t N,//非类型模板参数是个常量
size_t X = 4,
class K = string,
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash>
class BloomFilter
{public:void Set(const K& key){size_t len = X * N;size_t index1 = HashFunc1()(key) % len;size_t index2 = HashFunc2()(key) % len;size_t index3 = HashFunc3()(key) % len;_bs.set(index1);_bs.set(index2);_bs.set(index3);}bool Test(const K& key){size_t len = X * N;//三个数中有一个数不在,那就是不在,三个数都是true才说明这个数可能存在size_t index1 = HashFunc1()(key) % len;if (_bs.test(index1) == false)return false;size_t index2 = HashFunc2()(key) % len;if (_bs.test(index2) == false)return false;size_t index3 = HashFunc3()(key) % len;if (_bs.test(index3) == false)return false;return true;}void Reset(const K& key);//不支持删除,会影响其他值
private:bitset<X* N> _bs;
};void TestBloomFilter1()
{BloomFilter<100> bm;bm.Set("sort");bm.Set("left");bm.Set("right");bm.Set("eat");bm.Set("aet");bm.Set("https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html");
}void TestBloomFilter2()
{/*BloomFilter<100> bf;bf.Set("张三");bf.Set("李四");bf.Set("牛魔王");bf.Set("红孩儿");bf.Set("eat");cout << bf.Test("张三") << endl;cout << bf.Test("李四") << endl;cout << bf.Test("牛魔王") << endl;cout << bf.Test("红孩儿") << endl;cout << bf.Test("孙悟空") << endl;cout << bf.Test("二郎神") << endl;cout << bf.Test("猪八戒") << endl;cout << bf.Test("ate") << endl;*/BloomFilter<100> bf;srand(time(0));size_t N = 100;std::vector<std::string> v1;for (size_t i = 0; i < N; ++i){std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";url += std::to_string(1234 + i);v1.push_back(url);}for (auto& str : v1){bf.Set(str);}for (auto& str : v1){cout << bf.Test(str) << endl;}cout << endl << endl;std::vector<std::string> v2;for (size_t i = 0; i < N; ++i){std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";url += std::to_string(6789 + 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";//std::string url = "https://www.baidu.com/s?wd=ln2&rsv_spt=1&rsv_iqid=0xc1c7784f000040b1&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_dl=tb&rsv_enter=1&rsv_sug3=8&rsv_sug1=7&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&prefixsug=ln2&rsp=5&inputT=4576&rsv_sug4=5211";//std::string url = "https://zhidao.baidu.com/question/1945717405689377028.html?fr=iks&word=ln2&ie=gbk&dyTabStr=MCw0LDMsMiw2LDEsNSw3LDgsOQ==";//std::string url = "https://www.cnblogs.com/-clq/archive/2012/01/31/2333247.html";url += std::to_string(rand());v3.push_back(url);}size_t n3 = 0;for (auto& str : v3){if (bf.Test(str)){++n3;}}cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;}
使用布隆过滤器的需求:数据量大,节省空间,允许误判,这样的场景,可以使用布隆过滤器
举例场景1:注册时候的昵称有无被使用 ,如果允许昵称偶然的误判,就没多大影响,
注册没被注册过的”昵称“一定是准确的,为了解决上面的一点误判,”昵称被使用“
的情况下再去数据库里进行查询
举例场景2:垃圾邮箱的判断
举例场景3:前端放一个布隆过滤器,过滤数据(”不存在“是准确的)
布隆过滤器扩展
题1:给两个文件,分别由100亿个query,我们只有1G内存,如何找到两个文件的交集,分别给出精确算法
和近似算法
交集里面 存在不是交集的 query ,这种思维下 布隆过滤器
query是字符串,比如 是sql语句,是网络请求url
假设每个query是10byte,100亿个query需要多少存储空间? --100G左右
->思路:哈希切分
如果Ai和Bi都太大,超过内存,可以考虑换个哈希算法,再切分一次
题2:如何扩展BloomFilter使得它支持删除元素的操作
不支持删除一个数,删除可能会影响其他值
建议方法:每个标记使用多个比特位,存储引用技术(有几个值映射了当前位置)
一般用8位比特位,实在溢出了用其他容器单独存储
上面支持删除,但是整体而言消耗空间变多了,布隆过滤器的优势下降了
题3:给一个超过100G的log file,log中存折IP地址,设计算法找出出现次数最多的IP地址?
哈希切分
依次读取ip,i = BKDRHash(ip) % 100
i是多少,ip就进入对应的编号i小文件中
相同的ip一定进入同一个小文件中,那么我们使用map统计一个小文件中的ip次数,就是他准确的次数
每一次更新都要把上一个map中的数据clear掉
比如说要找这些ip中最大的10个ip
pair<string,int> maxCountIP;
出现次数最多的10个ip, priority_queue<pair<string,int>…> minHeap
小堆找最大
拓展:一次性哈希
哈希(解决哈希冲突,封装map/set,哈希的应用{位图/布隆过滤器})相关推荐
- C++ 第十节 ——哈希 unordered_map/unordered_set的封装 位图 布隆过滤器 海量数据处理
我们本节继续讲解STL里的内容. 哈希表又叫做散列表,它可以用来封装unordered_map和unordered_set. 我们还是按照老规矩来,先介绍其用法,再介绍其原理,并引到哈希上来. 目录 ...
- 哈希表、位图、布隆过滤器
目录 哈希的概念 哈希函数 哈希冲突和解决方法 闭散列 插入 查找 删除 开散列 插入 查找 删除 哈希表(开散列)整体代码 位图 位图模拟实现思路分析: 位图应用 布隆过滤器 本文介绍unorder ...
- 解决哈希(HASH)冲突的主要方法
虽然我们不希望发生冲突,但实际上发生冲突的可能性仍是存在的.当关键字值域远大于哈希表的长度,而且事先并不知道关键字的具体取值时.冲突就难免会发 生.另外,当关键字的实际取值大于哈希表的长度时,而且表中 ...
- 解决哈希(hash)冲突的方法
解决哈希(hash)冲突的方法 当关键字值域远大于哈希表的长度,而且事先并不知道关键字的具体取值时.冲突就难免会发生.另外,当关键字的实际取值大于哈希表的长度时,而且表中已装满了记录,如果插入一个新记 ...
- 【C++ 包装器类 map】C++ 标准库(std)中的map结构 哈希表(unordered_map)和黑红树(map)教程
目录标题 1. 哈希表(unordered_map)和黑红树(map)简介以及初始化 1.1 哈希表的基本介绍 1.1.1 哈希表初始化接口示例 1.1.2 哈希表的键值的注意事项 1.1.3 自定义 ...
- C++--哈希表--散列--冲突--哈希闭散列模拟实现--问答--1107
1.哈希 概念 可以不经过任何比较,直接从表中得到要搜索的元素. 关键在于通过某种散列函数,使元素的存储位置与它的关键码之间能够建立 一一映射的关系.这样就可以通过o(1)的时间复杂度来寻找到元素. ...
- 哈希表and处理冲突的方法
哈希法又称散列法.杂凑法以及关键字地址计算法等,相应的表称为哈希表.这种方法的基本思想是:首先在元素的关键字k和元素的存储位置p之间建立一个对应关系f,使得p=f(k),f称为哈希函数.创建哈希表时, ...
- 算法训练Day6 | LeetCode:242. 有效的字母异位词(数组作哈希表);349. 两个数组的交集(Set作哈希表);202.快乐数 (Set作哈希表);1. 两数之和(Map作哈希表)
目录 LeetCode242. 有效的字母异位词 方法:数组作哈希表 1. 思路 2. 代码实现 3. 复杂度分析 4. 思考 Leetcode349. 两个数组的交集 方法一:用Set作HashMa ...
- Python数据结构实战——哈希表中的冲突处理(Collision Handling In Hash Table)
文章目录 1.定义哈希类(能处理冲突) 2.测试 2.1.增加键值 2.2.查找对应键值 2.3.查看哈希表 2.4.更新值 2.5.删除键值对 1.定义哈希类(能处理冲突) class HashTa ...
最新文章
- 值得安利!推荐7款让人眼前一亮的宝藏软件
- 元素均匀排列自动换行二维数组前端遍历
- 《幸福资本论》读书笔记
- 踩到一个Emit的坑,留个纪念
- 《Effective Java》读书笔记八(异常)
- Docker源码分析(五):Docker Server的创建
- Android Service演义
- 百度NeurIPS全球顶会冠军团队,带你7日从零实践强化学习
- c语言判定三角形方法,c语言判定三角形的各种类型——请大家指点
- 史上最全的开发和设计资源大全
- Ipmi 远程管理工具
- guava-Retryer失败重试
- 几点减几点怎么列算式_结婚邀请函怎么写样板 结婚邀请函有哪几点是必写的...
- chrome pdf viewer 参数
- 人工免疫算法与物流中心选址问题
- scala练习——fold函数
- 阿里云-云开发平台存储篇——给应用接上后端存储能力
- 阿里巴巴编码规范 java认证记录
- IT笔记-电脑内存满之系统报告过多问题
- 数字经济2.0—趋势、逻辑、选择
热门文章
- 如果我的猫也会写字——猫的日记猜想
- Ubuntu20.04设置静态IP,安装Samba服务,实现win10远程访问文件夹
- ant自动化需要用到的命令
- tf.quantization
- 计算机断层扫描英语怎么说,“SPECT”是“Single-Photon Emission Computed Tomography”的缩写,意思是“单光子发射计算机断层扫描”...
- Prolog教程 16
- ai画面怎么调大小_ai怎么调整画布尺寸? ai设置画布大小的两种方法
- 【JavaSE】高淇300集第一季笔记
- 力扣 717. 1比特与2比特字符
- 计算机辅助药物设计 牛彦,牛彦