← 返回题目列表

如何颠倒一个 32 位无符号整数的二进制位?(LeetCode 190)

高频 简单 第 4 / 26 题 更新于 2026/07/28
位运算位反转无符号右移分治

简化版

把一个 32 位无符号整数的二进制位首尾颠倒(第 0 位和第 31 位互换、第 1 位和第 30 位互换……)。逐位法:循环 32 次,每次取出 n 的最低位,放到结果的对应高位——res = (res << 1) | (n & 1),然后 n >>>= 1(无符号右移)。Java 里因为是有符号 int,取位和右移要用 & 1>>> 避免符号位干扰。进阶可用分治交换(按 16-8-4-2-1 分块两两交换)做到 O(1)。

详细版

解法一:逐位反转(O(32))

int reverseBits(int n) {
    int res = 0;
    for (int i = 0; i < 32; i++) {
        res = (res << 1) | (n & 1);   // 结果左移腾位,接上 n 的最低位
        n >>>= 1;                      // n 无符号右移,处理下一位
    }
    return res;
}

解法二:分治交换(O(1),进阶)

int reverseBits(int n) {
    n = (n >>> 16) | (n << 16);                                 // 交换高低 16 位
    n = ((n & 0xff00ff00) >>> 8) | ((n & 0x00ff00ff) << 8);     // 每 16 位内交换字节
    n = ((n & 0xf0f0f0f0) >>> 4) | ((n & 0x0f0f0f0f) << 4);     // 每字节内交换 4 位
    n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333) << 2);     // 每 4 位内交换 2 位
    n = ((n & 0xaaaaaaaa) >>> 1) | ((n & 0x55555555) << 1);     // 相邻位交换
    return n;
}
  • 逐位法核心res 每轮左移一位腾出最低位,把 n 当前最低位接上去——相当于把 n 从低到高的位「倒着」堆进 res。
  • 必须用 >>>:n 是有符号 int,负数用 >> 会补符号位出错。
  • 复杂度:解法一 O(32),解法二 O(1)(固定 5 步分治)。

完整版教学

一、题意:位的镜像翻转

把 32 位整数看成一个长度 32 的 0/1 串,要做的是把这个串首尾倒过来:原来的第 i 位,变成结果的第 31 - i 位。注意是「无符号」——我们只关心 32 个 bit 的排列,不管它当有符号数解释是正是负。

二、逐位法:从低位取、往高位堆

思路是「一边拆 n、一边搭 res」:

  • 每一轮,取出 n 的最低位n & 1)。
  • 把它接到 res 的最低位上,但接之前先让 res 左移一位(res << 1)腾出位置:res = (res << 1) | (n & 1)
  • 然后 n >>>= 1,n 舍弃已处理的最低位。

循环 32 次。想清楚为什么这样就是反转:n 第一次取出的是第 0 位,它经过 32 次左移,最终被推到 res 的第 31 位;n 最后一次取出的是第 31 位,它刚放进 res 时在第 0 位、之后不再左移,留在第 0 位。恰好首尾对调——先取出的位被推得最高,后取出的留在最低,实现镜像。

三、为什么必须用无符号右移 >>>

Java 的 int 有符号。如果 n 是负数(最高位 1),用算术右移 >> 会在高位不断补 1,n >>= 1 永远清不空高位,导致取位错误甚至死循环。>>>(逻辑右移)高位补 0,才能把每一位干净地移下来。取最低位用 n & 1 本身没问题,关键是右移要 >>>。这是 Java 位运算处理负数/无符号的通用注意点。

四、解法二:分治交换(O(1) 常数步)

逐位法要 32 次循环。分治法用「二分交换」5 步搞定:

  1. 交换高 16 位和低 16 位;
  2. 在每 16 位块内,交换两个字节(8 位);
  3. 在每字节内,交换两个 4 位;
  4. 在每 4 位内,交换两个 2 位;
  5. 交换相邻的两个 1 位。

每一步用一对掩码 + 移位并行完成所有块的交换(如 0xaaaaaaaa 取偶数位、0x55555555 取奇数位,右移/左移一位后 | 合并)。5 步下来,所有位都镜像到位。它是「归并式」的位反转,无循环、纯常数操作,适合对性能极致要求或面试炫技。

五、易错点与应用

  • >>> vs >>:逐位法负数必须 >>>,这是最高频错误。
  • 循环固定 32 次:不能写「n != 0 就循环」——因为要输出满 32 位,高位的 0 也要参与移位占位。
  • 应用背景:位反转在图像处理(FFT 的位反转置换)、网络字节序、哈希扰动里都有用。面试主要考对「逐位搬移 + >>>」的理解。
  • 调用内置Integer.reverse(n) 直接返回结果,但面试要求手写。

