哈希->模拟实现+位图应用

致前行路上的人:

                要努力,但不要着急,繁花锦簇,硕果累累都需要过程!

 

目录

1. unordered系列关联式容器

1.1 unordered_map

1.1.1概念介绍:

1.1.2 unordered_map的接口说明

1.2unordered_set

1.3常见面试题oj

2.底层结构

2.1 哈希概念

2.2 哈希冲突

2.3哈希函数

2.4哈希冲突解决

2.4.1闭散列

2.4.2开散列

3.模拟实现

3.1哈希表的改造:

3.2unordered_map模拟实现

3.3unordered_set模拟实现

4.哈希的应用

4.1位图

4.1.1 位图概念

 4.1.2 位图的实现

 4.1.3 位图的应用

4.2哈希切割

4.3布隆过滤器

4.3.1布隆过滤器的提出

4.3.2布隆过滤的概念

 4.3.3布隆过滤器的插入

4.2.4 布隆过滤器的查找

4.2.5 布隆过滤器删除

4.2.6如何选择哈希函数个数和布隆过滤器长度

4.2.6布隆过滤器优点

4.2.7 布隆过滤器缺陷

4.2.8布隆过滤器的应用场景

4.2.9布隆过滤器代码实现

4.2.10布隆过滤器的面试题


1. unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到logN,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中对unordered_map和unordered_set进行介绍,unordered_multimap和unordered_multiset使用方式类型类似,后面两个相较于前两个,不同的是允许数据重复。

1.1 unordered_map

1.1.1概念介绍:

1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
2. 在unordered_map中,键值通常用于唯一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
3. 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
5. unordered_map实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
6. 它的迭代器至少是前向迭代器。

1.1.2 unordered_map的接口说明

1. unordered_map的构造

unordered_map                                构造不同格式的unordered_map对象

2. unordered_map的容量

bool empty() const 检测unordered_map是否为空
size_t size() const  获取unordered_map的有效元素个数

3. unordered_map的迭代器

begin 返回unordered_map第一个元素的迭代器
end  返回unordered_map最后一个元素下一个位置的迭代器
cbegin 返回unordered_map第一个元素的const迭代器
cend  返回unordered_map最后一个元素下一个位置的const迭代器

4. unordered_map的元素访问

operator[]  返回与key对应的value,没有一个默认值

注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶
中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,
将key对应的value返回。

#include <iostream>
#include <string>
#include <unordered_map>

int main()
{
    std::unordered_map<std::string, std::string> mymap;

    mymap["Bakery"] = "Barbara";  // new element inserted
    mymap["Seafood"] = "Lisa";    // new element inserted
    mymap["Produce"] = "John";    // new element inserted

    std::string name = mymap["Bakery"];   // existing element accessed (read)
    mymap["Seafood"] = name;              // existing element accessed (written)

    mymap["Bakery"] = mymap["Produce"];   // existing elements accessed (read/written)

    name = mymap["Deli"];      // non-existing element: new element "Deli" inserted!

    mymap["Produce"] = mymap["Gifts"];    // new element "Gifts" inserted, "Produce" written

    for (auto& x : mymap) {
        std::cout << x.first << ": " << x.second << std::endl;
    }

    return 0;
}

运行结果:

5. unordered_map的查询

iterator find(const K& key) 返回key在哈希桶中的位置
size_t count(const K& key) 返回哈希桶中关键码为key的键值对的个数

6. unordered_map的修改操作

insert 向容器中插入键值对
erase 删除容器中的键值对
clear 清空容器中有效元素个数
void swap(unordered_map&)  交换两个容器中的元素

7. unordered_map的桶操作

size_t bucket_count()const  返回哈希桶中桶的总个数
size_t bucket_size(size_t n)const 返回n号桶中有效元素的总个数
size_t bucket(const K& key)  返回元素key所在的桶号

1.2unordered_set

unordered_set文档说明

1.3常见面试题oj

在长度 2N 的数组中找出重复 N 次的元素

class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {
        size_t N = nums.size() / 2;
        unordered_map<int,int>m;
        //统计每个元素出现的次数
        for(auto& e : nums)
        {
            m[e]++;
        }
        //找到出现N次的元素:
        for(auto& e : m)
        {
            if(e.second == N)
                return e.first;
        }
        return -1;
    }
};

349. 两个数组的交集

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int>s1(nums1.begin(),nums1.end());
        unordered_set<int>s2(nums2.begin(),nums2.end());
        vector<int> result;
        for(auto& e : s1)
        {
            if(s2.find(e) != s2.end())
                result.push_back(e);
        }
        return result;
    }
};

 

2.底层结构

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

2.1 哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素
时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
O(log N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
一 一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:

插入元素
        根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
        对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称
哈希表(Hash Table)(或者称散列表

例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题 

2.2 哈希冲突

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

发生哈希冲突该如何处理呢?

2.3哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
        ~哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
        域必须在0到m-1之间
        ~哈希函数计算出来的地址能均匀分布在整个空间中
        ~哈希函数应该比较简单

常见哈希函数

1. 直接定址法--(常用)
取关键字的某个线性函数为散列地址:
Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
2. 除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:
Hash(key) = key% p(p<=m),将关键码转换成哈希地址、


2.4哈希冲突解决

解决哈希冲突的两种方法:闭散列和开散列

2.4.1闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置
呢?

1. 线性探测

现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

        插入
                1.通过哈希函数获取待插入元素在哈希表中的位置
                2.如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,
                使用线性探测找到下一个空位置,插入新元素

        扩容:

         散列表中有一个载荷因子(负载因子)= 散列表元素的个数 / 散列表的长度,载荷因子越大,产生冲突的可能性就越大,载荷因子越小,产生冲突的可能性就越小,但是可能会造成空间浪费,因此,一般建议将载荷因子控制在0.7~0.8之间

        删除
                采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
                会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影
                响。因此线性探测采用标记的伪删除法来删除一个元素。 

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};

线性探测的实现

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 = hash * 131 + e;
		}
		return hash;
	}
};

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
{
	typedef HashData<K, V> data;
public:
	HashTable()
		:_n(0)
	{
		_tables.resize(10);
	}
	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))
			return false;
		if (_n * 10 / _tables.size() >= 7)
		{
			HashTable<K, V,Hash> newHT;
			newHT._tables.resize(_tables.size() * 2);
			for (auto& e : _tables)
			{
				if (e._state == EXIST)
				{
					newHT.Insert(e._kv);
				}
			}
			_tables.swap(newHT._tables);
		}
		Hash ha;
		size_t hashi = ha(kv.first) % _tables.size();
		while (_tables[hashi]._state == EXIST)
		{
			//线性探测:
			++hashi;
			hashi %= _tables.size();
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
		return true;
	}
	data* Find(const K& key)
	{
		Hash ha;
		size_t hashi = ha(key) % _tables.size();
        size_t starti = hashi;
		while (_tables[hashi]._state != EMPTY)
		{
			if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key)
			{
				return &_tables[hashi];
			}
			++hashi;
            hashi %= _tables.size();
            //极端场景下,不是存在就是删除,可能会造成死循环的问题
            if(hashi == starti)
			{
				break;
			}
		}
		return nullptr;
	}
	bool Erase(const K& key)
	{
		data* ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			--_n;
			return true;
		}
		return false;
	}
private:
	vector<data> _tables;
	size_t _n = 0;
};

线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
低。

2.4.2开散列

1. 开散列概念
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。

 

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。 

