LeetCode第89题_格雷编码

LeetCode第89题:格雷编码

题目描述

n 位格雷码序列 是一个由 2^n 个整数组成的序列,其中:

  • 每个整数都在范围 [0, 2^n - 1] 内(含 02^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

解题思路

格雷码是一种特殊的二进制编码,其中相邻的两个数的二进制表示只有一位不同。这种编码在很多场景中都有应用,比如在模拟量到数字量的转换中,可以减少因为信号波动导致的误差。

方法一:镜像反射法

镜像反射法是构造格雷码的一种经典方法,具体步骤如下:

  1. 初始化格雷码序列为 [0]
  2. 对于 i1n
    • 将当前序列复制一份,得到镜像序列
    • 将镜像序列反转
    • 在原序列的每个数前添加 0
    • 在镜像序列的每个数前添加 1
    • 将两个序列合并

这种方法的直观理解是:每次我们将当前的格雷码序列复制一份并反转,然后在原序列的每个数前添加 0,在镜像序列的每个数前添加 1,这样可以保证相邻的两个数只有一位不同。

方法二:二进制转格雷码公式

有一个简单的公式可以将二进制数转换为格雷码:G(i) = i ^ (i >> 1),其中 ^ 表示异或操作,>> 表示右移操作。

使用这个公式,我们可以直接生成 n 位格雷码序列:

  1. 初始化一个长度为 2^n 的数组
  2. 对于 i02^n - 1
    • 计算 G(i) = i ^ (i >> 1)
    • G(i) 添加到结果数组中

关键点

  • 理解格雷码的定义和性质
  • 掌握镜像反射法或二进制转格雷码公式
  • 注意处理边界情况,如 n = 1 的情况

算法步骤分析

镜像反射法算法步骤

步骤 操作 说明
1 初始化 初始化格雷码序列为 [0]
2 循环 对于 i1n
3 复制并反转 复制当前序列并反转,得到镜像序列
4 添加前缀 在原序列的每个数前添加 0,在镜像序列的每个数前添加 1
5 合并 将两个序列合并
6 返回结果 返回最终的格雷码序列

二进制转格雷码公式算法步骤

步骤 操作 说明
1 初始化 初始化一个长度为 2^n 的数组
2 循环 对于 i02^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++ 提交

代码亮点

  1. 位运算优化:使用位运算 |<< 来高效地构造格雷码序列,避免了字符串操作。
  2. 原地构造:直接在结果数组上构造格雷码序列,不需要额外的空间。
  3. 迭代实现:使用迭代而不是递归,避免了递归调用的开销。
  4. 反向遍历:在添加镜像序列时,从后向前遍历,保证了相邻元素只有一位不同。
  5. 简洁高效:代码简洁明了,时间复杂度为 O(2^n),空间复杂度为 O(2^n)。

常见错误分析

  1. 位运算错误:在使用位运算时,容易混淆 |(按位或)和 ^(按位异或)操作。
  2. 序列反转错误:在镜像反射法中,如果不正确地反转序列,可能导致相邻元素不满足格雷码的性质。
  3. 边界条件处理:对于 n = 0n = 1 的特殊情况,需要特别处理。
  4. 溢出问题:当 n 较大时,需要注意可能的整数溢出问题。
  5. 循环条件错误:在构造格雷码序列时,循环条件设置不当可能导致结果不正确。

解法比较

解法 时间复杂度 空间复杂度 优点 缺点
镜像反射法 O(2^n) O(2^n) 直观易懂,容易实现 当 n 较大时,可能导致内存问题
二进制转格雷码公式 O(2^n) O(2^n) 实现简单,直接计算 需要理解格雷码的数学性质
回溯法 O(2^n) O(2^n) 可以生成所有可能的格雷码序列 实现复杂,效率较低

相关题目

  • LeetCode 717. 1比特与2比特字符
  • LeetCode 338. 比特位计数
  • LeetCode 1238. 循环码排列
  • LeetCode 1611. 使整数变为 0 的最少操作次数

你可能感兴趣的:(算法,leetcode,算法,职场和发展,c++,python,unity,游戏程序)