给定n个权值并作为n个叶结点按一定规则构造一棵二叉树,其带权路径长度达到最小值,
则这棵二叉树称为最优二叉树,也称为哈夫曼树(HuffmanTree)。
一、哈夫曼树的构建
1. 基本步骤
假设n个叶结点的权值分别为{w1,w2,……,wn},则
- 由已知给定的n个权值,构造一个由n棵二叉树所构成的森林F={T1,T2,……,Tn},其中每一棵二叉树只有一个根结点;
- 在二叉树森林F中选取根结点权值最小和次小的两棵二叉树,分别把它们作为左右子树去构造一棵新二叉树,新二叉树根结点权值为期左右子树根结点的权值之和;
- 作为新二叉树的左右子树的两棵二叉树从森林F中删除,将新产生的二叉树加入到森林F中;
- 重复步骤2和3,直到森林中只剩下一棵二叉树为止,则这棵二叉树就是所构造的哈夫曼树。
2. 示例
实现代码如下:
public class HuffmanTree {
public static void main(String[] args) {
//测试
int[] arr = {2,6,1,4,9,7}; //初始化权值
preRootTraverse(createHuffmanTree(arr)); //构建哈夫曼树并用先根遍历打印
}
/*构造哈夫曼树*/
public static Node createHuffmanTree(int[] arr) {
List<Node> nodes = new ArrayList<Node>(); //使用顺序表存储结点
for(int weight : arr) {
nodes.add(new Node(weight));
}
while(nodes.size() > 1) {
Collections.sort(nodes); //按权值大小对结点进行排序
Node leftNode = nodes.get(0); //权值最小的为左孩子
Node rightNode = nodes.get(1); //权值次小的为右孩子
Node parent = new Node(leftNode.weight + rightNode.weight); //父结点的权值为左右孩子权值之和
parent.left = leftNode;
parent.right = rightNode;
//从顺序表中删去已加入结点
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parent); //将新构造的父结点加入顺序表中
}
return nodes.get(0); //返回根结点
}
/*先根遍历*/
public static void preRootTraverse(Node T) {
if(T != null) {
System.out.print(T.weight + " "); //访问根结点
preRootTraverse(T.left); //递归遍历左子树
preRootTraverse(T.right); //递归遍历右子树
}
}
}
/*使用内部类构造结点类*/
class Node implements Comparable<Node>{
int weight;
Node left;
Node right;
//构造器
public Node(int weight) {
this.weight = weight;
}
//比较权值大小
public int compareTo(Node o) {
return this.weight - o.weight;
}
}
二、哈夫曼编码
1. 基本步骤
用电文中各个字符使用的频度作为叶结点的权,构造一棵具有最小带权路径长度的哈夫曼树,若对树中的每个左分支赋予标记0,右分支赋予标记1,则从根结点到每个叶结点的路径上的标记连接起来就构成一个二进制串,该二进制串就是哈夫曼编码。
2. 示例
实现代码如下:
public class HuffmanCode {
public static void main(String[] args) {
//测试(略)
}
/*求哈夫曼树编码的算法*/
public int[][] huffmanCoding(int[] W){
int n = W.length; //字符个数
int m = 2*n-1; //哈夫曼树的结点数
HuffmanNode[] HN = new HuffmanNode[m];
//构造n个具有权值的结点
for(int i=0;i<n;i++)
HN[i] = new HuffmanNode(W[i]);
//构建哈夫曼树
for(int i=0;i<m;i++) {
//选择不再哈夫曼树中且weight最小的两个结点min1、min2
HuffmanNode min1 = selectMin(HN,i-1);
min1.flag = 1;
HuffmanNode min2 = selectMin(HN,i-1);
min2.flag = 1;
//构造min1和min2的父结点,并修改其父结点的权值
HN[i] = new HuffmanNode();
min1.parent = HN[i];
min2.parent = HN[i];
HN[i].lchild = min1;
HN[i].rchild = min2;
HN[i].weight = min1.weight + min2.weight;
}
//从叶子到根逆向求每个字符的哈夫曼编码
int[][] HuffCode = new int[n][n]; //分配n个字符编码存储空间
for(int j=0;j<n;j++) {
int start = n-1; //编码的开始位置,初始化为数组的结尾
for(HuffmanNode c=HN[j],p=c.parent;p!=null;c=p,p=p.parent) {
if(p.lchild.equals(c))
HuffCode[j][start--] = 0; //左孩子编码为0
else
HuffCode[j][start--] = 1; //右孩子编码为1
}
HuffCode[j][start] = -1; //编码开始标志为-1,编码是-1之后的0、1序列
}
return HuffCode;
}
/*选择不再哈夫曼树且weight最小的结点*/
private HuffmanNode selectMin(HuffmanNode[] HN,int end) {
HuffmanNode min = HN[end];
for(int i=0;i<=end;i++) {
HuffmanNode h = HN[i];
if(h.flag==0 && h.weight<min.weight)
min = h;
}
return min;
}
}
//使用内部类构造哈夫曼树的结点类
class HuffmanNode{
public int weight; //结点的权值
public int flag; //标志结点是否已加入哈夫曼树
public HuffmanNode parent,lchild,rchild; //父结点和左右孩子结点
//构造一个空节点
public HuffmanNode() {
this(0);
}
//构造一个带权值的结点
public HuffmanNode(int weight) {
this.weight = weight;
flag = 0;
parent = lchild = rchild = null;
}
}