目录

题目描述

面试题 05.07. 配对交换

题意分析

把整数二进制表示中的第 0 位与第 1 位、第 2 位与第 3 位……成对交换,返回交换后的整数。下标从最低有效位开始计数,所以要交换的是所有偶数位与其左边相邻的奇数位。

逐对读取再写回当然可行,但需要循环和多次分支。所有偶数位可以被同一个掩码一次取出,所有奇数位也一样;分组后整体移动一格再合并,正好完成全部配对交换。

32 位交替掩码分别是 0x55555555(二进制 0101...,保留偶数位)和 0xAAAAAAAA(二进制 1010...,保留奇数位)。Java 中右移奇数位应使用无符号右移,避免符号扩展污染高位。

解法:奇偶位掩码分组移动

核心思路

num & 0x55555555 只留下原来的偶数位,把它左移一格后,每一位恰好落到同一对的奇数位置。num & 0xAAAAAAAA 只留下原来的奇数位,把它无符号右移一格后,恰好落到偶数位置。

两组移动后的 1 不会落在同一位置:前一组只占奇数位,后一组只占偶数位,因此用按位或合并不会产生冲突。每个原始位被且只被一个掩码保留,也不会丢位或重复。

这比逐对循环更适合作为面试答案:两次与、两次移位、一次或就完成固定 32 位上的所有交换,掩码的含义也可以直接从最低四位 0101 / 1010 推出来,而不必死记十六进制常量。

解题步骤

  • 0x55555555 取出偶数位,左移一格。
  • 0xAAAAAAAA 取出奇数位,无符号右移一格。
  • 对两部分按位或,得到每对位置都交换后的结果。

num = 10 = 0b1010 为例。偶数位部分为 0000,左移后仍为 0000;奇数位部分为 1010,右移后得到 0101,结果为 5。逐对看也是 (10)(10) → (01)(01)

代码实现

// 偶数位左移,奇数位无符号右移,再合并。
class Solution {
    public int exchangeBits(int num) {
        return ((num & 0x55555555) << 1) | ((num & 0xaaaaaaaa) >>> 1);
    }
}
// 偶数位左移,奇数位右移,再合并。
func exchangeBits(num int) int {
    return ((num & 0x55555555) << 1) | (num&0xaaaaaaaa)>>1
}

复杂度分析

  • 时间复杂度:$O(1)$,固定执行常数次位运算,与整数中 1 的个数无关。
  • 空间复杂度:$O(1)$。

关键点总结

  • 交替位分组问题优先考虑 0x555555550xAAAAAAAA;前者保留偶数位,后者保留奇数位。
  • 先掩码再移动非常重要:它保证被移出的位不会串入相邻分组,也让两部分合并时互不重叠。
  • 面试追问若改成 64 位,只需把掩码扩为 0x55555555555555550xAAAAAAAAAAAAAAAA,并使用 64 位无符号语义。

易错点总结

  • 两个掩码写反或移动方向写反num = 2(10) 应变成 1(01),若奇数位左移则会得到 4。
  • 先移动再掩码:边界位可能先被符号扩展或移出,再也无法用掩码恢复。
  • Java 对奇数位使用 >>:当最高位参与时会用 1 补高位;>>> 才表达“把位模式整体右移”的意图。
  • 用加法合并却没证明两部分不重叠:本题虽然结果相同,但按位或更直接表达“合并两个互斥位集合”。

相似题目

题目 难度 考察点
190. 颠倒二进制位 简单 按固定映射重排 32 个二进制位
191. 位1的个数 简单 基础掩码与位计数
461. 汉明距离 简单 异或后统计不同位
面试题 05.06. 整数转换 简单 与 461 同型,强调 32 位补码语义