位图概念
曾经有这样一个面试题,如果给你40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。【腾讯】方法如下:
- 遍历,时间复杂度O(N)。
- 排序(O(NlogN)),利用二分查找: logN。
- 位图解决。
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如:
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
bitset大体框架
- bitset能实现对数字的位的操作,同时也能通过类似于数组的下标来访问各个位的数值,以执行相应的操作。模拟bitset就是用一个普通的数组来存储数据以达到模拟的目的。
- 如果我们以一个整型作为比特位的容器,那么如果要求0~N范围的比特位,就需要有N/32+1 个整型来容纳这些比特位,同理如果以char为容器,则需要N/8+1个char来容纳N个比特位。这里我们用vector数组作为底层容纳比特位的容器,且其存储的数据类型为char。
#pragma once
#include<iostream>
#include<vector>
using namespace std;
namespace flash
{
//N个比特位的位图
template<size_t N>
class bitset
{
public:
//构造函数
bitset()
{
//+1保证足够比特位,最多浪费8个比特位
_bits.resize(N / 8 + 1, 0);
}
//把x映射的位标记成1
void set(size_t x)
{
//x映射的比特位在第几个char对象
size_t i = x / 8;
//x在char第几个比特位
size_t j = x % 8;
//利用按位或|把第j位标记成1
_bits[i] |= (1 << j);
}
//把x映射的位标记成0
void reset(size_t x)
{
//x映射的比特位在第几个char对象
size_t i = x / 8;
//x在char第几个比特位
size_t j = x % 8;
//将1左移 j 位再整体反转后与第 i 个char进行与运算
_bits[i] &= (~(1 << j));
}
//判断指定比特位x的状态是否为1
bool test(size_t x)
{
//x映射的比特位在第几个char对象
size_t i = x / 8;
//x在char第几个比特位
size_t j = x % 8;
//将1左移 j 位后与第 i 个char类型进行与运算得出结果
return _bits[i] & (1 << j);
}
//翻转指定位x
void flip(size_t x)
{
//x映射的比特位在第几个char对象
size_t i = x / 8;
//x在char第几个比特位
size_t j = x % 8;
//将1左移 j 位后与第 i 个char进行按位异或运算^即可。
_bits[i] ^= (1 << j);
}
//获取位图中可以容纳位N的个数
size_t size()
{
return N;
}
//统计set中1的位数
size_t count()
{
size_t count = 0;
for (auto e : _bits)
{
int n = e;
while (n)
{
int m = n - 1;
n = n & m;
count++;
}
}
return count;
}
//判断所有比特位若无置为1,返回true
bool none()
{
//遍历每个char
for (auto e : _bits)
{
if (e != 0)//说明有位被置为1,返回false
return false;
}
return true;//说明全为0,返回true
}
//判断位图中是否有位被置为1,若有则返回true
bool any()
{
return !none();
}
//全部NUM个bit位被set返回true
bool all()
{
size_t size = _bits.size();
//先检查前N-1个char
for (size_t i = 0; i < size - 1; i++)
{
if (~_bits[i] != 0)//取反应该为0,否则取反之前不全为1,返回false
return false;
}
//再检查最后一个char的前 N%8 个位
for (size_t j = 0; j < N % 8; j++)
{
if ((_bits[size - 1] & (1 << j)) == 0)//和test的原理一致
return false;
}
return true;
}
private:
vector<char> _bits;//位图
};
}
成员函数
构造函数
- 一个char类型有8个bit位,所以理想状态下N个比特位的位图就需要用到 N/8 个字节,但仅限于N是8的整数倍,如果N位10,那么计算下来就会少2个比特位,因此综合考虑,我们给出 N/8+1 个字节,这样算下来,所需的N个比特位绝对都能访问到,最多可以整除的情况下浪费了8个比特位(1字节)。
而构造函数,我们只需要对这所有的比特位(N/8+1)个字节的大小初始化为0即可。
//构造函数
bitset()
{
//+1保证足够比特位,最多浪费8个比特位
_bits.resize( N/8+1, 0);
}
set函数
set的作用是把x映射的位置标记成1,实现规则如下:
- 通过x / 8计算x在第i个char类型 。
- 通过x % 8计算x在char第j个比特位 。
- 利用按位或 | 把第i个char中的第j个比特位置为1。
//把x映射的位标记成1
void set(size_t x)
{
//x映射的比特位在第几个char对象
size_t i = x / 8;
//x在char第几个比特位
size_t j = x % 8;
//利用按位或|把第j位标记成1
_bits[i] |= (1 << j);
}
reset函数
reset的作用是把把x映射的位标记成0,实现规则如下:
- 通过 x/8 计算x在第i个char类型 。
- 通过 x%8 计算x在char第j个比特位 。
- 将1左移 j 位再整体反转后与第 i 个char类型进行与运算即可。
//把x映射的位标记成0
void reset(size_t x)
{
//x映射的比特位在第几个char对象
size_t i = x / 8;
//x在char第几个比特位
size_t j = x % 8;
//将1左移 j 位再整体反转后与第 i 个char进行与运算
_bits[i] &= (~(1 << j));
}
test函数
test的作用是判断指定比特位x的状态是否为1,实现规则如下:
- 通过x / 8计算x在第i个char类型 。
- 通过x % 8计算x在char第j个比特位 。
- 将1左移 j 位后与第 i个char类型进行与运算得出结果 。
- 若结果非0,则该位被设置,否则该位未被设置。
//判断指定比特位x的状态是否为1
bool test(size_t x)
{
//x映射的比特位在第几个char对象
size_t i = x / 8;
//x在char第几个比特位
size_t j = x % 8;
//将1左移 j 位后与第 i 个char类型进行与运算得出结果
return _bits[i] & (1 << j);
}
flip函数
flip的作用是用于翻转指定位,若指定位为0,翻转后为1,若指定位为1,反转后为0,实现规则如下:
- 通过x / 8计算x在第i个char类型。
- 通过x % 8计算x在char第j个比特位。
- 将1左移 j 位后与第 i 个char进行按位异或运算^即可。
//翻转指定位x
void flip(size_t x)
{
//x映射的比特位在第几个char对象
size_t i = x / 8;
//x在char第几个比特位
size_t j = x % 8;
//将1左移 j 位后与第 i 个char进行按位异或运算^即可。
_bits[i] ^= (1 << j);
}
count函数
count的作用是统计位图中被设计为1的个数,实现规则如下:
- n = n & (n-1) => 消去n的二进制数中最右边的1。
- n不为零继续执行第一步。
- 执行了几次说明n中有多少个1。
//统计set中1的个数
size_t count()
{
size_t count = 0;
for (auto e : _bits)
{
int n = e;
while (n)
{
n = n & (n - 1);
count++;
}
}
return count;
}
size函数
size的作用是获取位图中可以容纳位N的个数。
//获取位图中可以容纳位N的个数
size_t size()
{
return N;
}
none any all
- none:none的作用是遍历每一个char,如果全为0,则none返回true。
//判断所有比特位若无置为1,返回true
bool none()
{
//遍历每个char
for (auto e : _bits)
{
if (e != 0)//说明有位被置为1,返回false
return false;
}
return true;//说明全为0,返回true
}
- any:any的作用判断位图中是否有位被置为1,若有则返回true,这其实和none的作用刚好相反,因此我们直接复用none即可。
//判断位图中是否有位被置为1,若有则返回true
bool any()
{
return !none();
}
-
all:all的作用是判断位图中是否所有的位都被置为1,注意这里的特殊性,先前开辟空间时我们为了避免出现N/8不能整除的情况,特地在resize时多开了一个char类型(8比特)的空间,这8比特里面只有前N%8个比特位才是有效的(剩下的都是多开的空间),因此all函数需要分情况讨论:
- 先检查前n-1个char的二进制是否为全1。
- 再检查最后一个char的前N%8个比特位是否为全1。
//全部NUM个bit位被set返回true
bool all()
{
size_t size = _bits.size();
//先检查前N-1个char
for (size_t i = 0; i < size - 1; i++)
{
if (~_bits[i] != 0)//取反应该为0,否则取反之前不全为1,返回false
return false;
}
//再检查最后一个char的前 N%8 个位
for (size_t j = 0; j < N % 8; j++)
{
if ((_bits[size - 1] & (1 << j)) == 0)//和test的原理一致
return false;
}
return true;
}