31.【必备】位图

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解032【必备】位图_哔哩哔哩_bilibili

一.位图

定义与原理

  • 定义:位图是一种用每一位来存放某种状态的数据结构,适用于大规模数据但数据状态又不是很多的情况,通常用于判断某个数据存不存在。
  • 原理:将数据集合中的每个元素映射到一个位上,位数通常等于数据集合的大小,每个位的状态(0或1)表示对应元素是否存在。

相关操作

  • 置位:将指定位置的位设置为1,表示对应元素存在或某种状态为真。
  • 复位:将指定位置的位设置为0,表示对应元素不存在或某种状态为假。
  • 访问:检查指定位置的位的值,以判断对应元素是否存在或某种状态是否为真。
  • 扩容:当位图容量不足时,需要进行扩容操作,类似于向量的扩容,通常采用加倍策略重新分配内存并转移原数据。

应用场景

  • 数据去重:在处理大量数据时,可以快速去除重复元素,将所有元素添加到位图中,然后遍历位图即可得到去重后的结果。
  • 数据统计:快速统计一个数据集合中元素的数量,通过遍历位图,将所有为1的位相加即可得到元素的数量。
  • 数据查找:快速检查一个元素是否存在于数据集合中,通过调用相应的访问方法即可得到结果。
  • 数据排序:对有序数据进行排序,通过遍历有序数据,将每个元素添加到位图中,然后按照位图的顺序输出即可得到排序后的结果。
  • 权限管理:在系统中用于存储用户的权限信息,每个权限对应位图中的一位,通过位运算可以方便地进行权限的设置、检查和组合。

注意事项

  • 内存占用:虽然位图在存储大量数据时可以节省空间,但如果数据范围过大,可能需要较大的内存来存储位图,需要注意内存的使用情况。
  • 数据范围:位图适用于数据状态有限且数据量较大的情况,如果数据状态较多或数据量较小,可能不适合使用位图。
  • 位运算效率:位运算在大多数情况下效率较高,但在某些情况下可能需要注意编译器的优化和数据的对齐等问题,以确保位运算的效率。

算法原理

  • 整体思路
    • 这个Bitset类实现了一个简单的位图数据结构。位图用于表示一组整数的存在状态,通过位运算来高效地实现添加、删除、反转和检查元素是否存在等操作。同时,使用HashSet进行对比测试,以验证Bitset类功能的正确性。
  • 具体原理
    • 位图存储原理
      • Bitset类中,使用一个int数组set来存储位图数据。由于每个int类型可以存储32位,所以对于表示范围为0n - 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次)的随机操作(添加、删除、反转),每次操作都同时对BitsetHashSet进行相同的操作。最后,遍历位图表示的所有数字(0n - 1),检查BitsetHashSet对于每个数字的存在状态是否一致,以此来验证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数组中的索引indexi/32)和在该int中的位偏移biti%32)。
      • 如果reversefalse(位图正常状态):
        • 检查该位是否为0(通过(set[index]&(1 << bit)) == 0),如果是,则将zeros减1,ones加1,并使用位或操作(|)将该位置为1(set[index]|=(1 << bit))。
      • 如果reversetrue(位图翻转状态):
        • 检查该位是否为1(通过(set[index]&(1 << bit))!= 0),如果是,则将zeros减1,ones加1,并使用位异或操作(^)将该位置为0(set[index]^=(1 << bit))。
    • 设置位为0(unfix方法)
      • 同样先计算indexbit
      • 如果reversefalse
        • 检查该位是否为1(通过(set[index]&(1 << bit))!= 0),如果是,则将ones减1,zeros加1,并使用位异或操作(^)将该位置为0(set[index]^=(1 << bit))。
      • 如果reversetrue
        • 检查该位是否为0(通过(set[index]&(1 << bit)) == 0),如果是,则将ones减1,zeros加1,并使用位或操作(|)将该位置为1(set[index]|=(1 << bit))。
    • 翻转所有位(flip方法)
      • 简单地将reverse标志取反(reverse =!reverse)。
      • 同时交换zerosones的值,因为翻转后原来为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();
        }

    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值