LeetCode 190. 颠倒二进制位:两种解法详解

LeetCode 上一道经典的位运算题目------190. 颠倒二进制位。这道题看似简单,实则藏着位运算的核心技巧,尤其是第二种"分治颠倒"的思路,非常值得深入理解,既能巩固位运算基础,也能锻炼逻辑思维。

先明确题目要求:给定一个 32 位有符号整数,将其二进制位全部颠倒,返回颠倒后的整数。比如输入二进制 00000010100101000001111010011100,输出就是 00111001011110000010100101000000

解法一:逐位颠倒(基础易懂,适合入门)

这是最直观的思路:从原数字的最低位(最右边)开始,依次取出每一位二进制数,然后将其放到结果的对应高位(最左边),循环 32 次(因为是 32 位整数),最终得到颠倒后的结果。

先看完整代码:

typescript 复制代码
function reverseBits_1(n: number): number {
    let rev = 0; // 存储颠倒后的结果,初始为0(二进制全0)
    // 循环32次(覆盖32位),若n提前变为0,可提前退出(优化效率)
    for (let i = 0; i < 32 && n !== 0; ++i) {
        // 1. 取出n的最低位:n & 1(二进制中,只有最低位是1时结果为1,否则为0)
        // 2. 将取出的最低位移到对应高位:<< (31 - i)(第i次循环,对应31-i位)
        // 3. 用或运算(|)将该位存入rev,不影响已存入的高位
        rev |= (n & 1) << (31 - i);
        // 4. n右移1位(无符号右移>>>),丢弃已处理的最低位,准备处理下一位
        n >>>= 1; 
    }
    // 无符号右移0位,确保结果是32位无符号整数(避免符号位干扰)
    return rev >>> 0; 
}

关键细节解析(必看)

  • n & 1:这是位运算中"取最低位"的经典操作。比如 n=5(二进制 101),n&1=1(取最低位1);n=4(100),n&1=0(取最低位0)。

  • << (31 - i):循环第 i 次(从0开始),我们取出的是原数字的第 i 位(从右数),需要放到结果的第 31 - i 位(从右数,即从左数的对应位置)。比如 i=0 时,取最低位,放到最高位(31位);i=31时,取最高位,放到最低位(0位)。

  • n >>>= 1 :这里必须用无符号右移(>>>),而不是有符号右移(>>)。因为如果是有符号整数,右移时符号位会补1,导致处理负数时出错;无符号右移会在高位补0,符合32位无符号整数的处理逻辑。

  • return rev >>> 0:同样是为了确保结果是32位无符号整数。在TypeScript/JavaScript中,整数可能会有符号位,无符号右移0位可以将其转为无符号数,避免因符号位导致的结果错误。

解法一总结

优点:思路简单,容易理解,代码量少,适合新手入门位运算。

缺点:循环32次,时间复杂度是 O(32) = O(1)(固定循环次数,属于常数时间),效率其实不低,但还有更优的"分治"思路,可以减少位运算的次数。

解法二:分治颠倒(进阶技巧,高效简洁)

这种思路借鉴了"分而治之"的思想:将32位二进制数拆分成更小的单元(2位、4位、8位、16位),先颠倒每个小单元内部,再将颠倒后的小单元整体颠倒,最终实现整个32位的颠倒。

核心原理:利用位掩码(mask)分离出不同长度的单元,通过"右移+与掩码""左移+与掩码"的组合,实现单元内部的颠倒,再合并单元。

完整代码:

typescript 复制代码
function reverseBits_2(n: number): number {
    // 定义4个位掩码,用于分离不同长度的二进制单元
    const M1 = 0x55555555; // 01010101 01010101 01010101 01010101(每2位一组,01交替)
    const M2 = 0x33333333; // 00110011 00110011 00110011 00110011(每4位一组,0011交替)
    const M4 = 0x0f0f0f0f; // 00001111 00001111 00001111 00001111(每8位一组,00001111交替)
    const M8 = 0x00ff00ff; // 00000000 11111111 00000000 11111111(每16位一组,0000000011111111交替)
    
    let result: number = n;
    
    // 第一步:颠倒每2位(比如 01 → 10,10 → 01)
    result = ((result >>> 1) & M1) | ((result & M1) << 1);
    // 第二步:颠倒每4位(比如 0011 → 1100,0101 → 1010)
    result = ((result >>> 2) & M2) | ((result & M2) << 2);
    // 第三步:颠倒每8位
    result = ((result >>> 4) & M4) | ((result & M4) << 4);
    // 第四步:颠倒每16位
    result = ((result >>> 8) & M8) | ((result & M8) << 8);
    
    // 最后:颠倒整个32位(前16位和后16位互换),并转为无符号整数
    return ((result >>> 16) | (result << 16)) >>> 0;
}

