【初阶与进阶C++详解】第二十一篇:哈希应用(位图实现应用+布隆过滤器增删查优缺点+海量数据面试题)

🏆个人主页企鹅不叫的博客

​ 🌈专栏

⭐️ 博主码云gitee链接:代码仓库地址

⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!

💙系列文章💙

【初阶与进阶C++详解】第一篇:C++入门知识必备

【初阶与进阶C++详解】第二篇:C&&C++互相调用(创建静态库)并保护加密源文件

【初阶与进阶C++详解】第三篇:类和对象上(类和this指针)

【初阶与进阶C++详解】第四篇:类和对象中(类的六个默认成员函数)

【初阶与进阶C++详解】第五篇:类和对象下(构造+static+友元+内部类

【初阶与进阶C++详解】第六篇:C&C++内存管理(动态内存分布+内存管理+new&delete)

【初阶与进阶C++详解】第七篇:模板初阶(泛型编程+函数模板+类模板+模板特化+模板分离编译)

【初阶与进阶C++详解】第八篇:string类(标准库string类+string类模拟实现)

【初阶与进阶C++详解】第九篇:vector(vector接口介绍+vector模拟实现+vector迭代器区间构造/拷贝构造/赋值)

【初阶与进阶C++详解】第十篇:list(list接口介绍和使用+list模拟实现+反向迭代器和迭代器适配)

【初阶与进阶C++详解】第十一篇:stack+queue+priority_queue+deque(仿函数)

【初阶与进阶C++详解】第十二篇:模板进阶(函数模板特化+类模板特化+模板分离编译)

【初阶与进阶C++详解】第十三篇:继承(菱形继承+菱形虚拟继承+组合)

【初阶与进阶C++详解】第十四篇:多态(虚函数+重写(覆盖)+抽象类+单继承和多继承)

【初阶与进阶C++详解】第十五篇:二叉树搜索树(操作+实现+应用KVL+性能+习题)

【初阶与进阶C++详解】第十六篇:AVL树-平衡搜索二叉树(定义+插入+旋转+验证)

【初阶与进阶C++详解】第十七篇:红黑树(插入+验证+查找)

【初阶与进阶C++详解】第十八篇:map_set(map_set使用+multiset_multimap使用+模拟map_set)

【初阶与进阶C++详解】第十九篇:哈希(哈希函数+哈希冲突+哈希表+哈希桶)

【初阶与进阶C++详解】第二十篇:unordered_map和unordered_set(接口使用+模拟实现)



💎一、位图

🏆1.位图概念

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

🏆2.位图实现

框架

框架:每个元素用char表示,每一个char有八个bit’,可以表示八个数据,把N除以8再加1,判断多少个字节可以有这么多bit的空间,但是会浪费几个比特,这些空间都是可以忽略的

// N个比特位的位图  
template<size_t N>
class bitset
{
public:
bitset()
	{
		// +1保证足够比特位,最多浪费8个比特
		_bits.resize(N / 8 + 1, 0);
	}
private:
	//存放的是字节
	vector<char> _bits;
};

x映射的位标记成1

  1. 找到x所处在的哪一位:先确定这个数应该处在第几个字节,也就是i = x / 8;然后通过把x对8取模,得到x在第i个整形的第j个位置
  2. 把改为设置为1:把1左移j位,然后得到00…010…00,用这个数和第i整数进行按位的操作
//x映射的位标记成1
	void set(size_t x)
	{
		// x映射的比特位在第几个char对象
		size_t i = x / 8;

		// x在char第几个比特位
		size_t j = x % 8;
		//将第i个对象的第j位改成1
		_bits[i] |= (1 << j);
	}

x映射的位标记成0

  1. 找到x所处在的哪一位
  2. 把这位设置为0:把1左移j位,然后得到00…010…00,把这个数取反,然后和第i个整数进行按位的操作
//x映射的位标记成0
	void reset(size_t x)
	{
		// x映射的比特位在第几个char对象
		size_t i = x / 8;

		// x在char第几个比特位
		size_t j = x % 8;
		//将第i个对象的第j位改成0
		_bits[i] &= (~(1 << j));
	}

判断x映射的位是否为1

  1. 找到x所处在的哪一位
  2. 判断这一位的值:把1左移j位,然后得到00…010…00,然后返回这个数和第i个字节进行位的的结果
/判断x映射的位是否为1
	bool test(size_t x)
	{
		// x映射的比特位在第几个char对象
		size_t i = x / 8;

		// x在char第几个比特位
		size_t j = x % 8;

		return _bits[i] & (1 << j);
	}

测试代码:

void test_bit_set()
	{
		bitset<1000> bs;
		bs.set(22);
		bs.set(21);

		cout << bs.test(22) << endl;
		cout << bs.test(21) << endl;


		bitset<0xFFFFFFFF> bigBS;
	  //写法二
		//bitset<-1> bigBS;
	}

🏆3.位图应用

  1. 快速查找某个数据是否在一个集合中

  2. 排序+去重

  3. 操作系统中磁盘的标记等

    缺点:智能处理整形数据

💎二、布隆过滤器

🏆1.布隆过滤器概念

布隆过滤器通过多个哈希函数将一个数据映射到位图的结构中,也就是一个数据映射位图的多个位置,可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难

🏆2.布隆过滤器框架

四个个字符串哈希函数如下: 用他们作为缺省参数,默认处理字符串类型,映射越多消耗的空间就越多,效率会变低,哈希冲突概率也就越小

//各种字符串哈希函数
struct BKDRHash
{
	size_t operator()(const string& s)
	{
		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;
	}
};


struct JSHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 1315423911;
		for (auto ch : s)
		{
			hash ^= ((hash << 5) + ch + (hash >> 2));
		}
		return hash;
	}
};
//有多少HashFunc,一个Key就转换成几个整数去映射对应的几个比特位
//映射越多消耗的空间就越多,效率会变低,哈希冲突概率也就越小
template<size_t M,	//代表范围,能够映射元素个数 
	class K = string, //传送过来的数据
	class HashFunc1 = BKDRHash,
	class HashFunc2 = APHash,
	class HashFunc3 = DJBHash,
	class HashFunc4 = JSHash>
	class BloomFilter
{
public:

private:
	bitset<M> _bs;//数据
};

