编码生成与位运算

本节阅读量:

生成Huffman编码

递归前序遍历

Huffman编码的生成通过递归前序遍历实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void HuffmanCoder::generateCodes(HuffmanNode* root, const std::string& str) {
    if (root == nullptr) return;
    
    // 如果是叶子节点,保存编码
    if (!root->left && !root->right) {
        huffmanCode[root->data] = str;
        return;
    }
    
    // 递归遍历左子树(编码加0)
    generateCodes(root->left, str + "0");
    
    // 递归遍历右子树(编码加1)
    generateCodes(root->right, str + "1");
}

生成过程图解

假设有如下Huffman树:

        root
       /    \
     A(5)   B(7)
    /   \   /  \
  a(2) c(2) b(3) d(4)

执行过程

调用:generateCodes(root, "")

1. root不是叶子节点
   - 调用 generateCodes(A, "0")

2. A不是叶子节点
   - 调用 generateCodes(a, "00")

3. a是叶子节点
   - huffmanCode['a'] = "00"
   - 返回

4. 调用 generateCodes(c, "01")

5. c是叶子节点
   - huffmanCode['c'] = "01"
   - 返回

6. 调用 generateCodes(B, "1")

7. B不是叶子节点
   - 调用 generateCodes(b, "10")

8. b是叶子节点
   - huffmanCode['b'] = "10"
   - 返回

9. 调用 generateCodes(d, "11")

10. d是叶子节点
    - huffmanCode['d'] = "11"
    - 返回

最终编码表:
{'a': "00", 'c': "01", 'b': "10", 'd': "11"}

为什么前序遍历?

前序遍历(根→左→右)确保了:

  • 每个节点的编码路径是唯一的
  • 编码长度与树中节点的深度一致
  • 高频字符(更靠近根)获得更短的编码

位运算基础

位运算符介绍:

运算符 名称 示例 说明
<< 左移 5 << 2 = 20 二进制左移n位,相当于乘以2^n
>> 右移 20 >> 2 = 5 二进制右移n位,相当于除以2^n
& 按位与 5 & 3 = 1 对应位都为1才为1
| 按位或 5 | 3 = 7 对应位有1就为1
^ 按位异或 5 ^ 3 = 6 对应位不同才为1
~ 按位取反 ~5 所有位取反

左移 <<

1
2
3
4
5
unsigned char byte = 1;  // 00000001

byte = byte << 1;  // 00000010 = 2
byte = byte << 2;  // 00001000 = 8
byte = byte << 3;  // 01000000 = 64

按位与 &

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
unsigned char byte = 0b10110011;  // 179

// 取出最高位
int highestBit = (byte >> 7) & 1;  // 1

// 取出最低位
int lowestBit = byte & 1;  // 1

// 检查第3位是否为1
if (byte & (1 << 3)) {
    std::cout << "第3位是1" << std::endl;
}

按位或 |

1
2
3
4
unsigned char a = 0b10110011;  // 179
unsigned char b = 0b11001100;  // 204

unsigned char result = a | b;  // 11111111 = 255

按位异或 ^

1
2
3
4
5
6
// 加密/解密的简单例子
char key = 0b01010101;
char data = 'A';  // 0b01000001

char encrypted = data ^ key;  // 0b00010100
char decrypted = encrypted ^ key;  // 0b01000001 = 'A'

位运算 vs 算术运算

操作 位运算 算术运算
乘以2 x << 1 x * 2
除以2 x >> 1 x / 2
判断奇偶 x & 1 x % 2

位运算的应用

判断整数是否为2的幂

1
2
3
bool isPowerOfTwo(int n) {
    return n > 0 && (n & (n - 1)) == 0;
}

交换两个变量

1
2
3
4
5
void swap(int& a, int& b) {
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
}

获取某一位的值

1
2
3
4
5
6
7
8
int getBit(unsigned char byte, int position) {
    return (byte >> (7 - position)) & 1;
}

// 示例
unsigned char byte = 0b10110011;
int bit3 = getBit(byte, 3);  // 0
int bit7 = getBit(byte, 7);  // 1