分治步骤拆解(以8位二进制为例,便于理解)

假设我们有8位二进制数 11001010,用分治思路颠倒的过程如下:

  1. 每2位颠倒 :拆分为 11、00、10、10,颠倒后为 11、00、01、01,合并为 11000101

  2. 每4位颠倒 :拆分为 1100、0101,颠倒后为 0011、1010,合并为00111010

  3. 每8位颠倒 :拆分为 0011、1010(此时8位拆分为两个4位),颠倒后为 10100011,即最终颠倒结果。

32位的逻辑和8位完全一致,只是拆分的单元更长,通过4个掩码逐步实现"从小单元到整体"的颠倒。

关键细节解析

  • 位掩码的作用:比如 M1(0x55555555),二进制每2位为一组,每组是01,用它和result做"与运算",可以只保留result的奇数位(第1、3、5...31位);同理,M2保留每4位的前2位,M4保留每8位的前4位,M8保留每16位的前8位。

  • 颠倒单元的核心操作 :以 ((result >>> 1) & M1) | ((result & M1) << 1) 为例:

    • (result >>> 1) & M1:将result右移1位,再和M1做与运算,得到"原奇数位右移1位"的结果(即原奇数位变成偶数位);

    • (result & M1) << 1:将result和M1做与运算,得到原奇数位,再左移1位(即原奇数位变成偶数位);

    • 两者用或运算(|)合并,就实现了"每2位颠倒"。

  • 效率优势:整个过程只需要5次位运算(4次单元颠倒+1次整体颠倒),无论输入是什么,都不需要循环,时间复杂度依然是 O(1),但实际运算次数比解法一更少,效率更高。

解法二总结

优点:高效简洁,位运算技巧性强,适合深入理解位掩码和分治思想,在面试中写出这种解法,能体现对位运算的熟练掌握。

缺点:思路相对抽象,需要理解位掩码的作用和分治的拆分逻辑,新手可能需要多琢磨几遍。

两种解法对比 & 实战建议

解法 时间复杂度 空间复杂度 特点 适用场景
逐位颠倒(解法一) O(1) O(1) 思路简单,易理解,循环32次 新手入门、快速解题、面试中快速写出正确代码
分治颠倒(解法二) O(1) O(1) 技巧性强,运算次数少,效率高 深入理解位运算、面试加分、追求代码简洁高效

常见易错点提醒

  • 忘记用无符号右移(>>>):无论是处理n还是结果,都必须用无符号右移,否则符号位会干扰,导致负数处理出错。

  • 循环次数不足32次:即使n提前变为0,也需要循环32次(或者最后用rev >>> 0补全32位),否则会导致高位补0不完整,结果错误。

  • 位掩码记错:解法二中的4个掩码是固定的,记错掩码会导致拆分单元错误,最终结果出错,建议记住这4个常用掩码(对应2、4、8、16位拆分)。

最后总结

LeetCode 190题是位运算的经典入门题,两种解法各有优势:解法一胜在易懂,解法二胜在高效。建议新手先掌握解法一,理解"逐位取数、逐位放置"的核心逻辑,再深入研究解法二的分治思想和位掩码技巧。

其实位运算的核心就是"操作二进制的每一位",多练习这类题目,就能慢慢掌握各种位运算技巧(比如取位、移位、掩码、或/与/异或运算),后续遇到更复杂的位运算题目(如位1的个数、两数相加等)也能迎刃而解。

相关推荐
掘金者阿豪35 分钟前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
kyriewen1 小时前
折腾了半年 AI 编程工作流,最后发现效率瓶颈是桌上那块屏幕
前端·javascript·ai编程
蜗牛前端1 小时前
codex 全流程开发上线的高颜值礼簿小程序
前端·微信小程序
大龄秃头程序员2 小时前
我在图文流 App 里落地双层缓存、弱网降级与 OOM 治理
前端
老王以为2 小时前
React Renderer 分离的多平台架构
前端·react native·react.js
hunterandroid2 小时前
Kotlin Coroutines 与 Flow:让异步任务更清晰
前端
Bigger3 小时前
从零搭建 AI 代码审查服务:一份前端也能看懂的 Python 学习笔记
前端·ci/cd·ai编程
lichenyang4533 小时前
JSAPI、NAPI、Biz、Imp:ASCF Demo 如何真正调用系统能力和 C++ 能力
前端
lichenyang4533 小时前
IPC、JSVM、UIThread、libuv:ASCF 架构图里最容易混的几个词
前端
用户059540174463 小时前
Redis记忆存储故障恢复测试踩坑实录:手动测试让我漏掉了2个一致性Bug
前端·css