六、逐轮保持“已翻转前缀”不变量

固定处理 32 轮比“处理到 n 为 0”更稳,因为输入的前导零在输出中会变成尾随零,它们也属于 32 位反转的一部分。第 k 轮结束后,结果 res 的低 k 位等于原数低 k 位的逆序;下一轮先左移结果腾位,再接入原数当前最低位。

用 8 位演示 n=00010110
轮 1: res=00000000, 取 0
轮 2: res=00000001, 取 1
轮 3: res=00000011, 取 1
继续处理剩余 5 位(包括前导 0)
最终: res=01101000
校验:原第 i 位移动到结果第 7-i 位
校验维度本题必须保持的结论
循环/递推不变量完成 k 轮后,已经消费原数最低 k 位,并按相反顺序放入 res 的最低 k 位。
边界条件必须恰好循环 32 次;Java 没有无符号 int 类型,读取位模式时用 >>>
复杂度与代价逐位法固定 32 轮,O(1) 时间 O(1) 空间;分治掩码法固定 5 轮交换。
自检方式用最小输入、典型输入和触及边界的输入分别手推,结果应与定义一致。

这组推演的作用不是替代证明,而是把抽象规则落到每一步状态变化上。面试写完代码后,应主动指出不变量如何保证不漏解,以及边界为何不会越界或死循环。若手推结果与定义不一致,应先修正状态语义,而不是继续给代码打补丁。

七、面试现场如何验证这道题

位运算题的代码通常很短,真正容易错的是把数学整数、固定宽度位模式和语言移位规则混在一起。本题应先复述这条不变量:完成 k 轮后,已经消费原数最低 k 位,并按相反顺序放入 res 的最低 k 位。只有代码的初始化、更新顺序和终止条件都维持它,样例通过才具有证明力。

  • 典型路径从“用 8 位演示 n=00010110”开始手推,最后应得到“校验:原第 i 位移动到结果第 7-i 位”。
  • 边界复核:必须恰好循环 32 次;Java 没有无符号 int 类型,读取位模式时用 >>>
  • 代价复核:逐位法固定 32 轮,O(1) 时间 O(1) 空间;分治掩码法固定 5 轮交换。
  • 用全 0、只有一个 1、最高位为 1 三类位模式检查掩码。
  • 涉及负数时把值写成固定宽度补码,确认使用 >> 还是 >>>
  • 涉及左移时检查移位距离和溢出后是否仍符合题目的位模式语义。

最后要主动构造一个“少写一次循环、改变一个不等号或交换两行更新就会失败”的反例。能说清反例破坏了哪条不变量,比只报出复杂度更能证明真正掌握了算法。

记忆钩子:本题的代码可以压缩,但“完成 k 轮后,已经消费原数最低 k 位,并按相反顺序放入 res 的最低 k 位。”这条正确性主线不能省。

八、常见误区与追问

  • 误区:n 变成 0 就可以提前停止且不影响结果。 若仍按每轮左移的写法提前停止,会漏掉需要把已取位推到最终高位的剩余轮次。
  • 误区:>>>>> 对本题没有区别。 最高位为 1 时 >> 补 1,会污染后续取位;>>> 才按无符号位模式补 0。
  • 误区:题目是反转十进制数字。 这里反转固定 32 位二进制位,和整数 123→321 完全不同。
  • 追问:为什么每轮要先左移 res? 它为新取出的位腾出最低位,并把较早取出的低位逐步推向更高位置。
  • 追问:分治交换为何只需 5 轮? 32 位按 16、8、4、2、1 位块逐层交换,块大小每轮折半。
  • 追问:连续调用两次 reverseBits 会怎样? 位反转是对合变换,固定宽度下执行两次会恢复原位模式。

九、加强记忆

颠倒二进制位 = 逐位「从低取、往高堆」:循环 32 次,res = (res << 1) | (n & 1) 把 n 当前最低位接到 res(res 先左移腾位),n >>>= 1 处理下一位。先取出的位被推到最高、后取的留最低,实现镜像。Java 负数必须用无符号右移 >>>>> 补符号位会出错),且固定循环 32 次(高位 0 也要占位)。进阶用分治交换(16-8-4-2-1 五步掩码并行交换)达 O(1)。核心:>>> 别写成 >>