【C++篇】哈希扩展:位图和布隆过滤器+哈希切割


位图

所谓位图,就是用内存的每个比特位来存放某种状态适用于海量的整数数据,通常是用
来判断某个数据存不存在。

我们库中是有这个容器的:bitset文档介绍

使用需包头文件:include<bitset>

常用接口:
在这里插入图片描述

接口功能
size返回容器大小(单位:比特)
test返回给定数据对应比特位的状态(为1返回true,为0返回false)
set将给定数据映射的的比特位设为1
reset将给定数据映射的的比特位设为0

面试题

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

常规查找方法可以吗?
比如:

  1. 用set解决?
  2. 排序+二分查找?

答案是不行!
我们来算算如果用常规的整数去处理,需要多少空间?

40亿个整数大概占160亿比特位,也就是16个G左右,非常的夸张。

位图解决

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

将数据集{1, 3, 7,4,12, 16, 19, 13, 22, 18}映射到位图(本质上是一个vector<char>的一个对象)中:
在这里插入图片描述
最后判断一个数在不在,看其映射的byte位的值即可。

模拟实现位图

对于比特位数据的操作,无非就是0和1直接修改或判断。因此,位图的实现的核心思想是位运算。

本文我们用vector<int>,作为载体,实现位图:
在这里插入图片描述

template<size_t N>
class Bitset
{
public:
	Bitset()
	{
		_a.resize(N / 32 + 1);//开好空间,默认初始空间位32位
	}
	//…………
private:
	vector<int> _a;
};
1. set

具体逻辑:

  1. 查找该比特位在哪个整数中i = x%32
  2. 查找该比特位在整数的第几位j = x/32
  3. 将该整数与对应比特位为1且其余为0的整数按位或_a[i] |= 1<<j

在这里插入图片描述

	void set(size_t x)
	{
		int i = x / 32;
		int j = x % 32;
		_a[i] |= (1 << j);
	}
2. reset

具体逻辑:

  1. 查找该比特位在哪个整数中i = x%32
  2. 查找该比特位在整数的第几位j = x/32
  3. 将该整数与对应比特位为0且其余为1的整数按位与_a[i] &= (~(1<<j))
    在这里插入图片描述
void reset(size_t x)
{
	int i = x / 32;
	int j = x % 32;
	_a[i] &= (~(1 << j));
}
3. test
  1. 查找该比特位在哪个整数中i = x%32
  2. 查找该比特位在整数的第几位j = x/32
  3. 返回将该整数与对应比特位为1且其余为0的整数按位与的结果return _a[i] & 1<<j

在这里插入图片描述

bool test(size_t x)
{
	int i = x / 32;
	int j = x % 32;
	//非零则为真
	return _a[i] & (1 << j);
}

海量数据面试题(位图应用)

位图的功能:

  • 快速查找某个数据是否在一个集合中
  • 排序 + 去重
  • 求两个集合的交集、并集等
  • 操作系统中磁盘块标记
  1. 给定100亿个整数,设计算法找到只出现一次的整数?
    具体逻辑:
    用两个比特位:遍历标记,每出现一次就+1,当为10(3)时,就无需+1了。最后遍历找01(1)即可。

用双位图实现:

void Test2()
{
	int a[] = { 1,2,3,3,4,4,4,4,4,2,3,6,3,1,5,5,8,9 };

	Bitset<10> bs1;
	Bitset<10> bs2;

	//遍历标记,每出现一次就+1,当为10(3)时,就无需+1了。最后遍历找01(1)即可
	for (auto e : a)
	{
		//00->01
		if (!bs1.test(e) && !bs2.test(e))
		{
			bs2.set(e);
		}
		//01->10
		else if (!bs1.test(e) && bs2.test(e))
		{
			bs1.set(e);
			bs2.reset(e);
		}
	}

	//遍历找01
	for (auto e : a)
	{
		if (!bs1.test(e) && bs2.test(e))
		{
			cout << e << " ";
		}
	}
	cout << endl;

}
  1. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
    具体逻辑:
    应用位图功能之一:去重
    双位图解决问题:
    1. 将两文件的数据分别映射到两个位图中
    2. 若两位图对应的byte皆为1,则该对应的数是交集的
void Test3()
{
	int a1[] = { 1,2,3,3,4,4,4,4,4,2,3,6,3,1,5,5,8,9 };
	int a2[] = { 8,4,8,4,1,1,1,1 };

	Bitset<10> bs1;
	Bitset<10> bs2;

	// 去重
	for (auto e : a1)
	{
		bs1.set(e);
	}

	// 去重
	for (auto e : a2)
	{
		bs2.set(e);
	}

	for (int i = 0; i < 10; i++)
	{
		if (bs1.test(i) && bs2.test(i))
		{
			cout << i << " ";
		}
	}
	cout << endl;

}


