海量数据处理

时间:2022-12-25 14:04:18

目录

一、位图

1.1 位图概念

1.2 位图的实现

1.3 位图的特点

二、布隆过滤器

2.1 布隆过滤器的提出

2.2 概念

2.3 实现原理

2.4 哈希函数个数和布隆过滤器长度的选择

2.5 布隆过滤器的删除

2.6 特点

2.6.1 优点

2.6.2 缺点

2.7 完整代码实现

三、海量数据处理常见面试题​​​​​​​


一、位图

1.1 位图概念

位图就是用每一位来存放某种状态,适用于海量数据且数据无重复的场景。通常是用来判断某个数据是否存在的。
 

1.2 位图的实现

位图的底层结构依然是连续的线性空间,这里我们使用vector容器作为其底层结构。

#include <iostream>
#include <vector>
using std::vector;
using std::cout;
using std::endl;

namespace bjy {
	template<size_t N>
	class bitset {
	public:
		bitset() {
			if (N % 8 == 0) _bits.resize(N / 8, 0);
			else _bits.resize(N / 8 + 1, 0);
		}
		void set(size_t which){//将某个位设置为1
			size_t i = which / 8, j = which % 8;
			_bits[i] |= (1 << j);
		}

		void reset(size_t which) {//将某个位设置为0
			size_t i = which / 8, j = which % 8;
			_bits[i] &= ~(1 << j);
		}

		bool test(size_t which)const {
			size_t i = which / 8, j = which % 8;
			return _bits[i] & (1 << j);
		}
	private:
		vector<char> _bits;
	};

1.3 位图的特点

1. 速度快且节省时间。

2. 采用直接定址法,不存在哈希冲突。

3. 相对局限,只能处理整型数据。

4. STL中实现的bitset容器存在一定不足,其底层使用静态数组实现。其开辟在栈区会导致在处理海量数据时极易发生*。(使用时应特别注意)

二、布隆过滤器

2.1 布隆过滤器的提出

当使用新闻客户端看新闻时,其会不停地推荐新的内容,每次推荐时要去重。但新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何实现的呢?

方案一: 使用哈希表存储用户记录,但空间消耗过大。

方案二: 使用位图记录用户信息,明显不可取(位图只能处理整型数据)

方案三: 将位图与哈希进行结合,即布隆过滤器
 

2.2 概念

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

海量数据处理

Bloom Filters (jasondavies.com)海量数据处理https://www.jasondavies.com/bloomfilter/

2.3 实现原理

布隆过滤器是一个 bit 向量或者说 bit 数组

海量数据处理

若要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置进行set操作。例如针对key "百度" 用三个不同的哈希函数分别生成了哈希值 0、3、6,则上图转变为:

海量数据处理

  再存一个值 "腾讯",若哈希函数返回 2、3、7 的话,图继续变为:

海量数据处理

 3 这个bit位由于两个key的哈希函数都返回了3,因此它被覆盖了。

现在若想查询 "b站" 这个key是否存在,哈希函数返回了 0、4、7三个值,结果我们发现 4 这个 bit 位上的值为 0,说明没有任何一个key映射到这个 bit 位上,因此我们可以很确定地说 "b站"这个key不存在。而当我们需要查询 "百度" 这个值是否存在的话,那么哈希函数必然会返回 0、3、6,然后我们检查发现这三个 bit 位上的值均为 1,那么说明"baidu"可能存在

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

2.4 哈希函数个数和布隆过滤器长度的选择

在使用布隆过滤器时,哈希函数的个数和布隆过滤长度的设定都是我们需要考虑的问题。

1. 哈希函数个数越多则布隆过滤器bit位被置为1的速度越快,且会效率下降。但是若哈希函数过少则会导致误报率较高。

2. 布隆过滤器的长度会直接影响误报率,布隆过滤器长度越长则误报率越低。但若是过长了则失去布隆过滤器原有的优势(占用空间小),反而还不如选择哈希表。

直接根据下图中的公式选择即可

海量数据处理

2.5 布隆过滤器的删除

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

一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计
数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储
空间的代价来增加删除操作。

缺陷:
1. 无法确认元素是否真正在布隆过滤器中
2. 存在计数回绕
 

2.6 特点

2.6.1 优点

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

2.6.2 缺点

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

2.7 完整代码实现

#include <iostream>
#include <vector>
#include <string>
#include <bitset>
using namespace std;

namespace bjy {
	struct HashBKDR {
		size_t operator()(const string& key) {
			size_t sum = 0;
			for (auto& e : key) {
				sum = sum * 131 + e;
			}
			return sum;
		}
	};
	struct HashAP {
		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 {
		size_t operator()(const string& key) {
			size_t hash = 5381;
			for (auto ch : key) {
				hash += (hash << 5) + ch;
			}
			return hash;
		}
	};

