相邻两个数,只许翻一个开关——防"读串"的聪明计数法
一排机械开关表示数字:二进制数 3 是 011,下一个数 4 是 100——一下要同时翻 3 个开关!但机械开关不可能完全同时动作:可能先翻一个再翻另一个,中途会出现 001、000 这样的"中间状态",读的人正好在这一瞬间来看,就读出了错误的数。
格雷码(Gray Code)是一套管开关的"重新编号法":保证相邻两个编码之间,恰好只有一个开关状态不同(术语:汉明距离 = 1)。这样任何时刻最多只有一个开关在动,中间状态至多两种,而且都好防——老式的编码器、电梯楼层显示、数字通信都靠它防错。
x 异或 x 右移 1 位,即 g = x ^ (x >> 1);还原:从高位起逐位"当前位 = 格雷位 异或 已还原的高位"。左边是"开关故障"现场,右边是 3 位二进制 vs 格雷码全表——仔细看格雷码一列,上下相邻只变 1 位。
两个等长 01 串,不同位的个数叫汉明距离(Hamming Distance)。数一数右边表格里相邻两行的二进制:011 → 100 差 3 位;000→001、001→010 各差 1 位,010→011 差 1 位……二进制相邻数经常一次翻 2~3 位(位数越多越频繁)。
机械/电路世界里,"多位同时翻"是噩梦:各位翻动总有先后,中间会冒出一串谁也不是的"假编码"。格雷码的规定只有一条:相邻编码汉明距离 = 1——一次只动一个开关,过渡态只有"旧的还没翻完"和"新的已经翻完"两种,都能识别、不会读串。
格雷码有漂亮的递归构造,像照镜子:
为什么倒序?上半的最后一个(如 2 位时的 01)和下半的第一个必须只差 1 位:上半以"0 + X"结尾,下半若正着来会以"1 + X"开头——差 1 位,但倒序后下半第一个是"1 + X 的镜像首项",接缝处恰好只翻最高位。照镜子 + 倒序,是格雷码的构造灵魂。
二 → 格只有一行:把二进制右移 1 位,再和原数做异或:g = x ^ (x >> 1)。可以逐位理解:格雷码的第 i 位 = 二进制的第 i 位 异或 它左边那位(最高位没有左边,照抄)。手工推两个:
二进制 右移1位 异或结果(=格雷码)
011 ^ 001 = 010 (3 → 010)
100 ^ 010 = 110 (4 → 110)
对照大图里的表:3 → 010 ✓、4 → 110 ✓,公式和表完全对得上。
格 → 二要"顺藤摸瓜":从最高位开始,最高位照抄;往下的每一位 = 格雷码该位 异或 已经还原出来的上一位。注意:是"已经还原的二进制位",不是格雷码的上一位。推两个:
格雷 110: 最高位照抄 1
第二位 = 1 ⊕ 1(已还原高位) = 0
第三位 = 0 ⊕ 0(已还原上一位) = 0
→ 二进制 100 = 4 ✓(和表里 4 → 110 互逆)
格雷 101: 最高位照抄 1
第二位 = 0 ⊕ 1 = 1
第三位 = 1 ⊕ 1 = 0
→ 二进制 110 = 6 ✓
两个函数搞定互转的两个方向,再加一个"往返测试":0 到 999 每个数都转一圈再转回来,全对才算没写错。
// 格雷码:互转 + 往返测试 #include <bits/stdc++.h> using namespace std; int toGray(int x) { // 二 → 格:和"右移 1 位的自己"异或 return x ^ (x >> 1); } int toBin(int g) { // 格 → 二:当前位 = 格雷位 异或 已还原的高位 int b = 0; for (int i = 31; i >= 0; i--) { int gbit = (g >> i) & 1; // 格雷码第 i 位 int bbit = gbit ^ (b & 1); // 二进制第 i 位 b = (b << 1) | bbit; // 接到 b 的尾巴上 } return b; } void print3(int x) { // 把 x 的低 3 位逐位打出来 printf("%d%d%d", (x >> 2) & 1, (x >> 1) & 1, x & 1); } int main() { // 1) 打印 3 位对照表:十进制 / 二进制 / 格雷码 for (int i = 0; i < 8; i++) { printf("%d: 二进制=", i); print3(i); printf(" 格雷="); print3(toGray(i)); printf("\n"); } // 2) 往返测试:格 → 二 必须转回原数 for (int i = 0; i < 1000; i++) { if (toBin(toGray(i)) != i) { printf("往返失败: %d\n", i); return 1; } } cout << "往返测试 0~999 通过" << endl; return 0; }
toBin 里异或的是已还原出来的二进制位,不是格雷码的高位——拿错对象全盘皆错。② toBin 的循环必须从高位到低位(31 → 0),顺序不能反。③ 打印二进制别偷懒用 printf("%d")(那打的是十进制),要用 (x>>2)&1 这样的方式逐位取。