深入探索位图技术:原理及应用

一、引言

位图,以其高效、简洁的特性在数据处理、存储和检索等多个领域发挥着举足轻重的作用。通过二进制位的操作,位图能够实现对大量数据的快速访问、修改和查询,从而在众多实际应用场景中展现出其独特的优势。

随着大数据时代的来临,数据量呈现爆炸式增长,如何高效地处理这些数据成为了一个亟待解决的问题。位图技术以其对空间的高效利用和快速的位操作能力,成为了解决这一问题的有力工具。无论是在数据库系统、缓存机制、网络安全,还是在图形处理、用户权限管理等领域,位图都展现出了其不可或缺的价值。


二、位图(Bitset)基础知识

1、位图的概念

  • 位图及其基本组成单元(位)

位图(Bitmap)是一种基于二进制位的数据结构,用于高效地存储、检索和管理数据。其基本组成单元是位(Bit),即二进制数中的一个数字,其值只能是0或1。位图通常用于表示一组数据或某种状态的存在与否,其中每个数据项或状态都对应于位图中的一个或多个位。

在C++中,std::bitset是标准库提供的一种数据结构,用于表示固定大小的位序列,即一个固定长度的二进制序列。它提供了一组方法来进行位操作,例如设置、清除、翻转和测试位等。

std::bitset的大小在编译时确定,因此它是一种静态大小的位集合。你可以使用std::bitset来表示和处理布尔值序列,每个位可以代表一个布尔值(true或false)。而且可以单独访问每个位位置:例如,对于名为foo的位图,表达式foo[3]访问其第四个位,就像常规数组访问其元素一样。

在这里插入图片描述

  • 区分位图与像素图的概念

虽然位图在图形处理中也常被提及,但在此上下文中,我们需要明确区分两种不同类型的“位图”:一种是数据结构中的位图,如上所述,它主要用于数据的高效存储和检索;另一种是图形学中的位图,也被称为像素图,它是由像素(Pixel)组成的图像,每个像素都有其特定的颜色和位置。这两种位图在原理和应用上有本质的区别。数据结构中的位图关注的是数据的二进制表示和存储效率,而图形学中的位图则关注的是图像的视觉呈现和细节表现。

2、位图的表示

  • 使用二进制表示位图

位图,从数据结构的角度来看,是一种利用二进制位来标记数据存在性的高效方式。在二进制位图中,每一个二进制位(0或1)代表一个数据元素的状态:存在或不存在。通常,我们使用一个整数数组来实现位图,数组中的每个元素(如int或long类型)都由多个二进制位组成。

在这里插入图片描述

例如,如果我们使用一个32位的整数(int)来表示位图,那么每个整数可以表示32个不同的数据元素。当我们要标记第n个数据元素是否存在时,我们只需要找到对应的整数(通过n除以32得到整数索引),然后在该整数中找到对应的二进制位(通过n对32取余得到位索引),并将其设置为0或1。

  • 位图中位的编址和访问方式

在位图中,位的编址是基于其所在的整数和整数内的位位置来确定的。具体来说,如果我们有一个整数数组bitmap[],那么第n个数据元素对应的位在bitmap[n/32]的第(n%32)位上。

访问位图中的位通常涉及两个步骤:首先找到对应的整数,然后找到该整数中的对应位。这可以通过位运算来实现。例如,要设置第n位为1,我们可以使用以下代码:

int index = n / 32;      			// 计算整数索引
int position = n % 32;   			// 计算位索引
bitmap[index] |= (1 << position); 	// 设置对应位为1

同样地,要读取第n位的值,我们可以使用以下代码:

int index = n / 32;      							// 计算整数索引
int position = n % 32;   							// 计算位索引
int bitValue = (bitmap[index] >> position) & 1;  	// 读取对应位的值

这里的|=&=是位运算中的赋值操作符,分别表示按位或后赋值和按位与后赋值。<<>>是位移操作符,分别表示左移和右移。通过这些位运算操作,我们可以高效地访问和修改位图中的任意一位。

3、位图操作

