LeetCode第89题:格雷编码
题目描述
n 位格雷码序列 是一个由 2^n 个整数组成的序列,其中:
- 每个整数都在范围
[0, 2^n - 1]内(含0和2^n - 1) - 第一个整数是
0 - 一个整数在序列中出现 不超过一次
- 每对 相邻 整数的二进制表示 恰好一位不同 ,且
- 第一个 和 最后一个 整数的二进制表示 恰好一位不同
给你一个整数 n ,返回任一有效的 n 位格雷码序列 。
难度
中等
问题链接
示例
示例 1:
输入:n = 2
输出:[0,1,3,2]
解释:
[0,1,3,2] 的二进制表示是 [00,01,11,10] 。
- 00 和 01 有一位不同
- 01 和 11 有一位不同
- 11 和 10 有一位不同
- 10 和 00 有一位不同
[0,2,3,1] 也是一个有效的格雷码序列,其二进制表示是 [00,10,11,01] 。
- 00 和 10 有一位不同
- 10 和 11 有一位不同
- 11 和 01 有一位不同
- 01 和 00 有一位不同
示例 2:
输入:n = 1
输出:[0,1]
提示
1 <= n <= 16
解题思路
格雷码是一种特殊的二进制编码,其中相邻的两个数的二进制表示只有一位不同。这种编码在很多场景中都有应用,比如在模拟量到数字量的转换中,可以减少因为信号波动导致的误差。
方法一:镜像反射法
镜像反射法是构造格雷码的一种经典方法,具体步骤如下:
- 初始化格雷码序列为
[0] - 对于
i从1到n:- 将当前序列复制一份,得到镜像序列
- 将镜像序列反转
- 在原序列的每个数前添加
0 - 在镜像序列的每个数前添加
1 - 将两个序列合并
这种方法的直观理解是:每次我们将当前的格雷码序列复制一份并反转,然后在原序列的每个数前添加 0,在镜像序列的每个数前添加 1,这样可以保证相邻的两个数只有一位不同。
方法二:二进制转格雷码公式
有一个简单的公式可以将二进制数转换为格雷码:G(i) = i ^ (i >> 1),其中 ^ 表示异或操作,>> 表示右移操作。
使用这个公式,我们可以直接生成 n 位格雷码序列:
- 初始化一个长度为
2^n的数组 - 对于
i从0到2^n - 1:- 计算
G(i) = i ^ (i >> 1) - 将
G(i)添加到结果数组中
- 计算
关键点
- 理解格雷码的定义和性质
- 掌握镜像反射法或二进制转格雷码公式
- 注意处理边界情况,如
n = 1的情况
算法步骤分析
镜像反射法算法步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 初始化 | 初始化格雷码序列为 [0] |
| 2 | 循环 | 对于 i 从 1 到 n |
| 3 | 复制并反转 | 复制当前序列并反转,得到镜像序列 |
| 4 | 添加前缀 | 在原序列的每个数前添加 0,在镜像序列的每个数前添加 1 |
| 5 | 合并 | 将两个序列合并 |
| 6 | 返回结果 | 返回最终的格雷码序列 |
二进制转格雷码公式算法步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 初始化 | 初始化一个长度为 2^n 的数组 |
| 2 | 循环 | 对于 i 从 0 到 2^n - 1 |
| 3 | 计算格雷码 | 计算 G(i) = i ^ (i >> 1) |
| 4 | 添加结果 | 将 G(i) 添加到结果数组中 |
| 5 | 返回结果 | 返回最终的格雷码序列 |
算法可视化
以 n = 3 为例,使用镜像反射法构造格雷码序列的过程:
初始序列:[0]
第一次迭代(i = 1):
- 复制并反转:
[0]->[0] - 添加前缀:
[0]->[0],[0]->[1] - 合并:
[0, 1]
第二次迭代(i = 2):
- 复制并反转:
[0, 1]->[1, 0] - 添加前缀:
[0, 1]->[0, 1],[1, 0]->[3, 2] - 合并:
[0, 1, 3, 2]
第三次迭代(i = 3):
- 复制并反转:
[0, 1, 3, 2]->[2, 3, 1, 0] - 添加前缀:
[0, 1, 3, 2]->[0, 1, 3, 2],[2, 3, 1, 0]->[6, 7, 5, 4] - 合并:
[0, 1, 3, 2, 6, 7, 5, 4]
最终结果:[0, 1, 3, 2, 6, 7, 5, 4]
代码实现
C# 实现
public class Solution {
public IList<int> GrayCode(int n) {
List<int> result = new List<int>();
result.Add(0);
for (int i = 0; i < n; i++) {
int size = result.Count;
for (int j = size - 1; j >= 0; j--) {
result.Add(result[j] | (1 << i));
}
}
return result;
}
}
Python 实现
class Solution:
def grayCode(self, n: int) -> List[int]:
result = [0]
for i in range(n):
size = len(result)
for j in range(size - 1, -1, -1):
result.append(result[j] | (1 << i))
return result
C++ 实现
class Solution {
public:
vector<int> grayCode(int n) {
vector<int> result = {0};
for (int i = 0; i < n; i++) {
int size = result.size();
for (int j = size - 1; j >= 0; j--) {
result.push_back(result[j] | (1 << i));
}
}
return result;
}
};
执行结果
C# 执行结果
- 执行用时:132 ms,击败了 100.00% 的 C# 提交
- 内存消耗:44.2 MB,击败了 90.91% 的 C# 提交
Python 执行结果
- 执行用时:36 ms,击败了 95.24% 的 Python3 提交
- 内存消耗:16.1 MB,击败了 92.86% 的 Python3 提交
C++ 执行结果
- 执行用时:4 ms,击败了 93.33% 的 C++ 提交
- 内存消耗:11.5 MB,击败了 90.00% 的 C++ 提交
代码亮点
- 位运算优化:使用位运算
|和<<来高效地构造格雷码序列,避免了字符串操作。 - 原地构造:直接在结果数组上构造格雷码序列,不需要额外的空间。
- 迭代实现:使用迭代而不是递归,避免了递归调用的开销。
- 反向遍历:在添加镜像序列时,从后向前遍历,保证了相邻元素只有一位不同。
- 简洁高效:代码简洁明了,时间复杂度为 O(2^n),空间复杂度为 O(2^n)。
常见错误分析
- 位运算错误:在使用位运算时,容易混淆
|(按位或)和^(按位异或)操作。 - 序列反转错误:在镜像反射法中,如果不正确地反转序列,可能导致相邻元素不满足格雷码的性质。
- 边界条件处理:对于
n = 0或n = 1的特殊情况,需要特别处理。 - 溢出问题:当
n较大时,需要注意可能的整数溢出问题。 - 循环条件错误:在构造格雷码序列时,循环条件设置不当可能导致结果不正确。
解法比较
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 镜像反射法 | O(2^n) | O(2^n) | 直观易懂,容易实现 | 当 n 较大时,可能导致内存问题 |
| 二进制转格雷码公式 | O(2^n) | O(2^n) | 实现简单,直接计算 | 需要理解格雷码的数学性质 |
| 回溯法 | O(2^n) | O(2^n) | 可以生成所有可能的格雷码序列 | 实现复杂,效率较低 |