位图bitset与位运算
文章目录
前言
本文将探讨C++中的位操作,可以用来进行高效的数据处理和存储。通过介绍bitset
和two_bitset
类模板,我们将展示如何利用C++的位操作来解决实际问题,从而提高代码的性能和可读性。
一、基本的位运算
在C++中,位运算是对整数类型数据在位级别上进行操作的运算符。这些运算符包括:
- 位与(AND)运算符 :&
当两个操作数对应的位都为1时,结果位为1,否则为0。
// 位与(AND)运算符 &
int a = 5; // 二进制表示: 0101
int b = 9; // 二进制表示: 1001
int c = a & b; // 结果为 1 (0001)
- 位或(OR)运算符 :|
当两个操作数对应的位中至少有一个为1时,结果位为1,否则为0。
// 位或(OR)运算符 |
c = a | b; // 结果为 13 (1101)
- 位异或(XOR)运算符:^
当两个操作数对应的位只有一个为1时,结果位为1,如果两个都为0或都为1,则结果位为0。
// 位异或(XOR)运算符 ^
c = a ^ b; // 结果为 12 (1100)
- 位非(NOT)运算:~
对操作数的每一位进行取反操作,即1变0,0变1。
// 位非(NOT)运算符 ~
c = ~a; // 结果为 -6 (取决于系统的整数表示方法)
- 左移(Left Shift)运算符:<<
将操作数的所有位向左移动指定的位数,右边空出的位用0填充。
// 左移(Left Shift)运算符 <<
c = a << 1; // 结果为 10 (1010)
- 右移(Right Shift)运算符:>>
将操作数的所有位向右移动指定的位数,左边空出的位用0或符号位填充(取决于数据类型)。
// 右移(Right Shift)运算符 >>
c = a >> 1; // 结果为 2 (0010)
二、位图 (bitset)
bitset 类模板框架
template<size_t N>
class bitset
{
typedef bitset<N> Self;
public:
bitset();
public:
Self& set(size_t pos); // 将pos值所在比特位设为1
Self& reset(size_t pos); // 将pos值所在比特位清零
bool test(size_t pos); // 判断pos值是否在bitset中
private:
std::vector<int> _bits;
};
私有成员变量 _bits
_bits
是一个整数向量,用于存储位集合的实际位值。
private:
std::vector<int> _bits;
构造函数
bitset()
{
_bits.resize(N/32 + 1, 0);
}
由于 int 类型占 4 字节,1字节占 8 bit ,所以 vector 容器 _bits
需要设置的大小为 N/(4*8)
,但是由于C++中正数如果不被整除会存在向下取整
的问题,所以最好在设定空间时在原基础上 +1。
set 成员函数
set
函数将指定位置的位设置为1。
Self& set(size_t pos)
{
assert(pos <= N);
size_t i = pos / 32;
size_t j = pos % 32;
_bits.at(i) |= (1 << j);
return *this;
}
reset 成员函数
reset
函数将指定位置的位重置为0。
Self& reset(size_t pos)
{
assert(pos <= N);
size_t i = pos / 32;
size_t j = pos % 32;
_bits.at(i) &= ~(1 << j);
return *this;
}
test 成员函数
test
函数检查指定位置的位是否为1。
bool test(size_t pos)
{
assert(pos <= N);
size_t i = pos / 32;
size_t j = pos % 32;
return _bits.at(i) & (1 << j);
}
bitset 常用场景
当给定数亿级别个的整数,另问快速判断某个数是否在给定数集中?
// 快速判断一个整数是否在数组中
void test1()
{
bit_set::bitset<100> bs;
int arr[] = { 2, 6, 3, 4, 7, 6, 8, 3, 6, 1, 6 };
for (auto e : arr)
{
bs.set(e);
}
for (int i = 0; i < 10; i++)
{
if (bs.test(i))
{
cout << i << " -> " << "存在" << endl;
}
else
{
cout << i << " -> " << "不存在" << endl;
}
}
cout << "***************************" << endl;
bs.reset(3);
bs.reset(4);
for (int i = 0; i < 10; i++)
{
if (bs.test(i))
{
cout << i << " -> " << "存在" << endl;
}
else
{
cout << i << " -> " << "不存在" << endl;
}
}
}
给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
void test3()
{
int arr[] = { 2, 6, 3, 4, 7, 6, 8, 3, 6, 1, 6 };
int arr2[] = { 9, 1, 6, 5, 3, 3 };
bit_set::bitset<10> bs1;
bit_set::bitset<10> bs2;
for (auto e : arr)
{
bs1.set(e);
}
for (auto e : arr2)
{
bs2.set(e);
}
for (int i = 0; i < 10; i++)
{
if (bs1.test(i) && bs2.test(i))
{
cout << i << endl;
}
}
}
三、位图衍生 (two_bitset)
two_bitset 类模板框架
two_bitset
类模板是bitset
的扩展,它使用两个bitset
来跟踪每个位出现的次数。
template<size_t N>
class two_bitset
{
public:
two_bitset& set(size_t pos);
bool test_appear_not_upOf2(size_t pos);
private:
bitset<N> bs1;
bitset<N> bs2;
};
私有成员变量 _bs1, _bs2
bs1
和bs2
是两个bitset
实例,用于跟踪每个位的出现次数。
private:
bitset<N> bs1;
bitset<N> bs2;
构造函数
由于成员变量 bs1, bs2 都是自定义类型,所以类在调用默认构造函数时会回调自定义成员变量的构造函数,故在此使用默认构造函数。
set 成员函数
set
函数用于增加位的出现次数。
two_bitset& set(size_t pos)
{
// 00 表示未出现
// 01 表示出现一次
// 10 表示出现两次
// 11 表示出现三次及以上
// 00->01 => bs1:0 bs2:1
if (bs1.test(pos) == false && bs2.test(pos) == false)
{
bs2.set(pos);
}
else if (bs1.test(pos) == false && bs2.test(pos) == true) // 01->10 => bs1:1 bs2:0
{
bs1.set(pos);
bs2.reset(pos);
}
else if (bs1.test(pos) == true && bs2.test(pos) == false) // 10->11 => bs1:1 bs2:1
{
bs2.set(pos);
}
return *this;
}
test 成员函数
test 类型函数为了适应不同的场景而存在,完全允许通过特定场景设计特定的 test 函数。所以这里暂且搁置,先看场景,再根据需求回头构建我们的 test 函数。
two_bitset 应用场景
位图应用变形:
1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
- 此时根据场景我们即可设定属于我们的
test 函数
:
test_appear_not_upOf2
函数用于检查位的出现次数是否不超过2次。
bool test_appear_not_upOf2(size_t pos)
{
if (bs1.test(pos) == true && bs2.test(pos) == true)
{
return false;
}
return true; // 出现过两次及两次以下
}
自此,我们即可利用自己实现的 set 函数和 test 函数对该问题进行求解。
示例代码:
// 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
void test2()
{
bit_set::two_bitset<10> tbs;
int arr[] = { 2, 6, 3, 4, 7, 6, 8, 3, 6, 1, 7, 3, 7 };
for (auto e : arr)
{
tbs.set(e);
}
for (int i = 0; i < 10; i++)
{
if (tbs.test_appear_not_upOf2(i))
{
cout << i << endl;
}
else
{
cout << i << " -> occured less than 2" << endl;
}
}
}
总结
通过本文的介绍,我们可以看到位操作在C++中的强大之处。bitset
和two_bitset
类模板为处理大量数据提供了一种高效且直观的方法。无论是在内存受限的环境中处理大规模数据集,还是在日常编程中寻找优化的机会,位操作都是一个不可或缺的工具。