哈夫曼编码是一种前缀编码,也就是说,它编码的字符,任何一个字符的编码都不是另一个字符的前缀,这使得对哈夫曼编码进行解码变得容易。而使得哈夫曼编码是前缀编码的关键就是哈夫曼树。哈夫曼树也正是本文要说的。哈夫曼树是一颗二叉树,在这棵树上,从父节点到左孩子节点的边被标为0,从父节点到右孩子节点的边被标为1,所有字符被编码为一个从根结点到叶子节点的路径上的所有01构成的位串,这保证了哈夫曼编码是前缀编码的性质,并且由此可知,离根越远的节点,编码需要的位数就越多。构造哈夫曼树的算法是一个贪心算法:
(1)首先对所有字符进行次数统计;
(2)取出其中最小的两个字符,作为一个新节点的左右孩子,新节点的次数就是其左右孩子节点的次数之后,然后把新节点插入原先有序的字符集中;
(3)重复第二步,直到最后只有一个元素为止。
由算法可知,那些出现次数少的字符,编码需要的位数少,那些出现次数多的字符,编码需要的位数多。因为编码所有字符需要的位数等于:
∑ni=1
字符i * 字符i的次数
所以,这样能保证编码编码所有字符需要的位数尽量少。而每次选择次数最少的字符(这些字符的节点离根结点最远)正是算法的贪心所在。
由算法还知道,我们需要不断地获得所有字符串中出现次数最少的,并把由抽取出来的节点合成的新节点放回去,这些特点决定该算法很适合用最小优先级队列来实现,优先级就是字符出现的次数。下图是算法导论中给出的构建哈夫曼树的伪代码:
其中,C是待编码的字符集,Q是最小优先级队列。
对应的Java代码如下:
protected Node createTree(ArrayList<Node> letterList) {
int n = letterList.size();
Node[] a = new Node[n];
letterList.toArray(a);
MinPriorityQueue<Node> helper = new MinPriorityQueue<Node>(a, n);
// 需要n-1步
for(int i = 1; i <= n - 1; i++) {
Node left = helper.heapExtractMin();
Node right = helper.heapExtractMin();
Data data = new Data();
data.setFrequency(left.getData().getFrequency() +
right.getData().getFrequency());
Node parent = new Node();
parent.setData(data);
parent.setLeftChild(left);
parent.setRightChild(right);
helper.minHeapInsert(parent);
}
return helper.heapExtractMin();
}
其中的Data类和Node类源码如下:
/**
* Data用于存储一个字符及其出现的次数
* @author yuncong
*
*/
public class Data implements Comparable<Data>{
private char c = 0;
private int frequency = 0;
public char getC() {
return c;
}
public void setC(char c) {
this.c = c;
}
public int getFrequency() {
return frequency;
}
public void setFrequency(int frequency) {
this.frequency = frequency;
}
@Override
public String toString() {
return "Data [c=" + c + ", frequency=" + frequency + "]";
}
@Override
public int compareTo(Data o) {
if (this.frequency < o.getFrequency()) {
return -1;
} else if (this.frequency > o.getFrequency()) {
return 1;
} else {
return 0;
}
}
}
/**
* Node是哈夫曼树中的一个节点
* @author yuncong
*
*/
public class Node implements Comparable<Node>{
private Node leftChild = null;
private Data data = null;
private Node rightChild = null;
public Node getLeftChild() {
return leftChild;
}
public void setLeftChild(Node leftChild) {
this.leftChild = leftChild;
}
public Data getData() {
return data;
}
public void setData(Data data) {
this.data = data;
}
public Node getRightChild() {
return rightChild;
}
public void setRightChild(Node rightChild) {
this.rightChild = rightChild;
}
@Override
public String toString() {
return "Node [leftChild=" + leftChild + ", data=" + data
+ ", rightChild=" + rightChild + "]";
}
@Override
public int compareTo(Node o) {
return this.data.compareTo(o.getData());
}
}
哈夫曼编码解码算法的完整实现的github地址如下:
https://github.com/l294265421/algorithms-huffman.git