Cpp::STL—位图bitset的使用与模拟实现(39)
文章目录
- 前言
- 一、先来个题目引入
- 二、位图概念
- 三、位图的应用
- 四、位图的使用
- 五、模拟实现
- 接口总览
- 构造函数
- set、reset、flip、test
- size、count
- any、none、all
- 总结
前言
Hello,我们又倒回来更新C++篇了
好焦虑,时常在想着该如何调节~~
一、先来个题目引入
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中?
其实我第一反应是放到一个数组里面,然后排序,再二分查找;或者放到容器unordered_set里面,然后find一下,其实这样看起来还可以,第一种方法时间复杂度为 O(NlogN) ,第二种方法时间复杂度为 O(N) ,但是现在有一个很严肃的问题就是 40亿 个数据, 那一共需要 40 * 10^8 * 4 个Byte,然后再将这个数 / 1024 / 1024 / 1024 ≈ 16GB,内存很显然是放不了那么大的数据的,所以上述两个方法显然不太可行
二、位图概念
在上面这个问题中,我们只需要判断数据在不在,那么其实也就是两种状态,我们很容易联想到 0, 1 ,也就是说,表达一个数据在不在的话,根本不需要四个字节,其实只需要一个 bit 位就可以,而无符号整数的数量有 2^32 个,那么其实只需要 2^32 / 8 / 1024 / 1024 = 512M,内存消耗大大减少,很明显这是OK的,我们一下就看到了可能,也有了位图的一个概念:就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的
三、位图的应用
- 快速查找某个数据是否在一个集合中
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记(bitmap)
- 内核中信号标志位
其实我们之前都讲过,大家也可以自行回顾一下
四、位图的使用
有三种定义方式,分别如下:
方式一:构造一个 16 位的位图,所有位都初始化为 0
bitset<16> bs1; // 0000000000000000
方式二:构造一个 16位 的位图,根据所给值初始化位图的前 n 位
bitset<16> bs2(0xfa5); // 0000111110100101
方式三:构造一个 16位 的位图,根据字符串中的 0 / 1 序列初始化位图的前 n 位
bitset<16> bs3(string("10111001")); // 0000000010111001
常用函数如下:
成员函数 | 功能 |
---|---|
set | 设置指定位或所有位 |
reset | 清空指定位或所有位 |
flip | 反转指定位或所有位 |
test | 获取指定位的状态 |
count | 获取被设置位的个数 |
size | 获取可以容纳的位的个数 |
any | 如果有任何一个位被设置则返回true |
none | 如果没有位被设置则返回true |
all | 如果所有位都被设置则返回true |
#include <iostream>
#include <bitset>
using namespace std;int main()
{bitset<8> bs;bs.set(2); // 设置第2位bs.set(4); // 设置第4位cout << bs << endl; // 00010100bs.flip(); // 反转所有位cout << bs << endl; //11101011cout << bs.count() << endl; // 6cout << bs.test(3) << endl; // 1bs.reset(0); // 清空第0位cout << bs << endl; // 11101010bs.flip(7); // 反转第7位cout << bs << endl; // 01101010cout << bs.size() << endl; // 8cout << bs.any() << endl; // 1bs.reset(); // 清空所有位cout << bs.none() << endl; // 1bs.set(); // 设置所有位cout << bs.all() << endl; // 1return 0;
}
成员函数 set、reset、flip 时,若指定了某一位则操作该位,若未指定位则操作所有位
bitset容器对 >>、<< 等运算符进行了重载,我们可以直接使用 >>、<< 运算符对 biset 容器定义出来的对象进行输入输出操作
#include <iostream>
#include <bitset>using namespace std;int main()
{bitset<8> bs;cin >> bs; // 10110cout << bs << endl; // 00010110return 0;
}
#include <iostream>
#include <string>
#include <bitset>
using namespace std;int main()
{bitset<8> bs1(string("10101010"));bitset<8> bs2(string("10101010"));bs1 >>= 1;cout << bs1 << endl; // 01010101bs2 |= bs1;cout << bs2 << endl; // 11111111return 0;
}
#include <iostream>
#include <string>
#include <bitset>
using namespace std;int main()
{bitset<8> bs1(string("10101010"));bitset<8> bs2(string("01010101"));cout << (bs1 & bs2) << endl; // 00000000cout << (bs1 | bs2) << endl; // 11111111cout << (bs1 ^ bs2) << endl; // 11111111return 0;
}
#include <iostream>
#include <string>
#include <bitset>
using namespace std;int main()
{bitset<8> bs(string("00110101"));cout << bs[0] << endl; // 1bs[0] = 0;cout << bs << endl; // 00110100return 0;
}
五、模拟实现
接口总览
namespace HQ
{//模拟实现位图template<size_t N>class bitset{public://构造函数bitset();//设置位void set(size_t pos);//清空位void reset(size_t pos);//反转位void flip(size_t pos);//获取位的状态bool test(size_t pos);//获取可以容纳的位的个数size_t size();//获取被设置位的个数size_t count();//判断位图中是否有位被设置bool any();//判断位图中是否全部位都没有被设置bool none();//判断位图中是否全部位都被设置bool all();private:vector<int> _bits; //位图};
}
我们把 bitset 放到我们自己的命名空间里面,防止与标准库里面的 bitset 发生冲突
构造函数
我们传给模板的参数 N 表示的是比特位数,创建的时候要开出来并且要全部设置为0,而一个整数一共有32个比特位,所以我们需要 N / 32 个整数,又因为 / 是向下取整,所以我们加个 1 保证比特位数量足够
// 构造函数
bitset()
{_bits.resize(N / 32 + 1, 0);
}
set、reset、flip、test
很明显这些都要位运算了,我们可以先想想怎么找到指定的一个比特位,显然得先定位到一个 int , i = N / 32 ,接下来就该思考怎么定位到这个int 32 个比特位中的哪一个,显然是 N % 32
//设置位
void set(size_t pos)
{assert(pos < N);// 算出 pos 映射的位在第 i 个整数的第 j 个位int i = pos / 32;int j = pos % 32;_bits[i] |= (1 << j); // 将该位设置为 1(不影响其他位)
}// 清空位
void reset(size_t pos)
{assert(pos < N);// 算出 pos 映射的位在第 i 个整数的第 j 个位int i = pos / 32;int j = pos % 32;_bits[i] &= (~(1 << j)); // 将该位设置为 0(不影响其他位)
}// 反转位
void flip(size_t pos)
{assert(pos < N);// 算出 pos 映射的位在第 i 个整数的第 j 个位int i = pos / 32;int j = pos % 32;_bits[i] ^= (1 << j); // 将该进行反转(不影响其他位)
}// 获取位的状态
bool test(size_t pos)
{assert(pos < N);//算出 pos 映射的位在第 i 个整数的第 j 个位int i = pos / 32;int j = pos % 32;return _bits[i] & (1 << j)
}
size、count
很显然 size() 直接返回位数即可,而 count() 本质上也就是遍历二进制中1的个数,其实应该也早就接触过了,将 n 与 n - 1 进行与运算得到新的n就是去除二进制中最后一个1的过程,这个时候我们再判断一下 n 是否为 0 ,如果不是继续循环,无非就是加个计数器的问题
// 获取可以容纳的位的个数
size_t size()
{return N;
}// 获取被设置位的个数
size_t count()
{size_t count = 0;// 将每个整数中 1 的个数累加起来for (auto e : _bits){int num = e;// 计算整数 num 中 1 的个数while (num){num = num & (num - 1);count++;}}return count; // 位图中 1 的个数,即被设置位的个数
}
any、none、all
显然 any 遍历数组每一个整数就好了,注意到最后虽然我们可能多判断了一些位,但是因为在初始化的过程中它们全部被设置成为0,显然不造成影响,所以any也就很自然而然的写出来了,none 很显然就是 any 取反,而 all 函数就要考虑之前 N / 32 + 1的 +1 带来的问题了
//判断位图中是否有位被设置
bool any()
{// 遍历每个整数for (auto e : _bits){if (e) // 该整数中有位被设置return true;}return false; //全部整数都是 0 ,则没有位被设置过
}// 判断位图中是否全部位都没有被设置
bool none()
{return !any();
}//判断位图中是否全部位都被设置
bool all()
{size_t n = _bits.size(); // 整数个数size_t full = N / 32; // 完整使用的整数个数// 检查前full个整数(应该全为1)for (size_t i = 0; i < full; i++){if (_bits[i] != ~0u) // 不等于全1return false;}// 检查最后一个整数size_t rem = N % 32; // 剩余位数if (rem == 0){// 没有剩余位,最后一个整数也应该全1return _bits[full] == ~0u;}else{// 只检查前rem位uint32_t mask = (1u << rem) - 1;return (_bits[full] & mask) == mask;}
}
总结
好像不算太难,接下来我们要学的布隆过滤器才算是一个重点!