哈希的应用——位图

news2024/11/26 5:43:35

文章目录

  • 前言
  • 1. 面试题思考
  • 2. 位图
    • 2.1 位图的概念
    • 2.2 思路讲解及代码实现
      • 结构定义
      • 构造函数
      • set和reset接口实现
      • set和reset测试观察
      • test接口实现
      • test接口测试
      • 思考
  • 3. 位图的应用
    • 习题1
    • 习题2
    • 习题3
  • 4. 总结
  • 5. 源码
    • 5.1 bitset.h
    • 5.2 Test.c

前言

前面的文章里我们学习了哈希表,并用哈希表模拟实现了STL里面的unordered_map和unordered_set。
那接下来呢我们要再来学习一下哈希的应用——位图和布隆过滤器

这篇文章先来看第一个——位图

1. 面试题思考

首先我们来看一道腾讯曾经考过的面试题,引出我们今天要讨论的问题

问题是这样的:

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

那我们看到这个问题可能会想到这样的思路:

1. 遍历,时间复杂度O(N)
2. 排序+二分查找
3. 利用哈希表或红黑树,就是放到set或unordered_set里面进行查找嘛

那大家思考一下,上面这些方法有没有什么问题?

那这里我们要注意到的是它这里给的是40亿个整数。
那大家先算一下,40亿个整数大概占多少空间?
首先:
1G=1024MB=1024*1024KB=1024*1024*1024byte
约等于10亿byte(字节)
那40亿个整数(一个整型4字节)就约等于16G。

那大约占16G空间的话我们上面的方法还可行吗?

首先最关键的问题是16G的数据可能都不能一次全部放到到内存中,内存可能都不够用。
要遍历的话就得把它们全部放到内存里面啊
然后如果要排序那就是用归并排序了,分开放到一个个的小文件里面,进行归并。
但是文件里面也没法二分查找啊,都不能下标访问。
那你像放到set或unordered_set里面查找也是一样,内存可能不够,哈希表或红黑树还有额外的消耗,因为还要存一些指针啥的,记录颜色啥的。
当然你可以分开每次处理一小部分,但这样效率就不太行了。

所以上面这些思路都不太合适,而且我们这里只是要判断在不在,其实没必要把它们全部存起来。

那像这样的问题用我们接下来要学的位图来解决就比较好。

2. 位图

2.1 位图的概念

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

那对于上面的那个问题我们其实就是判断在不在嘛,所以我们只需要存储每个值的存放状态(是否存在)即可,那我们就可以用位图来解决。

怎么做呢?

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

题目说的是40亿个不重复的无符号整数,所以我们可以这样做:

无符号整数的最大值是2^32-1,即4294967295
所以我们开这样一个数组
在这里插入图片描述
下标从0到无符号整型的最大值2^32-1
每个元素的大小呢是1个比特位,因为我们用1个比特位就可以来标识当前位置下标所对应的值存不存在,所以它其实就是一个直接定址法的思想
当然没有类型的大小是1个bit,所以我们可以开成char数组(当然不一定非要用char),那每个位置就是8个比特位。
在这里插入图片描述
0到7映射到第一个char,8到15映射到第二个char,以此类推。
那大家看现在我们标识40亿个整数存不存在用了多大空间?
前面我们算了40亿个整数大约16G,一个整数4字节=32bit,现在相当于缩到了1bit,原来的1/32,那就是大约0.5G即512MB,这个大小肯定是没问题的。

2.2 思路讲解及代码实现

那接下来我们就来详细讲解一下如何使用位图解决上面那道题,并实现相应的代码。

结构定义

首先我们可以先简单定义一下位图的结构:

在这里插入图片描述
那它实际就是一个数组嘛。

构造函数

我们来看一下构造函数应该怎么写?

那我们初始的时候就把vector里面全放成0嘛。
那现在要开N的比特位的空间的话,我们的元素个数应该给多少呢?
🆗,这里给N/8+1是比较合适的
因为我们vector里面放的是char嘛,一个char8个bit,所以N/8,但是可能有余数,所以再+1,多开8个bit,这个确保肯定是够用的。
在这里插入图片描述

set和reset接口实现

然后这里我们首先要实现的两个核心操作叫做set和reset:

在这里插入图片描述
set就是把x映射的那个位置的比特位设置成1,reset就是把它设置成0。

那首先第一个问题我们如何通过x找到它映射的那个比特位?

🆗,那首先第一步我们要找到x映射到第几个char里面,因为我们现在开的是char数组,里面是一个个的char(8bit),所以这里就是去找它映射到第几个8比特位里面。
那要算这个的话我们是不是用x除8就得到了
在这里插入图片描述
比如就这个图我们算8,那就是8/10等于1,所以它就在下标为1(实际是第二个)的那个char里面,这没有问题。

那然后我们就要找它映射的是在这个char里面的哪个bit位上?

那用x%8是不是就行了
在这里插入图片描述

那紧接着,我们找到了这个比特位,如何把它设置成1(set)或者设置成0(reset)呢?

先来看set,把x映射的比特位设置成1,怎么做呢?

🆗,我们给这个位置按位或|上一个1是不是就可以了。
因为按位或的话是有1则1,全0才0嘛。
不论它原来是1还是0,按位或1之后都是1。
当然改变这个位置的同时我们不能影响其它位置
那我们如何做到呢?
我们让1左移j位(实际是向高位移动,只不过一般情况下都是左边为高位,所以如果我们画的图不同,移动方向可能就变了)然后和x映射的这个char进行按位或就行了
这里char和int进行按位或的话会发生整型提升
在这里插入图片描述
在这里插入图片描述
因为1的补码只有最低位一个1,其余位置都是0,所以向高位移动j个位置这个唯一的1就和x映射的那个位置对上了。
所以:
在这里插入图片描述

那reset呢?把x映射的比特位设置成0

很简单,让这个比特位按位与0就行了
按位与是有0则0,全1才1
那我们只需让x映射的这个char跟1左移j位之后再取反(按位取反~)的结果按位与就行了,这样x映射的比特位变成0,其它位置也不受影响(因为其它位置是跟1进行&)。
在这里插入图片描述

set和reset测试观察

我们测试测试,并调式观察一下

首先看一下set:

在这里插入图片描述
我们开一个大小100bit的,然后把10这个位置置成1,就表示插入10这个值,set之后10就存在了
我们来调试观察一下:
在这里插入图片描述
先拿到bs的起始地址
在这里插入图片描述
我们看到现在都是0(现在一列是1个字节,不支持按比特位监视)
在这里插入图片描述
然后我们执行set
在这里插入图片描述
那就是把10这个位置置成1
在这里插入图片描述
我们看一下对不对
在这里插入图片描述
没问题(我们看到监视窗口其实就对应我们后面画的那个图,他也是为了方便我们观察,符合人的习惯),大家可以自己再换一些值测试测试。

那我们再来测试一下reset:

在这里插入图片描述
刚才对10进行了set,现在再对10进行reset,看一下
在这里插入图片描述
在这里插入图片描述
🆗,没问题。

test接口实现

除了这两个呢,还有一个比较核心的接口——test,他其实就是去判断某个值存不存在(它映射的位置是否被设置成了1):

那其实也很简单,让x映射的这个比特位和1进行按位与,如果结果是0,就表明这个比特位是0,如果结果是1,就表明这个比特位是1。
所以让x映射的这个char跟1左移j位之后的结果进行按位与就行了
在这里插入图片描述
结果为0就是假,非0就是真

test接口测试

来测试一下test:

在这里插入图片描述
没问题。

那现在的话我们就可以很好的解决最开始的那个面试题了。

思考

那大家来思考一下:对于上面那个问题40亿个无符号整数,我们的位图应该给多大?

就开40亿个可以吗?
不可以的。
要注意我们不能按个数去开,而是要按照范围去开
就算现在变成10亿个无符号整数,我们也应该开4294967295(即2^32-1,无符号整型最大值)个,因为我们不知道这10亿个整数的取值范围,它可能就包含了最大值,所以我们要确保不论它多大,就可以映射到位图中一个确定的位置上。

那我们如何让位图开这么大空间呢?4294967295这个值也不好记啊?

🆗,我们给一个-1是不是就好了啊。
bitset<-1> bs;
在这里插入图片描述
因为我们的N是无符号整数啊,那-1变成无符号整数不就是整型最大值嘛,就跟那个npos是一样的玩法。
那除了用-1,其实我们还可以这样写:
在这里插入图片描述
字面值是可以这样写的。

