89. Gray Code(格雷编码)
1. 题目描述
格雷编码是一个二进制数字系统,在该系统中,两个连续的数值仅有一个位数的差异。
给定一个代表编码总位数的非负整数 n,打印其格雷编码序列。即使有多个不同答案,你也只需要返回其中一种。
格雷编码序列必须以 0 开头。
示例 1:
输入: 2
输出: [0,1,3,2]
解释:
00 - 0
01 - 1
11 - 3
10 - 2
————————————
对于给定的 n,其格雷编码序列并不唯一。
例如,[0,2,3,1] 也是一个有效的格雷编码序列。00 - 0
10 - 2
11 - 3
01 - 1
示例 2:
输入: 0
输出: [0]
解释: 我们定义格雷编码序列必须以 0 开头。给定编码总位数为 n 的格雷编码序列,其长度为 2n。当 n = 0 时,长度为 20 = 1。因此,当 n = 0 时,其格雷编码序列为 [0]。
2. 回溯(Backtracking)
2.1 解题思路
首先这道题的最后一种解法(4. 位操作)简直就是神之技巧哇,叹为观止,建议大家务必看看最后一种解法,万一面试遇到相似题目,又要求用这种解法,如果没有事先了解过,现场临场发挥基本不太可能的说。好啦,不废话了,让我们先来讲解一下回溯法的思路吧。
先看看n = 3,情况一:
- 000
- 001
- 011
- 010
- 110
- 111
- 100
- 101
这种排列方法的递归顺序是:先找到最右边的数字,翻转之后,向后找到其最右边的数字在进行翻转;递归思路如下图所示:
情况二:
- 000
- 001
- 011
- 010
- 110
- 100
- 101
- 111
情况二的递归顺序是:先找到最右边的数字,翻转之后,向后从左往右的顺序进行翻转;递归思路如下图所示:
貌似两种情况都是OK的,但是当n = 4时,情况二走到1111的时候,会出现前后两个数字位数超过两个不一样的情况:
- …
- 1111
- 1011
- 1001
- 1000
- 1101
- 1100
- …
所以只能使用情况一的思路进行回溯。最后存储每位数可以使用string,但这里我们新介绍一个容器:bitset;这个容器只能存储1和0,或者True或False,函数原型为:template <size_t N> class bitset,所以可以使用 bitset Variable’s Name 来初始化长度为N的bitset;
Bitset相关成员函数介绍:
1)unsigned long to_ulong() const:将bitset转换成无符号长整型数字;
2)bitset& flip() 或 itset& flip (size_t pos):如果pos没有指定,则翻转容器中的所有值;如果pos指定,则翻转pos位置处的数字;注意bitset从右边开始从0排排序列,和一般的vector不一样;
2.2 实例代码
class Solution {
vector<int> ans;
bitset<32> bits;
void utils(int idx) {
if (!idx) { ans.push_back(bits.to_ulong()); return; }
utils(idx - 1);
bits.flip(idx - 1);
utils(idx - 1);
}
public:
vector<int> grayCode(int n) {
utils(n);
return ans;
}
};
3. 迭代(Iteration)
3.1 解题思路
还是n = 3,我们换个思路进行观察:
- 000
- 001
- 011
- 010
- 110
- 111
- 100
- 101
换一种排列方式:
我们发现右边的数只需把左边的数最低位变1就行,接着看看n = 2:
也有同样的规律,那么来看看n从2变化到3:
发现只需要在n = 2的基础上,某个数字末尾加0,加把最低位变1即可从n = 2转换到n = 3。现在从n = 0 ~ 3完整看一下整个思路的过程:
所以,我们只需要初始化一个答案数组(ans),包含一个空string;每轮循环末尾加0,然后最低位变一,即可得到最终答案。这部分对应代码:3.2.1 字符串转换
上面的思路是末尾变化,那么我们可不可以从头部开始变化呢?答案当然是No Problem啦,而且也无需使用string来表示数字,直接使用integer进行操作就行。因为正数的二进制前面都是0,所以也不需要我们进行补零操作,只需把最高位变1就行。比如n = 3, 000 -> 100,我们只需把1左移2位(001 << 2 = 100),然后再或运算(|)即可,移动的位数刚好等于外层循环的当前循环数:for (int i = 0; i < n; i++)。这部分对应代码:3.2.2 整数转换(推荐)
3.2 实例代码
3.2.1 字符串转换
class Solution {
public:
vector<int> ans;
// 将字符串二进制数转换成integer
void convertoInt(string& nums) {
int num = 0;
for (int i = 0; i < nums.size(); i++) {
num <<= 1;
if (nums[i] == '1') num++;
}
//cout << num;
ans.push_back(num);
}
vector<int> grayCode(int n) {
if (!n) return vector<int>(1, 0);
vector<string> nums(1, "");
for (int i = 0; i < n; i++) {
for (string& str : nums) str.push_back('0'); // 末尾加'0'
// 末尾字符变为'1'
for (int j = nums.size() - 1; j >= 0; j--) {
nums.push_back(nums[j]);
nums.back()[nums.back().size() - 1] = '1';
}
}
for (string& str : nums) convertoInt(str);
return ans;
}
};
3.2.2 整数转换(推荐)
class Solution {
public:
vector<int> grayCode(int n) {
vector<int> ans(1, 0);
for (int i = 0; i < n; i++) {
int len = ans.size();
for (int j = len - 1; j >= 0; j--) ans.push_back(ans[j] | (1 << i));
}
return ans;
}
};
4. 位操作(Bit Manipulation)
4.1 解题思路
经过两种方法的洗礼,终于来到了屠龙的时刻哒!最后一种方法只需两三行代码就能秒杀“巨龙”!
首先我们知道某个数加1,会把左边所有连续的1置零,然后把其相邻的0变1,比如0111 + 1 = 1000;为了方便理解,先假设这里二进制数最多10位,然后某个数的二进制表示为(X和?表示未知):
i ^ (i >> 1) 和 (i + 1) ^ ((i + 1) >> 1)蓝框部分异或后的结果都是相同的,只有0 ^ ? 和 1 ^ ?不同,而且无论?取0还是1,这部分都是不一样的。到这里我们得到了什么呢?说明两个连续的正数通过上述处理,即满足了格雷编码的要求:两个连续的数值仅有一个位数的差异。所以代码就会变得非常简单,遍历0 ~ n,再通过上述计算即可得到答案。
当然还有另一个情况,+1不会进位,这种情况和上面是基本类似的,不懂的同学可以参考下图,这里就不再赘述了:
这个方法是不是很震惊呀(ΩДΩ)~
4.2 实例代码
class Solution {
public:
vector<int> grayCode(int n) {
vector<int> ans(1 << n); // 1 << n 表示一共有多少个答案: 2 ^ n
for (int i = 0; i < 1 << n; i++) ans[i] = i ^ (i >> 1);
return ans;
}
};
5. 参考资料
- Share my solution
- An accepted three line solution in JAVA
- Backtracking C++ solution
- 4 lines C++ code
- C++ bitset 用法
- std::bitset
- std::bitset::to_ulong
- std::bitset::flip