树是一种 有层次关系的 数据结构。它由结点组成。
树的结点由 数据域 和 子结点域 组成。数据域 作为数据的容器;子结点域 存放 子结点 的地址。一个结点是它的子结点的父结点。不同层级之间的结点通过 子结点域 形成 “父子关系”。
每个结点有零个或多个子结点;没有父结点的结点称为根结点;每一个非根结点有且只有一个父结点;没有 子结点的结点称为叶结点。
图一中的结点A 就是根结点。B、C是 A 的子结点。C、E、F三个结点组成了一个子树。E也是一个子树。D、E、F都是叶结点。
如果一个结点有 n 个子结点,那么这个 结点的度 是 n 。如果一个树中的结点的度的最大值是 n ,那么这个 树的度 是 n。
对于一个树中的任意两个不同的结点,如果从一个结点出发,按层次 自上而下 沿着一个个结点能到达另一结点,称它们之间存在着一条 路径。
根结点的 层次数 为1,每个结点的 层次数 都等于其父结点的 层次数 加一。
如果一个树中的结点的层次的最大值为 n,则这个树的 深度 为 n。
特别地,度为 2 的树称为 二叉树。
二叉树的结点类:
public class TreeNode {
public Object data; //数据域
public TreeNode left; //左子结点域
public TreeNode right; //右子结点域
}
哈夫曼树 (Huffman Tree,霍夫曼树,最优二叉树) 是一种二叉树。哈夫曼树是一种带权路径长度最短的树。
权 是与树的结点关联的一个实数,用 W 表示。非叶结点的 权 等于其各子结点的 权 的和。一个结点的 路径长度 是这个结点与根结点的层次数之差,也就是该结点的层次数减 1,用 l 表示。
结点的带权路径长度 为:从根结点到该结点之间的路径长度与该结点的权的乘积。
一个二叉树中的各个 叶结点 的 带权路径长度 之和为 树的带权路径长度 ,用WPL表示,即:
下图就是一个哈夫曼树。方框中的数表示结点的权。
下面这个树的带权路径长度 为
WPL = 12 × 3 + 14 × 2 + 21 × 2 + 33 × 2 = 172 。
最常见的应用之一应该是压缩。在英文中,字母 e 的出现频率最高,字母 z 的出现频率最低。如果我们用 1 bit 来表示字母 e ,那就节省了很多空间。
上面的想法很好,但是不可行。如果用 1 bit 的数据来表示字母 e ,比如说,用 0 来表示 e ,那么在解码的时候,很难从一串 0 和 1 中分辨出到底哪个 0 才是字母 e 对应的数据,因为其他的字母的编码也可能会含有 0 。
我们要做的是:保证每个字符的编码都不是其他任何字符编码的前缀,以至于不引起歧义;又保证每个字符的二进制编码足够短,以至于能够节省最多空间。
戴维·阿尔伯特·哈夫曼(David Albert Huffman, 1925-1999) 是一个天才。他解决了上面的问题。他对他的二叉树进行 编码:他把父结点到左子结点的路径编码为 0 ,把父结点到右子结点的路径编码为 1;在结点的数据域中存放字母,把字母的出现频数设为结点的权。在哈夫曼树中,把根结点到叶结点经过的路径的编码组合在一起,就得到了这个叶结点的哈夫曼编码。
举个例子:在一段文字中,字符 c 、I、M、r 分别出现了 8 次、5 次、3 次、2 次,没有其他字符。我们就把这三个字符存入叶结点的数据域,把它们的频数作为结点的权,建成哈夫曼树。
那么在这个哈夫曼树中, c 的哈夫曼编码就是 1,M 、r、I 的哈夫曼编码分别是 000、001、01。这是不引起歧义,又能节省最多空间的编码方案。
public class Node {
public char data; //数据域
public int weight; //权
public Node left; //左子结点
public Node right; //右子结点
public Node(char data, int weight) {
this.data = data;
this.weight = weight;
}
public Node(Node left, Node right) {
this.left = left;
this.right = right;
weight = left.weight + right.weight;
}
}
public class HuffmanTree {
private ArrayList<Node> nodeList = new ArrayList<>(48);
private int size;
private int[] classify(String data) {
int[] counter = new int[256]; (注 1)
for (int i = 0; i < data.length; i++) {
counter[data.charAt(i)]++;
}
return counter;
}
//...
}
为出现过的字符创建结点。把字符存入数据域,把频数设为权。把结点存入一个数组列表中。
private ArrayList<Node> createNode(int[] counter) {
ArrayList<Node> list = new ArrayList<>(50);
for (int i = 0; i < counter.length; i++) {
if (counter[i] > 0) {
list.add(new Node((char) i, counter[i]);
}
}
return list;
}
按权由小到大的顺序,对结点进行排序。
private void createTree(ArrayList<Node> list) { // list 中是已经排好序的结点
while (list.size() > 1) { // 最后剩下的一个结点就是根结点
Node left = list.remove(0), right = list.remove(0);
list.add(new Node(left, right));
}
}
依照实际情况,我们只要输出叶结点的哈夫曼编码就行了。
public void printCode(Node root) {
printCode(root, "");
}
private void printCode(Node node, String code) {
if (node.left != null) {
printCode(node.left, code + "0");
}
if (node.right != null) {
printCode(node.right, code + "1");
}
if (node.left == null && node.right == null) { // 如果是叶结点,就输出。
System.out.println(code + "\t" + node.data);
}
}
我写了一个接口 TestText, 里面只有一个属性 testText (StringBuilder)。存放的是测试用的字符串。
测试用的字符串内容如下:
"My son, the day you were born, " +
“the very forests of Paektu Mountain whispered the name \“Kim Jong-un\”.\n” +
“My child, I watched with pride as you grew into an heir of Juche.\n” +
“Remember - our family has always ruled with propaganda and violence.\n” +
“And I know you will show happiness when exercising your nuclear weapon.\n” +
“But the truest victory is controlling the minds of your people.\n” +
“I tell you this, for when my days have come to an end - you, shall be King.”
下面是我的代码:
public class HuffmanTree implements TestText {
private ArrayList<TreeNode> nodeList;
public TreeNode root;
private int size;
public static void main(String[] args) {
HuffmanTree sample = new HuffmanTree(testText);
sample.printCode(sample.root);
}
public HuffmanTree(StringBuilder string) {
sortByWeight(nodeList = createNodeList(classify(string)));
size = nodeList.size();
root = build();
}
public void printCode(TreeNode root) {
printCode(root, "");
}
private void printCode(TreeNode node, String string) {
if (node.left != null) {
printCode(node.left, string + 0);
}
if (node.right != null) {
printCode(node.right, string + 1);
}
if (node.left == null && node.right == null) {
System.out.println(node.data + "\t" + string + "\t" + node.weight);
}
}
private TreeNode build() {
while (nodeList.size() > 1) {
nodeList.add(new TreeNode(nodeList.remove(0), nodeList.remove(0)));
}
return nodeList.get(0);
}
public void print() {// for test
int length = nodeList.size();
for (int i = 0; i < length; i++) {
System.out.println(nodeList.get(i));
}
}
private void sortByWeight(ArrayList<TreeNode> list) {
int length = list.size(), mark;
TreeNode lightest;
for (int i = 0; i < length; i++) {
lightest = list.get(mark = i);
for (int j = i + 1; j < length; j++) {
if (lightest.weight > list.get(j).weight) {
lightest = list.get(mark = j);
}
}
if (mark > i) {
list.swap(i, mark);
}
}
}
private ArrayList<TreeNode> createNodeList(int[] originalData) {
ArrayList<TreeNode> r = new ArrayList<>(50);
int length = originalData.length;
for (int i = 0; i < length; i++) {
if (originalData[i] > 0) {
r.add(new TreeNode((char) i, originalData[i]));
}
}
return r;
}
private int[] classify(StringBuilder data) {
int[] r = new int[256];
int length = data.length();
for (int i = 0; i < length; i++) {
r[data.charAt(i)]++;
}
return r;
}
}