那我们可以看一下,给这个值他是不是按我们上面分析的一样开了512MB空间:

在这里插入图片描述
大家看现在是0.4MB
然后我们继续执行一步
在这里插入图片描述
🆗,就变成512.4MB了

最后想告诉大家的是:

我们上面讲的位图其实C++库里面也是提供的有现成的
在这里插入图片描述
我们上面实现的命名风格其实就是跟着库里面走的
比较核心的接口我们都带大家实现了
其它的接口大家用的的时候可以自己查阅文档
在这里插入图片描述

3. 位图的应用

下面我们再来一起看几个位图相关的练习题

习题1

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

大家思考一下,可以怎么解决?

首先这里是100亿个整数欸,我们还开0xFFFFFFFF这么多空间够吗?
当然是够的,虽然有100亿个,但它的范围还是不变的,肯定不会超过整型最大值,只能说明有很多重复值。
那我们肯定还是用位图来解决嘛,这道题我们可以这样做:
这道题让我们找出只出现一次的整数,其实通过两个比特位就可以判断。
在这里插入图片描述
我们只看两位,00就是0次,01就是一次,10就是1次以上,不一定就是两次,因为我们set的时候如果是10就可以不进行操作了,反正set到10的时候就已经超过1次了
所以我们可以给上面实现的位图改造一下,改造成每个位置占两个比特位的位图。
当然也可以不改造,我们还是用上面的位图,我们开两个位图,如果一个整数第一次出现就在第一个位图中把它映射的位置置成1,第二次出现就把它在第二个位图中映射的位置置成1

所以我们可以封装一个twobitset:

在这里插入图片描述

那对于它的set,就应该是这样的:

看当前值在两个位图中映射位置的值,如果是00,就变成01,如果是01,就变成10,如果是10,已经超过1次了,后续就可以不进行任何操作了
在这里插入图片描述

那然后我们可以写一个Print打印所有出现一次的值:

那我们如何找出twoset里面所有只出现一次的值呢?
很简单,我们去遍历找哪个值在第二个位图里面的映射位置是1(那一定是01,因为我们只有00、01、10三种状态),那它就是只出现一次的
在这里插入图片描述

那我们来搞一组数据测试一下:

测试的话我们数据量就不高那么大了
在这里插入图片描述
我们看一下打印的是不是只出现一次的
在这里插入图片描述
🆗,没有问题,大家可以自己再测试。

那我们再来看下一个问题:

习题2

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

首先第一种思路:

我们可以先读取一个文件的值放到内存中,然后再读取第二个文件,依次判断第二个文件里面的值在不在第一个位图里面,在的就是交集。
当然如果这样做的话有一个问题就是我们求出来的交集可能会有重复值出现,而求交集一般是要去重的。
当然我们可以解决一下:
我们读取第二个文件判断在不在位图里面的时候,如果在的话第一次就把它映射的这个位置置成0,这样后续再遇到就不再把它添到交集里面,就达到去重的效果了。

然后另外一种思路是这样的:

把两个文件的数据分别映射到两个位图里面。
然后遍历其中一个文件依次取值,判断如果某个值在两个位图里面映射的位置
都是1,那说明它在两个文件里都存在,就是交集
在这里插入图片描述
或者我们可以直接对两个位图进行按位与,结果中为1的位置对应的下标就是交集

那这两种方法的话:

如果数据量比较小用第一种其实比较好,因为第一种是遍历文件里面的数据去判断,而第二种我们遍历的数据的范围(跟数据量无关),所以如果数据量比较大可以用第二种。
所以要看场景选择合适的方法。

习题3

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

这个其实跟习题1有点类似嘛,就是第一题的一个变形:

还是用两个位图,这里我们可以分为4种状态
在这里插入图片描述
那还是两个比特位就够用了
我们最终要找到就是出现1次和2次的

那这个把第一题的代码稍微修改一下就行了

在这里插入图片描述

测试一下:

在这里插入图片描述
没有问题

4. 总结

最后总结一下:
在这里插入图片描述

那它的缺陷有没有办法解决呢?

有的就是我们下一篇文章要学的——布隆过滤器。

那我们下一篇文章在详细介绍

5. 源码