二进制字符串转换为字节

问题场景

假设我们有一个二进制字符串:

"10110011011101"  // 14个bit

需要将其转换为字节数组:

[0xB3, 0x74]  // [10110011, 01110100]

算法思路

  1. 从左到右遍历二进制字符串
  2. 每处理8位,组成一个字节
  3. 如果不足8位,补0
  4. 最后需要记录补了多少0(padding)

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void HuffmanCoder::stringToBytes(const std::string& binaryString, 
                                std::vector<unsigned char>& bytes) {
    unsigned char byte = 0;
    int bitCount = 0;
    
    for (char bit : binaryString) {
        // 左移1位,为新的bit腾出空间
        byte = byte << 1;
        
        // 如果当前bit是1,设置最低位
        if (bit == '1') {
            byte |= 1;  // byte = byte | 1
        }
        
        bitCount++;
        
        // 每8位组成一个字节
        if (bitCount == 8) {
            bytes.push_back(byte);
            byte = 0;  // 重置
            bitCount = 0;
        }
    }
    
    // 处理剩余的不足8位的bit
    if (bitCount > 0) {
        // 左移,空位补0
        byte = byte << (8 - bitCount);
        bytes.push_back(byte);
    }
}

图解转换过程

输入"10110011011101"(14位)

处理过程

初始状态:
byte = 00000000, bitCount = 0

第1位 '1':
byte = 00000000 << 1 = 00000000
byte = 00000000 | 1 = 00000001
bitCount = 1

第2位 '0':
byte = 00000001 << 1 = 00000010
bitCount = 2

第3位 '1':
byte = 00000010 << 1 = 00000100
byte = 00000100 | 1 = 00000101
bitCount = 3

第4位 '1':
byte = 00000101 << 1 = 00001010
byte = 00001010 | 1 = 00001011
bitCount = 4

第5位 '0':
byte = 00001011 << 1 = 00010110
bitCount = 5

第6位 '0':
byte = 00010110 << 1 = 00101100
bitCount = 6

第7位 '1':
byte = 00101100 << 1 = 01011000
byte = 01011000 | 1 = 01011001
bitCount = 7

第8位 '1':
byte = 01011001 << 1 = 10110010
byte = 10110010 | 1 = 10110011
bitCount = 8
→ 保存字节 0xB3,重置 byte=0, bitCount=0

第9位 '0':
byte = 00000000 << 1 = 00000000
bitCount = 1

第10位 '1':
byte = 00000000 << 1 = 00000000
byte = 00000000 | 1 = 00000001
bitCount = 2

第11位 '1':
byte = 00000001 << 1 = 00000010
byte = 00000010 | 1 = 00000011
bitCount = 3

第12位 '1':
byte = 00000011 << 1 = 00000110
byte = 00000110 | 1 = 00000111
bitCount = 4

第13位 '0':
byte = 00000111 << 1 = 00001110
bitCount = 5

第14位 '1':
byte = 00001110 << 1 = 00011100
byte = 00011100 | 1 = 00011101
bitCount = 6

剩余6位,需要左移2位补0:
byte = 00011101 << 2 = 01110100
→ 保存字节 0x74

最终结果:[0xB3, 0x74]

Padding的计算

1
int padding = (8 - (encodedString.length() % 8)) % 8;

示例

  • 14位:14 % 8 = 68 - 6 = 2 → padding = 2
  • 16位:16 % 8 = 08 - 0 = 88 % 8 = 0 → padding = 0
  • 15位:15 % 8 = 78 - 7 = 1 → padding = 1

字节转换为二进制字符串

算法思路:

  1. 遍历每个字节
  2. 对于普通字节,读取全部8位
  3. 对于最后一个字节,根据padding只读取有效位

代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void HuffmanCoder::bytesToString(const std::vector<unsigned char>& bytes,
                                std::string& binaryString, int padding) {
    for (size_t i = 0; i < bytes.size(); i++) {
        unsigned char byte = bytes[i];
        
        // 最后一个字节:根据padding处理
        if (i == bytes.size() - 1) {
            for (int j = 0; j < 8 - padding; j++) {
                binaryString += ((byte >> (7 - j)) & 1) ? '1' : '0';
            }
        } 
        // 普通字节:读取全部8位
        else {
            for (int j = 0; j < 8; j++) {
                binaryString += ((byte >> (7 - j)) & 1) ? '1' : '0';
            }
        }
    }
}

