Huffman编码算法详解与Java实现
1. 数据结构背景介绍
1.1 Huffman树基本原理
Huffman编码是一种变长编码方案,是一种典型的贪心算法应用。
核心思想:
- 出现频率高的字符赋予较短的编码,出现频率低的字符赋予较长的编码
- 通过构造带权路径长度最短的二叉树(即Huffman树)来实现最优前缀编码
- 前缀编码:任何一个字符的编码都不是另一个字符编码的前缀,保证解码的唯一性
关键概念:
- 权值:字符出现的频率
- 带权路径长度(WPL):从根节点到所有叶节点的路径长度与相应叶节点权值的乘积之和
- 最优二叉树:WPL最小的二叉树
1.2 应用场景
Huffman编码广泛应用于:
- 数据压缩:ZIP、RAR、7-Zip等压缩软件
- 图片格式:JPEG、PNG等图像编码标准
- 通信领域:数据传输中的编码优化
- 文本处理:各类文本文件的压缩存储
2. 代码实现详解
2.1 HFMTree.java - Huffman树构建与编码
2.1.1 树节点结构
首先看树节点定义:
class TreeNode {
int data; // 权值(出现频率)
public TreeNode left;
public TreeNode right;
String c; // 字符内容
String code; // Huffman编码
public TreeNode(int data) {
this.data = data;
}
public TreeNode(int data, String c) {
this.data = data;
this.c = c;
}
}
2.1.2 数据读取与频率统计
public void readData(){
File path=new File("需压缩文件路径");
StringBuilder sb=new StringBuilder();
try {
BufferedReader bufferedReader=new BufferedReader(new FileReader(path));
while((line=bufferedReader.readLine())!=null){
sb.append(line).append("\n");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
line=sb.toString();
Map<String,Integer> map=new HashMap<>();
for (int i = 0; i <line.length() ; i++) {
char c=line.charAt(i);
if(map.containsKey(c+"")){
map.put(c+"",map.get(c+"")+1);
}
else{
map.put(c+"",1);
}
}
List<TreeNode> list=new ArrayList<>();
for(Map.Entry<String,Integer> entry:map.entrySet()){
TreeNode node=new TreeNode(entry.getValue(),entry.getKey());
list.add(node);
}
creatTree(list);
}
逻辑分析:
- 读取源文件内容
- 使用HashMap统计每个字符的出现频率
- 将每个字符及其频率封装为TreeNode节点
- 调用creatTree()构建Huffman树
2.1.3 Huffman树构建算法
public void sort(List<TreeNode> list){
for (int i = 0; i < list.size()-1; i++) {
for (int j = 0; j < list.size()-1-i; j++) {
if(list.get(j).data>list.get(j+1).data){
TreeNode temp=list.get(j);
list.set(j,list.get(j+1));
list.set(j+1,temp);
}
}
}
}
public void creatTree(List<TreeNode> nodeList) {
while (nodeList.size() > 1) {
sort(nodeList);
TreeNode left = nodeList.remove(0);
TreeNode right = nodeList.remove(0);
TreeNode node = new TreeNode(left.data + right.data);
node.left = left;
node.right = right;
nodeList.add(node);
}
root=nodeList.remove(0);
}
构建步骤:
- 排序:使用冒泡排序按权值从小到大排序节点列表
- 选择:每次取出权值最小的两个节点
- 合并:创建新节点,权值为两节点权值之和
- 插入:将新节点放回列表
- 循环:重复上述步骤直到只剩一个节点(根节点)
2.1.4 生成Huffman编码
public void setcode(TreeNode node, String code){
if(node==null) return;
if(node.left==null&&node.right==null){
node.code=code;
return;
}
setcode(node.left,code+"0");
setcode(node.right,code+"1");
}
编码规则:
- 左子树路径标记为"0"
- 右子树路径标记为"1"
- 递归遍历,到达叶节点时记录编码
2.1.5 数据压缩写入
public void bitToByte(StringBuilder sb)throws Exception{
File file=new File("压缩文件路径");
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream oop=new ObjectOutputStream(fileOutputStream);
int len=(8-sb.length()%8)%8;
for (int i = 0; i < len; i++) {
sb.append("0");
}
oop.writeInt(len);
oop.writeObject(codemap);
int count=sb.length()/8;
for (int i = 0; i < count; i++) {
String s=sb.substring(i*8,(i+1)*8);
int data=Integer.parseInt(s,2);
oop.write(data);
}
oop.close();
System.out.println("压缩完成! 共写入 "+count+" 个字节,补零 "+len+" 位");
}
写入内容:
- 补零长度(用于解码时去除填充)
- 编码表(字符到编码的映射)
- 压缩后的二进制数据
2.2 解压算法
2.2.1 数据读取与解码准备
public void decompress() {
File file = new File("压缩文件路径");
try {
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
int paddingLen = ois.readInt();
System.out.println("补零数量: " + paddingLen);
Map<String, String> codemap = (Map<String, String>) ois.readObject();
System.out.println("编码表大小: " + codemap.size());
Map<String, String> decodeMap = new HashMap<>();
for (Map.Entry<String, String> entry : codemap.entrySet()) {
decodeMap.put(entry.getValue(), entry.getKey());
}
关键步骤:
- 读取补零长度
- 读取编码表
- 构建反向映射:将编码作为key,字符作为value,便于快速查找
2.2.2 二进制数据转换
StringBuilder binaryStr = new StringBuilder();
int byteData;
while ((byteData = fis.read()) != -1) {
String binary = Integer.toBinaryString(byteData);
while (binary.length() < 8) {
binary = "0" + binary;
}
binaryStr.append(binary);
}
System.out.println("读取的字节数据长度: " + binaryStr.length() + " 位");
if (paddingLen > 0) {
binaryStr.delete(binaryStr.length() - paddingLen, binaryStr.length());
}
转换逻辑:
- 逐个字节读取压缩数据
- 将每个字节转换为8位二进制字符串(不足8位前面补0)
- 去除末尾的补零
2.2.3 解码还原
4
StringBuilder result = new StringBuilder();
String currentCode = "";
for (int i = 0; i < binaryStr.length(); i++) {
currentCode += binaryStr.charAt(i);
if (decodeMap.containsKey(currentCode)) {
result.append(decodeMap.get(currentCode));
currentCode = "";
}
}
System.out.println("\n========== 解压后的文本内容 ==========");
System.out.println(result.toString());
System.out.println("======================================");
System.out.println("解压完成! 原文长度: " + result.length() + " 个字符");
解码算法:
- 逐位读取二进制字符串
- 累积当前编码
- 当累积的编码在decodeMap中找到匹配时,输出对应字符
- 清空当前编码,继续下一位
3. 算法步骤可视化
3.1 Huffman编码流程图
┌─────────────────────────────────────────────────────────┐
│ Huffman编码流程 │
└─────────────────────────────────────────────────────────┘
┌─────────────┐
│ 读取源文件 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 统计字符频率 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 创建叶节点 │
└──────┬──────┘
│
▼
┌──────────────────────────┐
│ 循环: │
│ 1. 排序节点列表 │
│ 2. 取出最小两个节点 │
│ 3. 创建父节点(权值和) │
│ 4. 插入回列表 │
│ 直到只剩一个节点(根) │
└──────┬──────┘
│
▼
┌─────────────┐
│ 递归生成编码 │ (左0右1)
└──────┬──────┘
│
▼
┌─────────────┐
│ 文本转编码串 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 补零对齐8位 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 写入压缩文件 │
└─────────────┘
3.2 Huffman解码流程图
┌─────────────────────────────────────────────────────────┐
│ Huffman解码流程 │
└─────────────────────────────────────────────────────────┘
┌─────────────┐
│ 读取压缩文件 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 读取补零长度 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 读取编码表 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 构建解码映射 │ (编码→字符)
└──────┬──────┘
│
▼
┌─────────────┐
│ 读取字节数据 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 转二进制串 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 去除补零 │
└──────┬──────┘
│
▼
┌──────────────────────────┐
│ 逐位匹配解码: │
│ 累积编码 → 查表匹配 → │
│ 输出字符 → 清空继续 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 输出还原文本 │
└─────────────┘
4. 时间/空间复杂度分析
4.1 编码阶段复杂度
| 步骤 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 字符频率统计 | O(n) | O(k) |
| 树构建排序 | O(k² log k) | O(k) |
| 树构建合并 | O(k log k) | O(k) |
| 生成编码 | O(k) | O(k) |
| 文本编码 | O(n) | O(n) |
| 总体 | O(n + k² log k) | O(n + k) |
注:n为文本长度,k为不同字符数
4.2 解码阶段复杂度
| 步骤 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 读取数据 | O(m) | O(m) |
| 构建解码映射 | O(k) | O(k) |
| 二进制转换 | O(m) | O(m) |
| 逐位解码 | O(m) | O(n) |
| 总体 | O(m) | O(m + n) |
注:m为压缩后数据长度,n为原文长度
4.3 与传统编码方式对比
| 编码方式 | 编码长度 | 压缩率 | 适用场景 |
|---|---|---|---|
| ASCII固定编码 | 8位/字符 | 无 | 通用文本 |
| UTF-8 | 1-4字节/字符 | 低 | 多语言文本 |
| Huffman编码 | 变长,频率相关 | 高(20%-90%) | 数据压缩 |
Huffman编码优势:
- 理论上可证明是最优前缀编码
- 对于有重复模式的数据压缩效果显著
- 实现相对简单,易于理解
5. 性能测试
使用该代码对自身源文件进行压缩测试:
| 指标 | 数值 |
|---|---|
| 原文字符数 | 约2000+ |
| 压缩后字节数 | ~(原文长度/2) |
| 补零位数 | 0-7 |
| 压缩率 | 约40%-60% |
6. 总结
Huffman编码作为经典的数据压缩算法,凭借其最优前缀编码特性和简洁的实现,至今仍在各种压缩场景中广泛应用。通过详细分析,我们可以看到:
- 贪心思想是Huffman算法的核心
- 树结构提供了高效的编码生成和解码路径
- 前缀编码保证了解码的唯一性
- 通过反向映射可以实现O(1)时间的字符查找

1430

被折叠的 条评论
为什么被折叠?