5.1 bitset.h

#pragma once
#include <vector>

template <size_t N>
class bitset
{
public:
	bitset()
	{
		_bits.resize(N / 8 + 1, 0);
	}

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

void test1()
{
	bitset<100> bs;
	bs.set(10);

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

	bs.reset(10);

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

void test2()
{
	//bitset<-1> bs;
	bitset<0xFFFFFFFF> bs;
}


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)
	//	{
	//		_bs2.reset(x);
	//		_bs1.set(x);
	//	}
	//	//10不用管
	//}

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

	//找到出现次数不超过2次的所有整数
	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)
		{
			_bs2.reset(x);
			_bs1.set(x);
		}
		//10->11
		else if (_bs1.test(x) == true
			&& _bs2.test(x) == false)
		{
			_bs2.set(x);
		}
		//11不用管
	}

	void Print()
	{
		for (size_t i = 0; i < N; i++)
		{
			if ((_bs1.test(i) == true && _bs2.test(i) == false)
				|| (_bs1.test(i) == false && _bs2.test(i) == true))
			{
				cout << i << " ";
			}
		}
		cout << endl;
	}
private:
	bitset<N> _bs1;
	bitset<N> _bs2;
};

void test_twobitset()
{
	int a[] = { 1,1,1,2,3,3,3,3,4,4,5,6,9,12,33,45,45,45,6 };
	twobitset<100> bs;
	for (auto e : a)
	{
		bs.set(e);
	}

	bs.Print();
}

5.2 Test.c

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include "bitset.h"

