基本概念
结点的路径
从根结点到该结点所经历的结点和分支序列。
结点的路径长度
从根结点到该结点的路径上分支的数目。
结点的权
结点被人为赋予的一个具有实际意义的数值,如重要程度、成本等。
结点的带权路径长度
结点的路径长度与该结点的权值的乘积。
树的带权路径长度
树中所有叶子结点的带权路径长度之和。
假设树有
n
n
n个叶子节点,该树的带权路径长度通常记作:
W
P
L
=
∑
i
=
1
n
W
i
×
L
i
WPL=\sum_{i=1}^{n}{W_i \times L_i}
WPL=i=1∑nWi×Li其中
W
i
W_i
Wi为带权
L
i
L_i
Li的叶子结点的路径长度。
最优二叉树
给定
n
n
n个权值的
n
n
n个叶子结点,按照一定的规则构造一棵二叉树,使树的带权路径长度达到最小值,则这棵树被程为最优二叉树,也称哈夫曼树(Huffman Tree)。
哈夫曼树的构造方法
- 根据给定的 n n n个权值,构造一个由 n n n棵二叉树所构成的集合(森林)。 F = T 1 , T 2 , . . . , T n , F={T_1, T_2, ..., T_n}, F=T1,T2,...,Tn,其中每棵二叉树均只带一个权值为 w i w_i wi的根结点,其左、右子树为空;
- 在二叉树森林 F F F中选取根结点的权值最小和次小的两棵二叉树 T i T_i Ti、 T j T_j Tj,分别把它们作为左、右子树构造一棵新二叉树,新二叉树的根结点权值为 T i T_i Ti、 T j T_j Tj的根结点的权值之和;
- 将 T i T_i Ti、 T j T_j Tj从森林 F F F中删除,同时加入新产生的二叉树;
- 重复2,3两步,直至 F F F中只有一棵二叉树为止,则这棵二叉树即为所构成的哈夫曼树。
例如:
给定权值
W
=
{
2
,
4
,
8
,
9
,
5
}
W=\{2,4,8,9,5\}
W={2,4,8,9,5}, 构成森林
F
F
F:
选取根结点最小和次小的两棵树,根结点分别为2,4,构成新二叉树,其根结点权值为左、右子树根结点的权值之和:
用生成的新树替换
F
F
F中它的左右子树:
再找出新生成的
F
F
F中根结点权值最小和次小的两棵树,重复以上步骤:
最终得到一棵二叉树,即为生成的哈夫曼树:
哈夫曼编码
用哈夫曼树编码
步骤如下:
- 将需要编码的每个字符用一个结点代表,按照具体的用途赋予相应的权值,构成结点集 F F F。
- 用 F F F中的所有结点构成一棵哈夫曼树。
- 对于每一个字符,其编码为:从根结点到该结点的路径,若为左分支,则该位为0,若为右分支,则该位为1。编码的长度即为结点的路径长度。
如对于以下哈夫曼树:
字符“B” 的结点的路径如蓝线所示,从根结点向下路径分支依次为"左-右-左", 因此其哈夫曼编码为“010”。同理,字符“A”的编码为“00”, 字符"C"的编码为"011",字符“D",“E"的编码分别为"10”,“11"。
如果各个字符的权值代表其出现频率,按照哈夫曼编码,权值越大的结点越晚被合并,出现频率越大的字符哈夫曼编码的长度越小,这将能够用于文本压缩。
哈夫曼编码是一种前缀编码。前缀编码指的是,任何一个字符的编码都不是同一字符集中另一个字符的编码的前缀。由哈夫曼编码的过程可以知道,编码即是代表这一字符所代表的结点在哈夫曼树中的路径。因为每一个字符的结点均为叶子结点,因此并不会有经由此结点到达的另一结点,因此不存在以此字符的编码为前缀的另一编码。
用哈夫曼树译码
译码过程是编码过程的逆过程。
译码方法为:从哈夫曼树的根开始,从左到右把二进制编码的每一位进行判别,若二进制编码为0,则选择左分支走向下一个结点;若遇到1,则选择右分支走向下一个结点。
代码实现
使用下表中的字符集(包含26个大写英文字母及字母频率权值)构造一棵huffman树,并根据此huffman树得到该字符集中每个字母的huffman编码。
字符 | Value | 字符 | Value | 字符 | Value | 字符 | Value | 字符 | Value | 字符 | Value | 字符 | Value |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
A | 7 | B | 2 | C | 2 | D | 3 | E | 11 | F | 2 | G | 2 |
H | 6 | I | 6 | J | 1 | K | 1 | L | 4 | M | 3 | N | 7 |
O | 9 | P | 2 | Q | 1 | R | 6 | S | 6 | T | 8 | U | 4 |
V | 1 | W | 2 | X | 1 | Y | 2 | Z | 1 |
这里使用了优先队和二叉树进行实现。调用的包是自己写的数据结构,换成java内置的库即可。
package dataStructure;
import dataStructure.linearList.PriorityQData;
import dataStructure.linearList.PriorityQueue;
import dataStructure.tree.BiTree;
import dataStructure.tree.BiTreeNode;
import dataStructure.linearList.Node;
public class ApplicationHuffmanTree {
public static void main(String[] args) throws Exception {
int[] wList = {7,2,2,3,11,2,2,6,6,1,1,4,3,7,9,2,1,6,6,8,4,1,2,1,2,1};
char[] cList = {'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','V','U','W','X','Y','Z'};
HuffmanTree hfmT = new HuffmanTree(cList,wList);
hfmT.getHuffmanCode(hfmT.getRoot(),"");
System.out.println();
}
}
class HuffmanTree extends BiTree {
public HuffmanTree(char[] cList, int[] wList) throws Exception {
if(cList.length != wList.length)
throw new Exception("权值序列与字母序列不匹配");
PriorityQueue que = new PriorityQueue();
for(int i = 0; i < cList.length; i++){
BiTreeNode node = new BiTreeNode(cList[i]);
PriorityQData<BiTreeNode> x = new PriorityQData(node,wList[i]);
que.offer(x);
}
for(int j = 0; j < cList.length-1; j++){
PriorityQData<BiTreeNode> l = que.poll();
PriorityQData<BiTreeNode> r = que.poll();
int p = l.getPriority()+r.getPriority();
BiTreeNode newNode = new BiTreeNode();
newNode.setLchild(l.getElem());
newNode.setRchild(r.getElem());
PriorityQData<BiTreeNode> y = new PriorityQData(newNode,p);
que.offer(y);
this.root = newNode;
}
}
//利用深度优先遍历求出huffman树的huffman编码
public void getHuffmanCode(BiTreeNode T, String code) {
if (T.getLchild() == null){
System.out.println(T.getData() + ": " + code);
}
else{
getHuffmanCode(T.getLchild(),code.concat("0"));
getHuffmanCode(T.getRchild(),code.concat("1"));
}
}
}
运行效果: