bitset位集合
困难5用bitset位集合高效管理“是/否”状态
你是不是经常遇到这样的场景:记录一周哪几天要早起跑步、游戏里哪些关卡已经解锁、班级里谁今天没交作业……这些“是/否”的问题,如果用 bool 数组记录,每个 bool 占1个字节(8位),其实只用了其中1位,太浪费了。C++ 的 bitset 就像一个小巧的“位袋子”,把多个“是/否”信息压缩到位级别,1个字节就能装8个开关,让你既省内存又能高速批量处理。
什么是位集合?
想象你有一个长方形的盒子,里面整齐地排着8个小格子(代表8个二进制位),每个格子可以涂成黑色(1)或白色(0)。这个盒子就是 bitset,它能帮你记录8个“是/否”状态。比如用第0位表示周一要跑步,第1位表示周二……这样7天的计划只用1个字节就装下了,而 bool 数组需要7个字节。
bitset 的位数在编译时就固定了(比如 bitset<8> 就是8位),你不能在程序运行中随意改变它的大小。但好处是它内部用最紧凑的方式存储,并且支持各种位运算(与、或、异或、移位),让你一行代码就能同时处理所有位。
创建和初始化 bitset
使用 bitset 前要包含头文件 <bitset>。它可以有几种创建方式:
- 默认全0:
bitset<8> flags;创建一个8位集合,所有位初始为0。 - 用二进制字符串初始化:
bitset<8> week("10101010");字符串长度必须等于位数,从左到右对应高位到低位(最左边是最高位)。 - 用无符号整数初始化:
bitset<8> num(170);(170的二进制是10101010) 整数会转换成对应的二进制位。
注意:下标从最右边开始,第0位是最低位(最右边)。当你用 cout 输出时,显示的顺序是高位在左,低位在右,和日常书写习惯一致。
常用操作:像数组一样访问和修改
bitset 支持用 [] 操作符像数组一样访问每一位。因为位是连续的,你可以直接赋值、读取:
#include <iostream>
#include <bitset>
using namespace std;
int main() {
bitset<8> flags; // 8位,初始全0
// 设置周一到周四要跑步
flags[0] = 1; // 周一(第0位)
flags[1] = 1; // 周二(第1位)
flags[2] = 1; // 周三(第2位)
flags[3] = 1; // 周四(第3位)
cout << "flags = " << flags << endl; // 输出: 00001111 (第0~3位是1)
cout << "周五是否要跑步?" << flags[4] << endl; // 0(不需要)
// 翻转某一位
flags.flip(3); // 把第3位(周四)从1变成0
cout << "修改后flags = " << flags << endl; // 00000111
// 统计1的个数
cout << "一周有" << flags.count() << "天要跑步" << endl; // 3
return 0;
}
位运算:同时处理所有状态
这是 bitset 最强大的地方——你可以对两个大小相同的 bitset 进行位运算,结果也是一个 bitset,每一位独立运算。这就像同时给全班同学批改作业,而不是一个个来。
生活中的例子:老师有一份“今天交作业名单”和一份“今天迟到名单”,用两个 bitset 表示(1表示交作业/迟到),然后可以用位运算找出“既交了作业又迟到的同学”(与运算)、“交了作业或迟到的同学”(或运算)等。
#include <iostream>
#include <bitset>
using namespace std;
int main() {
// 假设班级有8位同学,编号0~7
bitset<8> homework; // 交作业情况
bitset<8> late; // 迟到情况
homework[0] = 1; // 同学0交了作业
homework[2] = 1;
homework[4] = 1;
homework[7] = 1;
late[1] = 1; // 同学1迟到了
late[2] = 1;
late[5] = 1;
cout << "交作业: " << homework << endl; // 10010101
cout << "迟到的: " << late << endl; // 01001010
// 找出既交作业又迟到的同学(位与)
bitset<8> both = homework & late;
cout << "既交作业又迟到: " << both << endl; // 00010100 → 只有同学2
cout << "人数: " << both.count() << endl; // 1
// 找出交作业或迟到的同学(位或)
bitset<8> either = homework | late;
cout << "交作业或迟到: " << either << endl; // 11011111
cout << "人数: " << either.count() << endl; // 6
// 找出交了作业但没迟到的同学(位与之后取反)
bitset<8> only_homework = homework & (~late);
cout << "只交作业没迟到: " << only_homework << endl; // 10010101 & 10110101 = 10010101? 注意~late
// 实际: ~late = 10110101, homework & ~late = 10010101 & 10110101 = 10010101(同学0、4、7)
return 0;
}
移位操作:像移动队列一样移动位
bitset 支持左移 << 和右移 >>,效果和整数的移位一样。这在处理需要整体移动的场景中有用,比如模拟一个滑动窗口。
bitset<8> a("11001100");
cout << "a << 2 = " << (a << 2) << endl; // 00110000 (高位移出,低位补0)
cout << "a >> 3 = " << (a >> 3) << endl; // 00011001 (低位移出,高位补0)
其他好用的小功能
.any():检查是否有任何一位是1(返回true/false).none():检查是否所有位都是0.all():检查是否所有位都是1(C++11起支持).set():将所有位设为1(可以传入参数pos单独设置某一位).reset():将所有位设为0(可以传入参数pos单独复位某一位).flip():翻转所有位(可以传入参数pos单独翻转某一位).test(pos):检查第pos位是否为1,如果越界会抛出out_of_range异常(而[]不检查越界).to_ulong()/.to_ullong():将 bitset 转换成unsigned long/unsigned long long(如果位数超过会报错).to_string():转换成二进制字符串
常见错误与避坑指南
-
下标从0开始,且0是最低位
新手常把bitset<8>("10101010")的第0位当作最左边的1,其实第0位是最右边的0。输出bitset时高位在左,所以"10101010"的第0位其实是0(最右边)。建议画个位索引图避免混淆。 -
bitset 大小在编译时固定,不能改变
如果你需要动态大小的位集合,比如要根据输入决定位数,请使用vector<bool>(虽然它不完全符合标准容器的要求,但可以动态调整)。bitset适合固定位数的场景。 -
位运算必须两个 bitset 类型相同(位数相同)
不同大小的 bitset 不能直接进行&、|等运算,编译器会报错。 -
to_ulong() 或 to_ullong() 时可能溢出
如果 bitset 的位数超过unsigned long或unsigned long long的位数(通常64位),转换会抛出overflow_error异常。这时可以用to_string()先转成字符串。 -
不要试图对 bitset 进行逻辑运算(&&, ||)
bitset并没有定义&&和||操作符。如果你要判断“所有位都是1”之类的条件,请用.all()方法。
完整示例:用 bitset 管理游戏关卡的解锁情况
假设一个游戏有16个关卡(编号0~15),每通过一关就解锁下一关。我们用 bitset<16> 记录解锁状态,并演示如何判断某关是否能玩、如何一次性解锁多关。
#include <iostream>
#include <bitset>
using namespace std;
int main() {
// 16个关卡,初始只解锁第0关(新手关)
bitset<16> unlocked; // 初始全0
unlocked[0] = 1; // 第0关解锁
cout << "初始解锁状态: " << unlocked << endl;
cout << "已解锁关卡数: " << unlocked.count() << endl;
// 玩家通过了第0关,解锁第1关和第2关(一次性解锁)
unlocked.set(1); // 解锁第1关
unlocked.set(2); // 解锁第2关
cout << "通过第0关后: " << unlocked << endl;
// 检查第3关是否解锁
if (unlocked.test(3)) {
cout << "可以玩第3关" << endl;
} else {
cout << "第3关未解锁" << endl;
}
// 假设玩家通关了第1关和第2关,解锁第3、4、5关
unlocked |= bitset<16>("0000000000111000"); // 位或运算,解锁3,4,5
cout << "解锁更多关卡: " << unlocked << endl;
// 判断是否所有关卡都解锁了
if (unlocked.all()) {
cout << "恭喜!所有关卡解锁!" << endl;
} else {
cout << "还有" << (16 - unlocked.count()) << "关未解锁" << endl;
}
// 翻转已解锁状态(用于某些彩蛋模式)
unlocked.flip();
cout << "翻转后状态(彩蛋模式): " << unlocked << endl;
return 0;
}
输出示例:
初始解锁状态: 0000000000000001
已解锁关卡数: 1
通过第0关后: 0000000000000111
第3关未解锁
解锁更多关卡: 0000000000111111
还有10关未解锁
翻转后状态(彩蛋模式): 1111111111000000
相关知识点延伸
如果你还想了解更多位操作相关的知识,可以看看:
vector<bool>:能动态调整大小的位集合,但它的内部实现是一个特化版本,访问方式稍有不同(返回的不是真正的引用),适合需要动态位数的场景。- 位域(Bit-field):C++结构体中可以定义位域,用于精确控制每个成员占用的位数,适合在结构体中存放多个小标志。
- 位运算基础:
&(与)、|(或)、^(异或)、~(取反)、<<(左移)、>>(右移)是计算机底层的强大工具,理解它们能帮你写出更高效的代码。 - std::bitset 的局限性:当位数超过几百甚至几千时,
bitset仍然很高效(内部用多个整数实现)。但如果需要频繁插入/删除位,可以考虑std::vector<bool>或自建数据结构。
bitset 是一个小而精致的工具,在你需要管理多个“是/否”状态的场景中,它能帮你写出清晰、快速、省内存的代码。现在就试试用它记录下你的运动计划吧!
例题精讲
下列关于C++标准库中bitset的描述,哪一项是正确的?
对于bitset<8> bs("10101010"); 执行bs.count()后返回的值是多少?
bitset<32> bs; bs.set(40); 这段代码可以正常执行,将第40位(从0开始)设置为1。
以下代码使用bitset判断一个无符号整数x的二进制表示中是否有奇数个1。请补全空白处的表达式。
#include <bitset>
#include <iostream>
using namespace std;
bool hasOddParity(unsigned int x) {
bitset<sizeof(unsigned int)*8> bs(x);
return ___;
}下面代码用两个bitset表示两个集合,求它们的并集并输出。请补全空白处的代码。
#include <bitset>
#include <iostream>
using namespace std;
int main() {
bitset<10> setA("1010101010"); // 偶数位为1
bitset<10> setB("0101010101"); // 奇数位为1
bitset<10> unionSet = setA ___ setB;
cout << unionSet << endl; // 期望输出:1111111111
return 0;
}