在使用std::bitset时,常见的位操作包括设置位(setting a bit)、清除位(clearing a bit)、检测位(testing a bit)、翻转位(flipping a bit)等。

  1. 设置位(Setting a Bit)

要将std::bitset中的特定位设置为1,可以使用下标操作符[]配合赋值操作。

std::bitset<8> b; // 创建一个8位的bitset,所有位初始化为0
b[3] = 1;         // 设置第4位为1(bitset从右向左计数,从0开始)
  1. 清除位(Clearing a Bit)

要将std::bitset中的特定位清除(设置为0),同样可以使用下标操作符[]配合赋值操作。

std::bitset<8> b(0b11111111); // 创建一个8位的bitset,所有位初始化为1
b[3] = 0;                     // 清除第4位(设置为0)
  1. 检测位(Testing a Bit)

要检测std::bitset中的特定位是否为1,可以使用下标操作符[],这将返回一个可以转换为bool的类型。

std::bitset<8> b(0b10101010); 	// 创建一个8位的bitset
if (b[3])                  		// 检测第4位是否为1
    std::cout << "Bit 3 is set." << std::endl;
else
    std::cout << "Bit 3 is not set." << std::endl;
cout << b << endl;
// Bit 3 is set.
//10101010
  1. 翻转位(Flipping a Bit)

要翻转std::bitset中的特定位(从0变为1,或从1变为0),可以使用成员函数flip()

std::bitset<8> b(0b10101010); 	// 创建一个8位的bitset
b.flip(3);                   	// 翻转第4位
  1. 其他操作

std::bitset还提供了其他有用的成员函数,如:

  • size():返回bitset的大小(位数)。
  • count():返回bitset中设置为1的位数。
  • any():如果bitset中至少有一个位设置为1,则返回true。
  • none():如果bitset中所有位都设置为0,则返回true。
  • all():如果bitset中所有位都设置为1,则返回true。
  • to_ulong()to_ullong():将bitset转换为无符号长整型或无符号长长整型(如果bitset的大小允许)。
  • to_string():将bitset转换为字符串表示形式。
std::bitset<8> b(0b10101010);
std::cout << "Size: " << b.size() << std::endl;       	// 输出bitset的大小
std::cout << "Count: " << b.count() << std::endl;      	// 输出设置为1的位数
std::cout << "Any: " << b.any() << std::endl;          	// 检查是否有位设置为1
std::cout << "None: " << b.none() << std::endl;        	// 检查是否所有位都设置为0
std::cout << "All: " << b.all() << std::endl;          	// 检查是否所有位都设置为1
std::cout << "Value: " << b.to_ulong() << std::endl;   	// 转换为无符号长整型并输出
std::cout << "String: " << b.to_string() << std::endl; 	// 转换为字符串并输出

在这里插入图片描述


三、位图的应用场景

1、数据查找与存储

  • 使用位图实现快速数据查找

位图可以用于快速查找某个元素是否存在。例如,如果有一个包含大量整数的集合,并且整数的范围已知(例如,从1到1亿),那么可以使用一个位图来表示这个集合。每个整数对应位图中的一个位,如果该整数在集合中,则对应的位被设置为1,否则为0。这样,查找某个整数是否存在只需要检查其对应的位即可,时间复杂度为O(1)。

  • 位图在数据存储中的优势

空间效率:相比于存储原始数据,位图可以显著节省存储空间。在上面的例子中,如果使用一个整型数组来存储集合中的元素,每个整数需要4个字节(假设是32位系统),那么存储1亿个整数需要大约400MB的空间。而使用位图,只需要大约12MB的空间(1亿个位,每个位1/8字节)。

  • 使用整型数组:如果每个整数占用4个字节(在32位系统中,一个int通常是32位,即4字节),那么存储1亿个整数将需要大约400MB的空间,计算如下:

    1亿个整数 * 4字节/整数 = 400,000,000字节  
    400,000,000字节 / 1024 / 1024 = 381.47MB (约等于400MB)
    
  • 使用位图:在位图中,每个位占用1/8字节(因为1字节 = 8位)。所以,存储1亿个位将需要大约12MB的空间,计算如下:

    1亿个位 * 1/8字节/位 = 12,500,000字节  
    12,500,000字节 / 1024 / 1024 = 11.92MB (约等于12MB)
    

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

