【数据结构取经之路】位图全解

news2024/12/23 15:27:52

目录

前言

C++标准库里的位图

位图的设计及实现

位图几个关键接口的实现

set()

reset()

test()

 完整代码

位图的使用场景

位图的优缺点

位图的使用演示 —— 几道面试题的讲解


前言

位图(Bitmap)是一种非常高效的数据结构,主要用于处理大量数据的快速查找、去重等操作。它利用每一位(bit)来表示某个元素是否存在或某种状态,从而极大地节省了存储空间。在内存和存储空间相对受限的环境下,位图尤其有用。下面,我们先瞅一眼C++标准库里的位图,然后再抽出它的核心部分来自己实现。

C++标准库里的位图

template <size_t N> class bitset;

可以看到,bitset是一个类模板,顺便提一下,在一些地方位图也叫bitmap。下面来看一下它的操作。

b.any()b中是否存在置位的二进制位
b.all()b中所有位都置位了吗
b.none()b中不存在置位的二进制位吗
b.count()b中置位的位数
b.test(pos)若pos位置是置位的,则返回true,否则返回false
b.set(pos)将pos位置进行置位
b.reset(pos)将pos位置置为0(false)或者说将pos位置复位
b[pos]访问b中pos位置的位,即直接访问二进制位

以上就是部分bitset的操作了。其中,最核心的是set()、test()、reset()。

这里提一下,C++库中的位图并不是开在堆上的,所以当需要开很大的空间时,它会崩,解决方案是把位图整体开到堆上来,这里演示一下。

std::bitset<UINT_MAX> bs;//error
std::bitset<UINT_MAX>* pbs = new std::bitset<UINT_MAX>;//ok

下面我们开始着手位图的实现。

位图的设计及实现

位图的本质是一个直接地址法的哈希表,直接将整形映射到它所对应的比特位上。但是,遗憾的是C/C++中并没有能直接控制比特位的类型。所以我们只能对char/int等这样的整形通过位运算去控制相应的比特位。举个例子,在32为机器下,整型int的大小为4个字节,一共32个比特位。这32个比特位就可以被32个整型映射。有一个整型值x,下面是计算出x所对应的比特位的步骤。

1)i = x / 32; //这一步是计算出x对应的第几个整型

2)j = x % 32; //这一步是计算出x在第i个整型中对应第几个比特位

 下面,我假设 x = 20,然后计算出20所对应的二进制位。

i = x / 32 -> i = 20 / 32 -> i = 0 -> 可知 x 对应的是第0个int

j = x % 32 -> j = 20 % 32 -> j = 20 -> 可知 x 对应的是第0个int的第20个二进制位

x 映射的二进制位大概就是上图用红色三角形标记的位置。

 还有一个值得注意的细节,因为映射是直接按大小映射的,所以我们要按数据的范围开空间。例如有这样一个数组 {1,23,34,45};我们不能仅仅只开4个二进制位,我们要根据数据范围开,也就是说,至少要开46个二进制位(0~45)。有了一定的认识之后,下面我们开始实现位图的几个关键接口。

位图几个关键接口的实现

我们以vector为位图的底层,接下来我们将分别实现set()、reset()、test()这三个接口。

set()

把x映射的位标记为1。

上面我们已经知道了如何定位x映射的二进制位,接下来我们探讨如何将x对应的二进制位标记为1。

假设上图中上方数字串中被标记为红的0是x要映射的位。我们发现,当上方数字串或上下方数字串时,x要映射的位就置为1了,并且上方数字串中的其余位数不受影响,如果原来是0,那么或运算了之后还是0,如果原来是1,或运算之后还是1。这样就达到了我们的目的了,但问题来了,下方的数字串是如何得到的呢?其实也很简单,我们只需要将1左移 j 位 即可得到下方的数字串,然后拿该串去与对应的第 i 个整型进行或运算就达到目的了。

void set(size_t x)
{
	size_t i = x / 32;
	size_t j = x % 32;
	_bs[i] |= (1 << j);
}

reset()

把x映射的位标记为0。

我们发现,当上方的数字串与下方的数字串进行与(&)运算后,x映射的位置上原来的1被置成了0。同时,上方数字串中的其余位上,如果原来是0,那么进行与运算后还是0,如果原来是1,进行与运算后还是1,即其余位保持不变。现在,关键问题是如何拿到下方的数字串。其实,我们只需将set()中的数字串 0000 0000 0010 0000 ... 0000进行按位取反即可 。总结一下:将1左移 j 位后,对其左移的结果进行按位取反,然后在拿按位取反的结果去和第 i 个整形进行与运算。

//将x映射的位标记为0
void reset(size_t x)
{
	size_t i = x / 32;
	size_t j = x % 32;
	_bs[i] &= (~(1 << j));
}

test()

如果x对应的映射位置为1,则返回true,否则返回false。

假设上方数字串中被标记的0为x映射的位置。

我们发现,将上方的数字串与下方的数字串进行与运算,其结果是0,正好得到x映射的位置的值。我们知道,在进行与运算时,只有都是1与的结果才是1,而下方数字串中,除了红色的1以外,其余全为0,所以与运算的结果仅取决于x映射的位置的值是1还是0。如果x映射的位置的值是0,那么与的结果就是0,如果x映射的位置的值是1,那么与的结果为非0。下方的数字串如何得到上面已经提到过,就是将1左移 j 位即可。和上面两个函数不同的是,这里并不需要改变x映射的二进制位的值,只想知道是多少,所以就不能像reset()那样用 &= 了,与运算之后把结果返回就行了。

//如果x映射的二进制位为1,
//则返回true,否则放回false
bool test(size_t x)
{
	size_t i = x / 32;
	size_t j = x % 32;
	return _bs[i] & (1 << j);
}

 完整代码

#pragma once
#include <vector>

namespace pcz
{
	template <size_t N>
	class bitset
	{
	public:
		bitset() { _bs.resize(N / 32 + 1, 0); } //开空间

		//将x映射的位标记为1
		void set(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;
			_bs[i] |= (1 << j);
		}

		//将x映射的位标记为0
		void reset(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;
			_bs[i] &= (~(1 << j));
		}

		//如果x映射的二进制位为1,
		//则返回true,否则放回false
		bool test(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;
			return _bs[i] & (1 << j);
		}
	private:
		std::vector<size_t> _bs;
	};
}

位图的使用场景

位图的使用场景其实在前言部分有提到了,为了文章结构的完整性,我在这里再总结一下~

位图主要用于在大量数据中,判断某个元素是否存在。举个例子:在一个集合中,有40亿个无符号整数,未排序。给你一个无符号整数,如何快速判断这个数是否在这40亿个无符号整数中(腾讯\百度等公司出过的面试题)。在这种场景下,就非常适合使用位图了。这个例子中的问题会在下文演示位图的使用时模拟解决这个问题。

位图的优缺点

1)优点:可以快速判断一个元素是否在一个大的集合中,节省空间。

2)缺点:只适用于整型。

位图的使用演示 —— 几道面试题的讲解

1)一个集合中,有40亿个无符号整数,未排序。给你一个无符号整数,如何快速判断这个数是否在这40亿个无符号整数中(腾讯\百度等公司出过的面试题)。

思路一:暴力查找 —— 直接遍历一遍数据,时间复杂度为O(N)。

思路二:排序 + 二分查找 —— 时间复杂度为O(N*logN) + O(logN)。

思路三:位图。

接下来我们分析以上三种方法的可行性。

首先,第一种方法,效率太低了,弃用。

其次,针对第二种方法,我们先来算一算40亿个整型大概需要多少内存(二分查找是在内存中进行的,磁盘上不行)。1G = 1024MB = 1024 * 1024KB = 1024 * 1024 * 1024Byte ≈ 10亿Byte。一个整型4个字节,40亿个整型需要:40亿 * 4 = 160亿Byte,即 160亿 / 10亿 = 16G。16个G的数据是无法放到内存中的,只能存在磁盘里,但是二分查找只能对内存数组中的有序数据进行查找。所以第二种方法也是不可行的。

最后,我们来看看位图是如何解决这个问题的。判断一个整型是否在给定的整型数据集合中,结果只有两种——在或者不在。这两种状态正好可以用一个二进制位来表示(一个二进制位可以表示0也可以表示1),0表示给定数据不在40亿个整型数据中,1表示给定数据在40亿个整型数据中。我们只需要把这40亿个数据全部映射到位图中(set()),再test()即可。

看到这,你可能会有疑问——这40亿个数据放到位图中需要多少空间,它们能成功的映射到位图上吗?下面我们算一算。       

40亿个数据需要的二进制位:40亿个二进制位(每一个整型映射一个二进制位)

40亿个二进制位占用的字节数:40亿 / 8 = 5亿Byte(1Byte = 8 bit)

