赫夫曼树
1 前言
霍夫曼树是二叉树的一种特殊形式,又称为最优二叉树,其主要作用在于数据压缩和编码长度的优化。
2 重要概念
2.1 路径和路径长度
在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
![](http://upload-images.jianshu.io/upload_images/7043118-f75c7a1e99fdf648.png?imageMogr2/auto-orient/strip|imageView2/2/w/340/format/webp)
图2.1所示二叉树结点A到结点D的路径长度为2,结点A到达结点C的路径长度为1。
2.2 结点的权及带权路径长度
若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
图2.2展示了一棵带权的二叉树
![](http://upload-images.jianshu.io/upload_images/7043118-6091540162f48e8c.png?imageMogr2/auto-orient/strip|imageView2/2/w/357/format/webp)
2.3 树的带权路径长度
树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
图2.2所示二叉树的WPL:
WPL = 6 * 2 + 3 * 2 + 8 * 2 = 34;
3 霍夫曼树
3.1 定义
给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为霍夫曼树(Huffman Tree)。
如图3.1所示两棵二叉树
![](http://upload-images.jianshu.io/upload_images/7043118-60c5f01992b15153.png?imageMogr2/auto-orient/strip|imageView2/2/w/542/format/webp)
叶子结点为A、B、C、D,对应权值分别为7、5、2、4。
3.1.a树的WPL = 7 * 2 + 5 * 2 + 2 * 2 + 4 * 2 = 36
3.1.b树的WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3 = 35
由ABCD构成叶子结点的二叉树形态有许多种,但是WPL最小的树只有3.1.b所示的形态。则3.1.b树为一棵霍夫曼树。
3.2 构造霍夫曼树
构造霍夫曼树主要运用于编码,称为霍夫曼编码。现考虑使用3.1中ABCD结点以及对应的权值构成如下长度编码。
AACBCAADDBBADDAABB。
编码规则:从根节点出发,向左标记为0,向右标记为1。
采用上述编码规则,将图3.1编码为图3.2所示:
![](http://upload-images.jianshu.io/upload_images/7043118-1c37046c5ee37372.png?imageMogr2/auto-orient/strip|imageView2/2/w/545/format/webp)
构造过程:
3.1.a所示二叉树称为等长编码,由于共有4个结点,故需要2位编码来表示,编码结果为:
结点 | 编码 |
---|---|
A | 00 |
B | 01 |
C | 10 |
D | 11 |
则AACBCAADDBBADDAABB对应编码为:
00 00 10 01 10 00 00 11 11 01 01 00 11 11 00 00 01 01
长度为36。
3.1.b构造过程如下:
1)选择结点权值最小的两个结点构成一棵二叉树如图3.3:
![](http://upload-images.jianshu.io/upload_images/7043118-4ba1c6c17cd49fc1.png?imageMogr2/auto-orient/strip|imageView2/2/w/223/format/webp)
2)则现在可以看作由T1,A,B构造霍夫曼树,继续执行步骤1。
选则B和T1构成一棵二叉树如图3.4:
![](http://upload-images.jianshu.io/upload_images/7043118-3a2ec3c6a7495042.png?imageMogr2/auto-orient/strip|imageView2/2/w/254/format/webp)
3)现只有T2和A两个结点,继续执行步骤1。
选择A和T2构成一棵二叉树如图3.5:
![](http://upload-images.jianshu.io/upload_images/7043118-fb3a3e0c62c27d84.png?imageMogr2/auto-orient/strip|imageView2/2/w/379/format/webp)
经过上述步骤则可以构造完一棵霍夫曼树。通过观察可以发现,霍夫曼树中权值越大的结点距离根结点越近。
按照图3.5霍夫曼树编码结果:
结点 | 编码 |
---|---|
A | 0 |
B | 10 |
C | 110 |
D | 111 |
则AACBCAADDBBADDAABB对应编码为:
0 0 110 10 110 0 0 111 111 10 10 0 111 111 0 0 10 10
编码长度为35。
由此可见,采用二叉树可以适当降低编码长度,尤其是在编码长度较长,且权值分布不均匀时,采用霍夫曼编码可以大大缩短编码长度。
4.代码实现
1.创建结点
public class Node implements Comparable<Node> { int value; Node left;//左节点 Node right;//右节点 public Node(int value) { this.value = value; } @Override public String toString() { return "Node{" + "value=" + value + '}'; } @Override public int compareTo(Node o) { /*从小到大排序*/ return this.value-o.value; } /*前序遍历*/ public void preOrder(){ System.out.println(this); if (this.left!=null){ this.left.preOrder(); } if (this.right!=null){ this.right.preOrder(); } } }
2.创建霍夫曼树
public class HuffmanTree { public static void main(String[] args) { int[] arr = {13,7,8,3,29,6,1}; //1.数组进行升序排序 Node root = createHuffmanTree(arr); preOrder(root); } /*创建赫夫曼树*/ public static Node createHuffmanTree(int[] arr){ //数组进行升序排序 ArrayList<Node> list = new ArrayList<>(); for (int item : arr) { Node node = new Node(item); list.add(node); } while (list.size()>1){ Collections.sort(list); //System.out.println("list ="+list); /*取出权值最小的二叉树*/ Node leftNo = list.get(0); /*取出第二小的*/ Node rightNo = list.get(1); /*构建新得二叉树*/ Node parent = new Node(leftNo.value+rightNo.value); parent.left=leftNo; parent.right=rightNo; //删除list删除掉处理的二叉树 list.remove(leftNo); list.remove(rightNo); //将父节点加入到list list.add(parent); // System.out.println("第一次处理" +list); } return list.get(0); //返回赫夫曼数的root节点 } /*前序遍历*/ public static void preOrder(Node root){ if (root!=null){ root.preOrder(); }else { System.out.println("空赫夫曼数"); } } }
5.利用霍夫曼编码进行运算文件
1.结点类支持排序 实现比较器的接口
public class NodeCode implements Comparable<NodeCode> { Byte data;//存放数据本身a:97 " ":32 int weight;//字符出现的个数 NodeCode left;//左节点 NodeCode right;//右节点 public NodeCode(Byte data, int weight) { this.data = data; this.weight = weight; } @Override public int compareTo(NodeCode o) { /*从小到大排序*/ return this.weight-o.weight; } @Override public String toString() { return "NodeCode{" + "data=" + data + ", weight=" + weight + '}'; } /*前序遍历*/ public void preOrder(){ System.out.println(this); if (this.left!=null){ this.left.preOrder(); } if (this.right!=null){ this.right.preOrder(); } } }
2.赫夫曼编码对文件进行压缩和解压缩
public class HuffmanCodeTree { public static void main(String[] args) { //测试压缩文件 String src = "F:\\a.png"; String dst = "F:\\dst.zip"; //zipFile(src,dst); //System.out.println("压缩文件成功"); //测试解压文件 String zip = "F:\\dst.zip"; String dstFile = "F:\\a1.png"; unZipFile(zip,dstFile); System.out.println("解压缩成功"); String str = "i like like like java do you like a java"; byte[] strBytes = str.getBytes(); //分部测试 //System.out.println(strBytes.length); /* List<NodeCode> list = getNodes(strBytes); System.out.println("list= "+list); *//*测试创建的二叉树*//* System.out.println("赫夫曼二叉树"); NodeCode root = createHuffmanTree(list); preOrder(root); *//*测试生成对应的赫夫曼编码*//* Map<Byte, String> gethuffmanCode = gethuffmanCode(root); System.out.println("生成对应的赫夫曼编码: "+gethuffmanCode); //测试压缩 byte[] zip = zip(strBytes, gethuffmanCode); System.out.println("zip =" + Arrays.toString(zip));*/ /* byte[] huffmanZip = huffmanZip(strBytes); System.out.println("压缩后的结果 " +Arrays.toString(huffmanZip)); System.out.println("长度为:" + huffmanZip.length); //发送zip数组; *//*解压缩*//* System.out.println("toBinaryString ="+ byteToBitSring(true,(byte)1));; *//**//* //赫夫曼字节数组对应的二进制字符串 byte[] source = decode(huffmanCodes,huffmanZip); System.out.println("原来的字符串:" + new String(source));*/ } /*使用一个方法方便调用*/ /** * * @description:TODO * @params:bytes:原始字符串对应的数组 * @return: * @author: * @time: 2020/3/13 21:04 */ public static byte[] huffmanZip(byte[] bytes){ List<NodeCode> list = getNodes(bytes); //创建的赫夫曼树 NodeCode root = createHuffmanTree(list); //生成对应的赫夫曼编码 Map<Byte, String> gethuffmanCode = gethuffmanCode(root); byte[] huffmanZip = zip(bytes, gethuffmanCode); return huffmanZip; } /*为了方便调用,进行重载*/ public static Map<Byte,String> gethuffmanCode(NodeCode root){ if (root==null){ return null; } //处理root左子树 gethuffmanCode(root.left,"0",stringBuilder); gethuffmanCode(root.right,"1",stringBuilder); return huffmanCodes; } private static List<NodeCode> getNodes(byte[] bytes){ //创建list ArrayList<NodeCode> nodeList = new ArrayList<>(); Map<Byte, Integer> map = new HashMap<>(); //存储byte出现的次数 for (byte b : bytes) { Integer count =map.get(b); if (count == null){ map.put(b,1); }else { map.put(b,count+1); } } //把每一个键值对转成NodeCode对象,放到list集合中 for (Map.Entry<Byte,Integer> entry:map.entrySet()){ nodeList.add(new NodeCode(entry.getKey(),entry.getValue())); } return nodeList; } /*创建赫夫曼树*/ public static NodeCode createHuffmanTree(List<NodeCode> nodeList){ //数组进行升序排序 while (nodeList.size()>1){ Collections.sort(nodeList); //System.out.println("list ="+list); //取出权值最小的二叉树 NodeCode leftNo = nodeList.get(0); // 取出第二小的 NodeCode rightNo = nodeList.get(1); // 构建新得二叉树 /*根节点没有data只有权值*/ NodeCode parent = new NodeCode(null,leftNo.weight+rightNo.weight); parent.left=leftNo; parent.right=rightNo; //删除list删除掉处理的二叉树 nodeList.remove(leftNo); nodeList.remove(rightNo); //将父节点加入到list nodeList.add(parent); // System.out.println("第一次处理" +list); } return nodeList.get(0); //返回赫夫曼数的root节点 } /*前序遍历*/ public static void preOrder(NodeCode root){ if (root!=null){ root.preOrder(); }else { System.out.println("空赫夫曼数"); } } /*生成赫夫曼编码表 左节点:0 右节点:1*/ //生成对应的赫夫曼编码: {32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011} static StringBuffer stringBuilder = new StringBuffer();//存储某个叶子节点的路径 static Map<Byte,String> huffmanCodes = new HashMap<>(); /** * * @description:生成赫夫曼编码,存储到Map * @params:1.根节点2.路径 左节点:0右节点:1 3.存储某个叶子节点的路径编码 * @return: * @author: * @time: 2020/3/13 19:49 */ public static void gethuffmanCode(NodeCode node, String code, StringBuffer stringBuilder){ StringBuffer stringBuffer2= new StringBuffer(stringBuilder); stringBuffer2.append(code); if (node!=null){ if (node.data==null){//判断节点是否是叶子节点 //递归处理 //向左 gethuffmanCode(node.left,"0",stringBuffer2); //向右 gethuffmanCode(node.right,"1",stringBuffer2); }else { //找到叶子节点 huffmanCodes.put(node.data,stringBuffer2.toString()); } } } /*将一个字符对应的byte数组利用赫夫曼编码表,返回一个压缩编码的byte数组*/ /** * * @description:TODO * @params:1.原始数组2.赫夫曼编码 * @return:byte[]八位数组吧b[0] =10101000{补码} ===>补码-1(10100111)反码 * @author: * @time: 2020/3/13 20:16 */ public static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){ //利用赫夫曼编码表转成赫夫曼编码字符串 StringBuffer buffer = new StringBuffer(); for (byte b : bytes) { buffer.append(huffmanCodes.get(b)); } /* System.out.println("测试buffer= "+buffer.toString()); System.out.println(buffer.length());*/ //统计字符串长度 /*(buffer.length()+7) /8 */ int len; if (buffer.length()%8==0){ len = buffer.length()/8; }else { len = buffer.length()/8+1; } //创建压缩后数组 byte[] huffmanBys = new byte[len]; int index = 0;//第几个byte for (int i = 0; i <buffer.length() ; i+=8) {//步长为8 String strBy; if (i+8>buffer.length()){ strBy = buffer.substring(i); }else { strBy = buffer.substring(i,i+8); } //将strBy转换成byte 放到by数组 huffmanBys[index] = (byte)Integer.parseInt(strBy,2); index++; } return huffmanBys; } /*进行解压缩*/ /** * * @description:将byte转成一个二进制字符串 * @params:flag 真:需要不高位 * @return:b对应二进制的字符串(按照补码返回) * @author: * @time: 2020/3/13 21:20 */ public static String byteToBitSring(boolean flag , byte b) { //使用保存b int temp = b; /*如果是正数需要不高位*/ if (flag) { temp |= 256;//按位与 1 0000 0000 |0000 0001 =>1 0000 0001 } String str = Integer.toBinaryString(temp);//二进制的补码 if (flag) { return str.substring(str.length() - 8); } else { return str; } } /*解码的方法*/ /** * * @description: * @params:1.赫夫曼编码对照表 2.压缩的数组 * @return: * @author: * @time: 2020/3/13 21:46 */ public static byte[] decode(Map<Byte,String> huffmanCodes , byte[]huffmanZip){ //得到huffmanZip 对应的二进制的字符串101010000... StringBuilder builderString = new StringBuilder(); //byte转换成二进制字符串 /*如果是最后一个数不需要补高位*/ for (int i = 0; i < huffmanZip.length; i++) { byte by = huffmanZip[i]; boolean flag = (i==huffmanZip.length-1); builderString.append(byteToBitSring(!flag, by)); } System.out.println("赫夫曼字节数组对应的二进制字符串 ="+ builderString.toString()); /*把字符串按照对应的赫夫曼编码进行编码*/ Map<String,Byte> map =new HashMap<>(); for (Map.Entry<Byte,String> entry : huffmanCodes.entrySet()){ map.put(entry.getValue(),entry.getKey()); } System.out.println("map =" +map); //创建集合存放byte List<Byte> list = new ArrayList<>(); for (int i= 0; i < builderString.length();){ int count = 1;//小的计数器 boolean flag = true; Byte b = null; while (flag){ //递增取出key取出一个1或者0 String key = builderString.substring(i,i+count);//i不动count++直到匹配到一个字符 b = map.get(key); if (b == null){ count++; }else { flag = false; } } list.add(b); i += count;//让i移动到count; } //for循环结束后 list 存放为i like like like java do you like a java //将list 中的数据放到 byte[]并返回 byte[] b = new byte[list.size()]; for (int i = 0; i < b.length; i++) { b[i] = list.get(i); } return b; } /*编写方法,将一个文件进行压缩*/ /** * * @description:进行压缩文件 * @params:1.传入压缩文件的全部路径2.压缩文件存放的位置 * @return: * @author: * @time: 2020/3/13 22:41 */ public static void zipFile(String srcFile,String dstFile){ //创建输出流 //创建文件的输入流 FileInputStream is = null; FileOutputStream os = null; ObjectOutputStream oos = null; try { is = new FileInputStream(srcFile); //创建一个源文件大小相同的字节数组 byte[] b = new byte[is.available()]; //读取文件 is.read(b); //获取到文件进行压缩 得到字节数组 byte[] huffmanBys = huffmanZip(b); //创建输出流 os = new FileOutputStream(dstFile); //创建一个和文件输出流的objectOutStream oos = new ObjectOutputStream(os); //采用对象流将赫夫曼编码后的字节数组写入文件,可以有效的解压缩 oos.writeObject(huffmanBys); /*将赫夫曼编码写入文件*/ oos.writeObject(huffmanCodes); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); oos.close(); os.close(); } catch (IOException e) { e.printStackTrace(); } } } /*文件的解压操作的方法*/ /** * * @description:TODO * @params:1.解压文件文字 2.保存的路径 * @return: * @author: * @time: 2020/3/13 23:20 */ public static void unZipFile(String zipFile , String dsfFile){ //创建文件的输入流 InputStream is = null; //对象输入流 ObjectInputStream ois = null; //文件的输出流 OutputStream os = null; try { is = new FileInputStream(zipFile); //对象输入流 ois =new ObjectInputStream(is); //读取数组 byte[] huffmanBytes = (byte[]) ois.readObject(); //读取赫夫曼编码表 Map<Byte,String> huffmanCodes = (Map<Byte, String>) ois.readObject(); //进行解码 byte[] bytes = decode(huffmanCodes, huffmanBytes); //将bytes写入文件 os = new FileOutputStream(dsfFile); os.write(bytes); } catch (Exception e) { e.printStackTrace(); }finally { try { os.close(); ois.close(); is.close(); } catch (IOException e) { e.printStackTrace(); } } } }