百日算法修炼 · 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
思考问题:
- 一个整数应该映射到
long[]的哪个下标? - 它在这个
long中的第几位? - 删除时为什么要先取反再按位与?
提示:把整数拆成"组号 + 组内偏移"
一个 long 有 64 位。value / 64 是数组下标,value % 64 是位偏移;用右移和按位与可以更快地表达这两个计算。
写完后再看:[查看练习 1 参考答案](#查看练习 1 参考答案)
练习 2:用对数器验证位图
随机生成一批非负整数,同时放入位图和 HashSet。随机执行查询、删除,再比较两者结果是否始终一致。
思考问题:
- 测试数据为什么要包含重复值?
delete一个不存在的值,集合应该发生什么变化?- 如何覆盖
0、边界值63、64和最大允许值?
提示:参考实现不等于正确性证明
让 HashSet 作为可信基准,并主动测试空集合、重复添加、重复删除以及跨越 64 位边界的数字。
写完后再看:[查看练习 2 参考答案](#查看练习 2 参考答案)
练习 3:只用位运算实现加法
实现 add(a, b),不能使用 +、-、*、/。
text
输入:a = 10, b = 3
输出:13
思考问题:
a ^ b计算的是什么?a & b找到的为什么是"产生进位的位置"?- 为什么进位要左移一位,并重复迭代直到为 0?
提示:把加法拆成两部分
异或得到无进位相加结果;按位与后左移一位得到进位。下一轮把"无进位和"与"进位"再次相加。
写完后再看:[查看练习 3 参考答案](#查看练习 3 参考答案)
练习 4:只用位运算实现减法与乘法
在 add 的基础上实现:
minus(a, b):计算a - b;multiply(a, b):计算a * b。
思考问题:
- 补码中
-b如何由b得到? - 二进制乘法中,当前乘数最低位为 1 时应该做什么?
- 为什么乘数需要使用无符号右移
>>>?
提示:对应十进制的竖式乘法
-b = ~b + 1。乘法每轮检查最低位,若为 1 就把当前被乘数累加到结果,然后被乘数左移、乘数右移。
写完后再看:[查看练习 4 参考答案](#查看练习 4 参考答案)
练习 5:实现覆盖边界的整数除法
实现 divide(a, b),不能使用 +、-、*、/、%。结果向 0 截断,行为与 Java 整数除法一致。
text
输入:divide(10, 3)
输出:3
输入:divide(-10, 3)
输出:-3
思考问题:
- 为什么不能直接对
Integer.MIN_VALUE取相反数? - 从高位到低位尝试减去
divisor << bit,如何得到商的每一位? - 除数为 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),add、delete、contains 均为 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 × 11:11 的二进制为 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 比较,负除数会导致商构造错误;此外还必须处理除零。
加餐挑战
- 让位图同时支持
maxValue个数的计数,而不是只有存在/不存在; - 实现
isPowerOfTwo(value),判断一个正数是否为 2 的幂; - 实现
oddTimesOneNumber:数组中只有一个数出现奇数次,其余出现偶数次; - 将乘法改写为始终处理绝对值,最后统一恢复符号;
- 用
long版本实现位运算除法,观察可表示范围的变化。
Day 05 复盘清单
- 我能把整数拆成
wordIndex和bitIndex - 我能解释位图的添加、删除和查询为何都是 O(1)
- 我能用"异或 + 进位"写出加法
- 我能用补码解释减法
- 我能把二进制乘法写成移位与累加
- 我处理了除零和
Integer.MIN_VALUE的边界 - 我使用随机测试和边界测试验证了实现
一句话总结: 位运算题的关键不是记住某个公式,而是把十进制运算拆成可重复的位级不变量,再用边界测试把实现钉牢。