1G ≈ 10亿Byte   --->   5亿Byte ≈ 0.5G即500兆左右,堆上的空间完全够用(我们的位图底层为vector,vector在堆上开空间)

 方法三的可行性已得到验证,下面我们模拟解决一下这个问题。这里解释一下,所谓模拟解决就是不真的从40亿个数中判断某个数是否存在,而是我们造一组数据出来,然后通过我们写的程序判断某个元素是否在该组数据中,减少了数据量,但方法不变。

void TestBitSet()
{
	int arr[] = { 1, 2, 12, 5, 8, 9, 31, 21, 22, 10, 42 };
	pcz::bitset<45> bs;			//根据数据范围开空间
	for (auto val : arr)
	{
		bs.set(val);			//将数组中的元素全部映射到位图中
	}
	cout << bs.test(11) << endl;//判断11是否在数组中,预计结果为false,即输出0
}

                      

2)一个文件有100亿个整数,我们只有1G内存,设计算法找到出现次数不超过2次的所有整数。 

这道题是上一题的进阶版本,接下来我们思考一下如何解决这个问题。

我们的位图中,每一个整数映射到一个二进制位,而且每个二进制位只能表示两种状态,0表示不存在,1表示存在,根本无法标识出现次数。如果每一个整数映射2个二进制位,那么就可以表示4种状态:00 -> 出现0次,01 -> 出现1次,10 -> 出现2次,11 -> 出现3次及以上。这样一来就可以标识次数了,我们的问题也就迎刃而解。但另一个关键的问题来了:让一个整数映射2个二进制位,是不是意味着我们需要改造刚才实现的位图呢?也可以,但没必要。我们只需使用两个位图即可。上面我们已经算过了,开一个可以映射42亿整数的位图大概要500兆,开两个正好1G,所以是可行的。

x从原来的只映射一个位图中的一个二进制位到映射两个位图中相同的二进制位,接下来我们模拟解决。

#pragma once
#include <vector>

namespace pcz
{
	template <size_t N>
	class bitset
	{
	public:
		bitset() { _bs.resize(N / 32 + 1, 0); } //开空间

		//将x映射的位标记为1
		void set(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;
			_bs[i] |= (1 << j);
		}

		//将x映射的位标记为0
		void reset(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;
			_bs[i] &= (~(1 << j));
		}

		//如果x映射的二进制位为1,
		//则返回true,否则放回false
		bool test(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;
			return _bs[i] & (1 << j);
		}
	private:
		std::vector<size_t> _bs;
	};

	template <size_t N>
	class towbitset
	{
	public:
		void set(size_t x)
		{
			//检查两个位图中对应的位是0or1
			bool flag1 = _bs1.test(x);
			bool flag2 = _bs2.test(x);

			if (!flag1 && !flag2)    //00 -> 01
			{
				_bs2.set(x);
			}
			else if (!flag1 && flag2)//01 -> 10
			{
				_bs1.set(x);
				_bs2.reset(x);
			}
			else if (flag1 && !flag2)//10 -> 11
			{
				_bs2.set(x);
			}
		}

		void reset(size_t x)
		{
			_bs1.reset(x);
			_bs2.reset(x);
		}

		size_t get_count(size_t x)
		{
			bool flag1 = _bs1.test(x);
			bool flag2 = _bs2.test(x);

			if (!flag1 && !flag2)
				return 0;				//出现0次
			else if (!flag1 && flag2)
				return 1;				//出现1次
			else if (flag1 && !flag2)
				return 2;				//出现2次
			else
				return 3;				//出现3次及以上
		}
	private:
		bitset<N> _bs1;
		bitset<N> _bs2;
	};

    void TestTowBitSet()
	{
		towbitset<100> tbs;
		int arr[] = { 1, 12, 22, 13, 11, 7, 5, 8, 9, 32, 99, 99 };
		for (auto val : arr)
		{
			tbs.set(val);
		}
		for (auto val : arr)
		{
			if (tbs.get_count(val) == 2)
				cout << val << " ";
		}
		cout << endl;
	}
}

3) 给两个文件,分别有100亿个整数,只有1G内存,如何找到两个文件的交集。

这道题的解题思路是:开两个位图,分别将两个文件的数据映射到两个位图上,其中同一个数据在两个位图中映射的位置是一样的。所以我们只需判断x是否都在两个位图中,如果都在,就是交集。

namespace pcz
{
	template <size_t N>
	class bitset
	{
	public:
		bitset() { _bs.resize(N / 32 + 1, 0); } //开空间

