哈希思想及其应用

 

目录

1.unordered系列容器

1.1简介

1.2接口 (只对效果进行描述,具体可以自行参考文档)

1.2.1构造

1.2.2容量

1.2.3迭代器

1.2.4访问

1.2.5查询

1.2.6修改

1.2.7桶操作

2.哈希 

2.1概念

2.2哈希冲突

2.2.1闭散列

2.2.2开散列

2.2.2.1哈希文件

2.2.2.2unordered_map模拟

2.2.2.3unordered_set模拟

3.位图

模拟

应用 

4.布隆过滤器

模拟

优点:

缺陷:

应用


首先要说明的是unordered系列的容器,但是使用上unordered_map、unordered_set跟map、set区别不大,我这里不会再详细的介绍每个接口

1.unordered系列容器

1.1简介

set和unordered_set的区别,效率上unordered_set只是略微胜出一点,跟下面unordered_map和map的区别是差不多的,都是底层实现不同,所以不多赘述。详情可自行参考文档

unordered_map跟map的区别

1.unordered_map内部存储不会排序,为了能够常数级别访问,会有个哈希值,且把哈希值相同的放在同样的桶中(哈希值是什么,看下面具体哈希的内容,这里只是对容器做个说明)

unordered_map访问单个元素,比map快,但是遍历元素方面效率较低。

2.只有单向迭代器,没有反向迭代器。

1.2接口 (只对效果进行描述,具体可以自行参考文档)

1.2.1构造

unordered_map<int>has

1.2.2容量

empty()检测容器是否为空。

size获取容器有效元素个数

1.2.3迭代器

begin(),end(),cbegin(),cend().

1.2.4访问

has[3]。支持括号访问

原理跟map的括号一样,都是调用插入操作。

1.2.5查询

find(),返回key在哈希桶中的位置

count返回哈希桶中关键码为key的键值对的个数(unordered_map里面key不能重复,所以unordered_map的count只能返回1或0)

1.2.6修改

insert()插入,erase删除,clear清空,swap(unordered_map &)交换两个容器的元素

1.2.7桶操作

bucket count()   返回哈希中桶的总个数

bucket size(size_t n) 返回n号桶中有效元素的总个数

bucket(const K&key)返回元素key所在的桶号

2.哈希 

2.1概念

顺序结构以及AVL树种,元素关键码与其存储位置之间没有对应的关系,因此在查找在查找一个元素的时候,必须经过关键码的多次比较,顺序查找时间复杂度O(n),AVL树是为树的高度,即O(log n),搜索效率取决于搜索过程中元素的比较次数.

理想情况:不经过任何比较,一次直接获得要搜索的元素,即常数级别。

因此,哈希这种元素存储位置与关键码有映射关系的结构就出现了。

哈希:
根据插入元素的关键码,通过哈希函数获得对应存储位置,然后进行存放查找。

若查找的话,就是根据搜索元素的关键码通过哈希函数获得位置,然后与此位置的关键码进行比较。相等则成功。

在思想上跟桶排序有很大的关系,但是桶排序的空间浪费很大,所以这里不能直接通过下标确定位置。

哈希函数就是:hash(key)=key%capacity  capacity就是存储元素空间的总大小。

负载因子:对于闭散列,基本都是0.几,而开散列,可以到1。负载因子就是有效数据占容器容量的比例。

2.2哈希冲突

但是有哈希冲突,就是有可能不同的关键码,获得的存储位置确实一模一样的,这就是冲洗冲突或哈希相撞

哈希函数的设定方式有很多种,上面只是一种。

直接定址法、除留余数、平方取中、折叠法、随机数法、数学分析法。

而解放方式常见的:闭散列、开散列。

2.2.1闭散列

也叫开放定址法,发生哈希冲突时,如哈希表未满,则一定还有位置,那就把key放在冲突位置的下一个空位置去。注意,哈希结构我们人为的限制不能装满,有一个负载因子\载荷因子做评估,实际元素个数/总空间大小,闭散列一般是0.7,因为如果满了的话,查找效率是O(n)。所以对于元素存储个数达到限制后,一般就直接空间扩容,用空间换取效率

线性探测:

从发生冲突的位置开始,依次向后探测,如果找到的最后一个位置,则跳到空间的开头,直到找到一个空位置。

        插入:通过哈希函数获取元素的在哈希表的位置,如果该位置没有元素直接插入,有元素的话利用线性探测获取下一个空位置的地址,然后把该值放进去。

        搜索:如果该位置元素不相等,则线性往后找,直到找到\遇到空

        删除:闭散列删除的时候,不能直接删,因为我们搜索的逻辑是从直接获得地址开始往后找,这样的话,如果前面一个元素已经被删了,但是我们要查找的元素在后面,这样遇到空就不会继续找,也就不会找到我们要的元素而且我们如何确定删除的状态呢?。所以要采取标记的伪删除法来删除一个元素。