	template<size_t N, class K = string, class Hash1 = HashBKDR, class Hash2 = HashAP, class Hash3 = HashDJB>
	class Bloon_filter
	{
	public:
		void set(const K& key) {
			size_t pos1 = Hash1()(key) % (_ratio * N);
			size_t pos2 = Hash2()(key) % (_ratio * N);
			size_t pos3 = Hash3()(key) % (_ratio * N);
			_bits->set(pos1);
			_bits->set(pos2);
			_bits->set(pos3);
		}

		bool test(const K& key) {
			size_t pos1 = Hash1()(key) % (_ratio * N);
			size_t pos2 = Hash2()(key) % (_ratio * N);
			size_t pos3 = Hash3()(key) % (_ratio * N);
			if (_bits->test(pos1) && _bits->test(pos2) && _bits->test(pos3)) return true;
			else return false;
		}
	private:
		const static size_t _ratio = 5;
		bitset<_ratio* N>* _bits = new bitset<_ratio* N>;//库中bitset使用静态数组,开辟在栈上,容易导致栈溢出
	};
}

三、海量数据处理常见面试题

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

利用两个位图完成该问题的实现。采用KV的统计次数的模型,两个位图中各有一个bit位表示同一个整型。00表示该整型出现次数为0,01表示该整型出现次数为1,10表示该整型出现次数为2次,11表示该整型出现3次及以上。

template<size_t N>
class twobitset
{
public:
	void set(size_t x)
	{
		bool inset1 = _bs1.test(x);
		bool inset2 = _bs2.test(x);

		if (inset1 == false && inset2 == false)//00
        {
			_bs2.set(x);// -> 01
		}
		else if (inset1 == false && inset2 == true)//01
		{
			_bs1.set(x);
			_bs2.reset(x);// ->10
		}
		else if (inset1 == true && inset2 == false)//10
		{
			// ->11
			_bs1.set(x);
			_bs2.set(x);
		}
	}

	void print_once_num(){
		for (size_t i = 0; i < N; ++i){
			if (_bs1.test(i) == false && _bs2.test(i) == true){
				cout << i << endl;
			}
		}
	}

private:
	bitset<N> _bs1;
	bitset<N> _bs2;
};

2. 两个文件分别有100亿个整数,只有1G内存,找到两个文件交集

将文件A中的数据映射到位图A中,将文件B中的数据映射到位图B中,位图A和位图B对应且都为1的位所代表的key的集合即是两个文件的交集。

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

该问题不可使用bitset解决,其只适用于整型数据。query为字符串,应使用哈希切割思想或者布隆过滤器完成问题。

近似算法:

使用两个布隆过滤器分别处理两个文件,若两个布隆过滤器set(key)都为1,即这个key在两个文件中都可能存在,近似看做交集中的元素。

精确算法:

假设1个query为30Byte,100亿个query则大概需要300G以上的内存空间。我们的电脑肯定没有这么大的空间,所以这里需要使用哈希切割的思想。

海量数据处理

​​​​​​​ 若是处理完后某个小文件过大,可以换一个哈希函数对小文件再次进行哈希切割。

4. 给一个超过100G大小的log file, log中存着IP地址。(1)设计算法找到出现次数最多的IP地址。(2)找到top-K的IP。

问题一:

使用哈希切割,将一个大的log file分成500个小log file,相同的IP一定进入同一个小log file。再用map<string,int> 或者unordered_map<string,int>对每个小log file进行次数统计,找出出现次数最多的IP地址。

问题二:

建立一个K个<IP,count>元素的小堆(优先级队列)