GESP C++六级官方考试大纲中,要求掌握两种具体的编码方式及其原理。本篇将重点介绍格雷码 (Gray Code)的原理、构造方法及与二进制码的转换,并对哈夫曼编码进行简要回顾与应用总结。
一、哈夫曼编码 (Huffman Coding)
1.1 原理回顾
在上一篇关于哈夫曼树的文章中,已经详细讲解了其构造过程以及哈夫曼编码的生成原理。
核心要点:
- 变长编码:高频字符用短编码,低频字符用长编码,以达到压缩数据的目的。
- 前缀编码性质:哈夫曼编码是前缀码,即任一字符的编码都不是另一字符编码的前缀,保证了解码的唯一性。
- 构造依赖:通过构建哈夫曼树,左分支代表 0,右分支代表 1,从根到叶子的路径即为该字符的编码。
1.2 简单应用
在考试中,关于哈夫曼编码的考察通常涉及:
- 构造树并求WPL:给定一组权值,计算最小带权路径长度。
- 手动编码:给定字符频率,画出哈夫曼树并写出每个字符的二进制编码。
- 编码长度计算:计算编码后的总二进制位数,对比定长编码(如ASCII)计算压缩率。
复习建议:请务必熟练掌握哈夫曼树的“贪心合并”过程,这是解题的关键。
二、格雷码 (Gray Code)
2.1 什么是格雷码?
格雷码(Gray Code),又称循环二进制单位距离码,是一种特殊的二进制编码方式。它的核心特征是:任意两个相邻的数值(包括首尾),其编码只有一位二进制数不同。
示例(3位二进制码 vs 3位格雷码):
| 十进制值 |
普通二进制 (Binary) |
格雷码 (Gray) |
差异分析 |
| 0 |
000 |
000 |
|
| 1 |
001 |
001 |
|
| 2 |
010 |
011 |
二进制 001->010 变了2位;格雷码 001->011 变了1位 |
| 3 |
011 |
010 |
|
| 4 |
100 |
110 |
二进制 011->100 变了3位;格雷码 010->110 变了1位 |
| 5 |
101 |
111 |
|
| 6 |
110 |
101 |
|
| 7 |
111 |
100 |
|
如何解读格雷码 011?
格雷码不像普通二进制那样,每个位有固定的权重。你不能直接把011看作这样来计算它的十进制值。
正确理解方法:
- 格雷码
011 本身没有直接的十进制含义。
- 它代表的十进制值,需要通过将其转换回普通二进制码才能知道。
- 在我们的对比示例表格中,你可以看到
011 对应的十进制值是 2。这是因为 011 转换回普通二进制是 010,而 010 就是十进制的 2。
2.2 为什么需要格雷码?
格雷码的主要优势在于它的“一位变化”特性,这使得它在物理世界中进行状态编码时非常有用,尤其是在:
- 机械传感器 (如旋转编码器):如果使用普通二进制,在多位同时变化时可能因传感器不同步读到错误值。而格雷码保证相邻状态只变一位,完全避免了这种“瞬时误差”,让读取更稳定可靠。理解这类硬件通信原理,有助于构建更底层的系统知识,可以参考网络与系统专栏进行拓展学习。
- 数字电路和数据传输:在高速变化的信号中,可以减少由于不同信号线延迟不一致而产生的毛刺或错误。
2.3 格雷码的构造与转换
在实际应用和考试中,我们通常面临两种不同的需求:
- 构造整个序列:需要列出 n 位的所有格雷码(如题目要求“写出3位格雷码序列”)。
- 快速数值转换:已知一个整数 n,求其对应的格雷码(如编程题中 O(1) 时间求值)。
针对这两种需求,有两种对应的方法。
场景一:镜像递归法 (用于构造完整序列)
这是最直观的构造思路。n 位格雷码可以由 n-1 位格雷码推导出来:
- 将 n-1 位的格雷码序列写下来。
- 将该序列倒序(镜像)写在下方。
- 前半部分(原始序列)的开头补 0。
- 后半部分(镜像序列)的开头补 1。
演示:从 1 位推导 2 位
- 1位格雷码:
0, 1
- 镜像并补位后得到2位格雷码:
00, 01, 11, 10
演示:从 2 位推导 3 位
- 2位格雷码:
00, 01, 11, 10
- 镜像并补位后得到3位格雷码:
000, 001, 011, 010, 110, 111, 101, 100
C++ 代码实现 (递归):
#include <vector>
#include <string>
#include <algorithm> // 用于 std::reverse
#include <iostream>
// 函数:生成 n 位格雷码序列 (使用镜像法)
std::vector<std::string> generateGrayCodeMirror(int n) {
// 基本情况:0 位格雷码,返回一个包含空字符串的向量
if (n == 0) {
return {""};
}
// 基本情况:1 位格雷码,直接是 "0", "1"
if (n == 1) {
return {"0", "1"};
}
// 递归调用:获取 n-1 位格雷码序列
std::vector<std::string> prevGrayCodes = generateGrayCodeMirror(n - 1);
std::vector<std::string> currentGrayCodes;
// 1. 生成前半部分:在每个 n-1 位格雷码前加 '0'
for (const std::string& code : prevGrayCodes) {
currentGrayCodes.push_back("0" + code);
}
// 2. 生成后半部分:将 n-1 位格雷码序列倒序,然后在每个编码前加 '1'
// 首先复制并倒序
std::vector<std::string> reversedPrevGrayCodes = prevGrayCodes;
std::reverse(reversedPrevGrayCodes.begin(), reversedPrevGrayCodes.end());
for (const std::string& code : reversedPrevGrayCodes) {
currentGrayCodes.push_back("1" + code);
}
return currentGrayCodes;
}
// 示例用法
int main() {
int n_bits = 3; // 想要生成的格雷码位数
std::vector<std::string> grayCodes = generateGrayCodeMirror(n_bits);
std::cout << n_bits << " 位格雷码序列 (镜像法):\n";
for (const std::string& code : grayCodes) {
std::cout << code << "\n";
}
return 0;
}
场景二:位运算法 (用于数值快速转换)
在编程中,我们通常使用位运算公式直接进行转换,效率极高。
公式:二进制码 B 转 格雷码 G
G = B ^ (B >> 1)
即:格雷码 = 二进制码 ^ (二进制码右移一位)。(注: ^ 代表异或运算 XOR)
手动推导逻辑:
格雷码的每一位,记录的都是“对应二进制位”与“它左边那一位”是否不同。
- 不同记为 1
- 相同记为 0 (最高位的左边默认是 0)
C++ 代码实现:
unsigned long long binaryToGray(unsigned long long n) {
return n ^ (n >> 1);
}
图解演示:
- 例1:将十进制 6 (二进制 110) 转为格雷码
1 1 0 (二进制 B)
^ 0 1 1 (B >> 1)
-------
1 0 1 (结果 G)
- 例2:将十进制 7 (二进制 111) 转为格雷码
1 1 1 (二进制 B)
^ 0 1 1 (B >> 1)
-------
1 0 0 (结果 G)
验证特性:从 6 (110) 变到 7 (111),二进制码变了 1 位。对应的格雷码从101变到100,也只变了1位(末位)。这类位运算技巧是算法实现中的基本功,更多高效算法与数据结构知识可以帮助你优化代码。
2.4 格雷码转回二进制码 (逻辑逆推)
如果已知格雷码,如何还原为二进制码?
手动还原逻辑:从最高位开始,一位一位向右推导。
- 格雷码是 0 表示“与左边相同” -> 照抄左边刚刚算出来的二进制位。
- 格雷码是 1 表示“与左边不同” -> 翻转左边刚刚算出来的二进制位。
图解实例:将格雷码 1010 还原为二进制
- 第3位 (最高位):格雷码是
1,左边是 0 (默认),不同 -> 二进制为 1。
- 第2位:格雷码是
0 (表示“和左边一样”),左边是 1,照抄 -> 二进制为 1。
- 第1位:格雷码是
1 (表示“和左边不一样”),左边是 1,翻转 -> 二进制为 0。
- 第0位:格雷码是
0 (表示“和左边一样”),左边是 0,照抄 -> 二进制为 0。
- 最终结果:
1100
代码实现:
unsigned long long grayToBinary(unsigned long long g) {
unsigned long long b = g; // 1. 先记录 G 本身
// 2. 循环右移异或
while (g >>= 1) {
b ^= g;
}
return b;
}
三、总结与学习建议
| 知识点 |
核心原理 |
关键公式/方法 |
应用场景 |
| 哈夫曼编码 |
贪心策略,树形结构,前缀码 |
WPL 最小化构造 |
数据压缩 (ZIP, JPEG) |
| 格雷码 |
相邻数值仅一位二进制不同 |
G = B ^ (B >> 1) |
旋转编码器、减少硬件误差 |
学习建议:
- 对于哈夫曼编码,重点在于画树和计算 WPL,理解其作为压缩算法的后端逻辑。
- 对于格雷码,重点在于掌握生成规则(镜像法)和代码转换公式(异或法),特别是
G = B ^ (B >> 1) 这一公式在编程题中非常常用。
|