布隆过滤器

位图虽然强大,但却只能用于整数数据,那对于字符串数据呢?

其实,我们可以用一些字符串哈希函数将字符串其转换为整数数据,然后再用位图解决问题。

字符串哈希算法

但还是存在很大的问题:

假设经过字符串哈希算法计算后,“百度”对应20,“腾讯”对应59,“阿里”对应38,映射结果如下:
在这里插入图片描述
问查找“字节”(假设对应59)的结果如何?
查找结果:“字节”是存在的,因为腾讯也是59。

但实际上,“字节”是不存在的。
由此我们可以得出结论:

  • 判断数据存在是不准确的:可能因为冲突而导致误判
  • 判断数据不存在是准确的

那能否降低误判率呢?

布隆过滤器正是解决此问题的!

布隆过滤器原理:它是用多个字符串哈希算法,将一个数据映射到位图结构中。

比如,我们用三个字符串哈希算法,将一个数据对应3个整数,查找时,需要对应的3个整数在位图中映射都为1,才能判定该数据存在。
在这里插入图片描述
【注意】

  1. 布隆过滤器只是降低误判的概率(可以做到大幅度降低),但误判是无可避免的。
  2. 一般情况下不能从布隆过滤器中删除元素,因为可能会影响其他数据。
  3. 误判率与哈希函数个数和所给空间大小有关
    在这里插入图片描述
    参考文献:详解布隆过滤器的原理,使用场景和注意事项

布隆过滤器实现

我选用了三个评分最高的哈希函数来实现hhh
在这里插入图片描述
实现逻辑非常简单,对bitset进行封装即可

将哈希函数写为仿函数。

set:
用3个哈希函数来计算数据对应的整数,然后用set到位图中即可

test:
用3个哈希函数来计算数据对应的整数,然后判断各个整数在位图中是否为1

//BKDR哈希算法
struct BKDRHash
{
    size_t operator()(const string& str)
    {
        size_t hash = 0;
        for (auto ch : str)
        {
            hash = hash * 131 + ch;
        }

        return hash;
    }
};

//AP哈希算法
struct APHash
{
    size_t operator()(const string& str)
    {
        size_t hash = 0;
        for (size_t i = 0; i < str.size(); i++)
        {
            size_t ch = str[i];
            if ((i & 1) == 0)
            {
                hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
            }
            else
            {
                hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
            }
        }

        return hash;
    }
};

//DJB哈希算法
struct DJBHash
{
    size_t operator()(const string& str)
    {
        size_t hash = 5381;
        for (auto ch : str)
        {
            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;
        _bt.set(hash1);
        
        size_t hash2 = Hash2()(key) % N;
        _bt.set(hash2);

        size_t hash3 = Hash3()(key) % N;
        _bt.set(hash3);

    }

    bool test(const K& key)
    {
        size_t hash1 = Hash1()(key) % N;
        if (!_bt.test(hash1))
            return false;

        size_t hash2 = Hash2()(key) % N;
        if (!_bt.test(hash2))
            return false;

        size_t hash3 = Hash3()(key) % N;
        if (!_bt.test(hash3))
            return false;
        return true;
    }
private:
	bitset<N> _bt;
};

哈希切割

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

此处近似算法可以用布隆过滤器解决,但精确算法得用哈希切割。

何为哈希切割?

用哈希函数将一组数据进行分类,就是哈希切割。

本题我们可以分别将两个海量数据文件分割为1000份:
假设两文件分别为A和B
A被分为(A0,A1,A2,……,A998,A999)
B被分为(B0,B1,B2,……,B998,B999)

i = Hash(query) % 1000
i是多少,query就进入第i个小文件

最后依次找Ai和Bi的交集即可。

💡:切割后的每个小文件就像哈希表的桶
图解:
在这里插入图片描述

找交集方法:将Ai读出来放到一个set,然后再用这个set去查找Bi的query在不在,在就是交集。

如果是平均切割为1000份,每份的大小约为300MB,但是哈希切分并非平均切分,在冲突较为极端的情况下,会导致其中某个Ai文件太大,超过1G,那就不符合题意了。

那怎么办呢?

哈希冲突太多有两个情况:

  1. 大半部分都是相同的query
  2. 大多数的query都是冲突

解决方案:

先把Ai的query读到一个set,

  1. 如果set的insert报错抛异常(bad_alloc),说明是大多数的query都是冲突,在换一个哈希函数,再次进行哈希切割。
  2. 如果Ai能够全部被读取insert到set中,那就锁门ai中有大量重复的query。

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

与上题同样的方式进行哈希切割,相同的IP地址一定在一个小文件中,用map去分别统计每个小文件中IP出现的次数即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值