D11-堆排序和赫夫曼编码[Java数据结构和算法]

1.堆排序

  1.1 基本介绍

  (1)堆排序是利用堆这种数据结构设计的排序算法,是一种选择排序,他的最好最坏平均时间复杂度为O(nlogn),是不稳定排序;

  (2)堆是具有以下性质的完全二叉树,每个节点的值都大于或等于其左右孩子节点的值,称为顶堆;每个节点的值都小于或等于其左右孩子节点的值,称为顶堆;

  (3)一般升序采用大顶堆降序采用小顶堆

  1.2 堆排序的基本思想:

  (1)将待排序的序列构造成一个大顶堆,利用数组;

  (2)此时,整个序列的最大值就是堆顶的根节点;

  (3)将其与末尾元素进行交换,此时末尾就为最大值;

  (4)然后将剩余n-1个元素重新构造成一个堆,就会得到n个元素的次小值。反复执行后就能得到一个有序序列。

  1.3 源代码

package cn.atguigu.Tree;

import java.util.Arrays;

public class HeapSort {

    public static void main(String[] args) {
        //要求把数组升序排列
        int arr[]= {4,6,8,5,9};
        heapSort(arr);
            
    }
    
    //编写一个堆排序的方法
    public static void heapSort(int arr[]) {
        System.out.println("堆排序");
        int temp=0;
        //将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
        for(int i=arr.length/2-1;i>=0;i--) {
            adjustHeap(arr, i, arr.length);
        }
        
        /*
         * 1.将堆顶元素与末尾元素交换,将最大元素沉到数组末尾;
         * 2.重新调整结构,使其满足堆定义,然后继续交换堆顶元素和当前末尾元素,反复执行调整+交换步骤,直到整个有序序列
         */
        for(int j=arr.length-1;j>=0;j--) {
            //交换
            temp=arr[j];
            arr[j]=arr[0];
            arr[0]=temp;
            adjustHeap(arr, 0, j);
        }
        System.out.println(Arrays.toString(arr));
    }
    
    //将一个数组(二叉树),调整成一个大顶堆
    /**
     * 完成将以i对应的非叶子节点的树调整成大顶堆
     * [4,6,8,5,9]=>i=1=>adjustHeap()=>[4,9,8,5,6]
     * 如果再次调用adjustHeap 传入的是i=0=>[9,6,8,5,4]
     * @param arr 待调整的数组
     * @param i 表示非叶子节点在数组中的索引
     * @param length 表示对多少个元素继续调整, length 是在逐渐的减少
     */
    public static void adjustHeap(int arr[],int i,int length) {
        int temp=arr[i];//先取出当前元素的值,保存在临时变量
        //开始调整
        //说明 k=i*2+1,k指的是i节点的左子节点
        for(int k=i*2+1;k<length;k=k*2+1) {
            if(k+1<length&&arr[k]<arr[k+1]) {//说明左子节点的值小于右子节点的值
                k++;//k指向右子节点
            }
            if(arr[k]>temp) {//如果子节点大于父节点
                arr[i]=arr[k];//把较大的值赋给当前节点
                i=k;//i指向k,继续循环比较
            }else {
                break;//
            }
        }
        //当for循环结束后,已经将以i为父节点的树的最大值,放在了最顶(局部)
        arr[i]=temp;//将temp值放到调整后的位置
    }
}

