本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~
一.位图
定义与原理
- 定义:位图是一种用每一位来存放某种状态的数据结构,适用于大规模数据但数据状态又不是很多的情况,通常用于判断某个数据存不存在。
- 原理:将数据集合中的每个元素映射到一个位上,位数通常等于数据集合的大小,每个位的状态(0或1)表示对应元素是否存在。
相关操作
- 置位:将指定位置的位设置为1,表示对应元素存在或某种状态为真。
- 复位:将指定位置的位设置为0,表示对应元素不存在或某种状态为假。
- 访问:检查指定位置的位的值,以判断对应元素是否存在或某种状态是否为真。
- 扩容:当位图容量不足时,需要进行扩容操作,类似于向量的扩容,通常采用加倍策略重新分配内存并转移原数据。
应用场景
- 数据去重:在处理大量数据时,可以快速去除重复元素,将所有元素添加到位图中,然后遍历位图即可得到去重后的结果。
- 数据统计:快速统计一个数据集合中元素的数量,通过遍历位图,将所有为1的位相加即可得到元素的数量。
- 数据查找:快速检查一个元素是否存在于数据集合中,通过调用相应的访问方法即可得到结果。
- 数据排序:对有序数据进行排序,通过遍历有序数据,将每个元素添加到位图中,然后按照位图的顺序输出即可得到排序后的结果。
- 权限管理:在系统中用于存储用户的权限信息,每个权限对应位图中的一位,通过位运算可以方便地进行权限的设置、检查和组合。
注意事项
- 内存占用:虽然位图在存储大量数据时可以节省空间,但如果数据范围过大,可能需要较大的内存来存储位图,需要注意内存的使用情况。
- 数据范围:位图适用于数据状态有限且数据量较大的情况,如果数据状态较多或数据量较小,可能不适合使用位图。
- 位运算效率:位运算在大多数情况下效率较高,但在某些情况下可能需要注意编译器的优化和数据的对齐等问题,以确保位运算的效率。
算法原理
-
整体思路
- 这个
Bitset
类实现了一个简单的位图数据结构。位图用于表示一组整数的存在状态,通过位运算来高效地实现添加、删除、反转和检查元素是否存在等操作。同时,使用HashSet
进行对比测试,以验证Bitset
类功能的正确性。
- 这个
-
具体原理
- 位图存储原理
- 在
Bitset
类中,使用一个int
数组set
来存储位图数据。由于每个int
类型可以存储32位,所以对于表示范围为0
到n - 1
的位图,计算所需的int
数组大小为(n + 31)/32
。这是通过将总位数(n
位)除以32并向上取整得到的。
- 在
- 添加操作(
add
方法)- 当调用
add
方法添加数字num
时,首先计算num
对应的位在set
数组中的哪个整数中,通过num/32
得到所在的整数索引。然后,将1
左移num % 32
位(1 << (num % 32)
),这将得到一个只有对应位为1
,其余位为0
的数。最后,使用位或操作(|
)将这个数与set[num/32]
进行运算,将该位置为1
,表示num
存在于位图中。
- 当调用
- 删除操作(
remove
方法)- 对于
remove
方法,同样先确定num
对应的位在set
数组中的位置。然后,取反1
左移num % 32
位后的数(~(1 << (num % 32))
),这个数除了对应位为0
,其余位为1
。最后,使用位与操作(&
)将这个数与set[num/32]
进行运算,将该位置为0
,表示num
从位图中移除。
- 对于
- 反转操作(
reverse
方法)- 在
reverse
方法中,先找到num
对应的位在set
数组中的位置。然后,使用位异或操作(^
)将1
左移num % 32
位后的数与set[num/32]
进行运算。位异或操作的特性是,如果对应位不同则结果为1
,相同则结果为0
,所以这个操作会反转该位的状态。
- 在
- 检查操作(
contains
方法)- 对于
contains
方法,先计算num
对应的位在set
数组中的位置。然后,将set[num/32]
右移num % 32
位(set[num/32] >> (num % 32)
),再与1
进行位与操作(&
)。如果结果为1
,则表示num
存在于位图中,返回true
;否则,返回false
。
- 对于
- 对数器测试原理(
main
方法)- 在
main
方法中,创建了一个Bitset
实例和一个HashSet
实例,用于对比测试。通过大量(testTimes
次)的随机操作(添加、删除、反转),每次操作都同时对Bitset
和HashSet
进行相同的操作。最后,遍历位图表示的所有数字(0
到n - 1
),检查Bitset
和HashSet
对于每个数字的存在状态是否一致,以此来验证Bitset
类功能的正确性。
- 在
- 位图存储原理
代码实现
import java.util.HashSet;
// 位图的实现
// 这个类Bitset是一个简单的位图数据结构的实现,用于表示一组整数的存在状态。
// 它提供了添加、删除、反转和检查元素是否存在等功能。
// Bitset(int size):构造函数,用于创建一个指定大小的位图
// void add(int num):将指定的数字添加到位图中,表示该数字存在
// void remove(int num):将指定的数字从位图中移除,表示该数字不存在
// void reverse(int num):反转指定数字在位图中的状态,如果存在则变为不存在,反之亦然
// boolean contains(int num):检查指定的数字是否存在于位图中
public class Code01_Bitset {
// 位图的实现
// 使用时num不要超过初始化的大小
public static class Bitset {
// 用于存储位图数据的数组
// 每个整数在位图中由数组中的一个或多个位表示
public int[] set;
// Bitset的构造函数,接受一个整数n,表示位图能够表示的数字范围(0到n - 1)
public Bitset(int n) {
// a/b如果结果想向上取整,可以写成 : (a + b - 1)/b
// 前提是a和b都是非负数
// 这里计算需要多少个整数来存储位图数据
// 因为每个int类型可以存储32位,所以将总位数除以32向上取整得到所需的int数组大小
set = new int[(n + 31) / 32];
}
// 将指定的数字num添加到位图中
// 操作是通过位运算实现的
public void add(int num) {
// 计算num对应的位在set数组中的哪个整数中
// num / 32得到所在的整数索引
// 然后使用位或操作(|)将该位置为1,表示num存在
// 1 << (num % 32) 将1左移num % 32位,对应到正确的位上
set[num / 32] |= 1 << (num % 32);
}
// 将指定的数字num从位图中移除
public void remove(int num) {
// 同样先计算num对应的位在set数组中的位置
// 使用位与操作(&)和取反操作(~)将该位置为0,表示num不存在
// ~(1 << (num % 32)) 取反后的数,与原来的数进行位与操作,将对应的位清零
set[num / 32] &= ~(1 << (num % 32));
}
// 反转指定数字num在位图中的状态
public void reverse(int num) {
// 计算num对应的位在set数组中的位置
// 使用位异或操作(^)反转该位的状态
// 如果该位原来是0则变为1,原来是1则变为0
set[num / 32] ^= 1 << (num % 32);
}
// 检查指定的数字num是否存在于位图中
public boolean contains(int num) {
// 计算num对应的位在set数组中的位置
// 通过右移操作(>>)和位与操作(&)判断该位是否为1
// 如果为1则表示num存在,返回true,否则返回false
return ((set[num / 32] >> (num % 32)) & 1) == 1;
}
}
// 对数器测试
// 这个main方法用于测试Bitset类的功能是否正确
// 通过与HashSet进行对比测试,在大量随机操作后检查两者结果是否一致
public static void main(String[] args) {
// 定义位图能够表示的数字范围的上限,这里是0到999
int n = 1000;
// 定义测试的次数
int testTimes = 10000;
System.out.println("测试开始");
// 实现的位图结构
Bitset bitSet = new Bitset(n);
// 直接用HashSet做对比测试
HashSet<Integer> hashSet = new HashSet<>();
System.out.println("调用阶段开始");
for (int i = 0; i < testTimes; i++) {
// 生成一个0到1之间的随机数
double decide = Math.random();
// 生成一个0到n - 1之间的随机数,等概率得到
int number = (int) (Math.random() * n);
if (decide < 0.333) {
// 以1/3的概率执行添加操作
bitSet.add(number);
hashSet.add(number);
} else if (decide < 0.666) {
// 以1/3的概率执行删除操作
bitSet.remove(number);
hashSet.remove(number);
} else {
// 以1/3的概率执行反转操作
bitSet.reverse(number);
if (hashSet.contains(number)) {
hashSet.remove(number);
} else {
hashSet.add(number);
}
}
}
System.out.println("调用阶段结束");
System.out.println("验证阶段开始");
for (int i = 0; i < n; i++) {
// 检查位图和HashSet对于每个数字的存在状态是否一致
if (bitSet.contains(i)!= hashSet.contains(i)) {
System.out.println("出错了!");
}
}
System.out.println("验证阶段结束");
System.out.println("测试结束");
}
}
二.设计位集
题目:设计位集
算法原理
- 整体思路
- 这个
Bitset
类实现了一个位图数据结构,支持多种操作,如设置特定位为1或0、翻转所有位、检查所有位是否为1、检查是否至少有一位为1、计算1的数量以及将位图状态转换为字符串表示等。通过使用一个int
数组来存储位图数据,并利用位运算来高效地实现各种操作。
- 这个
- 具体原理
- 初始化(构造函数)
- 在构造函数中,创建一个
int
数组set
来存储位图数据,数组大小为(n + 31)/32
,其中n
是位图的位数。同时初始化一些变量,如size
表示位图的大小,zeros
表示初始状态下为0的位数(初始为n
),ones
表示初始状态下为1的位数(初始为0),reverse
表示位图是否处于翻转状态(初始为false
)。
- 在构造函数中,创建一个
- 设置位为1(
fix
方法)- 首先计算要操作的位在
int
数组中的索引index
(i/32
)和在该int
中的位偏移bit
(i%32
)。 - 如果
reverse
为false
(位图正常状态):- 检查该位是否为0(通过
(set[index]&(1 << bit)) == 0
),如果是,则将zeros
减1,ones
加1,并使用位或操作(|
)将该位置为1(set[index]|=(1 << bit)
)。
- 检查该位是否为0(通过
- 如果
reverse
为true
(位图翻转状态):- 检查该位是否为1(通过
(set[index]&(1 << bit))!= 0
),如果是,则将zeros
减1,ones
加1,并使用位异或操作(^
)将该位置为0(set[index]^=(1 << bit)
)。
- 检查该位是否为1(通过
- 首先计算要操作的位在
- 设置位为0(
unfix
方法)- 同样先计算
index
和bit
。 - 如果
reverse
为false
:- 检查该位是否为1(通过
(set[index]&(1 << bit))!= 0
),如果是,则将ones
减1,zeros
加1,并使用位异或操作(^
)将该位置为0(set[index]^=(1 << bit)
)。
- 检查该位是否为1(通过
- 如果
reverse
为true
:- 检查该位是否为0(通过
(set[index]&(1 << bit)) == 0
),如果是,则将ones
减1,zeros
加1,并使用位或操作(|
)将该位置为1(set[index]|=(1 << bit)
)。
- 检查该位是否为0(通过
- 同样先计算
- 翻转所有位(
flip
方法)- 简单地将
reverse
标志取反(reverse =!reverse
)。 - 同时交换
zeros
和ones
的值,因为翻转后原来为0的位变为1,原来为1的位变为0。
- 简单地将
- 检查所有位是否为1(
all
方法)- 直接比较
ones
是否等于size
,如果相等则表示所有位都是1,返回true
,否则返回false
。
- 直接比较
- 检查是否至少有一位为1(
one
方法)- 只需检查
ones
是否大于0,如果是则表示至少有一位为1,返回true
,否则返回false
。
- 只需检查
- 计算1的数量(
count
方法)- 直接返回
ones
的值,因为ones
变量一直维护着位图中1的数量。
- 直接返回
- 转换为字符串表示(
toString
方法)- 使用
StringBuilder
来构建字符串。 - 遍历位图的每一位,先计算每个
int
中的位,通过(number >> j)&1
得到位状态,然后根据reverse
标志对状态进行调整(status ^= reverse ? 1 : 0
),最后将状态添加到StringBuilder
中,最终返回构建好的字符串。
- 使用
- 初始化(构造函数)
代码实现
// 位图的实现
// Bitset是一种能以紧凑形式存储位的数据结构
// Bitset(int n) : 初始化n个位,所有位都是0
// void fix(int i) : 将下标i的位上的值更新为1
// void unfix(int i) : 将下标i的位上的值更新为0
// void flip() : 翻转所有位的值
// boolean all() : 是否所有位都是1
// boolean one() : 是否至少有一位是1
// int count() : 返回所有位中1的数量
// String toString() : 返回所有位的状态
public class Code02_DesignBitsetTest {
// 测试链接 : https://leetcode-cn.com/problems/design-bitset/
class Bitset {
private int[] set;
private final int size;
private int zeros;
private int ones;
private boolean reverse;
public Bitset(int n) {
set = new int[(n + 31) / 32];
size = n;
zeros = n;
ones = 0;
reverse = false;
}
// 把i这个数字加入到位图
public void fix(int i) {
int index = i / 32;
int bit = i % 32;
if (!reverse) {
// 位图所有位的状态,维持原始含义
// 0 : 不存在
// 1 : 存在
if ((set[index] & (1 << bit)) == 0) {
zeros--;
ones++;
set[index] |= (1 << bit);
}
} else {
// 位图所有位的状态,翻转了
// 0 : 存在
// 1 : 不存在
if ((set[index] & (1 << bit)) != 0) {
zeros--;
ones++;
set[index] ^= (1 << bit);
}
}
}
// 把i这个数字从位图中移除
public void unfix(int i) {
int index = i / 32;
int bit = i % 32;
if (!reverse) {
if ((set[index] & (1 << bit)) != 0) {
ones--;
zeros++;
set[index] ^= (1 << bit);
}
} else {
if ((set[index] & (1 << bit)) == 0) {
ones--;
zeros++;
set[index] |= (1 << bit);
}
}
}
public void flip() {
reverse = !reverse;
int tmp = zeros;
zeros = ones;
ones = tmp;
}
public boolean all() {
return ones == size;
}
public boolean one() {
return ones > 0;
}
public int count() {
return ones;
}
public String toString() {
StringBuilder builder = new StringBuilder();
for (int i = 0, k = 0, number, status; i < size; k++) {
number = set[k];
for (int j = 0; j < 32 && i < size; j++, i++) {
status = (number >> j) & 1;
status ^= reverse ? 1 : 0;
builder.append(status);
}
}
return builder.toString();
}
}
}