🏆3.布隆过滤器的插入

  1. 先用不同的哈希函数计算出该数据分别要映射到位图的哪几个位置
  2. 然后把位图中的这几个位置设置为1
//插入数据Key
	void Set(const K& key)
	{
		//有M个比特位,所以要映射到M个范围之内
		size_t hash1 = HashFunc1()(key) % M;
		size_t hash2 = HashFunc2()(key) % M;
		size_t hash3 = HashFunc3()(key) % M;
		size_t hash4 = HashFunc4()(key) % M;
		//将对应的位置set为1
		_bs.set(hash1);
		_bs.set(hash2);
		_bs.set(hash3);
		_bs.set(hash4);
	}

🏆4.布隆过滤器的查找

  1. 先用不同的哈希函数计算出该数据分别要映射到位图的哪几个位置
  2. 然后判断位图中的这几个位置是否都为1,如果有一个不为1,说明该元素一定不在容器中,只有一个为1,可能存在误判,如果三个位置都存在则一定存在误判
//判断Key是否存在,不在才是准确的
	bool Test(const K& key)
	{
		//如果有一个位置比特位不再,就返回false
		//只有一个在可能存在误判
		size_t hash1 = HashFunc1()(key) % M;
		if (_bs.test(hash1) == false)
		{
			return false;
		}

		size_t hash2 = HashFunc2()(key) % M;
		if (_bs.test(hash2) == false)
		{
			return false;
		}

		size_t hash3 = HashFunc3()(key) % M;
		if (_bs.test(hash3) == false)
		{
			return false;
		}

		size_t hash4 = HashFunc4()(key) % M;
		if (_bs.test(hash4) == false)
		{
			return false;
		}
		//三个位置都存在则存在误判
		return true; 
	}

