百日算法修炼 · Day 05

百日算法修炼 · Day 05

主题:位图、位运算实现加减乘除

语言:Java | 建议用时:60~90 分钟


阅读方式

本文分为两个区域:

  • 上半篇是练习区:只包含题目、示例和可折叠提示;
  • 下半篇是答案区:包含优化代码、图解、复杂度与易错点。

建议先完成题目,再通过"查看参考答案"跳转。答案中的代码可以直接复制到 Java 项目中运行。

今日任务地图

#mermaid-svg-5k4Ccj16s2zbgaQQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5k4Ccj16s2zbgaQQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5k4Ccj16s2zbgaQQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5k4Ccj16s2zbgaQQ .error-icon{fill:#552222;}#mermaid-svg-5k4Ccj16s2zbgaQQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5k4Ccj16s2zbgaQQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5k4Ccj16s2zbgaQQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5k4Ccj16s2zbgaQQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5k4Ccj16s2zbgaQQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5k4Ccj16s2zbgaQQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5k4Ccj16s2zbgaQQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5k4Ccj16s2zbgaQQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5k4Ccj16s2zbgaQQ .marker.cross{stroke:#333333;}#mermaid-svg-5k4Ccj16s2zbgaQQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5k4Ccj16s2zbgaQQ p{margin:0;}#mermaid-svg-5k4Ccj16s2zbgaQQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5k4Ccj16s2zbgaQQ .cluster-label text{fill:#333;}#mermaid-svg-5k4Ccj16s2zbgaQQ .cluster-label span{color:#333;}#mermaid-svg-5k4Ccj16s2zbgaQQ .cluster-label span p{background-color:transparent;}#mermaid-svg-5k4Ccj16s2zbgaQQ .label text,#mermaid-svg-5k4Ccj16s2zbgaQQ span{fill:#333;color:#333;}#mermaid-svg-5k4Ccj16s2zbgaQQ .node rect,#mermaid-svg-5k4Ccj16s2zbgaQQ .node circle,#mermaid-svg-5k4Ccj16s2zbgaQQ .node ellipse,#mermaid-svg-5k4Ccj16s2zbgaQQ .node polygon,#mermaid-svg-5k4Ccj16s2zbgaQQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5k4Ccj16s2zbgaQQ .rough-node .label text,#mermaid-svg-5k4Ccj16s2zbgaQQ .node .label text,#mermaid-svg-5k4Ccj16s2zbgaQQ .image-shape .label,#mermaid-svg-5k4Ccj16s2zbgaQQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-5k4Ccj16s2zbgaQQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5k4Ccj16s2zbgaQQ .rough-node .label,#mermaid-svg-5k4Ccj16s2zbgaQQ .node .label,#mermaid-svg-5k4Ccj16s2zbgaQQ .image-shape .label,#mermaid-svg-5k4Ccj16s2zbgaQQ .icon-shape .label{text-align:center;}#mermaid-svg-5k4Ccj16s2zbgaQQ .node.clickable{cursor:pointer;}#mermaid-svg-5k4Ccj16s2zbgaQQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5k4Ccj16s2zbgaQQ .arrowheadPath{fill:#333333;}#mermaid-svg-5k4Ccj16s2zbgaQQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5k4Ccj16s2zbgaQQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5k4Ccj16s2zbgaQQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5k4Ccj16s2zbgaQQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5k4Ccj16s2zbgaQQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5k4Ccj16s2zbgaQQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5k4Ccj16s2zbgaQQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5k4Ccj16s2zbgaQQ .cluster text{fill:#333;}#mermaid-svg-5k4Ccj16s2zbgaQQ .cluster span{color:#333;}#mermaid-svg-5k4Ccj16s2zbgaQQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5k4Ccj16s2zbgaQQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5k4Ccj16s2zbgaQQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-5k4Ccj16s2zbgaQQ .icon-shape,#mermaid-svg-5k4Ccj16s2zbgaQQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5k4Ccj16s2zbgaQQ .icon-shape p,#mermaid-svg-5k4Ccj16s2zbgaQQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5k4Ccj16s2zbgaQQ .icon-shape .label rect,#mermaid-svg-5k4Ccj16s2zbgaQQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5k4Ccj16s2zbgaQQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5k4Ccj16s2zbgaQQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5k4Ccj16s2zbgaQQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始 Day 05
位图设计
位图随机验证
无进位加法
减法与乘法
安全除法
边界测试与复盘

题号 主题 核心能力
1 位图 用一个 bit 表示一个整数是否出现
2 位图对数器 用随机测试验证集合语义
3 位运算加法 分离"无进位和"和"进位"
4 减法与乘法 补码、二进制快速累加
5 安全除法 处理符号与 Integer.MIN_VALUE

第一部分:独立练习区

先不要向下翻到答案区。请先写出自己的版本,再用答案检查边界。

练习 1:实现一个位图集合

设计 BitMap,支持以下操作:

  • add(value):加入非负整数;
  • delete(value):删除整数;
  • contains(value):判断整数是否存在。

要求:不能使用 HashSet 保存数据;集合的底层只使用 long[]

text 复制代码
操作:add(5), add(130), contains(5), delete(5), contains(5)
结果:true, false

思考问题:

  1. 一个整数应该映射到 long[] 的哪个下标?
  2. 它在这个 long 中的第几位?
  3. 删除时为什么要先取反再按位与?

提示:把整数拆成"组号 + 组内偏移"

一个 long 有 64 位。value / 64 是数组下标,value % 64 是位偏移;用右移和按位与可以更快地表达这两个计算。

写完后再看:[查看练习 1 参考答案](#查看练习 1 参考答案)


练习 2:用对数器验证位图

随机生成一批非负整数,同时放入位图和 HashSet。随机执行查询、删除,再比较两者结果是否始终一致。

思考问题:

  1. 测试数据为什么要包含重复值?
  2. delete 一个不存在的值,集合应该发生什么变化?
  3. 如何覆盖 0、边界值 6364 和最大允许值?

提示:参考实现不等于正确性证明

HashSet 作为可信基准,并主动测试空集合、重复添加、重复删除以及跨越 64 位边界的数字。

写完后再看:[查看练习 2 参考答案](#查看练习 2 参考答案)


练习 3:只用位运算实现加法

实现 add(a, b),不能使用 +-*/

text 复制代码
输入:a = 10, b = 3
输出:13

思考问题:

  1. a ^ b 计算的是什么?
  2. a & b 找到的为什么是"产生进位的位置"?
  3. 为什么进位要左移一位,并重复迭代直到为 0?

提示:把加法拆成两部分

异或得到无进位相加结果;按位与后左移一位得到进位。下一轮把"无进位和"与"进位"再次相加。

写完后再看:[查看练习 3 参考答案](#查看练习 3 参考答案)


练习 4:只用位运算实现减法与乘法

add 的基础上实现:

  • minus(a, b):计算 a - b
  • multiply(a, b):计算 a * b

思考问题:

  1. 补码中 -b 如何由 b 得到?
  2. 二进制乘法中,当前乘数最低位为 1 时应该做什么?
  3. 为什么乘数需要使用无符号右移 >>>

提示:对应十进制的竖式乘法

-b = ~b + 1。乘法每轮检查最低位,若为 1 就把当前被乘数累加到结果,然后被乘数左移、乘数右移。

写完后再看:[查看练习 4 参考答案](#查看练习 4 参考答案)


练习 5:实现覆盖边界的整数除法

实现 divide(a, b),不能使用 +-*/%。结果向 0 截断,行为与 Java 整数除法一致。

text 复制代码
输入:divide(10, 3)
输出:3

输入:divide(-10, 3)
输出:-3

思考问题:

  1. 为什么不能直接对 Integer.MIN_VALUE 取相反数?
  2. 从高位到低位尝试减去 divisor << bit,如何得到商的每一位?
  3. 除数为 0 时应如何处理?

提示:先处理特殊值,再处理普通情况

Java 的绝对值范围不对称。先单独处理除数为 0、最小值组合;普通情况转换为正数后,从 bit=30 向下尝试。

写完后再看:[查看练习 5 参考答案](#查看练习 5 参考答案)

第二部分:参考答案与图解

答案 1:位图集合

[返回练习 1](#返回练习 1)

核心思路

将整数 value 映射到两个坐标:

text 复制代码
wordIndex = value >> 6       // value / 64,定位第几个 long
bitIndex  = value & 63       // value % 64,定位 long 内的第几位
mask      = 1L << bitIndex

#mermaid-svg-NyoAsKtiVRz3AZTq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NyoAsKtiVRz3AZTq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NyoAsKtiVRz3AZTq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NyoAsKtiVRz3AZTq .error-icon{fill:#552222;}#mermaid-svg-NyoAsKtiVRz3AZTq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NyoAsKtiVRz3AZTq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NyoAsKtiVRz3AZTq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NyoAsKtiVRz3AZTq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NyoAsKtiVRz3AZTq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NyoAsKtiVRz3AZTq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NyoAsKtiVRz3AZTq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NyoAsKtiVRz3AZTq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NyoAsKtiVRz3AZTq .marker.cross{stroke:#333333;}#mermaid-svg-NyoAsKtiVRz3AZTq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NyoAsKtiVRz3AZTq p{margin:0;}#mermaid-svg-NyoAsKtiVRz3AZTq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NyoAsKtiVRz3AZTq .cluster-label text{fill:#333;}#mermaid-svg-NyoAsKtiVRz3AZTq .cluster-label span{color:#333;}#mermaid-svg-NyoAsKtiVRz3AZTq .cluster-label span p{background-color:transparent;}#mermaid-svg-NyoAsKtiVRz3AZTq .label text,#mermaid-svg-NyoAsKtiVRz3AZTq span{fill:#333;color:#333;}#mermaid-svg-NyoAsKtiVRz3AZTq .node rect,#mermaid-svg-NyoAsKtiVRz3AZTq .node circle,#mermaid-svg-NyoAsKtiVRz3AZTq .node ellipse,#mermaid-svg-NyoAsKtiVRz3AZTq .node polygon,#mermaid-svg-NyoAsKtiVRz3AZTq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NyoAsKtiVRz3AZTq .rough-node .label text,#mermaid-svg-NyoAsKtiVRz3AZTq .node .label text,#mermaid-svg-NyoAsKtiVRz3AZTq .image-shape .label,#mermaid-svg-NyoAsKtiVRz3AZTq .icon-shape .label{text-anchor:middle;}#mermaid-svg-NyoAsKtiVRz3AZTq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NyoAsKtiVRz3AZTq .rough-node .label,#mermaid-svg-NyoAsKtiVRz3AZTq .node .label,#mermaid-svg-NyoAsKtiVRz3AZTq .image-shape .label,#mermaid-svg-NyoAsKtiVRz3AZTq .icon-shape .label{text-align:center;}#mermaid-svg-NyoAsKtiVRz3AZTq .node.clickable{cursor:pointer;}#mermaid-svg-NyoAsKtiVRz3AZTq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NyoAsKtiVRz3AZTq .arrowheadPath{fill:#333333;}#mermaid-svg-NyoAsKtiVRz3AZTq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NyoAsKtiVRz3AZTq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NyoAsKtiVRz3AZTq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NyoAsKtiVRz3AZTq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NyoAsKtiVRz3AZTq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NyoAsKtiVRz3AZTq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NyoAsKtiVRz3AZTq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NyoAsKtiVRz3AZTq .cluster text{fill:#333;}#mermaid-svg-NyoAsKtiVRz3AZTq .cluster span{color:#333;}#mermaid-svg-NyoAsKtiVRz3AZTq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NyoAsKtiVRz3AZTq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NyoAsKtiVRz3AZTq rect.text{fill:none;stroke-width:0;}#mermaid-svg-NyoAsKtiVRz3AZTq .icon-shape,#mermaid-svg-NyoAsKtiVRz3AZTq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NyoAsKtiVRz3AZTq .icon-shape p,#mermaid-svg-NyoAsKtiVRz3AZTq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NyoAsKtiVRz3AZTq .icon-shape .label rect,#mermaid-svg-NyoAsKtiVRz3AZTq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NyoAsKtiVRz3AZTq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NyoAsKtiVRz3AZTq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NyoAsKtiVRz3AZTq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} value = 130
wordIndex = 2
bitIndex = 2
bits2 的第 2 位

完整代码

java 复制代码
package class05;

import java.util.HashSet;
import java.util.Random;

public class Code01_BitMap1 {

    /** 只支持 [0, maxValue) 范围内的非负整数。 */
    public static final class BitMap {
        private final long[] bits;
        private final int maxValue;

        public BitMap(int maxValue) {
            if (maxValue < 0) {
                throw new IllegalArgumentException("maxValue 不能为负数");
            }
            this.maxValue = maxValue;
            // 每个 long 存 64 个 bit,向上取整即可。
            this.bits = new long[(int) (((long) maxValue + 63) >> 6)];
        }

        public void add(int value) {
            checkRange(value);
            bits[value >> 6] |= 1L << (value & 63);
        }

        public void delete(int value) {
            checkRange(value);
            // ~(mask) 只有目标位为 0,其余位为 1。
            bits[value >> 6] &= ~(1L << (value & 63));
        }

        public boolean contains(int value) {
            checkRange(value);
            return (bits[value >> 6] & (1L << (value & 63))) != 0;
        }

        private void checkRange(int value) {
            if (value < 0 || value >= maxValue) {
                throw new IndexOutOfBoundsException("value 必须在 [0, maxValue) 内");
            }
        }
    }

    public static void main(String[] args) {
        Random random = new Random();
        for (int test = 0; test < 10_000; test++) {
            int max = 1 + random.nextInt(1_000);
            BitMap bitMap = new BitMap(max);
            HashSet<Integer> set = new HashSet<>();

            for (int i = 0; i < 500; i++) {
                int value = random.nextInt(max);
                bitMap.add(value);
                set.add(value);
            }
            for (int i = 0; i < 500; i++) {
                int value = random.nextInt(max);
                bitMap.delete(value);
                set.remove(value);
            }
            for (int value = 0; value < max; value++) {
                if (bitMap.contains(value) != set.contains(value)) {
                    throw new AssertionError("位图校验失败:" + value);
                }
            }
        }
        System.out.println("Accepted");
    }
}

空间复杂度为 O(maxValue / 64)adddeletecontains 均为 O(1)。位图适合值域较小且需要快速判断"是否存在"的场景,不适合值域极大或需要保存重复次数的场景。


答案 2:位图对数器

[返回练习 2](#返回练习 2)

对数器的关键是:同一组操作同时作用于两个实现,再逐项比较结果HashSet 只作为测试基准,不参与位图实现。
#mermaid-svg-eCqWbtHdJYQqVVPU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eCqWbtHdJYQqVVPU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eCqWbtHdJYQqVVPU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eCqWbtHdJYQqVVPU .error-icon{fill:#552222;}#mermaid-svg-eCqWbtHdJYQqVVPU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eCqWbtHdJYQqVVPU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eCqWbtHdJYQqVVPU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eCqWbtHdJYQqVVPU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eCqWbtHdJYQqVVPU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eCqWbtHdJYQqVVPU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eCqWbtHdJYQqVVPU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eCqWbtHdJYQqVVPU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eCqWbtHdJYQqVVPU .marker.cross{stroke:#333333;}#mermaid-svg-eCqWbtHdJYQqVVPU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eCqWbtHdJYQqVVPU p{margin:0;}#mermaid-svg-eCqWbtHdJYQqVVPU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eCqWbtHdJYQqVVPU .cluster-label text{fill:#333;}#mermaid-svg-eCqWbtHdJYQqVVPU .cluster-label span{color:#333;}#mermaid-svg-eCqWbtHdJYQqVVPU .cluster-label span p{background-color:transparent;}#mermaid-svg-eCqWbtHdJYQqVVPU .label text,#mermaid-svg-eCqWbtHdJYQqVVPU span{fill:#333;color:#333;}#mermaid-svg-eCqWbtHdJYQqVVPU .node rect,#mermaid-svg-eCqWbtHdJYQqVVPU .node circle,#mermaid-svg-eCqWbtHdJYQqVVPU .node ellipse,#mermaid-svg-eCqWbtHdJYQqVVPU .node polygon,#mermaid-svg-eCqWbtHdJYQqVVPU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eCqWbtHdJYQqVVPU .rough-node .label text,#mermaid-svg-eCqWbtHdJYQqVVPU .node .label text,#mermaid-svg-eCqWbtHdJYQqVVPU .image-shape .label,#mermaid-svg-eCqWbtHdJYQqVVPU .icon-shape .label{text-anchor:middle;}#mermaid-svg-eCqWbtHdJYQqVVPU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eCqWbtHdJYQqVVPU .rough-node .label,#mermaid-svg-eCqWbtHdJYQqVVPU .node .label,#mermaid-svg-eCqWbtHdJYQqVVPU .image-shape .label,#mermaid-svg-eCqWbtHdJYQqVVPU .icon-shape .label{text-align:center;}#mermaid-svg-eCqWbtHdJYQqVVPU .node.clickable{cursor:pointer;}#mermaid-svg-eCqWbtHdJYQqVVPU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eCqWbtHdJYQqVVPU .arrowheadPath{fill:#333333;}#mermaid-svg-eCqWbtHdJYQqVVPU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eCqWbtHdJYQqVVPU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eCqWbtHdJYQqVVPU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eCqWbtHdJYQqVVPU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eCqWbtHdJYQqVVPU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eCqWbtHdJYQqVVPU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eCqWbtHdJYQqVVPU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eCqWbtHdJYQqVVPU .cluster text{fill:#333;}#mermaid-svg-eCqWbtHdJYQqVVPU .cluster span{color:#333;}#mermaid-svg-eCqWbtHdJYQqVVPU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eCqWbtHdJYQqVVPU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eCqWbtHdJYQqVVPU rect.text{fill:none;stroke-width:0;}#mermaid-svg-eCqWbtHdJYQqVVPU .icon-shape,#mermaid-svg-eCqWbtHdJYQqVVPU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eCqWbtHdJYQqVVPU .icon-shape p,#mermaid-svg-eCqWbtHdJYQqVVPU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eCqWbtHdJYQqVVPU .icon-shape .label rect,#mermaid-svg-eCqWbtHdJYQqVVPU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eCqWbtHdJYQqVVPU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eCqWbtHdJYQqVVPU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eCqWbtHdJYQqVVPU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

随机操作
BitMap
HashSet
逐个 value 比较
一致?
打印反例并停止
继续下一轮

模板中的 main 已覆盖重复添加、删除和随机查询。进一步测试时,建议固定检查 0、63、64、maxValue - 1,它们最容易暴露分组边界错误。


答案 3:位运算加法

[返回练习 3](#返回练习 3)

加法的两个分量

text 复制代码
无进位和:a ^ b
进位信息:(a & b) << 1

5 (0101)3 (0011) 为例:

text 复制代码
0101 ^ 0011 = 0110   // 无进位和:6
0101 & 0011 = 0001
0001 << 1   = 0010   // 进位:2
0110 + 0010 = 1000   // 下一轮继续拆分,最终得到 8
java 复制代码
/** 不使用 +,通过异或和进位实现整数加法。 */
public static int add(int a, int b) {
    while (b != 0) {
        int sumWithoutCarry = a ^ b;
        int carry = (a & b) << 1;
        a = sumWithoutCarry;
        b = carry;
    }
    return a;
}

b 每轮代表仍未处理的进位;当进位为 0 时,a 就是最终结果。int 固定 32 位,因此循环最多进行常数次,时间复杂度为 O(1)


答案 4:补码减法与二进制乘法

[返回练习 4](#返回练习 4)

java 复制代码
/** a - b = a + (-b),而 -b = ~b + 1。 */
public static int minus(int a, int b) {
    return add(a, add(~b, 1));
}

/** 通过"检查最低位、累加、移位"模拟二进制乘法。 */
public static int multi(int a, int b) {
    int result = 0;
    int multiplicand = a;
    int multiplier = b;

    while (multiplier != 0) {
        if ((multiplier & 1) != 0) {
            result = add(result, multiplicand);
        }
        multiplicand <<= 1;
        // 无符号右移让负数的位模式也能逐位处理。
        multiplier >>>= 1;
    }
    return result;
}

例如 13 × 1111 的二进制为 1011,对应 1×13 + 1×(13<<1) + 0×(13<<2) + 1×(13<<3)。算法只循环 32 次以内,时间和额外空间均为 O(1)。溢出行为与 Java int 乘法一致:结果超出 32 位时保留低 32 位。


答案 5:覆盖边界的整数除法

[返回练习 5](#返回练习 5)

易错点

  • 除数为 0 时应抛出 ArithmeticException
  • Integer.MIN_VALUE 的相反数无法用 int 表示;
  • Integer.MIN_VALUE / -1 超出 int 范围,Java 结果约定为 Integer.MIN_VALUE

完整代码

java 复制代码
/** 不使用 + - * / %,实现与 Java int 除法一致的向 0 截断。 */
public static int divide(int dividend, int divisor) {
    if (divisor == 0) {
        throw new ArithmeticException("/ by zero");
    }
    if (dividend == Integer.MIN_VALUE && divisor == -1) {
        return Integer.MIN_VALUE; // Java int 溢出约定
    }
    if (dividend == Integer.MIN_VALUE && divisor == Integer.MIN_VALUE) {
        return 1;
    }
    if (divisor == Integer.MIN_VALUE) {
        return 0;
    }
    if (dividend == Integer.MIN_VALUE) {
        // 先加 1 变成可取绝对值的数,再补回余数带来的 1。
        int quotient = divide(add(dividend, 1), divisor);
        int remainder = minus(dividend, multi(divisor, quotient));
        return add(quotient, divide(remainder, divisor));
    }

    int x = dividend < 0 ? negate(dividend) : dividend;
    int y = divisor < 0 ? negate(divisor) : divisor;
    int quotient = 0;

    // x、y 均为正数,最高只需尝试到第 30 位。
    for (int bit = 30; bit >= 0; bit--) {
        if ((x >> bit) >= y) {
            quotient |= 1 << bit;
            x = minus(x, y << bit);
        }
    }
    return ((dividend < 0) ^ (divisor < 0)) ? negate(quotient) : quotient;
}

private static int negate(int value) {
    return add(~value, 1);
}

这里从高位到低位贪心地构造商:若 y << bit 不超过当前余数,就把商的第 bit 位设为 1,并减去这部分。与原实现相比,关键修正是比较时使用正数 y,而不是误用原始除数 b;同时显式处理除零和最小值边界。

复杂度

divide 最多检查 31 个 bit,时间复杂度为 O(1),额外空间复杂度为 O(1)


第三部分:统一验证

java 复制代码
import java.util.Random;

public class BitOperationVerifier {
    public static void main(String[] args) {
        Random random = new Random();
        for (int test = 0; test < 100_000; test++) {
            int a = random.nextInt();
            int b = random.nextInt();

            if (Code02_BitAddMinusMultiDiv.add(a, b) != a + b) {
                throw new AssertionError("add failed");
            }
            if (Code02_BitAddMinusMultiDiv.minus(a, b) != a - b) {
                throw new AssertionError("minus failed");
            }
            if (Code02_BitAddMinusMultiDiv.multi(a, b) != a * b) {
                throw new AssertionError("multiply failed");
            }

            if (b != 0
                    && Code02_BitAddMinusMultiDiv.divide(a, b) != a / b) {
                throw new AssertionError("divide failed");
            }
        }
        System.out.println("Accepted: 100000 tests");
    }
}

随机测试不能代替边界测试。请额外验证 0、1、-1、63、64、Integer.MAX_VALUE、Integer.MIN_VALUE,并检查除数为 0 的异常。

今日自测

1. 位图为什么节省空间?

查看答案

一个 long 同时保存 64 个布尔状态,每个整数只占 1 bit,而不是一个对象或一个引用。

2. (a & b) << 1 为什么是进位?

查看答案

只有两个操作数对应位都为 1 时才会产生进位;进位要进入更高一位,所以再左移 1 位。

3. 为什么除法从 bit=30 开始?

查看答案

普通正数 int 的最高有效位不超过第 30 位;第 31 位是符号位,不能作为正数商的位参与移位比较。

4. 原除法代码中最危险的一个细节是什么?

查看答案

普通除法分支已经把除数转成正数 y,却用原始的 b 比较,负除数会导致商构造错误;此外还必须处理除零。

加餐挑战

  1. 让位图同时支持 maxValue 个数的计数,而不是只有存在/不存在;
  2. 实现 isPowerOfTwo(value),判断一个正数是否为 2 的幂;
  3. 实现 oddTimesOneNumber:数组中只有一个数出现奇数次,其余出现偶数次;
  4. 将乘法改写为始终处理绝对值,最后统一恢复符号;
  5. long 版本实现位运算除法,观察可表示范围的变化。

Day 05 复盘清单

  • 我能把整数拆成 wordIndexbitIndex
  • 我能解释位图的添加、删除和查询为何都是 O(1)
  • 我能用"异或 + 进位"写出加法
  • 我能用补码解释减法
  • 我能把二进制乘法写成移位与累加
  • 我处理了除零和 Integer.MIN_VALUE 的边界
  • 我使用随机测试和边界测试验证了实现

一句话总结: 位运算题的关键不是记住某个公式,而是把十进制运算拆成可重复的位级不变量,再用边界测试把实现钉牢。

相关推荐
paopaokaka_luck1 小时前
基于springboot3+vue3的云南本土影视文旅推荐平台(协同过滤算法、Echarts图形化分析)
前端·spring boot·学习·算法·echarts·mybatis
摇滚侠1 小时前
云原生 Java 架构师的第一课 K8s+Docker+KubeSphere+DevOps 89-129
java·云原生·kubernetes
captain3761 小时前
多线程进阶
java·开发语言·数据库
Ivanqhz1 小时前
泰勒展开(Taylor Expansion)
算法·决策树·机器学习·集成学习
雨辰AI2 小时前
人大金仓 K8s 密评合规加固|国密全链路加密 + 权限收敛容器化落地(三级密评直通版)
java·云原生·容器·kubernetes·政务
zander2583 小时前
34. 在排序数组中查找元素的第一个和最后一个位置:用两个边界定位区间
数据结构·算法·leetcode
郑州光合科技余经理8 小时前
代驾系统架构拆解:订单链路、权限组织与私有化源码交付
开发语言·后端·算法·架构·系统架构·uni-app·php
To_OC10 小时前
LC 35 搜索插入位置:二分查找谁都会,边界条件谁写谁懵
javascript·算法·leetcode
码哥DFS10 小时前
二叉树的直径
开发语言·javascript·算法