咱们常常会用到文件压缩,压缩以后文件会变小,便于传输,使用的时候又将其解压出来。为何压缩以后会变小,并且压缩和解压也不会出错。赫夫曼编码和赫夫曼树了解一下。java
赫夫曼树:算法
它是一种的叶子结点带有权重的特殊二叉树,也叫最优二叉树。既然出现最优两个字确定就不是随便一个叶子结点带有权重的二叉树都叫作赫夫曼树了。app
赫夫曼树中有一个很重要的概念就是带权路径,带权路径最小的才是赫夫曼树。ide
树的路径长度是从根结点到每个结点的长度之和,带权路径就是每个结点的长度都乘以本身权重,记作WPL。ui
假设有abcd数据,权重分别是7 5 2 4。下面构建出来的三棵带权二叉树。this
A树:WPL=7*2+5*2+2*2+4*2=36编码
B树:WPL=7*3+5*3+2*1+4*2=46加密
C树:WPL=7*1+5*2+2*3+4*3=35spa
显然C树的带权是最小的。并且无构建出比它更小的了。因此C树就是赫夫曼树设计
咱们从C树发现了一个问题,就是要使得树的带权路径最小,那么权重越大的就应该离根结点越近。因此若是要构建一棵赫夫曼树,首先必定要将数据按权重排序。这是否是就是以前提到的贪心算法,必定有排序,从局部最优到总体最优。
赫夫曼编码:
咱们都知道之前的地下党发送电报。都是加密了发送,而后使用密码原本解密。
咱们仍是发送上面的abcd
显然计算机的世界都是0和1,假设咱们用三位来表示上面的字符。也就至关于制做一个密码本
a:000
b:001
c:010
d:011
那么我要传输的就变成了000001010011,而后收到以后按照三位一分来解密就能够了。可是若是数据不少以后。咱们可能就不能不用3位来表示了,多是8位,10位之类了的,那么这个二进制串的长度也至关可怕了。
再看赫夫曼树,若是咱们将上面的C图的每个左分支表示0,右分支表示1
那么如今表示abcd就能够用每一个结点长度路径上的值来表示了
a:0
b:10
c:110
d:111
abcd就能够表示为010110111,就从刚才的000001010011的12位缩减到了9位,若是数据量大,这个减小的位数是很可观的。
可是又有一个问题了,这样出来的编码长度不等,其实很容易混淆,因此要设计这种长短不等的编码,必须任意字符的编码都不是另外一个字符编码的前缀,这种编码称作前缀编码。显然经过二叉树这样构造出来的编码,每一个叶子结点都不一样的编码。而这棵赫夫曼树就是咱们的密码本。也就是说编码于解码都须要用一样结构的赫夫曼树。
解码:
每次从根开始寻找,找到叶子结点为止,而后又从根开始寻找,好比010110111,
0走左边,左边第一个就是叶子结点,因此找到a,
回到根继续寻找,编码串还剩下10110111,
1走右边,0走左边找到b,110 ->c, 111->d
通常来讲设要编码的字符集{c1,c2,c3...},设置各个字符出现的频率{w1,w2,w3...},以各字符做为叶子结点,以相应的频率做为权重来构造赫夫曼树。
赫夫曼树的构建:
以咱们上面的a:7 b:5 c:4 d:2为例。
1.上面从树的特色来看,首先咱们须要按照权重从小到大排序,注意赫夫曼树的构建是逆向构建的,就是说是从叶子结点往根结点构建。排序:d:2 c:4 b:5 a:7
2.取前面两个权值最小结点做为新结点n1的两个子结点,注意二叉树的左小右大规则。新结点的权重为两孩子权重之和,将操做过的结点从数据中移除,新结点放进去继续操做:
n1的权重是 cd权重之和为6,新的排序:b:5 n1:6 a:7
3.取出b和n1构成新做为新结点n2的两个子结点剩余。 新的排序:a:7 n2:11
直到操做到最后两个结点结束。
若是遇到操做的两个结点在已有的数上面尚未,那就另开一个子树,等到操做这个新子树的根结点的时候,再把这棵子树直接移植过去,好比这个数据来构建a:3 b:24 c:6 d:20 e:34 f:4 g:12
排序:a:3 f:4 c:6 g:12 d:20 b:24 e:34
d:20 和b:24 构造出来的子树就是后面移植上去的
代码实现:
如今就按照上面的逻辑,代码实现赫夫曼树的构建和编码解码,对比上面的第二个数据验证结果
package com.nijunyang.algorithm.tree; import java.util.*; /** * Description: 哈夫曼树 * Created by nijunyang on 2020/4/28 21:43 */ public class HuffmanTree { private static final byte ZERO = 0; private static final byte ONE = 1; HuffmanNode root; Map<Character, Integer> weightMap; //字符对应的权重 List<HuffmanNode> leavesList; // 叶子 Map<Character, String> leavesCodeMap; // 叶子结点的编码 public HuffmanTree(Map<Character, Integer> weightMap) { this.weightMap = weightMap; this.leavesList = new ArrayList<>(weightMap.size()); this.leavesCodeMap = new HashMap<>(weightMap.size()); creatTree(); } public static void main(String[] args) { Map<Character, Integer> weightMap = new HashMap<>(); //a:3 f:4 c:6 g:12 d:20 b:24 e:34 weightMap.put('a', 3); weightMap.put('b', 24); weightMap.put('c', 6); weightMap.put('d', 20); weightMap.put('e', 34); weightMap.put('f', 4); weightMap.put('g', 12); HuffmanTree huffmanTree = new HuffmanTree(weightMap); //abcd: 1011001101000 String code = huffmanTree.encode("abcd"); System.out.println(code); System.out.println("1011001101000".equals(code)); String msg = huffmanTree.decode(code); System.out.println(msg); } /** * 构造树结构 */ private void creatTree() { PriorityQueue<HuffmanNode> priorityQueue = new PriorityQueue<>(); weightMap.forEach((k,v) -> { HuffmanNode huffmanNode = new HuffmanNode(k, v); priorityQueue.add(huffmanNode); leavesList.add(huffmanNode); }); int len = priorityQueue.size();//先把长度取出来,由于等下取数据队列长度会变化 //HuffmanNode实现了Comparable接口,优先队列会帮咱们排序,咱们只须要每次弹出两个元素就能够了 for (int i = 0; i < len - 1; i++) { HuffmanNode huffmanNode1 = priorityQueue.poll(); HuffmanNode huffmanNode2 = priorityQueue.poll(); int weight12 = huffmanNode1.weight + huffmanNode2.weight; HuffmanNode parent12 = new HuffmanNode(null, weight12); //父结点不须要数据直接传个null parent12.left = huffmanNode1; //创建父子关系,由于排好序的,因此1确定是在左边,2确定是右边 parent12.right = huffmanNode2; huffmanNode1.parent = parent12; huffmanNode2.parent = parent12; priorityQueue.add(parent12); //父结点入队 } root = priorityQueue.poll(); //队列里面的最后一个便是咱们的根结点 /** * 遍历叶子结点获取叶子结点数据对应编码存放起来,编码时候直接拿出来用 */ leavesList.forEach(e -> { HuffmanNode current = e; StringBuilder code = new StringBuilder(); do { if (current.parent != null && current == current.parent.left) { // 说明当前点是左边 code.append(ZERO); //左边0 } else { code.append(ONE);//左边1 } current = current.parent; }while (current.parent != null); //父结点null是根结点 code.reverse(); //由于咱们是从叶子找回去的 ,因此最后须要将编码反转下 leavesCodeMap.put(e.data, code.toString()); }); } /** * 编码 */ public String encode(String msg) { char[] chars = msg.toCharArray(); StringBuilder code = new StringBuilder(); for (int i = 0; i < chars.length; i++) { code.append(leavesCodeMap.get(chars[i])); } return code.toString(); } /** * 解码 */ public String decode(String code) { char[] chars = code.toCharArray(); Queue<Byte> queue = new ArrayDeque(); for (int i = 0; i < chars.length; i++) { queue.add(Byte.parseByte(String.valueOf(chars[i]))); } HuffmanNode current = root; StringBuilder sb = new StringBuilder(); while (!queue.isEmpty() ){ Byte aByte = queue.poll(); if (aByte == ZERO) { current = current.left; } if (aByte == ONE) { current = current.right; } if (current.right == null && current.left == null) { sb.append(current.data); current = root; } } return sb.toString(); } /** * 结点 实现Comparable接口 方便使用优先队列(PriorityQueue)排序 */ private class HuffmanNode implements Comparable<HuffmanNode>{ Character data; //字符 int weight; //权重 HuffmanNode left; HuffmanNode right; HuffmanNode parent; @Override public int compareTo(HuffmanNode o) { return this.weight - o.weight; } public HuffmanNode(Character data, int weight) { this.data = data; this.weight = weight; } } }