🏆5.布隆过滤器的删除

在删除一个元素时,可能会影响其他元素。也就是说,要删除的元素映射的位置可能会是其它元素映射的位置,所以直接删除元素会给后期查找某个元素带来极大的误判。

🏆6.布隆过滤器的优缺点

优点:

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

缺点:

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

测试代码:

void TestBloomFilter()
{
	BloomFilter<44> bf;

	string a[] = { "榴莲", "香蕉", "西瓜", "1115551111", "eeehkfff", "草莓", "休息", "来了", "你好", "set" };

	for (auto& e : a)
	{
		bf.Set(e);
	}

	for (auto& e : a)
	{
		cout << bf.Test(e) << endl;
	}
	cout << endl;

	cout << bf.Test("set") << endl;
	cout << bf.Test("string") << endl;

}

💎三、海量数据面试题

🏆1.哈希切割

  1. 给一个超过100G大小的log_file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 如何找到top K的IP?如何直接用Linux系统命令实现?

    1. 先创建100个小文件A0-A99,然后计算 i = Hashhash(IP)%100(这样相同的ip一定进入同一个小文件),i是多少,IP就进入编号为i的文件中,先将一个小文件加载到内存中,依次读取放入map<string, int> countMap中依次对每个小文件统计,同时用一个pair<string, int> maxip录当前出现次数最多的IP,统计完一个文件,clear掉,然后再统计另一个文件
    2. priority_queue< pair<ip, int>, 仿函数 >minHeap,找出现次数topk的ip。
    3. 以上存在问题,某个小文件太大:某个文件相同ip太多,统计次数。映射冲突到这个编号文件的ip太多,捕获内存不足异常,说明内存不够,针对这个小文件,再次换个哈希函数,进行哈希切分,再切成小文件,再对这个小文件依次统计
      1. linux有专门的指令

🏆2.位图应用

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

    100亿个整数占用40G的空间,如果直接加载到内存中,空间肯定是不够的,所以我们这里可以考虑用位图来处理。改进位图,用两个比特位表示整数,用其中的三种状态00(没出现)、01(出现一次)和10(出现两次及以上)。消耗内存为:24(2^32-1)/32 byte≈1G

    template<size_t N>
    class _bitset
    {
    public:
    	//00代表出现0次,01代表出现1次,10代表出现两次及以上
    	void set(size_t x)
    	{
    		int in1 = _bs1.test(x);
    		int in2 = _bs2.test(x);
    		//00->01出现次数从0次变成1次
    		if (in1 == 0 && in2 == 0)
    		{
    			_bs2.set(x);
    		}
    		//01->10出现次数从1次变成两次
    		else if (in1 == 0 && in2 == 1)
    		{
    			_bs1.set(x);
    			_bs2.reset(x);
    		}
    	}
    	//返回出现一次的数
    	bool is_once(size_t x)
    	{
    		return _bs1.test(x) == 0 && _bs2.test(x) == 1;
    	}
    
    private:
    	//复用bitset
    	bitset<N> _bs1;
    	bitset<N> _bs2;
    };
    
  2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

    将文件1的整数映射到一个位图中, 将文件2的整数映射到另一个位图中,然后将两个位图进行按位与,与之后位图中为1的位就是两个文件的交集,代码参考上面一个问题

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

    和第一题类似,00代表出现0次,01代表出现一次,10代表出现两次及其以上,所以只要统计00和01即可

🏆3.布隆过滤器

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

    近似算法:将其中一个放到布隆过滤器,另外一个判断交集,在就是交集,不在就不是交集

    精确算法:i = Hashfunc(query) % 1000,把文件1和文件2分别切分成A0、A1…A999和B0、B1…B999,两个文件分别切分成1000个小文件,先将一个小文件加载到内存中,依次读取放入unordered_set中依次对每个小文件统计,然后读取另外一个文件中的query,判断是否在,在就是交集

  2. 如何扩展BloomFilter使得它支持删除元素的操作

    用几个比特位来表示计数器。


  • 12
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

penguin_bark

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值