		//将x映射的位标记为1
		void set(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;
			_bs[i] |= (1 << j);
		}

		//将x映射的位标记为0
		void reset(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;
			_bs[i] &= (~(1 << j));
		}

		//如果x映射的二进制位为1,
		//则返回true,否则放回false
		bool test(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;
			return _bs[i] & (1 << j);
		}
	private:
		std::vector<size_t> _bs;
	};

	void TestBitSet()
	{
		int arr1[] = { 1, 11, 2, 12, 99, 72, 31, 34, 88 };
		int arr2[] = { 5, 15, 66, 88 };

		bitset<100> _bs1;
		bitset<100> _bs2;
		for (auto val : arr1)
		{
			_bs1.set(val);
		}
		for (auto val : arr2)
		{
			_bs2.set(val);
		}

		for (size_t i = 0; i < 100; i++)
		{
			if (_bs1.test(i) && _bs2.test(i))
				cout << i << endl;
		}
	}
}


本文到这就结束啦,感谢你的支持~ 

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

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

相关文章

Claude Enterprise:Anthropic 推出企业级AI助手挑战OpenAI

Anthropic公司推出了Claude Enterprise&#xff0c;这是一项新的企业级AI服务&#xff0c;旨在提供更安全、更可控的AI聊天机器人体验。通过这个服务&#xff0c;企业可以将内部知识库与Claude机器人连接&#xff0c;使其能够访问和分析公司数据&#xff0c;从而回答员工的查询…

【MySQL】MySQL Workbench下载安装、环境变量配置、基本MySQL语句、新建Connection

1.MySQL Workbench 下载安装&#xff1a; 进入网址&#xff1a;MySQL :: MySQL Workbench Manual :: 2 Installation &#xff08;1&#xff09;点击“MySQL Workbench on Windows”&#xff08;下载Windows版本&#xff09;&#xff08;2&#xff09;点击“Installing” &…

前端Vue框架,本地数据库nedb

封装 db.js&#xff08;文章nedb版本^1.8.0&#xff09; // db.js// 导入 NeDB 模块 const Datastore require(nedb)// 创建数据库实例,最大600M或100W行 const db new Datastore({ filename: ./database.db, autoload: true, inMemoryOnly: false, maxFileSize: 600 * 1024…

如何将 Redshift Cryptomatte AOV 与 teamrender 结合使用,成都渲染101云渲染

这篇文章将讨论在 Cinema 4D 中将 cryptomatte AOV 与 teamrender 结合使用时常见的问题和解决方案。在 Cinema 4D 中使用 AOV 时&#xff0c;用户希望它们的工作方式与其他 AOV 完全相同。但事实并非如此&#xff0c;尤其是与 teamrender 结合使用时。 在 Cinema 4D 中&#x…

【JAVA高级】并发同步工具CyclicBarrier 的使用介绍

&#x1f4dd;个人主页&#x1f339;&#xff1a;个人主页 ⏩收录专栏⏪&#xff1a;JAVA进阶 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339;&#xff0c;让我们共同进步&#xff01; 文章目录 CyclicBarrier 简介CyclicBarrier 的场景示意图&#xff1…

AI绘画时代的自媒体引流攻略:如何实现粉丝暴涨与盈利

一、AI绘画在自媒体引流和赚钱中的应用 创作独特视觉内容&#xff0c;吸引粉丝关注 AI绘画技术可以帮助自媒体从业者创作出独一无二的视觉内容&#xff0c;这些内容在社交媒体上具有很高的辨识度和吸引力。通过以下方式&#xff0c;AI绘画助力引流和赚钱&#xff1a; &#xf…

软件厂商与集成平台协同--打造无缝企业解决方案

引言 在现在的众多项目当中&#xff0c;很多企业面临着日益复杂的业务需求和不断变化的市场环境。为了保持竞争力&#xff0c;企业会选择采用高效的工具和系统来管理和运营。CRM&#xff08;客户关系管理&#xff09;软件和ERP&#xff08;企业资源规划&#xff09;系统是企业…

PMF源解析软件下载、安装、运行;Fpeak模式运行结果优化及误差评估;大气颗粒物理化性质等基础知识和通过PMF方法对其来源解析

目录 专题一 PMF源解析技术简要及其输入文件准备 专题二 PMF源解析技术的原理&#xff0c;PMF软件的实操及应用举例 专题三 PMF源解析结果的优化及误差评估 更多应用 颗粒物污染不仅对气候和环境有重要影响&#xff0c;而且对人体健康有严重损害&#xff0c;尤其在一些重污…

