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)如果一个文件中内容重复的数据量不多,压缩效果也不明显。