法→原理, 算法实现

字符压缩编码之香农-范诺编码(Shannon-Fano Coding)

钱魏Way · · 9 次浏览
!文章内容如有错误或排版问题,请提交反馈,非常感谢!

香农-范诺编码简介

香农-范诺编码(Shannon-Fano Coding)是一种经典的无损数据压缩算法,由克劳德·香农(Claude Shannon)和罗伯特·范诺(Robert Fano)于1948年左右独立提出。这是第一种基于信息熵理论的压缩算法,比著名的哈夫曼编码出现得更早。

核心思想与原理

香农-范诺编码的核心思想是通过递归划分概率空间来实现压缩:

  • 信息熵概念:香农信息论证明,字符$s_i$的理想码长应为$\log_2(1/p_i)$,其中$/p_i$是字符出现概率
  • 概率空间划分:将字符集按概率降序排列,不断将其分割为两个概率和尽可能相等的子集
  • 二进制分配:每个分割点分配二进制位:

左子集 → 添加0

右子集 → 添加1

  • 前缀编码:最终生成的是前缀码(无前缀冲突),无需分隔符即可解码

算法步骤详解

假设有5个字符:A(0.38), B(0.18), C(0.17), D(0.15), E(0.12)

步骤1:概率排序

字符 概率
A 0.38
B 0.18
C 0.17
D 0.15
E 0.12

步骤2:递归分割概率空间

步骤3:生成编码

  • 第一次分割:左子集=A添加0,右子集添加1
  • 第二次分割:右子集中左=B+C添加0,右=D+E添加1
  • 第三次分割:
    • B添加0→ 完整编码=10
    • C添加1→ 完整编码=11
  • 第四次分割:
    • D添加0→ 完整编码=10
    • E添加1→ 完整编码=111

最终编码表:

字符 概率 编码 理想码长 实际码长
A 0.38 0 1.39 1
B 0.18 100 2.47 3
C 0.17 101 2.56 3
D 0.15 110 2.74 3
E 0.12 111 3.06 3

关键特点

  • 计算复杂度:
    • 时间:$O(n\log n)$(主要来自排序操作)
    • 空间:$O(n)$
  • 压缩效率:
    • 平均码长:$\sum p_il_i \approx 2.19$比特/符号
  • 信息熵:$H = -\sum p_i\log _2 p_i \approx 2.17$比特/符号
  • 冗余度:约02比特/符号
  • 最优分割问题:通过贪心算法找局部最优分割,但不保证全局最优:

与哈夫曼编码对比

特性 香农-范诺编码 哈夫曼编码
提出时间 1948年 1952年
构建方向 自顶向下(递归分割) 自底向上(节点合并)
最优性保证 近似最优(接近熵但非最优) 严格最优(最小平均码长)
码长分布 有时长于哈夫曼编码 总是最短平均码长
复杂度 $O(n\log n)$ $O(n\log n)$
前缀码性质 满足 满足
编码唯一性 不唯一(取决于分割顺序) 不唯一(相同概率时)
典型应用 GZIP(早期版本)、多媒体压缩 ZIP、JPEG、MP3等主流压缩

实际应用

  • UNIX压缩工具:早期compact命令使用香农-范诺变体
  • 图像压缩:TIFF格式可选香农-范诺压缩,医疗图像传输标准DICOM。
  • 通信协议:25分组无线电协议的数据压缩
  • 音频编码:SHORTEN无损音频压缩算法(在FLAC出现前)

历史意义与局限性

历史贡献:

  • 首个基于信息论的压缩算法
  • 证明可实现无损压缩接近香农熵
  • 推动哈夫曼等后续算法的发展

局限性:

  • 非最优性:存在压缩率更高的算法
  • 分割依赖:编码结果受分割顺序影响
  • 冗余问题:部分情况下码长超过熵值
  • 实现复杂性:相比哈夫曼实现较复杂

八、现代演变

  • 自适应变体:根据输入流动态更新概率模型
  • 分组编码:将字符组合成”超字符”提高压缩率
  • 与LZ结合:在DEFLATE等算法中作为熵编码阶段

