哈夫曼树和哈夫曼编码

Posted Zephyr丶J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了哈夫曼树和哈夫曼编码相关的知识,希望对你有一定的参考价值。

哈夫曼树

最近笔试选择题老是遇到,有一次编程还让计算什么哈夫曼树最小权值…完全不知道在说啥,来学习一下

看小灰,简洁明了:
哈夫曼树:https://baijiahao.baidu.com/s?id=1663514710675419737&wfr=spider&for=pc

首先要明确哈夫曼树是一颗二叉树
然后明确树的路径,路径长度,带权路径长度,WPL(树的带权路径长度,所有叶子结点带权路径长度的和)
哈夫曼树(Huffman Tree)是在叶子结点权重确定的情况下,带权路径长度最小的二叉树,也被称为最优二叉树。特别注意,是叶子结点

然后构建哈夫曼树,简单而言,就是从所有结点队列中,取出两个最小权值的结点,相加,生成一个父节点(权值为和),然后将父节点放到原来的队列中

其实写起来很简单,这里将小灰的代码整理了一下

import java.util.*;

public class Main{
    private Node root;
    private Node[] nodes;//构建哈夫曼树

    public void createHuffman(int[] weights){
        //优先队列,用于辅助构建哈夫曼树
        Queue<Node> nodeQueue = new PriorityQueue<>();
        nodes = new Node[weights.length];
        //构建森林,初始化nodes数组
        for(int i = 0; i < weights.length; i++){
            nodes[i] = new Node(weights[i]);
            nodeQueue.add(nodes[i]);
        }
        //主循环,当结点队列只剩一个结点时结束
        while (nodeQueue.size() > 1) {
            //从结点队列选择权值最小的两个结点
            Node left = nodeQueue.poll();
            Node right = nodeQueue.poll();
            //创建新结点作为两结点的父节点
            Node parent = new Node(left.weight + right.weight, left, right);
            nodeQueue.add(parent);
        }
        //最后弹出的结点就是根节点
        root = nodeQueue.poll();
    }
    //按照前序遍历输出
    public void output(Node head){
        if(head == null){
            return;
        }
        System.out.println(head.weight);
        output(head.lChild);
        output(head.rChild);
    }
    public static class Node implements Comparable<Node>{
        int weight;
        Node lChild;
        Node rChild;
        public Node(int weight){
            this.weight = weight;
        }
        public Node(int weight, Node lChild, Node rChild){
            this.weight = weight;
            this.lChild = lChild;
            this.rChild = rChild;
        }
        @Override
        public int compareTo(Node o){
            return new Integer(this.weight).compareTo(new Integer(o.weight));
        }
    }
    public static void main(String[] args){
        int[] weights = {2,3,7,9,18,25};
        Main main = new Main();
        main.createHuffman(weights);
        main.output(main.root);
    }
}

那么如何计算整个哈夫曼树的WPL(树的带权路径长度)呢?
其实建好树以后,可以发现还是比较简单的,求每个叶子节点的路径长度,其实就相当于BFS,也可以说是树的层序遍历
从根节点开始,一层一层遍历,当遍历到的节点没有子节点或者说遍历到的节点在最初给的节点集合中,就说明是叶子节点,那么就将当前层数乘以该节点的权值加到最后的结果中
遍历到最后就可以得到哈夫曼树的WPL

哈夫曼编码

哈夫曼编码:https://baijiahao.baidu.com/s?id=1664724684084187981&wfr=spider&for=pc

为什么要有哈夫曼编码?
计算机存储信息是转换成二进制的形式存储的,被称为编码;
例如ASCII编码中,每个字符都是用一个等长的二进制数表示的,也就是编码长度是相同的
长度相同的编码方式容易设计,也方便读写,但是由于计算机存储空间、网络带宽有限,所以等长编码最大缺点就是会占用过多的资源

所以出现了长度不同的编码方式,但是这种编码方式很容易想到的一个问题就是,比如A字符编码是0,B字符编码方式是00,那么如果出现00等情况,解码方式就有多种,比较混乱;也就是说如果一个字符的编码恰好是另一个字符编码的前缀,那么就会产生这种歧义问题
所以不定长的编码需要特殊的设计,而哈夫曼编码就是这样一种不定长的编码方式

