实验要求:
编程实现Huffman编码问题,并理解其核心思想。
对字符串进行01编码,输出编码后的01序列,并比较其相对于定长编码的压缩率。例如对于字符串“AABBBEEEEGZ”,如果使用定长编码,‘A’、‘B’、‘E’、‘G’、‘Z’字符各需要3位01串编码,编码后的字符长度为3*11=33位,如果使用Huffman编码,可编码为下图,编码后的字符长度为2*3+3*2+4*1+4+4=24,压缩率为24/33=72.73%.
对文件data.txt的字符串按照Huffman编码方式编码为01序列,并输出到encode.txt文件,控制台打印压缩率。
1.实验内容
Huffman编码可以有效的压缩数据,通常可以节省20%~90%的空间,具体压缩率依赖于数据的特性。本实验实现Huffman树的生成,完成Huffman编码的过程。
2.实验目的
1.掌握Huffman树的概念、存储结构
2.掌握建立Huffman树和Huffman编码的方法及带权路径长度的计算
3.掌握二叉树的应用
3.Huffman树结点定义
Huffman树的结点具有一个数据域data ,用来存放该结点所代表的数据;freq 表示data 在待编码序列中出现的频数;left 和right 指明了当前节点的左孩子和右孩子;code 为当前节点的Huffman编码。
public class Node<T> implements Comparable<Node<T>>
{
private T data; //数据域
private int freq; //数据出现的频率
private Node<T> left; //左孩子
private Node<T> right; //右孩子
String code; //编码
}
在结点中重写了compareTo 方法来实现将结点按照freq 从大到小排序。
//从大到小排序
@Override
public int compareTo(Node<T> other)
{
if (other.getFreq() > this.getFreq())
{
return 1;
}
if (other.getFreq() < this.getFreq())
{
return -1;
}
return 0;
}
4.Huffman树的构造及编码
4.1Huffman树的构造
HuffmanTree 类包括一个Node 类型的属性root ,为Huffman树的树根。creatTree 方法来构造一棵Huffman树,该方法接收一个由待编码序列中的各个不同的数据所创建的对象的列表nodes ,该方法执行结束后,Huffman树创建完成,root 中保存了该树的树根。
creatTree 首先判断列表nodes 中的元素个数,若nodes.size()>1 ,则进入while 循环;当条件不满足时,nodes 中只剩一个结点,也就是树根节点。
while (nodes.size() > 1) //直到list中剩一个结点也就是树根
进入循环后,首先将nodes 中的结点按照freq 从大到小进行排序
Collections.sort(nodes); //按从大到小排序
排序结束后,nodes 的倒数第一个和倒数第二个元素分别为列表中freq 最小和次小的元素,按照Huffman树的构件规则,选取这两个结点为左右孩子构建其父结点,父结点的data 为null ,freq 为这两个结点的freq 之和。对两个孩子进行编码,这里规定倒数第二个结点编码为0,为左孩子;倒数第一个结点编码为1,为右孩子。
Node<T> left = nodes.get(nodes.size() - 2); //倒数第二个结点设为左孩子 倒数第二小
left.setCode(0 + ""); //编码为0
Node<T> right = nodes.get(nodes.size() - 1); //倒数第一个结点设为右孩子 最小
right.setCode(1 + ""); //编码为1
//左右孩子的父结点的权值为两个孩子权值之和
Node<T> parent = new Node<>(null, left.getFreq() + right.getFreq());
parent.setLeft(left);
parent.setRight(right);
将左右孩子从列表中删除,并将父结点加入的列表中,至此一次循环结束。以列表中剩余的结点继续重复上述过程,便可构建其一棵Huffman树。
//将左右孩子删除并把父结点放入list中
nodes.remove(left);
nodes.remove(right);
nodes.add(parent);
循环结束后,nodes 中有且只有一个结点,为Huffman树的根节点,将其赋给root 。
root = nodes.get(0); //树根赋值
4.2序列编码
用HuffmanTree 中的广度优先遍历breadth 方法遍历整棵树,并将结果保存到哈希表codeSet 中,key 为结点的data ,value 为结点的Huffman编码。对待编码序列进行编码,遍历待编码序列,每一个字符在codeSet 中寻找其对应的编码,即可完成对待编码序列的编码。
//广度优先遍历,存放字符与对应的编码
HashMap<Character, String> codeSet = huffmanTree.breadth(huffmanTree.getRoot());
for (int i = 0; i < str.length(); i++)
{
codeStr.append(codeSet.get(str.charAt(i)));
}
4.3压缩率计算
压缩率的定义为Huffman编码的位数与传统编码的位数之比。Huffman编码的位数由codeStr.length() 容易求得。传统编码位数的计算由
Integer.toBinaryString(codeSet.size()).length() * str.length()
求得。式中codeSet.size() 为待编码序列中不同数据的个数,将其转为二进制字符串并求其长度即可得到传统编码下每一数据的编码长度,再乘上序列的长度即可得到传统编码的编码长度。
rate = (double) codeStr.length() / (Integer.toBinaryString(codeSet.size()).length() * str.length());
5.算法测试结果
输入字符串为“AASMABBAAARRAABCAACCRRSNEEFF”,其中各个元素的个数如下表
元素 | 出现次数 |
A | 10 |
B | 3 |
C | 3 |
E | 2 |
F | 2 |
M | 1 |
N | 1 |
R | 4 |
S | 2 |
根据Huffman树的编码规则可以得到各个字符得到编码为
元素 | 编码 |
A | 1 |
B | 0000 |
C | 0001 |
E | 0111 |
F | 0100 |
M | 01010 |
N | 01011 |
R | 001 |
S | 0110 |
则Huffman编码得到的长度为80,普通编码的长度为112,压缩率为71.4%。输出结果如下图所示
6.实验过程中遇到的困难及收获
Huffman之所以能够压缩编码长度,是因为采用了变长编码的技术,其思想是赋予高频字符短码字,赋予低频字符长码字。Huffman编码采用的是前缀码,即没有任何码字是其他码字的前缀。前缀码的作用是简化解码过程,由于没有码字是其他码字的前缀,因此解码的过程是无歧义的,二进制串可以唯一的解析。
Huffman树的构造过程采用了贪心的思想,每次贪心的选取出现频率最低的字符,这样可以保证低频字符出现在树的深层次,码字长;高频字符出现在浅层次码字短,即达到了压缩编码长度的目的。
7.实验源代码
Node.java
//哈夫曼树节点类
public class Node<T> implements Comparable<Node<T>>
{
private T data; //数据域
private int freq; //数据出现的频率
private Node<T> left; //左孩子
private Node<T> right; //右孩子
String code; //编码
public Node(T data, int freq)
{
this.data = data;
this.freq = freq;
this.code = "";
}
public T getData()
{
return data;
}
public int getFreq()
{
return freq;
}
public Node<T> getLeft()
{
return left;
}
public void setLeft(Node<T> left)
{
this.left = left;
}
public Node<T> getRight()
{
return right;
}
public void setRight(Node<T> right)
{
this.right = right;
}
public String getCode()
{
return code;
}
public void setCode(String str)
{
code = str;
}
@Override
public String toString()
{
return "data:" + this.data + ";weight:" + this.freq + ";code: " + this.code;
}
//从大到小排序
@Override
public int compareTo(Node<T> other)
{
if (other.getFreq() > this.getFreq())
{
return 1;
}
if (other.getFreq() < this.getFreq())
{
return -1;
}
return 0;
}
}
HuffmanTree.java
/*
* @Title:
* @Package
* @Description:
* @author yangf257
* @date 2021/12/14 17:51
*/
import java.util.*;
public class HuffmanTree<T>
{
private Node<T> root;
public Node<T> getRoot()
{
return root;
}
public void setRoot(Node<T> root)
{
this.root = root;
}
public void createTree(List<Node<T>> nodes)
{
while (nodes.size() > 1) //直到list中剩一个结点也就是树根
{
Collections.sort(nodes); //按从大到小排序
Node<T> left = nodes.get(nodes.size() - 2); //倒数第二个结点设为左孩子 倒数第二小
left.setCode(0 + ""); //编码为0
Node<T> right = nodes.get(nodes.size() - 1); //倒数第一个结点设为右孩子 最小
right.setCode(1 + ""); //编码为1
//左右孩子的父结点的权值为两个孩子权值之和
Node<T> parent = new Node<>(null, left.getFreq() + right.getFreq());
parent.setLeft(left);
parent.setRight(right);
//将左右孩子删除并把父结点放入list中
nodes.remove(left);
nodes.remove(right);
nodes.add(parent);
}
root = nodes.get(0); //树根赋值
}
//广度优先遍历
public HashMap<T,String> breadth(Node<T> root)
{
Queue<Node<T>> queue = new ArrayDeque<>();
HashMap<T, String> map = new HashMap<>();
if (root != null)
{
queue.offer(root);
root.getLeft().setCode(root.getCode() + "0"); //左孩子为0
root.getRight().setCode(root.getCode() + "1"); //右孩子为1
}
while (!queue.isEmpty())
{
map.put(queue.peek().getData(), queue.peek().getCode());
Node<T> node = queue.poll();
if (node.getLeft() != null) node.getLeft().setCode(node.getCode() + "0");
if (node.getRight() != null) node.getRight().setCode(node.getCode() + "1");
if (node.getLeft() != null)
{
queue.offer(node.getLeft());
}
if (node.getRight() != null)
{
queue.offer(node.getRight());
}
}
return map;
}
}
HuffmanCodeTest.java
/*
* @Title:
* @Package
* @Description:
* @author yangf257
* @date 2021/12/14 19:34
*/
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class HuffmanCodeTest
{
public static void main(String[] args)
{
// String str = "AABBBEEEEGZ";
String str = readFile(); //需要处理的字符串
StringBuilder codeStr = new StringBuilder(); //编码过后的字符串
HuffmanTree<Character> huffmanTree = new HuffmanTree<>();
huffmanTree.createTree(preOper(str));
//广度优先遍历,存放字符与对应的编码
HashMap<Character, String> codeSet = huffmanTree.breadth(huffmanTree.getRoot());
for (int i = 0; i < str.length(); i++)
{
codeStr.append(codeSet.get(str.charAt(i)));
}
double rate;
//codeSet.size():字符串中不同字符的个数
//toBinaryString:将字符串中不同字符的个数转化为二进制字符串,然后求其长度即为传统编码下的一个字符长度
rate = (double) codeStr.length() / (Integer.toBinaryString(codeSet.size()).length() * str.length());
System.out.println(rate);
try
{
File writeName = new File(".\\6.HuffmanCode\\encode.txt");
writeName.createNewFile();
BufferedWriter out = new BufferedWriter(new FileWriter(writeName));
out.write(codeStr.toString());
out.flush();
out.close();
} catch (Exception e)
{
e.printStackTrace();
}
}
//读文件
public static String readFile()
{
String pathname = ".\\6.HuffmanCode\\data.txt";
String nodestr = "";
try (FileReader reader = new FileReader(pathname); BufferedReader br = new BufferedReader(reader) // 建立一个对象,它把文件内容转成计算机能读懂的语言
)
{
String line = "";
while ((line = br.readLine()) != null)
{
nodestr += line;
}
} catch (IOException e)
{
e.printStackTrace();
}
return nodestr;
}
/**
* 预处理,把每一个字符创建为Node对象,并保存在nodes列表中
* @param nodestr:需要处理的字符串
*/
public static List<Node<Character>> preOper(String nodestr)
{
HashMap<Character, Integer> map = new HashMap<>();
for (int i = 0; i < nodestr.length(); i++)
{
map.put((nodestr.charAt(i)), map.getOrDefault(nodestr.charAt(i), 0) + 1);
}
List<Node<Character>> nodes = new ArrayList<>();
for (char key : map.keySet())
{
Node<Character> node = new Node<>(key, map.get(key));
nodes.add(node);
}
return nodes;
}
}