2. 开散列实现

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 = hash * 131 + e;
		}
		return hash;
	}
};
template<class K,class V>
struct HashNode
{
	pair<K, V> _kv;
	HashNode* next;
	HashNode(const pair<K,V>&kv)
		:_kv(kv),next(nullptr)
	{}
};
template<class K,class V,class Hash=HashFunc<K>>
class HashTable
{
	typedef HashNode<K, V> node;
public:
	HashTable()
		:_n(0)
	{
		_tables.resize(10);
	}
	~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;
		}
	}
	bool Insert(const pair<K, V>& kv)
	{
		//负载因子控制在1,超过就扩容
		if (_n == _tables.size())
		{
			/*HashTable<K, V, Hash> newHT;
			newHT._tables.resize(_tables.size() * 2);
			for (auto cur : _tables)
			{
				while (cur)
				{
					newHT.Insert(cur->_kv);
					cur = cur->next;
				}
			}
			_tables.swap(newHT._tables);*/
			vector<node*> newTable;
			newTable.resize(2 * _tables.size(), nullptr);
			for (size_t i = 0; i < _tables.size(); i++)
			{
				node* cur = _tables[i];
				while (cur)
				{
					node* next = cur->next;
					size_t hashi = Hash()(cur->_kv.first) % newTable.size();
					//头插到新表
					cur->next = newTable[hashi];
					newTable[hashi] = cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
			_tables.swap(newTable);
		}
		size_t hashi = Hash()(kv.first) % _tables.size();
		node* newNode = new node(kv);
		newNode->next = _tables[hashi];
		_tables[hashi] = newNode;
		++_n;
		return true;
	}
	node* Find(const K& key)
	{
		size_t hashi = Hash()(key) % _tables.size();
		node* cur = _tables[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				return cur;
			}
			else
			{
				cur = cur->next;
			}
		}
		return nullptr;
	}
	bool Erase(const K& key)
	{
		size_t hashi = Hash()(key) % _tables.size();
		node* prev = nullptr;
		node* cur = _tables[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				if (cur == _tables[hashi])
				{
					_tables[hashi] = cur->next;
					delete  cur;
				}
				else
				{
					prev->next = cur->next;
					delete cur;
				}
				--_n;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->next;
			}
		}
		return false;
	}
private:
	vector<node*> _tables;
	size_t _n = 0;
};

3.模拟实现

3.1哈希表的改造:

1. 模板参数列表的改造:

K:关键码类型
T: 不同容器V的类型不同,如果是unordered_map,T代表一个键值对,如果是
unordered_set,T为 K
KeyOfT: 因为V的类型不同,通过value取key的方式就不同

Hash:哈希函数仿函数对象类型,哈希函数使用除留余数法,需要将Key转换为整形数字才能
取模

2.代码实现:

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 = hash * 131 + e;
		}
		return hash;
	}
};
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 __HTiterator
{
	typedef HashNode<T> node;
	typedef __HTiterator<K, T, Hash, KeyOfT> Self;
	typedef HashTable<K, T, Hash, KeyOfT> HT;
	node* _node;
	HT* ht;
	__HTiterator(node* node, HT* ht)
		:_node(node), ht(ht) {}
	T& operator*()
	{
		return _node->_data;
	}
	T* operator->()
	{
		return &_node->_data;
	}
	bool operator !=(const Self& s)
	{
		return _node != s._node;
	}
	Self& operator++()
	{
		if (_node->next)
		{
			_node = _node->next;
		}
		else
		{
			//当前桶走完了,走桶的下一个:
			KeyOfT kot;
			size_t hashi = Hash()(kot(_node->_data)) % ht->_tables.size();
			++hashi;
			while (hashi < ht->_tables.size())
			{
				if (ht->_tables[hashi])
				{
					_node = ht->_tables[hashi];
					break;
				}
				else
				{
					++hashi;
				}
			}
			if (hashi == ht->_tables.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 __HTiterator;
public:
	typedef __HTiterator<K, T, Hash, KeyOfT> iterator;
	iterator begin()
	{
		for (size_t i = 0; i < _tables.size(); i++)
		{
			if (_tables[i])
			{
				return iterator(_tables[i], this);
			}
		}
		return iterator(nullptr, this);
	}
	iterator end()
	{
		return iterator(nullptr, this);
	}
	HashTable()
		:_n(0)
	{
		_tables.resize(10);
	}
	~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);
		//负载因子控制在1,超过就扩容
		if (_n == _tables.size())
		{
			/*HashTable<K, V, Hash> newHT;
			newHT._tables.resize(_tables.size() * 2);
			for (auto cur : _tables)
			{
				while (cur)
				{
					newHT.Insert(cur->_kv);
					cur = cur->next;
				}
			}
			_tables.swap(newHT._tables);*/
			vector<node*> newTable;
			newTable.resize(2 * _tables.size(), nullptr);
			for (size_t i = 0; i < _tables.size(); i++)
			{
				node* cur = _tables[i];
				while (cur)
				{
					node* next = cur->next;
					size_t hashi = Hash()(kot(cur->_data)) % newTable.size();
					//头插到新表
					cur->next = newTable[hashi];
					newTable[hashi] = cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
			_tables.swap(newTable);
		}
		size_t hashi = Hash()(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;
		size_t hashi = Hash()(key) % _tables.size();
		node* cur = _tables[hashi];
		while (cur)
		{
			if (kot(cur->_data) == key)
			{
				return iterator(cur, this);
			}
			else
			{
				cur = cur->next;
			}
		}
		return end();
	}
	bool Erase(const K& key)
	{
		KeyOfT kot;
		size_t hashi = Hash()(key) % _tables.size();
		node* prev = nullptr;
		node* cur = _tables[hashi];
		while (cur)
		{
			if (kot(cur->_data) == key)
			{
				if (cur == _tables[hashi])
				{
					_tables[hashi] = cur->next;
					delete  cur;
				}
				else
				{
					prev->next = cur->next;
					delete cur;
				}
				--_n;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->next;
			}
		}
		return false;
	}
private:
	vector<node*> _tables;
	size_t _n = 0;
};

3.2unordered_map模拟实现

namespace ns
{
	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 HashTable<K, pair<const K, V>, Hash, MapKeyOfT>::iterator iterator;
		iterator begin()
		{
			return _ht.begin();
		}
		iterator end()
		{
			return _ht.end();
		}
		pair<iterator,bool> Insert(const pair<const 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;
		}
        iterator Find(const K& key)
		{
			return _ht.Find(key);
		}
		iterator Erase(const K& key)
		{
			return _ht.Erase(key);
		}
	private:
		HashTable<K, pair<const K,V>, Hash, MapKeyOfT>_ht;
	};

3.3unordered_set模拟实现

namespace ns
{
	template<class K,class Hash = HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename 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);
		}
        iterator Find(const K& key)
		{
			return _ht.Find(key);
		}
		iterator Erase(const K& key)
		{
			return _ht.Erase(key);
		}
	private:
		HashTable<K, K, Hash, SetKeyOfT>_ht;
	};

4.哈希的应用

4.1位图

4.1.1 位图概念

关于位图的概念,下面通过一道面试题来进行介绍:

1. 面试题

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
这40亿个数中。【腾讯】

这道题直观的思路是使用排序+二分的方法,或者是使用红黑树,或者是使用哈希表,但是这三种方法对于40亿个整数,不合适,因为40个整数大概占16GB的空间,而普通计算机提供不了16GB的空间,所以这三种方式都被否决了!

下面介绍一种新的思路,使用位图的方式进行判断。

数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一
个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0
代表不存在。比如:

 4.1.2 位图的实现

namespace ns
{
	template<size_t N>
	class bit_set
	{
	public:
		bit_set()
		{
			_bits.resize(N / 8 + 1, 0);
		}
		void set(size_t x)
		{
			size_t i = x / 8; //表示在第几个char上
			size_t j = x % 8; //表示在几个char的bit位上
			_bits[i] |= (1 << j);
		}
		void reset(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;
			_bits[i] &= (~(1 << j));
		}
		bool 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_bit_set()
	{
		bit_set<-1> bs1;
		bs1.set(10);
		bs1.set(30);
		bs1.set(40);
		bs1.set(20);
		bs1.set(1000);
		bs1.set(1090);
		bs1.set(4);
		cout << bs1.test(10) << endl;
		cout << bs1.test(1) << endl;
		cout << bs1.test(40) << endl;
		bs1.reset(40);
		cout << bs1.test(40) << endl;

	}
}

 4.1.3 位图的应用

1. 给定100亿个整数,设计算法找到只出现一次的整数?

实现思路:定义两个bit_set的对象,00表示一次都没有出现,01表示出现一次,10表示出现一次以上

template<size_t N>
	class two_bit_set
	{
	public:
		void set(size_t x)
		{
			if (!_bs1.test(x) && !_bs2.test(x)) // 00
			{
				_bs2.set(x);
			}
			else if (!_bs1.test(x) && _bs2.test(x)) //01
			{
				_bs1.set(x);
				_bs2.reset(x);
			}
			//10
		}
		void PrintOnce()
		{
			for (size_t i = 0; i < N; i++)
			{
				if (!_bs1.test(i) && _bs2.test(i))
				{
					cout << i << " ";
				}
			}
			cout << endl;
		}
	private:
		bit_set<N> _bs1;
		bit_set<N> _bs2;
	};
	void test_two_bit_set()
	{
		two_bit_set<100> tbs;
		int a[] = { 3, 5, 6, 7, 8, 9, 33, 55, 67, 3, 3, 3, 5, 9, 33 };
		for (auto e : a)
		{
			tbs.set(e);
		}

		tbs.PrintOnce();
	}

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

实现思路:定义两个bit_set的对象,将100一个整数分别设置到两个对象中,然后遍历找,如果同时在两个对象中则说明就是交集

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

实现思路:定义两个bit_set的对象,将100一个整数分别设置到两个对象中,00表示一次都没有出现,01表示出现一次,10表示出现2次,11表示出现3次及以上,然后遍历查找出现次数不超过两次的所有整数

4.2哈希切割

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?

实现思路:可以将100G大小的文件,按照哈希切分的方式,分别切到大小为1G的小文件,然后通过map来一个一个统计次数,统计完一个,clear掉map,再统计下一个,当Ai小文件超过1个G的时候,分两种情况,第一种是这个小文件冲突的ip很多,都是不同的ip,大多数都是不重复的,map统计不下,可以换一个字符串哈希函数,递归再切分,进行统计,第二种情况是这个小文件冲突的ip很多,大多数都是重复的map可以统计

如图所示:

直接用map统计,如果是第二种情况,可以统计出来,不会报错,如果是第一种情况,map insert插入失败,相当于new结点失败,可以捕获异常

4.3布隆过滤器

4.3.1布隆过滤器的提出

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉
那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用
户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那
些已经存在的记录。 如何快速查找呢?
1. 用哈希表存储用户记录,缺点:浪费空间
2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
3. 将哈希与位图结合,即布隆过滤器

4.3.2布隆过滤的概念

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构的多个位置中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。

存在是不准确的,因为可能不存在,但是这个位置可能和别人冲突,出现误判

不存在是准确的

 4.3.3布隆过滤器的插入

布隆过滤器是一个bit数组,如图所示:

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为: 

Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:

值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在。

4.2.4 布隆过滤器的查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特
位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为
零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可
能存在,因为有些哈希函数存在一定的误判。
比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其
他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

4.2.5 布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。

比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也
被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计
数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储
空间的代价来增加删除操作。

注:上面的方法也不能确保一定就能够删除,因为要删除的元素,无法确保是否真正存在在布隆过滤器中

4.2.6如何选择哈希函数个数和布隆过滤器长度

很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

                     k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率

如何选择适合业务的 k 和 m 值呢,这里直接贴一个公式:

4.2.6布隆过滤器优点

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

2. 哈希函数相互之间没有关系,方便硬件并行运算
3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

4.2.7 布隆过滤器缺陷

1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再
建立一个白名单,存储可能会误判的数据)
2. 不能获取元素本身
3. 一般情况下不能从布隆过滤器中删除元素
4. 如果采用计数方式删除,可能会存在计数回绕问题

4.2.8布隆过滤器的应用场景

1.不需要一定准确的场景

2.可以在注册昵称的时候判重

如图所示:

当客户端要查询某个id是否存在的时候,首先在布隆过滤器中查找,查找结果为不在就可以直接返回了,如果查找的结果在,就到数据库外设中查找,这种方式就大大提高了查询效率

4.2.9布隆过滤器代码实现

#include<iostream>
#include<string>
#include<vector>
#include<bitset>
#include<stdlib.h>
using namespace std;
namespace ns
{
	struct BKDRHash
	{
		size_t operator()(const string& s)
		{
			// BKDR
			size_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;
		}
	};
	//N是最多存储的元素个数,X:平均存储一个值,开辟X个位
	template<size_t N,size_t X,class K = string,
	class HashFunc1 = BKDRHash,
	class HashFunc2 = APHash,
	class HashFunc3 = DJBHash>
	class BloomFilter
	{
	public:
		void set(const K& key)
		{
			size_t hashi1 = HashFunc1()(key) % (N * X);
			size_t hashi2 = HashFunc2()(key) % (N * X);
			size_t hashi3 = HashFunc3()(key) % (N * X);
			_bs.set(hashi1);
			_bs.set(hashi2);
			_bs.set(hashi3);
		}
		bool test(const K& key)
		{
			size_t hashi1 = HashFunc1()(key) % (N * X);
			if (!_bs.test(hashi1))
				return false;
			size_t hashi2 = HashFunc1()(key) % (N * X);
			if (!_bs.test(hashi2))
				return false;
			size_t hashi3 = HashFunc1()(key) % (N * X);
			if (!_bs.test(hashi3))
				return false;
			//前面不存在判读都是准确的,不存在误判
			return true; //可能会存在误判,映射的几个位置冲突
		}
	private:
		bitset<N* X> _bs;
	};
	void test_bloomfilter1()
	{
		string str[] = { "猪八戒", "孙悟空", "沙悟净", "唐三藏", \
			"白龙马1","1白龙马","白1龙马","白11龙马","1白龙马1" };
		BloomFilter<10,5> bf;
		for (auto& str : str)
		{
			bf.set(str);
		}

		for (auto& s : str)
		{
			cout << bf.test(s) << endl;
		}
		cout << endl;

		srand(time(0));
		for (const auto& s : str)
		{
			cout << bf.test(s + to_string(rand())) << endl;
		}
	}
	void test_bloomfilter2()
	{
		srand(time(0));
		const size_t N = 100000;
		BloomFilter<N,6> bf;

		std::vector<std::string> v1;
		std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";

		for (size_t i = 0; i < N; ++i)
		{
			v1.push_back(url + std::to_string(i));
		}

		for (auto& str : v1)
		{
			bf.set(str);
		}

		// v2跟v1是相似字符串集,但是不一样
		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(999999 + 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(i + rand());
			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.10布隆过滤器的面试题

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

近似算法:

可以使用布隆过滤器,将一个文件的内容都set到布隆过滤器当中,然后遍历另一个文件依次查找,如果存在就说明是交集。

精确算法:

将两个文件的内容,分别通过哈希切分的方式,切分成一个一个的小文件,然后将小文件放到map中,进行找交集。

切分的时候存在的两个问题:

1.切分完之后的文件大小超过1G,大多数都是不重复的,可以换一个哈希切分函数,递归在切分

2.切分完之后的文件大小超过1G

猜你喜欢

转载自blog.csdn.net/qq_65307907/article/details/129124107