虽然哈夫曼编码在实际应用中更受欢迎,但香农-范诺编码作为信息论的第一个实际应用,仍然在压缩算法发展史上占有重要地位。它展示了如何利用信息熵实现高效数据压缩的核心思想,为后续所有熵编码算法奠定了基础。

香农-范诺编码Python实现

import math
from collections import Counter

class ShannonFanoNode:
    def __init__(self, char=None, prob=0):
        self.char = char    # 字符
        self.prob = prob    # 概率
        self.code = ""      # 二进制编码
        self.left = None    # 左子节点
        self.right = None   # 右子节点

def calculate_probabilities(text):
    """计算字符概率分布"""
    counter = Counter(text)
    total = len(text)
    return [(char, count/total) for char, count in counter.items()]

def find_optimal_split(items):
    """寻找最佳分割点"""
    total = sum(prob for _, prob in items)
    min_diff = float('inf')
    split_idx = 1
    left_sum = 0
    
    for i in range(1, len(items)):
        left_sum += items[i-1][1]
        right_sum = total - left_sum
        diff = abs(left_sum - right_sum)
        
        if diff < min_diff:
            min_diff = diff
            split_idx = i
            
    return split_idx

def build_shannon_fano_tree(items):
    """递归构建香农-范诺树"""
    # 基本情况:单个节点
    if len(items) == 1:
        char, prob = items[0]
        return ShannonFanoNode(char, prob)
    
    # 按概率降序排序
    sorted_items = sorted(items, key=lambda x: x[1], reverse=True)
    
    # 寻找最佳分割点
    split_index = find_optimal_split(sorted_items)
    left_items = sorted_items[:split_index]
    right_items = sorted_items[split_index:]
    
    # 创建父节点
    node = ShannonFanoNode()
    
    # 递归构建子树
    node.left = build_shannon_fano_tree(left_items)
    node.right = build_shannon_fano_tree(right_items)
    
    # 更新子树编码
    if node.left:
        if isinstance(node.left, list):
            for child in node.left:
                child.code = '0' + child.code
        else:
            node.left.code = '0' + node.left.code
            
    if node.right:
        if isinstance(node.right, list):
            for child in node.right:
                child.code = '1' + child.code
        else:
            node.right.code = '1' + node.right.code
    
    return [node.left, node.right] if isinstance(node.left, ShannonFanoNode) else node

def generate_codes(node, code_dict, prefix=""):
    """生成字符编码字典"""
    if node.char is not None:
        node.code = prefix
        code_dict[node.char] = prefix
    else:
        if node.left:
            generate_codes(node.left, code_dict, prefix + '0')
        if node.right:
            generate_codes(node.right, code_dict, prefix + '1')

def shannon_fano_encode(text):
    """香农-范诺编码主函数"""
    if not text:
        return "", {}
    
    # 计算概率分布
    prob_dist = calculate_probabilities(text)
    
    # 构建编码树
    root = build_shannon_fano_tree(prob_dist)
    
    # 生成编码字典
    code_dict = {}
    if isinstance(root, list):
        for node in root:
            generate_codes(node, code_dict)
    else:
        generate_codes(root, code_dict)
    
    # 编码文本
    encoded = ''.join(code_dict[char] for char in text)
    
    return encoded, code_dict

def shannon_fano_decode(encoded, code_dict):
    """香农-范诺解码"""
    reverse_dict = {code: char for char, code in code_dict.items()}
    decoded = []
    current_code = ""
    
    for bit in encoded:
        current_code += bit
        if current_code in reverse_dict:
            decoded.append(reverse_dict[current_code])
            current_code = ""
    
    return ''.join(decoded)

# 测试示例
if __name__ == "__main__":
    text = "ABRACADABRA"
    print(f"原始文本: {text}")
    
    encoded, code_dict = shannon_fano_encode(text)
    print(f"编码结果: {encoded}")
    print("编码表:")
    for char, code in sorted(code_dict.items()):
        print(f"  {char}: {code}")
    
    decoded = shannon_fano_decode(encoded, code_dict)
    print(f"解码结果: {decoded}")
    print(f"原始长度: {len(text)*8} bits")
    print(f"编码长度: {len(encoded)} bits")
    print(f"压缩率: {1 - len(encoded)/(len(text)*8):.1%}")

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注