
面试现场问位运算是很多候选人始料未及的。大多数人觉得“Java 又不做嵌入式位运算有什么用”结果被一道只出现一次的数字问倒或者聊到位图时只能说出“好像是在 bitmap 里”讲不清原理。但实际上位运算在 Java 面试里出现的频率一直很高而且往往是作为“拉差距”的题出现——会的人三分钟就能写出最优解不会的人只能先排序再硬扛一对比高下立判。这篇文章围绕位运算最常考的 5 类高频题型展开位图、异或、比特计数再加上它们的组合变形。我会把每类题的原理讲清楚给出完整可运行的代码再奉上只有实际刷过、被面试官追问过才会知道的细节和坑。不管你是刚学 Java 基础、准备社招跳槽还是想补一补算法底子这篇都值得认真读一遍。1. 先搞清楚位运算在 Java 里的底子1.1 六个操作符藏着三个最容易踩坑的地方Java 提供了、|、^、~、、、前四个是逻辑位运算后三个是移位运算。很多人把漏掉因为实际项目里用得少但面试里“用还是”正是高频追问点。按位与两个 bit 都为 1 结果才是 1。|按位或两个 bit 有一个为 1 结果就是 1。^按位异或两个 bit 不同为 1相同为 0。~按位取反0 变 11 变 0。左移低位补 0相当于乘以 2 的 n 次方。右移高位补符号位负数补 1相当于除以 2 的 n 次方。无符号右移高位永远补 0。第一个坑是、的优先级比、-低但比、^、|高。所以a b 0这类写法几乎一定会出问题——的优先级高于JVM 会先算b 0再把布尔值和a做位运算。正确写法永远是(a b) 0我在面试里见过不止一个人在这里翻车。第二个坑是对负数的影响。比如-8 1结果不是-4之后的高位补 0而是补 1。原因是 Java 整数用补码表示右移要维持负数的符号所以高位补符号位。如果循环里用while (n 0) n 1没问题可一旦 n 是负数而你还期望它变成正数就会陷入死循环。这时候必须用无符号右移。第三个坑是位移距离会被模 32 处理。1 32并不是 0而是1 0等于 1。Java 规范里规定int 的移位操作只取右侧操作数的低 5 位long 则取低 6 位。这个行为很反直觉面试官也很爱拿来当附加题。1.2 位运算为什么值得在面试前突击因为位运算是少有的“代码量短、效率高、考核点集中”的主题。一道题从暴力解法到位运算解法时间复杂度和代码量可能同时下降一个量级比如找出现一次的数字HashMap 法和异或法都能做但只有异或法能做到 O(n) 时间和 O(1) 空间。更重要的是位运算隐藏在大量 Java 底层代码里。HashMap扩容时判断桶下标(n - 1) hash用了位与Integer.bitCount()内部用了分组统计ConcurrentHashMap的 sizeCtl 也用了位标记。面试官问位运算表面是考数学思维实际上是在看你对底层原理有没有好奇心。另一个理由是位运算很容易考“边界处理”。当候选人写出1 31时结果其实是Integer.MIN_VALUE因为 int 只有 31 位数值位这是溢出风险而1L 63则没问题。很多看似简单的题一旦涉及类型宽度就立刻把只会背题的人和真正理解二进制的人区分开了。1.3 面试前必须背下来的基础套路判断奇偶(x 1) 1效率高于x % 2 1。乘 2 或除 2x 1、x 1右移要小心负数。位图定位x / 64等价于x 6x % 64等价于x 63前提是除数是 2 的幂。取最右边的 1x (-x)也叫 lowbit树状数组的核心。消去最右边的 1x (x - 1)数 1 的个数的经典技巧。判断 2 的幂(x (x - 1)) 0但要先排除x 0。这几个套路加上异或的规律基本能解掉 80% 的位运算面试题。下面进入正题。2. 位图不只考概念还要你能十分钟写出来2.1 位图解决什么问题为什么能省内存位图本质上是一个用 bit 当“状态位”的数组。普通数组每个 int 占 32 bit如果用 int 存 0/1 状态1 亿个数据就要占约 400 MB 内存而位图把 32 个状态塞进一个 int 里同样 1 亿个状态只需要约 12.5 MB。这个对比很直观位图就是用一个二进制位来标记一个事物是否存在。面试里经常出现的场景是“40 亿个整数找出重复的那个内存只给 512 MB”。40 亿个 int 如果用 HashSet 存至少 16 GB显然不行。但用位图就很简单用两个 bit 表示一个数字的状态00 表示未出现01 表示出现一次11 表示出现多次40 亿个数字大约只需要 1 GB再配合分片还能继续压。当然还可以用 boolean 数组但 Java 的 boolean 数组每个元素实际占 1 字节比位图多 8 倍。位图的核心价值就是“极端节约内存”。另一个真实应用是操作系统的页分配器。系统的内存页只有空闲和占用两种状态用位图管理时分配一页就是把某个 bit 从 0 置 1释放则反向操作。这里头“定位”和“修改”都依赖位运算和我们在 Java 里写一个 Bitmap 类完全是一回事。2.2 动手实现一个位图set、clear、get 三件套面试时让你手写位图的题至少出现过两种一种是让你实现set(int index)和get(int index)另一种是让你统计有多少个位置被占用了。我建议直接把完整实现背下来。这里以 long 数组为载体一个 long 占 64 bit。public class Bitmap { private final long[] words; private final int capacity; public Bitmap(int capacity) { this.capacity capacity; // 需要多少个 long 来容纳 capacity 个 bit向上取整 this.words new long[(capacity 63) 6]; } public void set(int index) { if (index 0 || index capacity) { throw new IndexOutOfBoundsException(index out of range); } int wordIndex index 6; // 等价于 index / 64 int bitIndex index 63; // 等价于 index % 64 words[wordIndex] | (1L bitIndex); } public void clear(int index) { if (index 0 || index capacity) { throw new IndexOutOfBoundsException(index out of range); } int wordIndex index 6; int bitIndex index 63; words[wordIndex] ~(1L bitIndex); } public boolean get(int index) { if (index 0 || index capacity) { throw new IndexOutOfBoundsException(index out of range); } int wordIndex index 6; int bitIndex index 63; return (words[wordIndex] (1L bitIndex)) ! 0; } public int size() { int count 0; for (long w : words) { count Long.bitCount(w); } return count; } public static void main(String[] args) { Bitmap bm new Bitmap(1000); bm.set(1); bm.set(64); bm.set(127); System.out.println(bm.get(1)); // true System.out.println(bm.get(63)); // false System.out.println(bm.size()); // 3 bm.clear(64); System.out.println(bm.get(64)); // false } }这里有几个面试官非常爱追问的点。第一为什么用 long 而不用 intlong 是 64 bit数组长度更短、遍历统计 1 的数量时调Long.bitCount的次数更少。无论是内存还是时间long 都更优。但注意1L bitIndex必须带 L否则当bitIndex 32时1 bitIndex是 int 运算会得到完全错误的结果。这个坑我见过太多次了。第二为什么index 6能替代index / 64因为 64 是 2 的 6 次方对于一个非负整数右移 6 位就是除以 64 取整。而index 63拿到的是低 6 位恰好等于index % 64。面试时把这个等价关系说清楚面试官会知道你懂“对齐”的本质。第三set操作是words[wordIndex] | (1L bitIndex)为什么是或因为或操作只把目标 bit 置 1其他 bit 保持不变如果误用当目标 bit 已经是 1 时会进位污染旁边的 bit。同理clear用 ~(1L bitIndex)先取反生成一个“只有目标位是 0、其余全是 1”的掩码再与运算。2.3 位图的高频面试扩展判重和布隆过滤器有了上面的 Bitmap面试官会接着问“如果我不想拿一个亿的整数做状态只想判断某个字符串是不是出现过怎么办”这就是布隆过滤器。布隆过滤器也用一个位数组但它不是直接存数据而是对数据做 k 次哈希把 k 个位置的 bit 都置为 1。查询时同样做 k 次哈希只要某一个 bit 是 0就说明数据一定不在如果都在 1只能说“可能存在”。它牺牲了“精确性”换取了内存的极度压缩。面试官问布隆过滤器通常还会问“误判率怎么算”——k 个哈希函数、m 个 bit、n 个元素时误判率大概是(1 - e^(-kn/m))^k。这个公式不用死背但你要能说清楚“误判只发生在假阳性不会假阴性”以及“bit 数组越大误判率越低”这个直觉。如果你连“BloomFilter 实际有 8 个哈希函数128MB bit 数组能容纳上千万条记录”这种工程经验都能聊出来那这一关就稳了。3. 异或运算五道高频题里至少有三道跟它有关3.1 异或的四条规律是解所有异或题的基础异或有四条性质面试中所有异或题的解法都建立在这上面归零律a ^ a 0任何数和自身异或得 0。恒等律a ^ 0 a任何数和 0 异或保持不变。交换律a ^ b b ^ a。结合律(a ^ b) ^ c a ^ (b ^ c)。由这两条定律可以推出一个结论一个序列里如果只有一个数字出现奇数次其他数字都出现偶数次把所有数字连续异或剩下的就是那个出现奇数次的数字。因为偶数次的数字会两两消掉变成 00 再去异或其他数字不改变结果。生活里比较好理解的方式是把异或当成“开关”一个 bit 被异或 1 就翻转被异或 0 就保持不变。连续按两次同一个开关灯的状态恢复原样这就是a ^ a 0。手算时我建议大家先把数据转成二进制再对齐逐位算。比如2 ^ 102 是二进制0001010 是01010逐位不同为 1、相同为 0结果是01000也就是 8。看一眼就知道答案比背公式强得多。3.2 只出现一次的数字O(n) 时间 O(1) 空间的最优解这是 LeetCode 136几乎是 Java 位运算面试的代名词。题目给定一个非空整数数组除了某个元素只出现一次以外其余每个元素均出现两次找出那个只出现一次的元素。public int singleNumber(int[] nums) { int result 0; for (int num : nums) { result ^ num; } return result; }代码只有四行。原理就是归零律和结合律数组里有 2n1 个数字n 对重复数字每对的异或结果都是 0最后只有目标数字剩下来。面试官一般会问两个延伸问题。一是“如果每个数字出现三次只有一个出现一次怎么做”这个就不能直接异或了因为三个相同数字异或不会消掉。解法是对 32 个 bit 分别统计每个 bit 上 1 出现的次数模 3 的余数就是目标数字在那个 bit 上的值。二是“如果数组里有两个只出现一次的数字其他都出现两次怎么做”先把全体异或得到的是两个目标数字的异或值找到其中任意一个 1 的位置按这个位置把数组分成两组每组内再异或一次两个目标数字就分别出来了。面试官问延伸题的时候本质是在看你能不能从“背题”升级成“理解”。两次用异或三次用逐位统计这是两个完全不同的关注点。3.3 缺失的数字异或 vs 求和谁更稳LeetCode 268一个长度为 n 的数组包含 0 到 n 中的 n 个数找出缺失的那个。两个常见解法// 解法一异或 public int missingNumber(int[] nums) { int res nums.length; for (int i 0; i nums.length; i) { res ^ i ^ nums[i]; } return res; } // 解法二数学求和 public int missingNumberMath(int[] nums) { int n nums.length; int total n * (n 1) / 2; for (int num : nums) total - num; return total; }两种都能过但在面试回答里异或法是更好的答案。原因是求和法有理论上的溢出风险——n * (n 1)在 n 很大的时候会超过 int 范围虽然题目限定int数组且答案也是int但如果你把total声明成int在某些边界测试里差值被溢出吃掉就可能出错。异或法的推导逻辑是把0 到 n的所有下标和数组里的所有数字全部异或一遍数组里存在的数字会和自己对应的下标异或成 0最后只剩下缺失的下标。我之前在面试里这么答完后面试官追加问了一句“n 从 0 开始会影响结果吗”这里要注意数组包含 0缺失的数字可能是 0也可能在中间所以初始化res nums.length很关键把 n 这个“额外数字”提前放进去。3.4 用异或交换两个数解法炫酷但陷阱不少不用第三个变量交换两个数经典写法a a ^ b; b a ^ b; a a ^ b;三步推导第一步后a变成a ^ b第二步b (a ^ b) ^ b a第三步a (a ^ b) ^ a b。这个方法在面试题里属于“知道有这个方法就行”的级别实际业务里我强烈不建议用。第一当a和b实际上是同一个引用时比如数组里swap(arr[i], arr[i])第一步就把元素变成 0后面全是 0。第二可读性太差维护代码的人很可能看不懂为什么要这么做。第三现代 JVM 对临时变量的优化足够好多用一个局部变量没有任何性能损失。面试官问交换变量你的答案应该是“临时变量法”然后补充“如果笔试要求不能引入变量我可以用异或”这样既展现知识广度又体现了工程判断力。4. 比特计数从逐位数到一行消去法4.1 最容易想到的逐位统计为什么不是好答案题目是统计一个整数的二进制表示中 1 的个数。暴力解法public int hammingWeight(int n) { int count 0; while (n ! 0) { count (n 1); n 1; // 注意是 不是 } return count; }这里最重要的就是。如果用当 n 是负数时右移会在高位补 1n 永远不会变成 0直接死循环。但即使换成这段代码也要循环 32 次因为二进制位有多少位你就要看多少位复杂度 O(bitLength)。面试官听了这种方案不会说错但会追问一句“有没有更快的”。4.2 Kernighan 算法每循环一次就干掉一个 1Brian Kernighan 提出的经典技巧基于一个观察n (n - 1)会让 n 的最右侧那一个 1 变成 0。为什么如果 n 的最后一位是 1n-1 会把最后一位变 0其余不变相与后最后一位被消掉如果 n 的最后一位是 0n-1 会向高位借位直到借到第一个 1把这个 1 变成 0同时把后面所有 0 变成 1但这个尾巴和 n 相与时正好全被消掉。一句话总结n-1 把最右侧 1 以及它后面的所有 0 都反转了相与后只剩这个 1 前面的部分。public int hammingWeight(int n) { int count 0; while (n ! 0) { n n (n - 1); count; } return count; }假设 n 12二进制是1100n - 1 10111100 1011 1000count 1。1000 - 1 01111000 0111 0000count 2。只循环两次。这个算法的复杂度是 O(count)也就是说“有多少个 1就循环多少次”对稀疏二进制非常高效。面试官最喜欢你补上这句复杂度分析因为它把“背代码”和“理解原理”区分开了。4.3 查表法和分治法空间换时间和并行计算如果一段代码里要频繁统计很多整数的 bitCount比如上亿次逐个循环肯定不行。实用方案是查表法预先算好 0 到 65535 每个数的 1 的个数查询时把 32 位整数拆成高 16 位和低 16 位两次查表相加。private static final int[] BIT_TABLE new int[65536]; static { for (int i 0; i 65536; i) { BIT_TABLE[i] BIT_TABLE[i 1] (i 1); } } public int bitCount(int n) { return BIT_TABLE[n 0xffff] BIT_TABLE[(n 16) 0xffff]; }查表法把时间复杂度压到 O(1)代价是一张 256 KB 的 int 表如果 16 位表面试时你只要能把“预计算 拆分”的思路讲清楚就够不用现场真的建大表。另一条路线是分治SWAR思路是“先两两相加统计相邻 1 的个数再四四相加再八八相加最后折叠相加”。JDK 的Integer.bitCount实现就是这个套路它的代码虽然看起来像魔法但核心是每两位看成一个小计数单元然后用掩码提取和累加。面试里能说出0x55555555是奇数位掩码、0x33333333是两位掩码就已经能证明你深入读过源码。三种方法对比如下方法时间复杂度空间复杂度适用场景逐位扫描O(bitLength)O(1)最简单适合讲解KernighanO(bitCount)O(1)稀疏数字极快面试首选查表法O(1)O(2^16)高频调用、性能敏感分治 SWARO(1)O(1)JDK 内部实现无额外空间4.4 比特计数的邻居题目lowbit 和 2 的幂判断比特计数经常搭配两个变形题一起考。第一个是“判断一个数是不是 2 的整数次幂”。2 的幂二进制只有一个 1所以(n (n - 1)) 0就是答案。但要记住排除n 0因为 0 也满足这个条件但 0 不是 2 的幂。完整写法是n 0 (n (n - 1)) 0。第二个是“求一个数二进制里最右侧的 1 的位置”也就是 lowbit。n (-n)可以提取出最右边那个 1 代表的数值。比如12 -1212 是1100-12 是补码0100按 4 位看实际 int 是 32 位相与得到0100即 4。这个技巧在树状数组里是核心操作面试官如果问到“怎么快速拿到最低位的 1”你能写出n (-n)并且解释补码取反加一的过程就是加分项。5. 综合应用当位图、异或、比特计数出现在同一道题里5.1 判断字符串是否所有字符都不重复26 个 bit 的状态机经典题给定一个全是小写字母的字符串判断每个字母是否只出现一次不能使用额外的数据结构。常规思路是用 HashSet但更漂亮的解法是位图。因为小写字母只有 26 个一个 int 的 32 个 bit 完全够用每个 bit 对应一个字母是否出现过。public boolean isUnique(String s) { int marker 0; for (int i 0; i s.length(); i) { int bit 1 (s.charAt(i) - a); if ((marker bit) ! 0) { return false; } marker | bit; } return true; }这里的细节是1 (s.charAt(i) - a)不加括号也会出问题其实的优先级比-低所以1 (s.charAt(i) - a)中减法必须先加括号否则会先算1 s.charAt(i)再减 97结果完全错误。我在代码评审里见过有人把marker bit ! 0写成marker bit 0因为优先级更高这种 bug 很难肉眼发现但用一个有重复字符的字符串一测就露馅。面试时如果你能主动说出“这里必须加括号因为位运算优先级低于比较运算符”面试官对你的信任度会明显提升。5.2 汉明距离一行异或一行计数汉明距离就是两个整数二进制位不同的个数。计算方式是先异或再统计结果里的 1 的数量——这刚好把异或和比特计数两个知识点串起来了。public int hammingDistance(int x, int y) { int xor x ^ y; return Integer.bitCount(xor); }如果面试官要求不用 JDK 方法再手写 Kernighan 循环。这里异或负责“找不同”相同位异或为 0不同位异或为 1剩下只有统计 1 的个数一个问题。5.3 4 的幂判断2 的幂后再加一个“奇数位”条件判断一个数是不是 4 的幂有两个方案。方案一是 4 的幂必然是 2 的幂而且唯一的那个 1 必须出现在二进制第 0、2、4、6……偶数位。用掩码0x55555555表示二进制0101 0101 ...也就是所有奇数位为 0、偶数位为 1从右往左从第 0 位开始。public boolean isPowerOfFour(int n) { return n 0 (n (n - 1)) 0 (n 0x55555555) ! 0; }判断逻辑是先保证是正数再保证只有一个 1最后保证这个 1 落在偶数位上。如果你选择判断“1 不在奇数位”那就是(n 0xaaaaaaaa) 0两种等价。这个题的价值在于它要求你能快速把 2 的幂和位索引结合起来思考也是面试官用来区分“死记硬背”和“灵活运用”的常见手段。5.4 面试实战中的三条建议都是血泪教训第一条写位运算代码时把所有判断条件用括号包死。位运算符的优先级低于关系运算符、等于运算符、逻辑运算符高于赋值运算符。if (n 1 0)实际会执行1 0结果永远是 falsen 的奇偶性压根没判断上。第二条区分和的使用场景。只要你在循环右移一个可能是负数的数或者把一个 int 当无符号数看待就一律用。反过来当你想快点实现“除以 2”并且确认数字非负时用没问题。面试官看到你在非负场景刻意用、在无符号场景用就知道你是懂边界而不是瞎写的。第三条现场和面试官沟通时先把时间复杂度说清楚。位运算题通常能到 O(n) 时间 O(1) 空间但要注意“n”的指代。比如比特计数中“n 是非负数”时复杂度写 O(number of 1s) 比写 O(log n) 更精确位图统计时复杂度写 O(total bits / word size) 比写 O(n) 更体现功底。面试官问复杂度你要答的是“为什么是这个量级”而不是背结论。5.5 位运算代码的常见错误速查表这个表是我在陪跑的模拟面试里总结出来的几乎每个候选人都会中一招错误写法问题正确写法if ((n 1) 1)写成if (n 1 1)优先级高先算1 1再与 n 做位与(n 1) 11 bitIndexbitIndex 可能超过 31int 位移只取低 5 位结果错误用1L bitIndex循环右移用负数高位补 1死循环用n (n - 1)没判断n 00 会被误判为 2 的幂n 0 (n (n - 1)) 0位图set用而不用 目标位为 1 时加法产生进位污染相邻位(a b) 0少了括号位运算优先级低于必须写成(a b) 06. 复盘位运算刷题的三层境界我在带人准备 Java 面试时经常说位运算这种题考查的不只是“会不会”而是三层递进的理解。第一层是“能写对代码”知道异或、位图、比特计数的基本套路把题解出来第二层是“能说清为什么”解释为什么用异或而不是 HashMap、为什么用 long 而不是 int、为什么而非第三层是“能联系到生产环境”比如 HashMap 的桶定位(n - 1) hash实际上就是一个位图思想——用位与运算替代取模前提是 n 必须是 2 的幂。我个人在实际刷题过程中的体会是位运算最怕的不是不会而是会了但不敢写。很多人看到位运算代码短以为面试官会觉得“这太简单了”于是故意写复杂版本——这是本末倒置。面试官要的是优雅、准确的解法你写return n 0 (n (n - 1)) 0的时候如果还能补一句“这里利用了 2 的幂二进制只有一个 1 的性质”那就是满分回答。最后再分享一个准备时对我帮助很大的小技巧把上面几道题的代码写完以后不要只看测试用例通过就完事把每个解法用几个边界值手推一遍。比如singleNumber(new int[]{-1, -1, 2})missingNumber(new int[]{0, 1, 3})hammingWeight(-1)。负数、零、最大值这几个边界一旦你手推过面试时脑子里的反应速度会快非常多。位运算的直觉不是看出来的是推出来的。