实现代码:

#pragma once
#include<vector>
enum State {EMPTY,EXIST,DELETE
};
template<class K, class V>
struct HashData {pair<K, V> _kv;State _state=EMPTY;//标记该位置状态
};//对于非整数的key,默认采用强转的方式
template<class K>
struct HashFunc {size_t operator()(const K& key){return (size_t)key;}
};
//如果不能通过强制的类型,则单独写个仿函数
//自己写的类,也可以这样。日期类就可以把年月日加起来,中间*权值
//不一定都是成员加起来,像是个人信息,就可以单独把身份证号的ASCII码加起来,中间*权值
//核心就是把非唯一的值尽量拼起来,然后以拼起来的值(中间可以*权值),作为key
//struct HashFuncString {
//	size_t operator()(const string& s) {
//		size_t hash = 0;
//		for (auto e : s)
//		{
//			hash += e;
//			hash *= 131;//权值,否则的话,abcd==dcba,太容易冲突了
//		}
//		return hash;
//	}
//};//一般来说,string经常用,所以直接特化(库里也是这样的,但自己写的类,还是要自己传仿函数)
//而且库里还有一个仿函数,是用来比较两个key是否相等的仿函数
template<>
struct HashFunc<string> {size_t operator()(const string& s) {size_t hash = 0;for (auto e : s){hash += e;hash *= 131;//权值,否则的话,abcd==dcba,太容易冲突了}return hash;}
};
template<class K,class V, class Hash=HashFunc<K>>
class HashTable {
public:HashTable(size_t size=10) {_tables.resize(size);}HashData<K,V>* Find(const K&key){Hash hs;//线性探测size_t hashi = hs(key) % _tables.size();while (_tables[hashi]._state!=EMPTY){if (key == _tables[hashi]._kv.first && _tables[hashi]._state == EXIST){return &_tables[hashi];}hashi++;hashi %= _tables.size();}return nullptr;}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;//如果负载因子大于等于0.7要扩容if (_n*10 / _tables.size() >= 7){HashTable<K, V,Hash>newHT(_tables.size() * 2);//遍历旧表,插入新表for (auto& e : _tables){if (e._state == EXIST){newHT.Insert(e._kv);}}//最后交换,旧表在newHT出了作用域之后,//newHT会自动调用析构//这里也不用特意写析构,因为默认生成的析构足够了//vector会调用vector的析构函数,int类型内置类型_tables.swap(newHT._tables);}Hash hs;//线性探测size_t hashi = hs(kv.first)%_tables.size();while (_tables[hashi]._state == EXIST){hashi++;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){_n--;ret->_state = DELETE;return true;}else{return false;}}
private:vector<HashData< K,V>>_tables;size_t _n;//实际存储的数据个数
};void TestHT1() {int a[] = { 1,4,24,34,45,7,44,17,37 };HashTable<int, int>ht;for (auto& e : a){ht.Insert(make_pair(e, e));}for (auto& e : a){auto ret = ht.Find(e);cout << ret->_kv.first << ":" << ret->_state << endl;}ht.Erase(34);ht.Erase(4);cout << endl;for (auto& e : a){auto ret = ht.Find(e);if (ret)cout << ret->_kv.first << ": EXIST" << endl;else cout << e << ": DELETE" << endl;}}
void TestHT2() {HashTable<string,string>ht;ht.Insert(make_pair("dwadwa", "dwadwa"));ht.Insert(make_pair("dsxzdwa", "dwadwa"));
}

二次探测:在线性探测的基础上,每次跨越的步伐更加大些。

线性探测是:hashi+i,i>=0,二次探测就是hashi+i^2,i>=0

让整个空间的存放更加均匀,而不是挤在某一块

结论:

在闭散列的结构下,这些探测本质还是一种互相挤占空间的方式,近似恶意循环。

2.2.2开散列

相对于闭散列,开散列不将冲突的值放其他位置,开散列下,每个空间都是一个链表,这个链表里存哈希值相同的元素

结构很清晰,就是利用链式结构。

2.2.2.1哈希文件

这是底层头文件,后面的unordered_map和unordered_set模拟,都是在这个头文件的基础上封装的

#pragma once
#include<vector>
//对于非整数的key,默认采用强转的方式
template<class K>
struct HashFunc {size_t operator()(const K& key){return (size_t)key;}
};
//如果不能通过强制的类型,则单独写个仿函数
//自己写的类,也可以这样。日期类就可以把年月日加起来,中间*权值
//不一定都是成员加起来,像是个人信息,就可以单独把身份证号的ASCII码加起来,中间*权值
//核心就是把非唯一的值尽量拼起来,然后以拼起来的值(中间可以*权值),作为key
//struct HashFuncString {
//	size_t operator()(const string& s) {
//		size_t hash = 0;
//		for (auto e : s)
//		{
//			hash += e;
//			hash *= 131;//权值,否则的话,abcd==dcba,太容易冲突了
//		}
//		return hash;
//	}
//};//一般来说,string经常用,所以直接特化(库里也是这样的,但自己写的类,还是要自己传仿函数)
//而且库里还有一个仿函数,是用来比较两个key是否相等的仿函数
template<>
struct HashFunc<string> {size_t operator()(const string& s) {size_t hash = 0;for (auto e : s){hash += e;hash *= 131;//权值,否则的话,abcd==dcba,太容易冲突了}return hash;}
};
namespace openadress {enum State {EMPTY,EXIST,DELETE};template<class K, class V>struct HashData {pair<K, V> _kv;State _state = EMPTY;//标记该位置状态};template<class K, class V, class Hash = HashFunc<K>>class HashTable {public:HashTable(size_t size = 10) {_tables.resize(size);}HashData<K, V>* Find(const K& key){Hash hs;//线性探测size_t hashi = hs(key) % _tables.size();while (_tables[hashi]._state != EMPTY){if (key == _tables[hashi]._kv.first && _tables[hashi]._state == EXIST){return &_tables[hashi];}hashi++;hashi %= _tables.size();}return nullptr;}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;//如果负载因子大于等于0.7要扩容if (_n * 10 / _tables.size() >= 7){HashTable<K, V, Hash>newHT(_tables.size() * 2);//遍历旧表,插入新表for (auto& e : _tables){if (e._state == EXIST){newHT.Insert(e._kv);}}//最后交换,旧表在newHT出了作用域之后,//newHT会自动调用析构//这里也不用特意写析构,因为默认生成的析构足够了//vector会调用vector的析构函数,int类型内置类型_tables.swap(newHT._tables);}Hash hs;//线性探测size_t hashi = hs(kv.first) % _tables.size();while (_tables[hashi]._state == EXIST){hashi++;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){_n--;ret->_state = DELETE;return true;}else{return false;}}private:vector<HashData< K, V>>_tables;size_t _n;//实际存储的数据个数};
}
namespace hash_bucket {//数据量很大的时候,table的每个位置,可以不挂单链表,而是直接挂红黑树。//这样就算遇到极端情况,查找效率也很高。//从复用角度看,如果不想自己手搓,可以直接让Node//里面分别放list(),set(),当前桶里元素个数超过8个,遍历放入set里。//然后list.clear(),反之,放入list(),set.clear();template<class T>struct HashNode {HashNode<T>* _next;T _data;HashNode(const T&data):_next(nullptr), _data(data){}};//前置声明,因为下面迭代器类typedef了哈希表类,但是哈希表类下迭代器类的下面。//编译器找不到,也就识别不了迭代器类里的哈希表//但单纯两者对换也不行,因为哈希表类里也typedef了迭代器类//这样的话,互相引用。解决方法就是把处于下面的哈希表类进行前置声明//这样编译器就能识别了template<class K, class T, class KeyOfT, class Hash>class HashTable;template<class K, class T, class KeyOfT, class Hash>struct _HTIterator {size_t hti;typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;typedef _HTIterator<K, T, KeyOfT, Hash> Self;HT* _ht;Node* _node;_HTIterator(Node* node, HT* ht):_node(node), _ht(ht){}T& operator*() {return _node->_data;}T* operator->() {return &_node->_data;}Self& operator++() {if (_node->_next){//当前桶没有走完_node = _node->_next;}else{//当前桶走完了,找下一个KeyOfT kot;Hash hs;size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size();hashi++;//找下一个桶while (hashi < _ht->_tables.size()){if (_ht->_tables[hashi]){_node = _ht->_tables[hashi];break;}hashi++;}//后面没有桶了if (hashi == _ht->_tables.size()){_node = nullptr;}}return *this;}bool operator!=(const Self& s){return _node != s._node;}};template<class K, class T, class KeyOfT,class Hash>class HashTable {template<class K, class T, class KeyOfT, class Hash>friend struct _HTIterator;typedef HashNode<T> Node;public:typedef _HTIterator<K, T, KeyOfT, Hash> 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);}//素数大小的容器,没有定论,vs下没有这样int GetNextPrimer(size_t val) {const int prime[28] = { 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 < 28; i++){if (prime[i] > val)return prime[i];}return prime[27];}HashTable() {_tables.resize(GetNextPrimer(1), nullptr);_n = 0;}~HashTable() {//一个个桶找过来,诶个释放for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}//为了用[]的结构pair<iterator,bool> Insert(const T& data){KeyOfT kot;iterator it = Find(kot(data));if (it!=end()){return make_pair(it,false);}Hash hs;if (_n == _tables.size()){//这里不复用insert,是vector里的每个地址指向的空间//在复用的时候,都要经历new n个节点,析构n个节点。//消耗很大vector<Node*>newTables(GetNextPrimer(_n), nullptr);//遍历旧表,头插新表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = hs(kot(cur->_data)) % newTables.size();cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}size_t hashi = hs(kot(data)) % _tables.size();Node* newnode = new Node(data);//头插newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return make_pair(iterator(newnode,this),true);}iterator Find(const K& key){KeyOfT kot;Hash hs;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];while (cur){//这里其实还要再配一个比较的仿函数的if (kot(cur->_data) == key){return iterator(cur, this);}cur = cur->_next;}return end();}iterator Erase(const K& key){KeyOfT kot;Hash hs;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (kot(cur->_data) == key){if (prev){prev->_next = cur->_next;delete cur;--_n;if(prev->_next)return iterator(prev->_next,this);else{for (size_t i = hashi+1; i < _tables.size(); i++){//从左往右,第一个桶if (_tables[i]){return iterator(_tables[i], this);}}return end();}}else{_tables[hashi] = cur->_next;delete cur;--_n;if(_tables[hashi])return iterator(_tables[hashi], this);else{for (size_t i = hashi+1; i < _tables.size(); i++){//从左往右,第一个桶if (_tables[i]){return iterator(_tables[i], this);}}return end();}}}prev = cur;cur = cur->_next;}return end();}private://vector<list<pair<K,V>>>_tables;可行,但自己写个也不错vector<Node*>_tables;//指针数组size_t _n;};}
2.2.2.2unordered_map模拟
#pragma once
#include"newHashTable.h"namespace bit {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 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 = Insert(make_pair(key, V()));return ret.first->second;}iterator Find(const K& key){return _ht.Find(key);}iterator erase(const K& key){return _ht.Erase(key);}private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT,Hash>_ht;};
}
2.2.2.3unordered_set模拟
#pragma once
#include"newHashTable.h"namespace bit {template<class K,class Hash=HashFunc<K>>class unordered_set{struct SetKeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;iterator begin() {return _ht.begin();}iterator end() {return _ht.end();}pair<iterator,bool> Insert(const K& key){return _ht.Insert(key);}iterator Find(const K& key){return _ht.Find(key);}iterator erase(const K& key){return _ht.Erase(key);}private:hash_bucket::HashTable<K, const K, SetKeyOfT,Hash>_ht;};void test_set1() {unordered_set<int>us;for (int i = 0; i < 200; i++)us.Insert(i);unordered_set<int>::iterator it = us.begin();while (it != us.end()){cout << (*it) << endl;++it;}}
}

3.位图

位图, 就是每一位用来存放状态,适用于海量数据,数据不重复。一般是查找数据在不在。思想也是用哈希实现。库里也有实现,是bitset。

这里是用的直接定址法。所以开多大取决于数据范围,而不是数据量

位图可以用在什么地方?

1. 快速查找某个数据是否在一个集合中
2. 排序 + 去重
3. 求两个集合的交集、并集等
4. 操作系统中磁盘块标记

接下来是具体的模拟,附带几道变形题

模拟

#pragma once
#include<vector>
#include<cassert>namespace bit {template<size_t N>class bitset {public://不用担心负数的问题,负数会转成size_t,还是可以做映射的。//bitset() {_bits.resize(N / 32 + 1, 0);}//把x映射的标记为1void set(size_t x){assert(x <= N);//因为没法开位数组,所以必须开int的话,要以32位为一组,分为存储。size_t i = x / 32;size_t j = x % 32;_bits[i] |= (1 << j);//利用或,有1为1,让1左移j位,此时二进制位上,第j位是1,其他为0,然后让这个数跟_bits[i]或以下,这样就能让第i个整形,第j位修改为1//或许会疑惑,为什么不是32-j,因为左移是向高位移动,所以不管是小端机还是大端机,都一样。x%32,得到的是从低位到x的距离,整个内部存储,//映射不是线性的。}//把x映射的标记为0void reset(size_t x){assert(x <= N);size_t i = x / 32;size_t j = x % 32;//跟上面类似,只是这里要让_bits[i]与上一个11111110111111111_bits[i] &= ~(1 << j);}//查找数是不是存在bool Test(size_t x){assert(x <= N);size_t i = x / 32;size_t j = x % 32;//这里不修改,只探测,返回一个整型值,整型值转bool,非0真,0假。所以只要该二进制位为1,那最后返回就是真,为0就是假。return _bits[i] & (1 << j);}private:vector<int>_bits;};void test_bitset() {bitset<31>mp;mp.set(30);mp.set(20);mp.set(10);for (int i = 1; i <= 30; i++){if (mp.Test(i)){cout <<i<< "在" << endl;}else{cout <<i<< "不在" << endl;}}mp.reset(30);mp.set(29);for (int i = 1; i <= 30; i++){if (mp.Test(i)){cout << i << "在" << endl;}else{cout << i << "不在" << endl;}}}// 给定100亿个整数,设计算法找到只出现一次的整数?template<size_t N>class two_bitset {public:void set(size_t x){//00 0次,01 1次,10 2次及以上if (_bs1.Test(x) == false && _bs2.Test(x) == false)_bs2.set(x);else if (_bs1.Test(x) == false && _bs2.Test(x) == true) {_bs1.set(x);_bs2.reset(x);}}int test(size_t x){if (_bs1.Test(x) == false && _bs2.Test(x) == false)return 0;else if (_bs1.Test(x) == false && _bs2.Test(x) == true) {return 1;}else return 2;}private:bitset<N>_bs1;bitset<N>_bs2;};void test_two_bitset() {two_bitset<100>mp;int a[] = { 2,4,4,56,65,45,56,76,67,76 };for (auto e : a){mp.set(e);}for (auto e : a){cout << mp.test(e) << endl;}}
}

应用 

Q2:给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

  依旧是利用位图,代码不写了,直接写思路。两个位图,两个文件分别存值,然后看是否两个位图里都存在这个数。存在就是交集,否则就不是交集一部分。

注意,不要被100亿数字吓到,整数,这里默认都是int范围的,也就是说位图肯定是可以存的,那这100亿有很多重复的数字,是不用管的,默认去重了。

Q3: 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

是第一道题的变形,只是00 0次,01 1次, 10 2次 11 3次及以上

对于第一个问题,我们还可以限制内存大小,比如100亿,1G是够的,但是如果只让用512M呢?我们的思路是多批次读取,每次只统计一段范围的值。比如0-2^31-1是512M,2^31-2^32-1也是512M。这样每次读取都只用到512M。

4.布隆过滤器

 主要是对位图的一个小修改,位图对于非整型数据,虽然也能存(通过转换成整型再映射的方式,但是冲突的概率很大),所以布隆的主要思想就是让单个数据映射多个位置,只要多个位置有一个位0,则一定不存在。多个位置全为1,只有很低的概率才是不存在,大概率是存在的。只要让映射的位置变多,冲突的概率就变小了。

模拟

#pragma once
#include<bitset>
#include<string>
struct HashFuncBKDF {//BKDRsize_t operator()(const string& s){size_t hash = 0;for (auto ch : s){hash += ch;hash *= 131;}return hash;}
};
struct HashFuncAP {//APsize_t operator()(const string& s){size_t hash = 0;for (size_t 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 HashFuncDJB {//DJBsize_t operator()(const string& s){size_t hash = 5381;for (auto ch : s){hash = hash * 33 ^ ch;}return hash;}
};
template<size_t N, class K = string, class Hash1 =HashFuncBKDF, class Hash2= HashFuncAP, class Hash3 = HashFuncDJB>
class BloomFilter {
public:void Set(const K& key){size_t hash1 = Hash1()(key) % M;size_t hash2 = Hash2()(key) % M;size_t hash3 = Hash3()(key) % M;_bs.set(hash1);_bs.set(hash2);_bs.set(hash3);}bool Test(const K& key){size_t hash1 = Hash1()(key) % M;if (_bs.test(hash1) == false)return false;size_t hash2 = Hash2()(key) % M;if (_bs.test(hash2) == false)return false;size_t hash3 = Hash3()(key) % M;if (_bs.test(hash3) == false)return false;//不存在是准确的return true;//存在是有可能误判的,因为可能这3个映射位置都跟别的冲突了//这也是为什么说映射位置增加在一定程度上是可以减少误判概率的}//为什么不实现reset?//因为删了某一个,会影响与他有冲突的元素,导致该元素被误判成不存在。
private://这里开多大,知乎上有一个大佬做了分析//https://zhuanlan.zhihu.com/p/43263751/static const size_t M = 8 * N;//为什么要用static?因为就算是常量,在这里也只是类里面的成员的声明,//在不实例化前是不占空间的,也就是说不会在常量区里面存放,这样编译器//就无法在常量区中找到他,无法找到,就说明这不是个常量,而bitset内部也是数组//存储,数组的大小必须是常量,所以只const,是会报错的//而static,是不属于单个对象,而是整个类的,就算没有实例化,他也是占据空间的。bitset<M> _bs;};void TestBloomFilter()
{string x[] = { "西瓜","苹果","梨子" };BloomFilter<10>bf;for (auto& s : x){bf.Set(s);}for (auto& s : x){cout << bf.Test(s) << endl;}for (auto& s : x){cout << bf.Test(s+" ") << endl;}
}

这里说个题外话,stl里的bitset,一千万级是可能栈溢出的,因为stl里是把数据放在栈上,用的是静态数组,而不是vector,可以实测,sizeof库里的bitset和我上面实现的bitset。

为什么可以测出是在栈上而不是堆上?我也不是很清楚,对这方面可以看下这篇文章

程序员应了解的那些事(9) STL vector:sizeof(vector)分析 + sizeof小结-CSDN博客

那实际使用的时候不想手撕怎么办,可以让布隆的成员bitset,以指针的形式存在,然后new一个就好,这样bitset就放在堆上了。

这里总结下布隆过滤器

优点:

1.增加和查询元素的时间复杂度O(K),K是哈希函数的个数,一般比较小。与数据量大小无关

2.哈希函数相互之间没有关系,方便硬件并行运算

3.布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势

4.在能够承受一定的误判时,布隆过滤器比其他数据结构有很大的空间优势

5.数据量很大时,布隆过滤器可以表示全集,其他数据结构不能

6.使用同一组散列函数的布隆过滤器可以进行交、并、差运算

缺陷:

1.有误判率,有个说法叫假阳性(False,Position),只能准确判断元素不在集合中,不能准确判断元素在集合中,(可以建立白名单,存储可能误判的数据)

2.不能获取元素本身

3.一般情况下不能从布隆过滤器中删除元素

4.如果采用计数方式删除,存在计数回绕问题。

应用

对于一些特殊场景,虽然不容忍误判,但是布隆过滤器还是可以做到减少数据库压力的作用。因为布隆过滤器判断不在,那一定是不在,可以直接返回结果,如果判断在,再去数据库进行查找,实现减少数据库压力的作用

1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法

近似算法,采用布隆过滤器,在,就是在,不在就是不在,因为有误判可能,所以只能是近似算法。

而精确算法:布隆做不到,而用哈希map或者红黑树等,也不现实,因为100亿个查询,就算一个查询只占50字节,那也是相当高的占用空间,所以还是采用哈希的思想,用哈希切分,将大文件差分成小文件,利用哈希函数,映射查询,将大文件拆分下来的文件进行编号,让哈希值做编号,对应查询放入对应的小文件中,因为哈希函数对同一个查询返回的哈希值一定是相同的,所以对于交集,只需要两个大文件A和B的每个Ai和Bi放入内存比较即可(这时候用红黑树、平衡树、哈希表等等,都可以)。具体操作,可以试着把100亿的文件,拆成1000份,也就是hash函数%的数是1000,之所以不拆500份,是因为查询的占据空间不是一样的,有些文件很大,有些文件很小,如果拆成500份,冲突多的话,两个文件会超过1g,就不能放入内存了,这样拆成1000份,就可以让冲突的可能性降低,如果冲突多,可以考虑%1000以上)

但是还是有极端情况,比如某些查询的值想当接近,导致某个文件过大,还是超过了1g,这时候如果对这个文件再次进行哈希切分,效果不大,因为在这个情况下,不管是什么哈希函数,最后还是可能会分到一个文件,还是超过了1g。这个问题其实不用管太多,因为如果是因为查询的值过于接近、冲突过多,导致的Ai文件过大,那直接读入bitset,或者set,map等,都是可以直接读的,因为这些容器基本都是去重的,虽然这个文件可能很大,但是去掉重复,也就那么点。

如果是大于1g,且重复内容不多的文件,那么才可以采取最开始说的,二次哈希切分,换个哈希函数。

总结下,对于冲突多的,采用二次哈希切分。对于重复多的,采用set、map、bitset等可以去重的容器读取文件即可。这也是哈希切分下来,某个文件过大的处理方案
2. 如何扩展BloomFilter使得它支持删除元素的操作

可以用引用计数的方式,这样就可以让布隆支持删除,但是这样空间消耗会大很多,一个标记要开8位。极端情况,消耗跟红黑树之类的差不多,这样就没有意义了

3.给一个超过100G大小的log文件,log中存着ip地址,设计算法找到出现次数最多的ip地址,并且如何找到top k的ip

这题跟上一次的思路是相似的。同样是哈希切分,让相同ip进入相同的文件,一个文件只会是相同的ip和有冲突的ip。这时候用map读文件,虽然没有限制内存,但是map的容量也是有限的,并且极端情况可能出现一个文件很大(因为冲突多而不是重复多,重复多放map里,只是让value++罢了),所以map有几率爆,这时候采取二次处理,即换个哈希函数,再次对该小文件进行哈希切分。而找到出现次数多的ip,只需要让每个文件挨个放进map计数即可,就可以知道答案了。而对于top k的ip,则只需要弄个优先队列的小堆,自己写个仿函数或比较函数即可。

结论:

海量数据问题特征:数据量大,单台机器内存存不下

1.先考虑是否有合适的数据结构可以解决,比如位图、堆、布隆过滤器等等

2.大事化小思路。哈希切分(不能平均切分,就算可以复杂度也很高,效率很慢,有些问题就算平均切分了,也无法解决),切小之后,就能放内存中处理了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/461867.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++STL详解(九)map和set的使用

一.关联式容器的介绍 CSTL包含了序列式容器和关联式容器&#xff1a; 序列式容器里面存储的是元素本身&#xff0c;其底层是线性的数据结构&#xff0c;就譬如我们之前学习的vector&#xff0c;list&#xff0c;deque等等。关联式容器里面存储的是<key,value>的键值对&…

WPF+MVVM案例实战(九)- 霓虹灯字效果控件封装实现

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 1、运行效果2、主菜单与界面实现1、主菜单2、霓虹灯字界面实现3、字体资源获取3、控件封装1.创建自定义控件2、依赖属性实现3、封装控件使用4、运行效果4、源代码获取1、运行效果 2、主菜单与界面实…

凸极式发电机的相量图分析和计算,内功率因数角和外功率因数角和功角的定义。

图1&#xff1a;同步发电机稳态相量图 若发电机为凸极式&#xff0c;由于凸极机正、交轴同步电抗不等&#xff0c;即xd≠xq&#xff0c;因此必须先借助虚构电动势 E ˙ Q E ˙ q − ( x d − x q ) I ˙ d \dot{E}_Q\dot{E}_q-(x_d-x_q)\dot{I}_d E˙Q​E˙q​−(xd​−xq​)…

基于Spring Boot的员工与部门信息管理系统:增删改查全攻略

介绍项目的搭建过程&#xff0c;包括依赖管理、数据库设计、实体类的创建、控制器的编写以及前端的简单实现。希望通过本项目的学习&#xff0c;能够加深大家对Spring Boot及相关技术的理解&#xff0c;为后续的开发奠定基础。 文章目录 前言 环境搭建 开发规范 查询部门 删除部…

Java Executor ScheduledExecutorService 源码

前言 相关系列 《Java & Executor & 目录》《Java & Executor & ScheduledExecutorService & 源码》《Java & Executor & ScheduledExecutorService & 总结》《Java & Executor & ScheduledExecutorService & 问题》 涉及内容 …

SmartX 在新能源:支撑多家头部企业 MES 等核心系统稳定运行与 VMware 替换

在过去几年中&#xff0c;中国新能源企业经历了迅猛的增长。随着电池技术、光伏发电和风电等领域的不断进步&#xff0c;新能源企业不仅面临生产能力的提升需求&#xff0c;还需要优化运营效率和管理复杂度&#xff0c;其基础设施建设则需要不断升级以适应这种快速扩展的需求&a…

最新出炉!ffmpeg视频滤镜:提取灰度图像-extractplanes

滤镜的描述 extractplanes 滤镜的官网 》 FFmpeg Filters Documentation 这个滤镜可以将视频的像素格式的各个分类分别提取出来&#xff0c;比如你的像素格式是yuv420, 通过这个滤镜可以分别将y/u/v提取出来并进行存储&#xff0c;此时存储y分量的图片&#xff0c;就是灰色…

Webserver(1.6)Linux系统IO函数

目录 open函数打开已有文件创建新文件 read和write函数lseek函数stat和lstat函数 open函数 man 2 open 打开已有文件 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h>int main(){i…

【02基础】- RabbitMQ基础

目录 2- RabbitMQ2-1 介绍和安装安装 2-2 RabbitMQ 快速入门2-3 RabbitMQ 数据隔离 3- Java客户端3-1 快速入门AMQP快速入门&#x1f4d1;小结&#xff1a;SpringAMQP如何收发消息&#xff1f; 3-2 WorkQueues 任务模型案例-使用 WorkQueue 单队列绑定多消费者&#x1f4d1;小结…

Linux版更新流程

一.下载更新包 下载地址&#xff1a;https://www.nvisual.com/%e4%b8%8b%e8%bd%bd/ 二.更新包组成 更新包由三部分组成&#xff1a; 前端更新包&#xff1a;压缩的ZIP文件&#xff0c;例如&#xff1a;dist-2.2.26-20231227.zip (2.2.26是版本号 20231227是发布日期)后端更…

音视频入门基础:FLV专题(18)——Audio Tag简介

一、引言 根据《video_file_format_spec_v10_1.pdf》第75页&#xff0c;如果某个Tag的Tag header中的TagType值为8&#xff0c;表示该Tag为Audio Tag&#xff1a; 这时StreamID之后紧接着的就是AudioTagHeader&#xff0c;也就是说这时Tag header之后的就是AudioTagHeader&…

再探“构造函数”

文章目录 一. 初始化列表1.1 实现1.2 何时必须使用初始化列表2.3 尽量使用初始化列表 二. 类型转换2.1 内置类型 转换 类类型2.2 explicit&#xff1a;不转换2.3 构造函数多参数2.4 使用隐式转换 2.5 自定义---转换为--->自定义类型 三. 静态成员变量概念在main函数调用私有…

静态路由实现路由互通

静态路由 实现 pc1 ping通 pc2&#xff0c;展示静态路由效果。 默认 pc1 无法ping通 pc2 ar1 ar2 互相添加静态路由 sy Enter system view, return user view with CtrlZ. [ar1]ip route-static 2.2.2.0 255.255.255.0 12.1.1.2 sy Enter system view, return user view wit…

Python爬虫入门篇!

毕设是做爬虫相关的&#xff0c;本来想的是用java写&#xff0c;也写了几个爬虫&#xff0c;其中一个是爬网易云音乐的用户信息&#xff0c;爬了大概100多万&#xff0c;效果不是太满意。之前听说Python这方面比较强&#xff0c;就想用Python试试&#xff0c;之前也没用过Pytho…

【OpenGL】知识点

VAO 和webgl一致 给个完整案例&#xff0c;可以对比 案例&#xff1a;WebGL中VAO调用&#xff0c;是一致的 void prepareSingleBuffer() {//1 准备positions colors数据float positions[] {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f, 0.5f, 0.0f};float colors[] {1.0f,…

基于NVIDIA NIM平台实现盲人过马路的demo(一)

前言:利用NVIDIA NIM平台提供的大模型进行编辑,通过llama-3.2-90b-vision-instruct模型进行初步的图片检测 step1: 部署大模型到本地,引用所需要的库 import os import requests import base64 import cv2 import time from datetime import datetimestep2: 观看官方使用文…

【大数据学习 | kafka】producer端的回调和ack

主线程将数据放入到本地累加器中record accumulator中进行存储&#xff0c;sender线程会异步的拉取数据到kafka集群中&#xff0c;这个数据拉取并且复制到kafka集群中以后&#xff0c;kafka需要返回给sender线程一个确认应答ack&#xff0c;这个确认应答用于在sender线程中进行…

硅谷甄选(11)角色管理

角色管理模块 10.1 角色管理模块静态搭建 还是熟悉的组件&#xff1a;el-card、el-table 、el-pagination、el-form <template><el-card><el-form :inline"true" class"form"><el-form-item label"职位搜索"><el-…

使用Git进行版本控制的最佳实践

文章目录 Git简介基本概念仓库&#xff08;Repository&#xff09;提交&#xff08;Commit&#xff09;分支&#xff08;Branching&#xff09; 常用命令初始化仓库添加文件提交修改查看状态克隆仓库分支操作合并分支推送更改 最佳实践使用有意义的提交信息定期推送至远程仓库使…

开源模型应用落地-Qwen2.5-7B-Instruct与TGI实现推理加速

一、前言 目前&#xff0c;大语言模型已升级至Qwen2.5版本。无论是语言模型还是多模态模型&#xff0c;均在大规模多语言和多模态数据上进行预训练&#xff0c;并通过高质量数据进行后期微调以贴近人类偏好。在本篇学习中&#xff0c;将集成 Hugging Face的TGI框架实现模型推理…