步骤 1: 确定位图大小

首先,需要确定无符号整数的位数,这决定了位图的大小。假设我们处理的是32位无符号整数,那么整数的范围是从0到232 - 1。因此,你需要一个位图,其中包含232个位,每个位对应一个可能的整数。

步骤 2: 分配内存

接下来,就需要分配足够的内存来存储这个位图。由于每个位可以是0或1,可以使用位操作来紧凑地存储这些位。在实际应用中,通常会使用一个字节数组(byte array)或位数组(bit array)来表示位图。

对于一个包含232个位的位图,你需要大约512MB的内存(因为232位 / 8位/字节 = 229字节 = 512MB)。

步骤 3: 初始化位图

将位图中的所有位初始化为0,表示没有任何整数被标记为存在。

步骤 4: 填充位图

遍历40亿个无符号整数,对于每个整数,计算它在位图中的索引,并将该索引对应的位设置为1。索引的计算通常是通过整数本身直接转换得到的。例如,对于一个32位整数num,你可以直接将其用作索引(可能需要一些转换来适应字节数组的索引方式)。

步骤 5: 查询整数

当需要查询一个整数是否存在于集合中时,你只需计算该整数在位图中的索引,并检查该索引对应的位是否为1。如果是1,表示该整数存在于集合中;如果是0,表示不存在。

有了上面的思路方法,我们来解决如下问题:

一、给定100亿个整数,我们如何找到只出现一次的整数呢?

由于整数可能有正有负,我们先假设整数的范围是已知的,例如是32位有符号整数。使用一个标准的位图来记录每个整数出现的次数。但需要扩展位图的概念,使得每个整数对应多个位,用以计数。例如,可以使用2个位来表示一个整数出现的次数:00表示0次,01表示1次,10表示2次或更多。

📽 我们可以这样做:

初始化一个大小为2 * INT_MAX + 1的位图(考虑负数和0),每个整数对应2位。

遍历整数集合,对每个整数更新其对应的计数器。

  • 如果对应位是00,则设置为01。
  • 如果是01,则设置为10。
  • 如果是10,则保持不变(因为我们只关心出现1次的)。
    再次遍历位图,找出所有对应位为01的整数。

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

首先,假设这些整数是32位无符号整数,那么理论上需要4GB的内存来存储一个完整的位图(因为232位 = 4GB)。由于只有1GB的内存可用,因此不能一次性加载所有整数到位图中。

因此我们借助哈希函数,分块处理。

  1. 准备阶段
    • 先设计一个哈希函数。这个函数的任务是,当我们给它一个整数时,它会告诉我们这个整数应该属于哪个“小团队”。想象一下,我们有1000个“小团队”,每个团队负责处理大约1000万个整数。
  2. 处理第一个文件
    • 我们开始遍历第一个文件,对里面的每个整数都用哈希函数“问问路”,看它们应该属于哪个“小团队”。
    • 比如,哈希函数告诉我们某个整数应该属于“团队500”,那我们就把这个整数记录到 “a500” 这个文件里。
    • 这样,我们就把第一个大文件拆分成了1000个小文件,每个文件大约40M,这样我们的1G内存就能轻松应对了。
  3. 处理第二个文件
    • 对第二个文件也如法炮制。还是使用相同的哈希函数,把整数分配到它们各自的“小团队”中。
    • 这次我们把结果记录在b1, b2, …, b1000这一系列文件中。
  4. 找交集
    • 有了这两组“小团队”,我们就可以开始找交集了。
    • 由于我们是用同样的哈希函数来分配整数的,所以,如果两个文件中有共同的整数,那么这些整数一定会出现在同样编号的“小团队”里。
    • 比如,a500和b500里就应该有相同的整数,我们只需要在这两个文件里找交集就行了。
    • 这样,我们就在每个“小团队”内部找交集,由于数据量小了很多,这个任务就变得容易多了。
  5. 汇总成果
    • 最后,我们把所有“小团队”找到的交集汇总起来,就是我们要找的答案了。

2、数据去重与排序

使用位图去除数据集中的重复元素。

基于位图的排序算法通常适用于数据值范围已知且较小的情况。其原理是利用位图来记录每个数据值的出现情况,并根据位图的顺序生成已排序的数组。具体步骤如下:

  1. 初始化位图:创建一个能够覆盖所有数据值的位图,并初始化为0。
  2. 遍历并设置位图:遍历原始数据,对于每个数据值,将其对应的位在位图中设置为1。
  3. 生成排序数组:从位图的最低位开始,依次检查每个位。如果某个位为1,则表示其对应的数据值存在,将其添加到排序数组中。继续这个过程,直到检查完位图的所有位。

四、位图的实现

template<size_t N>
class bitset {
public:
    bitset( ) {
        _bits.resize(N / 32 + 1, 0);
    }
    //	把 x 映射的位 标记成 1
    void set(size_t x) {
        assert(x < N);
        size_t i = x / 32;
        size_t j = x % 32;

        _bits[i] |= (1 << j);
    }
    //	把 x 映射的位 标记成 0
    void reset(size_t x) {
        assert(x < N);
        size_t i = x / 32;
        size_t j = x % 32;

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

    bool test(size_t x) {
        size_t i = x / 32;
        size_t j = x % 32;
        return _bits[i] & (1 << j);
    }
private:
    vector<int> _bits;
};

下面关于位图说法错误的是(D

A.位图就是用比特比特位表示一个数据的状态信息
B.通过位图可以求两个集合的交集
C.位图实际是哈希变形思想的一种应用
D.位图可以很方便的进行字符串的映射以及查找

A:正确。
B:正确,将两个序列分别映射到两个位图上,对两个位图的每个字节进行按位与操作,结果为1的比特位对应的数据 的就是两个序列的交集。
C:正确,位图就是将数据与数据在位图中对应的比特位进行了一一对应,是哈希的一种变形。
D:错误,采用位图标记字符串时,必须先将字符串转化为整形的数字,找到位图中对应的比特位,但是在字符串转 整形的过程中,可能会出现不同字符串转化为同一个整形数字,即冲突,因此一般不会直接用位图处理字符串。

  • 43
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 15
    评论
ext4是一种现代的Linux文件系统,它是ext3文件系统的后继者。它具有更好的性能和可靠性,支持更大的文件系统和更大的文件。 ext4文件系统的结构包括以下几个部分: 1. 引导块(boot block):包含引导程序和文件系统元数据,用于引导系统。 2. 超级块(superblock):包含文件系统的元数据,例如文件系统大小、块大小、inode数量等。每个文件系统只有一个超级块。 3. 块组描述符表(block group descriptor table):包含有关块组的元数据,例如块组的大小、inode表的位置、数据块位图的位置等。 4. inode表(inode table):存储所有文件和目录的元数据,包括文件的所有者、权限、访问时间、修改时间、创建时间等。 5. 数据块位图(data block bitmap):用于跟踪哪些数据块已经分配给文件或目录。 6. inode位图(inode bitmap):用于跟踪哪些inode已经分配给文件或目录。 7. 数据块(data blocks):存储实际的文件和目录数据。 这些部分组合在一起形成一个完整的ext4文件系统。每个文件和目录都有一个对应的inode,inode存储了文件或目录的元数据,包括文件大小、访问权限、创建时间、修改时间等。数据块存储实际的文件或目录数据,它们可以是连续的或离散的。数据块位图和inode位图用于跟踪哪些块和inode已经被分配。块组描述符表包含有关块组的元数据,例如块组的大小、inode表的位置、数据块位图的位置等。超级块包含文件系统的元数据,例如文件系统大小、块大小、inode数量等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

无敌岩雀

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

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

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

打赏作者

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

抵扣说明:

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

余额充值