用 JAVA 实现哈夫曼树(Huffman Tree)

-1. 什么是树

树是一种 有层次关系的 数据结构。它由结点组成。

图一:
用 JAVA 实现哈夫曼树(Huffman Tree)_第1张图片

树的结点由 数据域子结点域 组成。数据域 作为数据的容器;子结点域 存放 子结点 的地址。一个结点是它的子结点父结点。不同层级之间的结点通过 子结点域 形成 “父子关系”。

每个结点有零个或多个子结点;没有父结点的结点称为根结点;每一个非根结点有且只有一个父结点没有 子结点的结点称为叶结点

图一中的结点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; //右子结点域
}

0. 什么是哈夫曼树

哈夫曼树 (Huffman Tree,霍夫曼树,最优二叉树) 是一种二叉树。哈夫曼树是一种带权路径长度最短的树。

是与树的结点关联的一个实数,用 W 表示。非叶结点的 等于其各子结点的 的和。一个结点的 路径长度 是这个结点与根结点的层次数之差,也就是该结点的层次数减 1,用 l 表示。

结点的带权路径长度 为:从根结点到该结点之间的路径长度与该结点的权的乘积。

一个二叉树中的各个 叶结点带权路径长度 之和为 树的带权路径长度 ,用WPL表示,即:
用 JAVA 实现哈夫曼树(Huffman Tree)_第2张图片
下图就是一个哈夫曼树。方框中的数表示结点的权。
下面这个树的带权路径长度
WPL = 12 × 3 + 14 × 2 + 21 × 2 + 33 × 2 = 172 。
用 JAVA 实现哈夫曼树(Huffman Tree)_第3张图片

7. 哈夫曼树的应用

最常见的应用之一应该是压缩。在英文中,字母 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。这是不引起歧义,又能节省最多空间的编码方案

用 JAVA 实现哈夫曼树(Huffman Tree)_第4张图片

1. 结点类

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;
	}
}

2. 读取字符串

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;
	}

	//...
}
  • 注 1:为什么是 256?
    这里我们先把字符串中的各个字符分拣出来。为每一种字符都设一个计数器。比如说,A 的 ASCII 码是 97 ,那就把 counter[97] 作为 A 的计数器,每找到一个 A ,就给 counter[97] 增加 1。
    ASCII 码中只有 256 个字符,所以把数组大小设为 256。这个 data 中就必须只含有普普通通的英文字母和英文符号。

3. 创建结点

为出现过的字符创建结点。把字符存入数据域,把频数设为权。把结点存入一个数组列表中。

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;
}

4. 按权排序

按权由小到大的顺序,对结点进行排序。

5. 建树

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));
	}
}

6. 输出哈夫曼编码

依照实际情况,我们只要输出叶结点的哈夫曼编码就行了。

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);
	}
}

8. 我的代码

我写了一个接口 TestText, 里面只有一个属性 testTextStringBuilder)。存放的是测试用的字符串。
测试用的字符串内容如下:

"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;
    }
}

你可能感兴趣的:(数据结构和算法,JAVA入门学习笔记)