int main()
{
	test_twobitset();
	return 0;
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/977152.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Etherpad安装手册(win10 64位电脑)

Etherpad安装手册 我使用的为win10 64位电脑 一、按照教程下载安装Etherpad 按照该文章下载安装&#xff0c;下面记录我的过程 1.下载etherpad 2.解压缩etherpad到d盘 3.双击start.bat&#xff0c;启动编辑器服务 注意&#xff1a;可能出现问题如下图&#xff0c;跳转二 4.在…

数据库管理-第100期 Refreshable Clone PDBs(20230905)

数据库管理-第100期 Refreshable Clone PDBs&#xff08;20230905&#xff09; 上周五&#xff0c;9月1日&#xff0c;我于上海Oracle办公室OU考场完成了19c OCM Upgrade考试&#xff0c;感受就是&#xff0c;很多新特性考的比想象的更加深入&#xff0c;希望最终结果是好的吧…

AVL树的旋转与插入

题&#xff1a;点我 题目大意&#xff1a;旋转 A V L AVL AVL树 在 A V L AVL AVL操作中主要有两种主要操作&#xff0c;分别是 L L LL LL和 R R RR RR&#xff0c;其中 L L LL LL的旋转本质是希望发生以下的转变 而 R R RR RR的本质是希望发生以下的转变&#xff1a; 如我们…

【面试题】如何实现数组去重的?有几种方式?

前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 【国庆头像】- 国庆爱国 程序员头像&#xff01;总有一款适合你&#xff01; 1. 方法一&#xff1a;利用两层循环数组的splice方法 通过两层循环对数组…

TTransportException SASL authentication not complete

今天遇见了一个异常&#xff0c;但是发现是自己智障了 但还是记录一下 在close的时候先close了conn再close的statement导致报的这个错

工业、交通、电力、水务】LEAP模型应用及碳排放预测

中文名&#xff1a;LEAP模型 外文名&#xff1a;Long Range Energy Alternatives Planning System/ Low emission analysis platform LEAP模型[1]允许研究者根据研究目的、数据可获取度、研究对象特点等灵活构建模型结构&#xff0c;十分适用于能源数据不全面情况&#xff0c…

网络安全-IP地址信息收集

本文为作者学习文章&#xff0c;按作者习惯写成&#xff0c;如有错误或需要追加内容请留言&#xff08;不喜勿喷&#xff09; 本文为追加文章&#xff0c;后期慢慢追加 IP反查域名 http://stool.chinaz.com/same https://tools.ipip.net/ipdomain.php 如果渗透目标为虚拟主机…

HCIP自我重修总笔记

第一章.复习OSITCP/IP 模型 &#xff08;2023 9/5&#xff09; OSI 模型: 开放式系统互联参考模型 应用层&#xff1a;抽象语言-->编码表示层&#xff1a;编码--->二进制会话层&#xff1a;提供会话地址&#xff0c;建立应用程序端到端的会话 上三层为应用程序对数据加…

SQL注入 - 宽字节注入

文章目录 SQL注入 - 宽字节注入宽字节注入前置知识宽字节靶场实战判断是否存在SQL注入判断位数判显错位判库名判表名判列名 SQL注入 - 宽字节注入 靶场 sqli - labs less-32 宽字节注入主要是绕过魔术引号的&#xff0c;数据库解析中除了UTF-8编码外的所有编码如&#xff1a;G…

ctfshow 反序列化

PHP反序列化前置知识 序列化和反序列化 对象是不能在字节流中传输的&#xff0c;序列化就是把对象转化为字符串以便存储和传输&#xff0c;反序列化就是将字符串转化为对象 魔术方法 __construct() //构造&#xff0c;当对象new时调用 __wakeup() //执行unserialize()时&am…

结构体做函数参数

①值传递 ②地址传递 #include <iostream> #include <algorithm> #include <string> using namespace std; struct stu {int age;string name; }; void printStu(stu a) {cout << a.age << << a.name << endl; } void printstu(…

英语之美:用一句话解释句子结构

以下是一个包含主语、谓语、宾语、表语、定语、同位语、补足语和状语的扩展句子&#xff0c;使用 “I love you” 作为基础&#xff1a; “I, the person who truly loves you, consider our love a beautiful gift, and I love you more deeply with each passing day.” 在…

复制tr的一行数据或者复制数据使用,使用jq和php

效果图&#xff1a; 2.Html <!--复制的tr数据&#xff0c;s----------------------------------------------------------------------------------------------->{foreach from$arrs keykk itemvv} <tr><td style"text-align:center;" >1</t…

华为OD机试 - 关联子串 - 滑动窗口(Java 2023 B卷 100分)

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明4、换个思路 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A…

【C++】STL-函数对象-内建函数对象

0.前言 1.算术仿函数 #include <iostream> using namespace std;// STL-内建函数对象-算术仿函数 #include<functional> // 内建函数对象头文件//以下举例两个仿函数 -- negate and plus// negate 一元仿函数 取反仿函数 void test01() {negate<int>n;cout …

autojs修改顶部标题栏颜色

顶部标题栏的名字是statusBarColor 不是toolbar。难怪我搜索半天搜不到 修改之后变成这样了 代码如下&#xff1a; "ui"; importClass(android.view.View); importClass(android.graphics.Color); ui.statusBarColor(Color.parseColor("#ffffff")); ui.…

信息系统概述-生命周期-开发方法

信息系统概述-生命周期 考点分析信息系统概述信息系统分类企业目前所使用的具体的信息化系统信息系统的生命周期&#xff08;重要&#xff09;信息系统的开发方法&#xff08;重要&#xff09; 考点分析 每年都会考3分&#xff0c;2分会超纲 信息系统概述 信息系统分类 业务处理…

Redis——Java中的客户端和API

Java客户端 在大多数的业务实现中&#xff0c;我们还是使用编码去操作Redis&#xff0c;对于命令的学习只是知道这些数据库可以做什么操作&#xff0c;以及在后面学习到了Java的API之后知道什么方法对应什么命令即可。 官方推荐的Java的客户端网页链接如下&#xff1a; 爪哇…

XSS漏洞学习小结

声明: 所有文档均用于安全测试加强防护,若作它用后果自负. 目录 一、xss介绍 二、xss的危害 三、攻击原理 四、xss分类 反射型XSS 存储型XSS DOM型XSS 五、XSS常见攻击利用 六、XSS编码绕过 1、gpc过滤字符 2、过滤alert 3、过滤标签 4、ascii编码 5、url编码 6、j…

day 46 | ● 1143.最长公共子序列 ● 1035.不相交的线 ● 53. 最大子序和 动态规划

1143.最长公共子序列 func longestCommonSubsequence(text1 string, text2 string) int {res : 0dp : make([][]int, len(text1) 1)for i : 0; i < len(text1);i{dp[i] make([]int, len(text2) 1)}for i : 1; i < len(text1); i{for j : 1; j < len(text2); j{if t…