首先了解一下什么是哈夫曼树:
给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,即权值较大的结点离根较近。
哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。树的路径长度是从树根到每一结点的路径长度之和,记为WPL=(W1*L1+W2*L2+W3*L3+...+Wn*Ln),N个权值Wi(i=1,2,...n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,...n)。可以证明哈夫曼树的WPL是最小的。
下面给出关于哈夫曼树的一些基本概念:
1、路径和路径长度
在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
2、结点的权及带权路径长度
若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
3、树的带权路径长度
树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL,最优二叉树的形态不唯一,WPL最小。
构造哈夫曼树
1) 根据给定的n个权值{w1, w2, w3, w4......wn}构成n棵二叉树的森林 F={T1 , T2 , T3.....Tn},其中每棵二叉树只有一个权值为wi 的根节点,其左右子树都为空;
2) 在森林F中选择两棵根节点的权值最小的二叉树,作为一棵新的二叉树的左右子树,且令新的二叉树的根节点的权值为其左右子树的权值和;
3)从F中删除被选中的那两棵子树,并且把构成的新的二叉树加到F森林中;
4)重复2 ,3 操作,直到森林只含有一棵二叉树为止,此时得到的这棵二叉树就是哈夫曼树。
下面给出哈夫曼树的Java实现。(较原文有一些改动)
package gxu.wjb.tree;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* 哈夫曼树的构建
* @version 创建时间:2019-8-9 下午2:51:02
*
*
*/
public class HuffmanTree {
public static class Node<E> {
E data;
double weight;
Node<E> leftChild;
Node<E> rightChild;
public Node(E data, double weight) {
this.data = data;
this.weight = weight;
}
}
public static void main(String[] args) {
List<Node<String>> nodes = new ArrayList<Node<String>>();
nodes.add(new Node<String>("A", 40.0));
nodes.add(new Node<String>("B", 8.0));
nodes.add(new Node<String>("C", 10.0));
nodes.add(new Node<String>("D", 30.0));
nodes.add(new Node<String>("E", 10.0));
nodes.add(new Node<String>("F", 2.0));
Node<String> root = HuffmanTree.createTree(nodes);
breadthFirst(root);
}
public static Node<String> createTree(List<Node<String>> nodes) {
// 只要nodes列表中还有2个及以上的节点
while (nodes.size() > 1) {
quickSort(nodes, 0, nodes.size() - 1);
// 获取权值最小的两个点
Node<String> left = nodes.get(0);
Node<String> right = nodes.get(1);
// 生成新节点,新节点的权值为两个子节点权值之和
Node<String> parent = new Node<String>(null, left.weight
+ right.weight);
// 让新节点作为两个最小节点的父节点
parent.leftChild = left;
parent.rightChild = right;
// 删除两个最小节点
nodes.remove(0);
nodes.remove(0);
// 将新节点加入到集合中
nodes.add(parent);
}
// 剩余一个节点的时候返回即可
return nodes.get(0);
}
/**
* 实现快速排序算法,该快排是基于荷兰国旗的思想进行改进后的实现,用于对节点进行排序
*/
public static void quickSort(List<Node<String>> nodes, int L, int R) {
if (L < R) {
int[] p = getProcess(nodes, nodes.get(L), L, R);
quickSort(nodes, L, p[0] - 1);
quickSort(nodes, p[1], R);
}
}
private static int[] getProcess(List<Node<String>> nodes,
Node<String> base, int L, int R) {
int less = L - 1;
int more = R + 1;
int cur = L;
while (cur < more) {
if (nodes.get(cur).weight < base.weight) {
swap(nodes, ++less, cur++);
} else if (nodes.get(cur).weight == base.weight) {
cur++;
} else {
swap(nodes, --more, cur++);
}
}
return new int[] { less + 1, more };
}
/**
* 将指定集合中的i和j索引处的元素交换
*
* @param nodes
* @param i
* @param j
* @return
*/
private static void swap(List<Node<String>> nodes, int i, int j) {
Node<String> temp = nodes.get(i);
nodes.set(i, nodes.get(j));
nodes.set(j, temp);
}
// 广度优先遍历,也就是按层次遍历
public static void breadthFirst(Node<String> head) {
if (head == null) {
return;
}
Queue<Node<String>> queue = new LinkedList<Node<String>>();
queue.offer(head);
while (!queue.isEmpty()) {
head = queue.poll();
System.out.println(head.data);
if (head.leftChild != null) {
queue.offer(head.leftChild);
}
if (head.rightChild != null) {
queue.offer(head.rightChild);
}
}
}
}
哈夫曼编码
根据哈夫曼树可以解决报文编码的问题。假设需要把一个字符串,如“abcdabcaba”进行编码,将它转换为唯一的二进制码,但是要求转换出来的二进制码的长度最小。
假设每个字符在字符串中出现频率为W,其编码长度为L,编码字符n个,则编码后二进制码的总长度为W1L1+W2L2+…+WnLn,这恰好是哈夫曼树的处理原则。因此可以采用哈夫曼树的构造原理进行二进制编码,从而使得电文长度最短。
对于“abcdabcaba”,共有a、b、c、d4个字符,出现次数分别为4、3、2、1,相当于它们的权值,将a、b、c、d以出现次数为权值构造哈夫曼树,得到下左图的结果。
从哈夫曼树根节点开始,对左子树分配代码“0”,对右子树分配“1”,一直到达叶子节点。然后,将从树根沿着每条路径到达叶子节点的代码排列起来,便得到每个叶子节点的哈夫曼编码,如下右图。
0
从图中可以看出,a、b、c、d对应的编码分别为0、10、110、111,然后将字符串“abcdabcaba”转换为对应的二进制码就是0101101110101100100,长度仅为19。这也就是最短二进制编码,也称为哈夫曼编码。