2.赫夫曼树

  2.1 基本介绍

  (1)给定n个权值作为n个叶子节点,构造一一颗二叉树,若该输的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,,也成为了赫夫曼树(Huffman Tree);

  (2)赫夫曼树是带权路径长度最短的树,权值较大的节点离根较近;

  (3)路径和路径长度:在一棵树中,从一个节点往下可以达到孩子或孙子节点的通路,称为路径。通路中分支的数目称为路径长度。若规定根节点的层数为1,则从根几点到第I层节点的路径长度为L-1;

  (4)节点的权及带权路径长度:若书中节点赋给一个有着某种含义的数值,则这个数值称为该节点的权。带权路径长度为从根节点到该节点之间的路径长度与该节点的权的乘积;

  (5)树的带权路径长度:所有叶子节点的带权路径长度之和,即为WPL(Weighted path length),权值越大的节点离根节点越近。

  2.2 创建赫夫曼树的思路

  (1)从小到大进行排序,将每一个数据看成一个节点,可以将每个节点看成一颗最简单的二叉树;

  (2)取出根节点权值最小的两颗二叉树;

  (3)组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值之和;

  (4)在将这颗心的二叉树,以根节点的权值大小再次排序,不断重1-2-3-4的步骤,直到数列中所有的数据都被处理,就得到一颗赫夫曼树;

  2.3 源代码

 1 package cn.atguigu.huffmanTree;
 2 
 3 import java.util.ArrayList;
 4 import java.util.Collections;
 5 import java.util.List;
 6 
 7 public class HuffmanTree {
 8 
 9     public static void main(String[] args) {
10         int arr[]= {13,7,8,3,29,6,1};
11         Node node=createHuffmanTree(arr);//返回根节点
12         
13         preOrder(node);
14     }
15     
16     //创建赫夫曼树的方法
17     public static Node createHuffmanTree(int[] arr) {
18         //为了操作方便
19         //1.遍历arr数组
20         //2.将arr的每个元素构成一个Node
21         //3.将Node放入到ArrayList中
22         List<Node> nodes=new ArrayList<Node>();
23         for(int value:arr) {
24             nodes.add(new Node(value));
25         }
26         //处理的过程是一个循环的过程
27         while(nodes.size()>1) {
28             
29             //排序:从小到大
30             Collections.sort(nodes);
31             
32             //取出根节点权值最小的两颗二叉树
33             //1取出权值最小的节点
34             Node leftNode=nodes.get(0);
35             //2取出权值次小的节点
36             Node rightNode=nodes.get(1);
37             
38             //构建一颗新的二叉树
39             Node parent=new Node(leftNode.value+rightNode.value);
40             parent.left=leftNode;
41             parent.right=rightNode;
42             
43             //从ArrayList中删除处理过的二叉树
44             nodes.remove(leftNode);
45             nodes.remove(rightNode);
46             
47             //加入新处理后的二叉树
48             nodes.add(parent);
49         }
50         return nodes.get(0);
51     }
52     
53     public static void preOrder(Node root) {
54         if(root!=null) {
55             root.preOrder();
56         }else {
57             System.out.println("是空树,不能遍历");
58         }
59     }
60 }
61 
62 //创建节点类
63 //为了让Node对象支持排序Collections集合排序
64 //让Node实现 comparable接口
65 class Node implements Comparable<Node>{
66     int value;//权值
67     Node left;//指向左子节点
68     Node right;//指向右子节点
69     
70     //写一个前序遍历
71     public void preOrder() {
72         System.out.println(this);
73         if(this.left!=null) {
74             this.left.preOrder();
75         }
76         if(this.right!=null) {
77             this.right.preOrder();
78         }
79     }
80     
81     public Node(int value) {
82         this.value=value;
83     }
84 
85     @Override
86     public String toString() {
87         return "Node [value=" + value + "]";
88     }
89     
90     public int compareTo(Node o) {
91         //表示从小到大排序
92         return this.value-o.value;
93     }
94 }

  