图解转换过程

输入[0xB3, 0x74],padding = 2

处理过程

第1个字节 0xB3 = 10110011:
j=0: (0xB3 >> 7) & 1 = 1 → '1'
j=1: (0xB3 >> 6) & 1 = 0 → '0'
j=2: (0xB3 >> 5) & 1 = 1 → '1'
j=3: (0xB3 >> 4) & 1 = 1 → '1'
j=4: (0xB3 >> 3) & 1 = 0 → '0'
j=5: (0xB3 >> 2) & 1 = 0 → '0'
j=6: (0xB3 >> 1) & 1 = 1 → '1'
j=7: (0xB3 >> 0) & 1 = 1 → '1'
结果:10110011

第2个字节 0x74 = 01110100, padding=2:
只读取 8-2=6 位:
j=0: (0x74 >> 7) & 1 = 0 → '0'
j=1: (0x74 >> 6) & 1 = 1 → '1'
j=2: (0x74 >> 5) & 1 = 1 → '1'
j=3: (0x74 >> 4) & 1 = 1 → '1'
j=4: (0x74 >> 3) & 1 = 0 → '0'
j=5: (0x74 >> 2) & 1 = 1 → '1'
结果:011101(忽略最后2位0)

最终二进制字符串:10110011011101

测试代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 在 main.cpp 中添加测试
void testBitConversion() {
    std::cout << "=== 测试位转换 ===" << std::endl;
    
    std::string binaryString = "10110011011101";
    std::vector<unsigned char> bytes;
    
    std::cout << "原始二进制字符串: " << binaryString << std::endl;
    std::cout << "长度: " << binaryString.length() << " bits" << std::endl;
    
    // 计算padding
    int padding = (8 - (binaryString.length() % 8)) % 8;
    std::cout << "Padding: " << padding << " bits" << std::endl;
    
    // 转换为字节
    HuffmanCoder coder;
    coder.stringToBytes(binaryString, bytes);
    
    std::cout << "字节数组: [";
    for (size_t i = 0; i < bytes.size(); i++) {
        printf("0x%02X", bytes[i]);
        if (i < bytes.size() - 1) std::cout << ", ";
    }
    std::cout << "]" << std::endl;
    
    // 转换回二进制字符串
    std::string recovered;
    coder.bytesToString(bytes, recovered, padding);
    
    std::cout << "恢复的二进制字符串: " << recovered << std::endl;
    
    if (binaryString == recovered) {
        std::cout << "✓ 转换正确!" << std::endl;
    } else {
        std::cout << "✗ 转换错误!" << std::endl;
    }
}

int main() {
    testBitConversion();
    return 0;
}

小结与练习

本章总结:

  1. Huffman编码生成

    • 使用递归前序遍历生成编码
    • 左0右1的约定
    • 编码存储在 unordered_map 中
  2. 位运算基础

    • 掌握常用位运算符
    • 理解位运算的性能优势
    • 学会在实际问题中应用位运算
  3. 二进制与字节转换

    • 理解位与字节的转换过程
    • 掌握padding的概念和计算
    • 能够手动进行转换计算

练习1:位运算

写出以下表达式的结果:

1
2
3
4
(0b10110011 >> 3) & 1
(0b10110011 << 2) | 0b00000011
0b10110011 & 0b11001100
~0b10110011 & 0xFF

练习2:手动转换

手动将以下二进制字符串转换为字节数组,并计算padding:

  • "10110"
  • "1111000011110000"
  • "1010101010101"

练习3:优化代码

如何优化 stringToBytes 函数,使其更高效?

练习4:理解编码

为什么高频字符的Huffman编码更短?这与树的结构有什么关系?


0.2 频率统计与Huffman树构建

上一节

0.4 树的序列化与反序列化

下一节