计算机毕设选题推荐-基于python的校园班级课程表管理系统

&#x1f496;&#x1f525;作者主页&#xff1a;毕设木哥 精彩专栏推荐订阅&#xff1a;在 下方专栏&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; 实战项目 文章目录 实战项目 一、基于python的校园班级课程表…

【C++】简述STL——string类的使用

文章目录 一、STL的简述1.STL的框架2.STL版本 二、string1、string的介绍2、为什么string类要实现为模板&#xff1f; 三、string的构造接口四、string的容量相关的接口五、string对象修改相关的接口1、insert2.earse3、assign4、replace 六、string对象字符串运算相关接口1、c…

【Linux】《Linux 常见指令全攻略》

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

JavaScript 知识:this、apply/call/bind、Promise、async/await、HTTP 库 Axios

1、变量、声明、传递 (值、引用) javascript:void(0) 含义 javascript:void(0) 中最关键的是 void 关键字&#xff0c; void 是 JavaScript 中非常重要的关键字&#xff0c;该操作符指定要计算一个表达式但是不返回值。void() 仅仅是代表不返回任何值&#xff0c;但是括号内的表…

【C++ 第二十章】智能指针

1.为什么需要智能指针&#xff1f; 下面我们先分析一下下面这段程序有没有什么内存方面的问题&#xff1f;提示一下&#xff1a;注意分析下面 Func 函数中的问题。 #include<exception> int div() {int a, b;cin >> a >> b;if (b 0)throw invalid_argume…

【Python基础】这篇文章带你了解Python的基本特点,让学习Python变得事半功倍!!!

一、Python的基本特点 简单易学&#xff1a;Python语法简洁清晰&#xff0c;拥有极其简单的说明文档&#xff0c;对于初学者来说非常友好。面向对象&#xff1a;Python既支持面向过程的编程也支持面向对象的编程&#xff0c;这使得Python能够灵活地应对各种编程需求。可移植性…

投放Facebook广告开户全流程解析:从开户到广告投放的实用指南

Facebook作为全球最大的社交平台之一&#xff0c;广告业务覆盖范围广泛&#xff0c;已成为各类企业推广产品和服务的重要渠道。要在Facebook上成功投放广告&#xff0c;首先需要完成广告账户的开户流程。本文将详细介绍投放Facebook广告开户的步骤和条件&#xff0c;并解释如何…

VBA Excel 出报表

源数据 目标 PS:调休 以高亮颜色区分 整理一下 CMDBUT命令 VBA代码 Private Sub CommandButton1_Click() Dim ps As Integer Dim pe As Integer Dim srcs As Integer Dim srce As Integer Dim i As Integer Dim j As Integer Dim m As Integer Dim pname As Variant Dim pn…

力扣刷题--442. 数组中重复的数据【中等】

题目描述 给你一个长度为 n 的整数数组 nums &#xff0c;其中 nums 的所有整数都在范围 [1, n] 内&#xff0c;且每个整数出现 一次 或 两次 。请你找出所有出现 两次 的整数&#xff0c;并以数组形式返回。 你必须设计并实现一个时间复杂度为 O(n) 且仅使用常量额外空间&am…

【深度学习】线性回归的从零开始实现与简洁实现

前言 我原本后面打算用李沐老师那本《动手学深度学习》继续“抄书”&#xff0c;他们团队也免费提供了电子版(https://zh-v2.d2l.ai/d2l-zh-pytorch.pdf)。但书里涉及到代码&#xff0c;一方面展示起来不太方便&#xff0c;另一方面我自己也有很多地方看不太懂。 这让我开始思…

栈和队列的习题详解(2):用队列实现栈

前言&#xff1a; 小编在上一篇博客写了栈和队列其中一个习题&#xff0c;为了体现出题目的重要性所以我把每个题目都分开写了&#xff0c;下面废话不多说&#xff0c;开启我们今天的做题之旅~ 目录 1.用队列实现栈 1.1.题目介绍 1.2.做题方法介绍 1.3.栈功能的实现 1.3.1.…

天聚数行®近期上线了六个实用的API接口

天聚数行近期上线了一系列实用的API接口服务&#xff0c;涵盖了多种场景下的数据处理和信息查询的需求&#xff0c;为企业和开发者带来了便捷高效的工具支持。这些服务包括工商信息查询、手机状态检测&#xff08;如在网状态和空号检测&#xff09;、坐标系转换等功能&#xff…