跳到主要内容

LeetCode 89. 格雷编码

本节目标

用 i ^ (i >> 1) 直接构造相邻编码恰好一位不同的循环序列。

格雷编码把连续编号映射为相邻仅一位不同的二进制串。这是位运算拓展中“构造不变量”框架的一个直接应用:不搜索下一项,而是由每个编号一次算出它的位置。

查看原题

题意与约束

给定非负整数 n,返回一个长度为 2^n 的整数序列。序列中的每个数都在 [0, 2^n) 内,任意相邻两项的二进制表示恰好一位不同;最后一项与第一项也必须满足同一条件。

  • 1 <= n <= 16
  • 序列从 0 开始。
  • 题目允许多种有效答案;这里构造标准的二进制反射格雷码顺序。

从二进制编号构造格雷码

按编号 i = 0, 1, ..., 2^n - 1 依次计算:

gray(i) = i ^ (i >> 1)

二位编号的映射如下:

i二进制 ii >> 1gray(i)
0000000
1010001
2100111
3110110

因此得到 00, 01, 11, 10,相邻项依次只翻转最低位、次低位、最低位。

为什么 i ^ (i >> 1) 有效

相邻的二进制编号 ii + 1 会因为进位改变一段连续后缀:若末尾有若干个 1,它们会变为 0,紧邻它们左侧的 0 变为 1。也就是说,编号的变化不是通常只改一位,而是一段连续位同时改变。

i ^ (i >> 1) 中,右移结果的第 k 位来自原数第 k + 1 位;按通常从高位写到低位的二进制记法,就是每一位都与它左边相邻的更高位比较。连续后缀内部的两个相邻位会一起翻转,异或结果不变;只有这段变化的边界会留下一个不同的位。因此连续的 gray(i)gray(i + 1) 恰好相差一位。

此外,异或变换可逆:从高位到低位逐位恢复 i,所以不同的编号不会映射到同一个格雷码。遍历全部 2^n 个编号便恰好得到范围内的全部编码。

首尾相邻条件

第一项是 gray(0) = 0。最后一个编号为 2^n - 1,它的低 n 位全为 1;右移一位后低 n - 1 位为 1。两者异或后只剩最高的第 n - 1 位,即 gray(2^n - 1) = 2^(n - 1)

它与 0 的异或正好是一个二进制单比特,因此最后一项与第一项同样只相差一位,序列构成环。

代码实现

C++ 和 Python 都直接遍历编号并计算 i ^ (i >> 1),无需递归、回溯或维护已生成序列。

C++17
#include <vector>

using namespace std;

class Solution {
public:
vector<int> grayCode(int n) {
vector<int> answer;
for (int i = 0; i < (1 << n); i++) {
answer.push_back(i ^ (i >> 1));
}
return answer;
}
};

复杂度分析

需要生成 2^n 个结果,每项只做常数次位运算,时间复杂度为 O(2^n)。返回数组本身占用 O(2^n) 空间,除结果外只使用常数额外空间。

易错点

  • 写成 i ^ (i << 1):移位方向错误,不能得到标准格雷码顺序。
  • 只检查相邻项,不检查最后一项与 0:题目要求的是循环序列。
  • 把普通二进制递增序列当作格雷码:进位可能同时改变多位,例如 0110 改变两位。
  • 用回溯搜索所有排列:本题已有直接构造公式,搜索既更慢也更难证明覆盖完整。

模式迁移

遇到“连续状态的变化必须局部化”的构造题时,先观察原始编号变化会在哪一段扩散,再寻找能让段内变化相互抵消的相邻位关系。本题的右移异或正好把连续后缀的翻转压缩为边界的一位;这一思路可与位运算拓展中的二进制结构和构造不变量一起使用。