C++学习记录——이십사 位图、布隆过滤器、哈希切割


本篇gitee

1、位图

先看一个题目:

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

这里可能会想到两种思路,排序+二分查找;放到哈希表或者红黑树。但是不要忽略一个重要的问题,40亿。实际换算一下,40亿个整数占用的空间四舍五入一下就是15G。15G,难道找这个数我需要先有个15G空间?这肯定不行。还可能想到一个方法,分成好几份查找,但是这并没有解决本质问题,占用空间大,假如要查找的数不止一个呢?

这个问题判断的是在不在的问题,所以没有必要将数字放进去某个结构,可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在,这也就是位图的操作。比如设置一个int类型的变量,占32个比特位,它的二进制位中所有可能的结果是2^32 - 1个,也就是大约42亿9千万多,那么就有可以存放足够的数据存在状态了。也可以把32个比特位分成4个char。

在之前Linux的博客中写到过,按位操作也就是位图。

我们写一个set函数用来设置为1,reset用来设置为0。传过来一个数后,我们如何判断在哪个char里?x / 8,在char中的第几个比特位? x % 8。找到就要开始设置。但是这是由几个char组成的,地址的存放方式还需要考虑,左移是低位向高位移,右移是高位向低位移动,不过不需要太担心这方面,编译器对此都有自己的做法。

定义一个vector< char >的_bits。

	void set(size_t x)//设置1
	{
    
    
		size_t i = x / 8;
		size_t j = x % 8;
		_bits[i] |= (1 << j);
	}

	void reset(size_t x)//设置0
	{
    
    
		size_t i = x / 8;
		size_t j = x % 8;
		_bits[i] &= ~(1 << j);
	}

另有一个test函数来看看标识过的值在不在。

	bool test(size_t x)
	{
    
    
		size_t i = x / 8;
		size_t j = x % 8;
		return _bits[i] & (1 << j);
	}

测试代码

void test_bitset1()
{
    
    
	bitset<100> bs;
	bs.set(10);
	bs.set(11);
	bs.set(15);
	cout << bs.test(10) << endl;
	cout << bs.test(15) << endl;

	bs.reset(10);

	cout << bs.test(10) << endl;
	cout << bs.test(15) << endl;

	bs.reset(10);
	bs.reset(15);

	cout << bs.test(10) << endl;
	cout << bs.test(15) << endl;
}

如果要用的数字很大,比如42亿9千万这种最大的,就可以写zydset< -1 > zs或者zydset< 0xFFFFFFFF > zs,编译器就会实际开这些空间。

template<size_t N>
class bitset
{
    
    
public:
	bitset()
	{
    
    
		_bits.resize(N / 8 + 1, 0);//想象一下,100 / 8就是12,实际数值是12.5,+1就包含了这个多出来的值,也就是开辟了13个char,全都初始化为0,也就是每个char的比特位全为0。实际问题中,40亿算下来就是开辟476MB的空间,这样就大大减小了空间消耗。
	}

	void set(size_t x)
	{
    
    
		size_t i = x / 8;
		size_t j = x % 8;

		_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;
};

位图应用

100亿个数字,找其中只出现一次的数。

可以开两个位图,也可以改造一下位图,变成两位位图,两位位图就是N / 4了,原先的8变为4,一次性看两个比特位来检查状态,00就是0次,01就是1次,10就是1次以上。

借助上面写好的类,再写一个解决这个问题的类

template<size_t N>
class twobitset
{
    
    
public:
	void set(size_t x)
	{
    
    
		// 00 -> 01
		if (_bs1.test(x) == false
		&& _bs2.test(x) == false)
		{
    
    
			_bs2.set(x);
		}
		// 01 -> 10
		else if (_bs1.test(x) == false
			&& _bs2.test(x) == true)
		{
    
    
			_bs1.set(x);
			_bs2.reset(x);
		}
		// 10
	}

	void Print()
	{
    
    
		for (size_t i = 0; i < N; ++i)
		{
    
    
			if (_bs2.test(i))
			{
    
    
				cout << i << endl;
			}
		}
	}

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

给定两个文件,分别有100亿个整数,现在只有1G内存,如何找到两个文件交集?

可以创建两个位图,然后循环,将一个个char都&&一遍,留下来的就是出现一次的。

也可以将一个文件的值读到一个位图中,再读取另一个文件,判断在不在上面位图中,在就是交集,但这样得出来的结果需要再次去重,这里的改进办法就是每次找到交集值,都将上面位图对应的值设置为0.

如果数据量大,用第一个方法更好。比如100亿时,用第一个,1亿时用第二个就行,因为第一个创建的是固定数,第二个则是有多少创建多少。

100亿个整数,1G内存,找到出现次数不超过2次的整数。那么就可以用两位位图,00为0次,01为1次,10为2次,11为3次。

优缺点

优点:速度快,节省空间

缺点:只能映射整形,其他类型入浮点数,string等都不能存储映射

2、布隆过滤器

针对一个字符串,假设有10个字符,每个按照ANSII码表会发现有256个,所以就是256的10次方,这时候再用上面的位图就不行了。这时候一定有很多冲突。

布隆过滤器的思路是这样,要解决全部冲突很难,也没什么好办法。布隆的做法就是降低冲突,之前的方法是一对一,一个映射一个位置,那么布隆就一个映射多个位置就可以降低误判率了。这里就一对三。

template<size_t N, class K, class Hash1, class Hash2, class Hash3>
class BloomFilter
{
    
    
public:
	void set(const K& key)
	{
    
    
		size_t hash1 = Hash1()(key) % N;
		_bs.set(hash1);
		size_t hash2 = Hash2()(key) % N;
		_bs.set(hash2);
		size_t hash3 = Hash3()(key) % N;
		_bs.set(hash3);
	}

	bool test(const K& key)
	{
    
    
		size_t hash1 = Hash1()(key) % N;
		if (!_bs.test(hash1))
		{
    
    
			return false;
		}
		size_t hash2 = Hash2()(key) % N;
		if (!_bs.test(hash2))
		{
    
    
			return false;
		}
		size_t hash3 = Hash3()(key) % N;
		if (!_bs.test(hash3))
		{
    
    
			return false;
		}
	}
private:
	bitset<N> _bs;
};

还是会用到之前的位图类。

布隆过滤器如果判断不在是准确的,但是判断在是不准确的。因为不在的话位置上一定都是0,没有任何映射过的痕迹,所以不在很准确;但是在就不确定了,因为有可能有其他的字符串映射到了这个位置。

布隆过滤器被使用在能容忍误判的场景。比如:注册时,快速判断昵称是否被使用过,对于手机在不在,可以先用布隆过滤器来判断不在,如果在那就进入数据库查找。

优点:快,节省内存
缺点:存在误判

1、哈希函数

补充完整的字符串的哈希函数,这里用的是一些成型的函数,里面经过了一些数学计算。

struct BKDRHash
{
    
    
	size_t operator()(const string& s)
	{
    
    
		register size_t hash = 0;
		for (auto ch : s)
		{
    
    
			hash = hash * 131 + ch;
		}
		return hash;
	}
};

struct APHash
{
    
    
	size_t operator()(const string& s)
	{
    
    
		register size_t hash = 0;
		size_t ch;
		for (long i = 0; i < s.size(); i++)
		{
    
    
			size_t ch = s[i];
			if ((i & 1) == 0)
			{
    
    
				hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
			}
			else
			{
    
    
				hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
			}
		}
		return hash;
	}
};

struct DJBHash
{
    
    
	size_t operator()(const string& s)
	{
    
    
		register size_t hash = 5381;
		for(auto ch : s)
		{
    
    
			hash += (hash << 5) + ch;
		}
		return hash;
	}
};

template<size_t N, class K = string, class Hash1 = BKDRHash, class Hash2 = APHash, class Hash3 = DJBHash>
class BloomFilter
{
    
    
public:
	void set(const K& key)
	{
    
    
		size_t hash1 = Hash1()(key) % N;
		_bs.set(hash1);
		size_t hash2 = Hash2()(key) % N;
		_bs.set(hash2);
		size_t hash3 = Hash3()(key) % N;
		_bs.set(hash3);
	}

	bool test(const K& key)
	{
    
    
		size_t hash1 = Hash1()(key) % N;
		if (!_bs.test(hash1))
		{
    
    
			return false;
		}
		size_t hash2 = Hash2()(key) % N;
		if (!_bs.test(hash2))
		{
    
    
			return false;
		}
		size_t hash3 = Hash3()(key) % N;
		if (!_bs.test(hash3))
		{
    
    
			return false;
		}
	}
private:
	bitset<N> _bs;
};

void test_bloomfilter()
{
    
    
	BloomFilter<100>;
}

在这里插入图片描述

哈希函数个数,代表一个值映射几个位,函数越多,误判率越低,消耗空间越多。k和m的平衡看下图

在这里插入图片描述

按照我们的代码使用三个哈希函数,所以k == 3,算出m / n是4多点,所以修改一下代码,一次性开4倍的空间。

class BloomFilter
{
    
    
public:
	void set(const K& key)
	{
    
    
		size_t len = N * _X;
		size_t hash1 = Hash1()(key) % len;
		_bs.set(hash1);
		size_t hash2 = Hash2()(key) % len;
		_bs.set(hash2);
		size_t hash3 = Hash3()(key) % len;
		_bs.set(hash3);

		cout << hash1 << " " << hash2 << " " << hash3 << endl;
	}