两个目标:

  1. 任何一个字符编码,都不是其他字符编码的前缀。
  2. 信息编码的总长度最小。

其实到这里想一下刚刚的哈夫曼树,可以发现因为每一个节点都是在树的叶子节点上,所以每一个节点都不可能是其他节点的前缀,所以可以用哈夫曼树来实现这种不定长的编码

哈夫曼编码不是固定的一套编码,而是根据各个字符出现的频次,动态生成的最优的编码
哈夫曼树的每一个结点包括左、右两个分支,二进制的每一位有0、1两种状态,所以将结点的左分支当做0,结点的右分支当做1
这样就可以得到每一个叶子节点的编码(从根到叶子节点的01字符串),因为叶子结点的层数不同,所以节点代表字符的编码也不同

另外,因为哈夫曼树的重要特性,就是所有叶子结点的(权重 * 路径长度)之和最小。放在信息编码的场景下,叶子结点的权重对应字符出现的频次,结点的路径长度对应字符的编码长度。所有字符的(频次 * 编码长度)之和最小,自然就说明总的编码长度最小

代码流程,先根据节点权值创建哈夫曼树;
然后用前序遍历的方式填充每个节点的二进制编码;
然后遍历字符串,将每个字符对应的二进制编码拼接起来

class HuffmanCode{
	private Node root;
	private Node[] nodes;
	
	//构建哈夫曼树
	public void createHuffmanTree(int[] weights){
		//优先队列,用于辅助构建哈夫曼树
		Queue<Node> nodeQueue = new PriorityQueue<>(); 
		nodes = new Node[weights.length];
		//构建森林,初始化nodes数组
		for(int i=0; i<weights.length; i++){ 
			nodes[i] = new Node(weights[i]); 
			nodeQueue.add(nodes[i]);
		}
		//主循环,当结点队列只剩一个结点时结束
		while (nodeQueue.size() > 1) {
			//从结点队列选择权值最小的两个结点
			Node left = nodeQueue.poll();
			Node right = nodeQueue.poll();
			//创建新结点作为两结点的父节点
			Node parent = new Node(left.weight + right.weight, left, right); 	
			nodeQueue.add(parent);
		} 
		root = nodeQueue.poll();
	}
	//输入字符下表,输出对应的哈夫曼编码
	public String convertHuffmanCode(int index){
		return nodes[index].code;
	}
	//用递归的方式,填充各个结点的二进制编码
	public void encode(Node node, String code){
		if(node == null){
			return;
		} 
		node.code = code; 
		encode(node.lChild, node.code+"0"); 
		encode(node.rChild,node.code+"1");
	}
	public static class Node implements Comparable<Node>{
		int weight;
		//结点对应的二进制编码
		String code;
		Node lChild;
		Node rChild;
		public Node(int weight){
			this.weight = weight;
		}
		public Node(int weight, Node lChild, Node rChild){
			this.weight = weight;
			this.lChild = lChild;
			this.rChild = rChild;
		}
		@Override
		public int compareTo(Node o){
			return new Integer(this.weight).compareTo(new Integer(o.weight));
		}
	}
	public static void main(String[] args){
		char[] chars = {'A','B','C','D','E','F'};
		int[] weights = {2,3,7,9,18,25};
		HuffmanCode huffmanCode = new HuffmanCode();
		huffmanCode.createHuffmanTree(weights);
		huffmanCode.encode(huffmanCode.root, "");
		for(int i=0; i<chars.length; i++){
			System.out.println(chars[i] +":" + huffmanCode.convertHuffmanCode(i));
		}
	}
}

以上是关于哈夫曼树和哈夫曼编码的主要内容,如果未能解决你的问题,请参考以下文章

霍夫曼树和霍夫曼编码以及霍夫曼编码的应用

哈夫曼树和哈夫曼编码

哈夫曼树和哈夫曼编码

数据结构—— 树:哈夫曼树和哈夫曼编码

哈夫曼(Huffman)树和哈夫曼编码

树——哈夫曼树和哈夫曼编码