3.赫夫曼编码

  3.1 赫夫曼编码介绍

  (1)是一种编码方式,属于一种程序算法;

  (2)赫夫曼编码是赫夫曼树在电讯通信中经典的应用;

  (3)赫夫曼编码广泛地应用于数据文件压缩,其压缩率在20%-90%之间;

  (4)赫夫曼编码是可变字长编码(VLC)的一种;Huffman于1953年提出一种编码方法,称之为最佳编码; 

  3.2 赫夫曼编码原理剖析(不会造成匹配的多义性,是无损压缩

  (1)将传输的字符串分解,得到各个字符对应的个数;

  (2)按照上面字符出现的次数构建一颗赫夫曼树,次数作为权值;

  (3)构造赫夫曼树;

  (4)根据赫夫曼树,给各个字符规定编码:向左的路径为0,向右的路径为1;

  (5)按照赫夫曼编码,得到字符串对应的编码;

  注:赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl是一样的,都是最小的;

   3.3 赫夫曼编码进行压缩思路

  (1)Node{data,weight,left,right};

  (2)得到字符串对应的byte[]数组;

  (3)编写一个方法,将准备构建赫夫曼树的Node节点放到List中,形式为[Node[data=97,weight=5],Node[data=32,weight=9].......],体现各个字符对应的个数;

  (4)通过List创建对应的赫夫曼树;

  (5)通过赫夫曼树得到每个字符对应的赫夫曼编码;

  (6)得到字符串对应的赫夫曼编码,将其转换为字节数组,得到压缩;

  3.4 赫夫曼编码解码思路

  (1)将压缩数组重新转换成赫夫曼编码对应的二进制字符串;

  (2)赫夫曼编码对应的二进制字符串对应赫夫曼编码转换成字符串;

   3.4 赫夫曼编码的实践-实现文件的压缩和解压

  1 package cn.atguigu.huffmancode;
  2 
  3 import java.io.FileInputStream;
  4 import java.io.FileOutputStream;
  5 import java.io.InputStream;
  6 import java.io.ObjectInput;
  7 import java.io.ObjectInputStream;
  8 import java.io.ObjectOutputStream;
  9 import java.io.OutputStream;
 10 import java.sql.Array;
 11 import java.sql.ResultSet;
 12 import java.sql.SQLException;
 13 import java.util.ArrayList;
 14 import java.util.Arrays;
 15 import java.util.Collections;
 16 import java.util.HashMap;
 17 import java.util.List;
 18 import java.util.Map;
 19 
 20 import org.xml.sax.InputSource;
 21 
 22 public class HuffmanCode {
 23     public static void main(String[] args) {
 24         String content="i like like like java do you like a java";
 25         byte[] contentBytes=content.getBytes();
 26         byte[] huffmanCodeBytes=huffmanZip(contentBytes);
 27         System.out.println("压缩后的结果是:"+Arrays.toString(huffmanCodeBytes));
 28         
 29         byte[] sourceBytes=decode(huffmanCodes, huffmanCodeBytes);
 30         System.out.println("原来的字符串"+new String(sourceBytes));
 31     
 32         //测试压缩文件
 33         String srcFile="C://Users//ZMQ//Pictures//1.bmp";
 34         String dstFile="C://Users//ZMQ//Pictures//1.zip";
 35         zipFile(srcFile, dstFile);
 36         System.out.println("压缩文件ok");
 37         
 38         //测试解压文件
 39         String zipFile="C://Users//ZMQ//Pictures//1.zip";
 40         dstFile="C://Users//ZMQ//Pictures//2.bmp";
 41         unZipFile(zipFile, dstFile);
 42         System.out.println("解压文件ok");
 43         }
 44     //编写方法,完成对压缩文件的解压
 45     /**
 46      * 
 47      * @param zipFile 压缩的文件
 48      * @param dstFile 将文件解压到哪个路径
 49      */
 50     public static void unZipFile(String zipFile,String dstFile) {
 51         //定义文件输入流
 52         InputStream is=null;
 53         //定义对象输入流
 54         ObjectInputStream ois=null;
 55         //定义文件输出流
 56         OutputStream os=null;
 57         try {
 58             //创建文件输入流
 59             is=new FileInputStream(zipFile);
 60             //创建对象输入流
 61             ois=new ObjectInputStream(is);
 62             //读取压缩文件对应的压缩字节数组
 63             byte[] huffmanBytes=(byte[]) ois.readObject();
 64             //读取压缩文件对应的赫夫曼编码
 65             Map<Byte,String> huffmanCodes=(Map<Byte,String>)ois.readObject();
 66             //解码
 67             byte[] bytes=decode(huffmanCodes, huffmanBytes);
 68             //创建文件输出流
 69             os=new FileOutputStream(dstFile);
 70             os.write(bytes);
 71         }catch (Exception e) {
 72             System.out.println(e.getMessage());
 73         }finally {
 74             try {
 75                 os.close();
 76                 ois.close();
 77                 is.close();
 78             }catch (Exception e) {
 79                 System.out.println(e.getMessage());
 80             }
 81             
 82         }
 83     }
 84     
 85     //编写方法,将一个文件进行压缩
 86     /**
 87      * 
 88      * @param srcFile 传入希望压缩的文件的全路径
 89      * @param dstFile 压缩后将压缩文件存放在哪个目录
 90      */
 91     public static void zipFile(String srcFile,String dstFile) {
 92         //创建文件输出流
 93         OutputStream out=null;
 94         //创建文件输入流
 95         FileInputStream is=null;
 96         ObjectOutputStream oos=null;
 97         try {
 98             is=new FileInputStream(srcFile);
 99             //创建文件大小的字节数组
100             byte[] bytes=new byte[is.available()];
101             //读取文件
102             is.read(bytes);
103             //得到对应huffman编码,直接对源文件进行压缩
104             byte[] hufffmanCodesBytes=huffmanZip(bytes);
105             //创建文件输出流,准备压缩文件
106             out=new FileOutputStream(dstFile);
107             //创建一个和文件输出流相关的ObjectOutputStream
108             oos=new ObjectOutputStream(out);
109             //将压缩后的字节数组存放到压缩文件中
110             oos.writeObject(hufffmanCodesBytes);
111             //以对象流的方式写入赫夫曼编码,以便恢复源文件时使用
112             oos.writeObject(huffmanCodes);
113         }catch (Exception e) {
114             System.out.println(e.getMessage());
115         }finally {
116             try {
117                 is.close();    
118                 oos.close();
119                 out.close();
120             }catch (Exception e) {
121                 System.out.println(e.getMessage());
122             }
123         }
124     }
125     
126     //完成数据的解压
127     //思路
128     //1.将huffmanCodeBytes重新转换成赫夫曼编码对应的二进制字符串
129     //2.赫夫曼编码对应的二进制字符串对应赫夫曼编码转换成字符串
130     
131     //编写一个方法,完成对压缩数据的解码
132     /**
133      * 
134      * @param huffmanCodes 赫夫曼编码表 map
135      * @param huffmanBytes 赫夫曼编码得到的字节数组
136      * @return 就是原来字符串对应的数组
137      */
138     private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes) {
139         //1.先得到huffmanBytes对应的二进制字符串,形式1010100111000...
140         StringBuilder stringBuilder=new StringBuilder();
141         //将byte数组转成二进制的字符串
142         for(int i=0;i<huffmanBytes.length;i++) {
143             //判断是不是最有一个字节
144             boolean flag=(i==huffmanBytes.length-1);
145             stringBuilder.append(byteToBitString(!flag, huffmanBytes[i]));
146         }
147         
148         //把字符串按照指定的赫夫曼编码进行解码
149         //把赫夫曼编码表进行调换,因为需要反向查询
150         Map<String,Byte> map=new HashMap<String, Byte>();
151         for(Map.Entry<Byte, String> entry:huffmanCodes.entrySet()) {
152             map.put(entry.getValue(), entry.getKey());
153         }
154         
155         //创建集合,存放byte
156         List<Byte> list=new ArrayList<Byte>();
157         //1.可以理解成索引,不断扫描
158         for(int i=0;i<stringBuilder.length();) {
159             int count=1;//小的计算器
160             boolean flag=true;
161             Byte b=null;
162             while(flag) {
163                 //取出一个'1' '0'
164                 String key=stringBuilder.substring(i,i+count);//i不动,让count移动,指定匹配到一个字符
165                 b=map.get(key);
166                 if(b==null) {//说明没有匹配到
167                     count++;
168                 }else {
169                     flag=false;
170                     
171                 }
172             }
173             list.add(b);
174             i+=count;//i直接移动到i+count的位置
175         }
176         //当for循环结束后,list中存放所有的字符
177         //把list中的数据放入到byte[]返回
178         byte b[]=new byte[list.size()];
179         for(int i=0;i<b.length;i++) {
180             b[i]=list.get(i);
181         }
182         return b;
183         
184     }
185     
186     /**
187      * 将一个byte转换成二进制字符串
188      * @param bytes
189      * @param flag 表示是否需要补高位,如果是true,表示需要补高位
190      * @return 是该b 对应的二进制的字符串(注意是按照补码的形式返回)
191      */
192     private static String byteToBitString(boolean flag,byte b) {
193         //使用变量保存b
194         int temp=b;//将b转换成int
195         if(flag) {
196             //如果是整数,还需要补高位
197             temp|=256;//按位与 1 0000 0000 | 0000 0001=> 1 0000 0001
198         }
199         
200         String str=Integer.toBinaryString(temp);//返回的是temp对应的补码
201         if(flag) {
202             return str.substring(str.length()-8);
203         }else {
204             return str;
205         }
206         
207     }
208     
209     
210     //使用一个方法,将前面的方法封装起来,便于调用
211     /**
212      * 
213      * @param bytes原始字符串对应的数组
214      * @return 经过赫夫曼编码处理后的字节,压缩后的数组
215      */
216     private static byte[] huffmanZip(byte[] bytes) {
217         //将原始字符串对应的数组计算得到每个字符对应的个数
218         List<Node> node=getNodes(bytes);
219         //将这些节点构成哈夫曼树,返回哈夫曼树的根节点
220         Node HuffmanRoot=createHuffmanTree(node);
221         //根据哈夫曼树形成哈夫曼编码数组,返回存放字符和字符对应哈夫曼编码的哈希表
222         Map<Byte,String> huffmanMap=getCodes(HuffmanRoot);
223         //进行压缩,得到压缩后的数组
224         byte[] huffmanCodeBytes=zip(bytes, huffmanMap);
225         return huffmanCodeBytes;
226     }
227     
228     //编写方法,将字符串对应的byte[]数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
229     /**
230      * 
231      * @param bytes 原始字符串对应的byte[]
232      * @param huffmanCodes 生成的赫夫曼编码
233      * @return 返回赫夫曼编码处理后的byte[]
234      */
235     private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes) {
236         //1.先利用huffmanCodes将bytes转成赫夫曼编码对应的字符串
237         StringBuilder stringBuilder=new StringBuilder();
238         //遍历bytes数组
239         for(byte b:bytes) {
240             stringBuilder.append(huffmanCodes.get(b));
241         }
242         //将赫夫曼编码对应的字符串转换成byte[]
243         //统计返回byte[] huffmanCodeBytes长度
244         int len=(stringBuilder.length()+7)/8;
245         //创建 存储压缩后的byte数组
246         //创建HuffmanCodeBytes
247         byte[] huffmanCodeBytes=new byte[len];
248         int index=0;//记录第几个byte
249         for(int i=0;i<stringBuilder.length();i=i+8) {//因为每8位对应一个byte,所以步长+8
250             String strByte;
251             if(i+8>stringBuilder.length()) {
252                 strByte=stringBuilder.substring(i);
253             }else {
254                 strByte=stringBuilder.substring(i,i+8);
255             }
256             
257             //将strByte转换成byte,放到huffmanCodeBytes[]
258             huffmanCodeBytes[index]=(byte)Integer.parseInt(strByte, 2);
259             index++;
260         }
261         return huffmanCodeBytes;
262     }
263     
264     //生成赫夫曼树对应的赫夫曼编码
265     //思路
266     //1.将赫夫曼编码表存放在Map<Byte,String>形式
267     static Map<Byte, String> huffmanCodes=new HashMap<Byte, String>();
268     //2.在生成赫夫满编码表时,需要拼接路径,定义一个StringBuilder,存储某个叶子节点的路径
269     static StringBuilder stringBuilder=new StringBuilder();
270     //重载getCodes
271     private static Map<Byte,String> getCodes(Node root){
272         if(root==null) {return null;}
273         //处理root的左子树
274         getCodes(root.left,"0", stringBuilder);
275         //处理root的右子树
276         getCodes(root.right, "1", stringBuilder);
277         return huffmanCodes;
278     }
279     /**
280      * 功能:将传入的node节点的所有叶子节点的赫夫曼编码得到,并存入到huffmanCodes集合
281      * @param node 传入节点
282      * @param code 路径:左子节点是0,右子节点是1
283      * @param stringBuilder 用于拼接路径
284      */
285     private static void getCodes(Node node,String code,StringBuilder stringBuilder) {
286         StringBuilder stringBuilder2=new StringBuilder(stringBuilder);
287         //将code 加入到stringBuilder2中
288         stringBuilder2.append(code);
289         if(node!=null) {//如果node==null就不处理
290             if(node.data==null) {//如果node节点是非叶子节点
291                 //递归处理
292                 //向左递归
293                 getCodes(node.left, "0", stringBuilder2);
294                 //向右递归
295                 getCodes(node.right, "1", stringBuilder2);
296             }else {//如果node节点是叶子节点
297                 //说明找到某个子节点的最后
298                 huffmanCodes.put(node.data, stringBuilder2.toString());
299             }    
300         }
301     }
302     
303     //前序遍历方法
304     public static void preOrder(Node root) {
305         if(root!=null) {
306             root.preOrder();
307         }else {
308             System.out.println("赫夫曼树为空,不能遍历");
309         }
310     }
311     /**
312      * 功能:得到每个字符对应的个数,存放在List集合中
313      * @param bytes 接受字节数组
314      * @return list
315      */
316     private static List<Node> getNodes(byte[] bytes){
317         //创建ArrayList 存放节点
318         ArrayList<Node> list=new ArrayList<Node>();
319         
320         //通过map获取得到对应的值和权值
321         HashMap<Byte, Integer> counts=new HashMap<Byte, Integer>();
322         for(byte b:bytes) {
323             Integer count=counts.get(b);
324             if(count==null) {
325                 counts.put(b, 1);
326             }else {
327                 counts.put(b,count+1);
328             }
329         }
330         //将map转换为node节点
331         for(Map.Entry<Byte, Integer> entry:counts.entrySet()) {
332             list.add(new Node(entry.getKey(), entry.getValue()));
333         }
334         return list;
335     }
336     
337     //通过list创建对应的赫夫曼树
338     private static Node createHuffmanTree(List<Node> list) {
339         while(list.size()>1) {
340             //排序,从小到大
341             Collections.sort(list);
342             //对第0个和第1个相加
343             Node leftNode=list.get(0);
344             Node rightNode=list.get(1);
345             //创建父节点,它的根节点没有data
346             Node parent=new Node(null, leftNode.weight+rightNode.weight);
347             //挂起
348             parent.left=leftNode;
349             parent.right=rightNode;
350             //删除第0,1个节点
351             list.remove(leftNode);
352             list.remove(rightNode);
353             //添加父节点
354             list.add(parent);
355         }
356         return list.get(0);//最后的节点就是荷夫曼树的根节点
357     }
358 }
359  //创建Node,带数据和权值
360 class Node implements Comparable<Node>{
361     Byte data;//数据
362     int weight;//权值,表示字符串出现的次数
363     Node left;//指向左子节点
364     Node right;//指向右子节点
365     public Node(Byte data, int weight) {
366         this.data = data;
367         this.weight = weight;
368     }
369     @Override
370     public String toString() {
371         return "Node [data=" + data + ", weight=" + weight + "]";
372     }
373     @Override
374     public int compareTo(Node o) {
375         //从小到大排序
376         return this.weight-o.weight;
377     }
378     public void preOrder() {
379         System.out.println(this);
380         if(this.left!=null) {
381             this.left.preOrder();
382         }
383         if(this.right!=null) {
384             this.right.preOrder();
385         }
386     }
387 }

   3.5 赫夫曼编码压缩解压文件的注意事项

  (1)如果文件本身就是经过处理的,使用赫夫曼编码再压缩效率不会有明显的变化,比如视频,ppt等文件;

  (2)赫夫曼编码是按照字节处理的,因此可以处理所有的文件(二进制文件,文本文件);

  (3)如果一个文件中内容重复的数据量不多,压缩效果也不明显。

原文地址:https://www.cnblogs.com/ERFishing/p/11346666.html