	bool test(const K& key)
	{
    
    
		size_t len = N * _X;
		size_t hash1 = Hash1()(key) % len;
		if (!_bs.test(hash1))
		{
    
    
			return false;
		}
		size_t hash2 = Hash2()(key) % len;
		if (!_bs.test(hash2))
		{
    
    
			return false;
		}
		size_t hash3 = Hash3()(key) % len;
		if (!_bs.test(hash3))
		{
    
    
			return false;
		}
	}
private:
	static const size_t _X = 4;
	bitset<N * _X> _bs;
};

测试代码

void test_bloomfilter()
{
    
    
	BloomFilter<100> zs;
	zs.set("sort");
	zs.set("bloom");
	zs.set("string");
	zs.set("test");
	zs.set("etst");
	zs.set("estt");

	cout << zs.test("sort") << endl;
	cout << zs.test("bloom") << endl;
	cout << zs.test("string") << endl;
	cout << zs.test("test") << endl;
	cout << zs.test("etst") << endl;
	cout << zs.test("estt") << endl;
	cout << zs.test("zyd") << endl;
	cout << zs.test("int") << endl;
	cout << zs.test("float") << endl;
}
//测试误判率
void test_bloomfilter2()
{
    
    
	srand(time(0));
	const size_t N = 10000;
	BloomFilter<N> bf;
	vector<string> v1;
	string url = "https://blog.csdn.net/kongqizyd146?spm=1011.2415.3001.5343";
	for (size_t i = 0; i < N; ++i)
	{
    
    
		v1.push_back(url + to_string(i));
	}
	for (auto& str : v1)
	{
    
    
		bf.set(str);
	}
	vector<string> v2;//v2和v1相似,但不一样
	for (size_t i = 0; i < N; ++i)
	{
    
    
		string url = "https://blog.csdn.net/kongqi146?spm=1011.2415.3001.5343";
		url += 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;
	vector<string> v3;
	for (size_t i = 0; i < N; ++i)
	{
    
    
		string url = "zhihu.com";
		url += 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;
}

在这里插入图片描述

十万个数据

在这里插入图片描述

可以控制映射多少个位置来控制误判率。

2、删除

删除函数不能直接删除,可以用计数的思路,用比特位来当作计数,1个比特位代表2个,2个比特位代表4个,不过呢实际上布隆过滤器不支持删除,也就不用考虑删除了。

3、哈希切割

两个文件,分别有100亿个query,现在还有1G内存,如何找到文件交集?给出精确算法和近似算法。query看作字符串。

假设查找一个字符串需要50个字节,100亿就是5千亿字节,大概是466G,两个文件就是932G。

这里的解决思路就是一个哈希切分i = HashFunc(query) % 1000;HashFunc用一种哈希函数即可。每个query算出来的i是多少,就进入Ai号小文件,另一个文件则是放入Bi号小文件。这里的前提是把两个文件各自分成若干个小文件,然后A1和B1找交集,A2和B2找交集,最后得到的就是总体的交集。文件A和B中相同的query会进入编号相同的小文件。

这个方法还有点缺陷。因为每个字符串的长度不同,每个小文件的大小控制不了都相同,也就是出现了冲突,而且可能换哈希函数也不一定解决得了问题。

单个文件中,有某个大量重复的query
单个文件中,有大量不同的query

第一个情况,重复的那些不需要再存放,所以可以使用/unordered_set/set来依次读取文件query,插入set中。

如果读取整个小文件,都可以成功插入,则是情况1;如果插入过程中抛异常,则是情况2,换其他哈希函数,再次分割,再求交集。

set插入如果已经存在,返回false,如果没有内存,会抛bad_alloc异,剩下的都会置空。

应用

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

这里就是哈希切割,分成比如500个小文件,依次读取数据,i = HashFunc(ip) % 500,这个ip就是第i个小文件;依次插入小文件,使用map统计IP出现次数;统计过程中,出现抛内存异常,说明单个小文件过大,冲突太多,需要换哈希函数,再次哈希切割这个文件;如果没有抛异常,则正常统计,统计完一个就记录最大的。清理map,再去找下一个小文件。找top k,就把每个得到的最大值放到堆里。

相同的IP一定进入相同的小文件,读取单个小文件,就可以统计IP出现次数。

结束。

猜你喜欢

转载自blog.csdn.net/kongqizyd146/article/details/130792046
今日推荐