一、解壓原理:
了解了壓縮原理之后,要解壓文件就是壓縮文件的逆過程;拿昨天的例子來說,如果我們收到這樣一串二進制1 1 01 1 1 01 00(昨天漏掉了一個問題,這里是9個0 1,每8個一個字節,那么剩下的那個0需要補7個0,構成一個完整的字節,這樣才能寫出文件)怎么解壓出aabbac呢?很自然的想到,我們需要拿到對應的哈夫曼編碼;a的編碼是1,b的編碼是01,c的編碼是00;拿到這個編碼后,我們開始對這個0 1串分割,先取出一個0或1,看是否有對應的編碼,如上,我們先取第一個1,編碼中有1的編碼,對應a,那么把第一個1還原為a,接下來,再去一個0或1,得到1,編碼中有1,對應a,那么還原為a,接下來去0,沒有編碼,取01對應b,把01還原為b......以此類推
有人會懷疑這樣的正確性,不會解壓錯誤嗎?比如如果編碼中有1 和11,那么11改怎么還原呢?是解析成兩個1進行還原,還是解析成一個11進行還原呢?其實不用擔心,這就是哈夫曼編碼的優勢,哈夫曼編碼中不會出現這樣的問題;不相信的話,你可以自己去檢驗下;
將這么多,其實很簡單,就類似於情報的破解,我只要有密碼本就可以了;而哈夫曼編碼就是我們的密碼本;
二、哈夫曼樹文件解壓實現:
文件的壓縮和解壓縮是兩個相對獨立的程序;所以,我們在把壓縮數據寫入文件之前,需要把該文件對應的哈夫曼編碼一起寫入文件,相當於解壓時的密碼本;
所以,昨天的壓縮程序還少了一步,就是把編碼寫入壓縮文件中;我們只需把每個字母對應的哈夫曼編碼的長度以及所有字母對應的哈夫曼編碼寫入文件即可;
讀取文件的時候,先讀取每個哈夫曼編碼的長度,在根據長度去分割寫入的哈夫曼編碼,同時把哈夫曼編碼寫入對應的位置即可;如上圖所示,前面的96長度都是0,不需要分割哈夫曼編碼;97的長度是1,則分割1,並把1存入對應的字符數組中;同時分割01,把01存儲在字符數組的第98個位置;以此類推;
難點:處理不夠8位01的寫入,記得把補0的個數一起寫入文件;
三、整個思路整理如下:
四、壓縮工程源代碼和解壓縮工程源代碼:
兩個獨立的工程,代碼不共享;
用壓縮工程壓縮的文件,可以用解壓縮的工程文件解壓縮;
壓縮工程源代碼:
HuffmNode類和昨天的一樣,就不上傳了;
更改后的compress類:
- package com.huaxin.compress;
- import java.io.FileInputStream;
- import java.io.FileNotFoundException;
- import java.io.FileOutputStream;
- import java.util.LinkedList;
- public class Compress {
- public int [] times = new int[256];
- public String [] HuffmCodes=new String[256];
- public LinkedList<HuffmNode> list = new LinkedList<HuffmNode>();
- //統計次數
- //初始化
- public Compress(){
- for (int i = 0; i < HuffmCodes.length; i++) {
- HuffmCodes[i]="";
- }
- }
- public void countTimes(String path) throws Exception{
- //構造文件輸入流
- FileInputStream fis = new FileInputStream(path);
- //讀取文件
- int value=fis.read();
- while(value!=-1){
- times[value]++;
- value=fis.read();
- }
- //關閉流
- fis.close();
- }
- //構造哈夫曼樹
- public HuffmNode createTree(){
- //將次數作為權值構造森林
- for (int i = 0; i < times.length; i++) {
- if(times[i]!=0){
- HuffmNode node = new HuffmNode(times[i],i);
- //將構造好的節點加入到容器中的正確位置
- list.add(getIndex(node), node);
- }
- }
- //將森林(容器中的各個節點)構造成哈夫曼樹
- while(list.size()>1) {
- //獲取容器中第一個元素(權值最小的節點)
- HuffmNode firstNode =list.removeFirst();
- //獲取中新的第一個元素,原來的第一個元素已經被移除了(權值次小的節點)
- HuffmNode secondNode =list.removeFirst();
- //將權值最小的兩個節點構造成父節點
- HuffmNode fatherNode =
- new HuffmNode(firstNode.getData()+secondNode.getData(),-1);
- fatherNode.setLeft(firstNode);
- fatherNode.setRight(secondNode);
- //父節點加入到容器中的正確位置
- list.add(getIndex(fatherNode),fatherNode);
- }
- //返回整顆樹的根節點
- return list.getFirst();
- }
- //利用前序遍歷獲取編碼表
- public void getHuffmCode(HuffmNode root,String code){
- //往左走,哈夫曼編碼加0
- if(root.getLeft()!=null){
- getHuffmCode(root.getLeft(),code+"0");
- }
- //往右走,哈夫曼編碼加1
- if(root.getRight()!=null){
- getHuffmCode(root.getRight(),code+"1");
- }
- //如果是葉子節點,返回該葉子節點的哈夫曼編碼
- if(root.getLeft()==null && root.getRight()==null){
- // System.out.println(root.getIndex()+"的編碼為:"+code);
- HuffmCodes[root.getIndex()]=code;
- }
- }
- //壓縮文件
- public void compress(String path,String destpath) throws Exception{
- //構建文件輸出流
- FileOutputStream fos = new FileOutputStream(destpath);
- FileInputStream fis = new FileInputStream(path);
- /**===============把碼表寫入文件================*/
- //將整個哈夫曼編碼以及每個編碼的長度寫入文件
- String code ="";
- for (int i = 0; i < 256; i++) {
- fos.write(HuffmCodes[i].length());
- code+=HuffmCodes[i];
- fos.flush();
- }
- //把哈夫曼編碼寫入文件
- // System.out.println("code="+code);
- String str1="";
- while(code.length()>=8){
- str1=code.substring(0, 8);
- int c=changeStringToInt(str1);
- // System.out.println(c);
- fos.write(c);
- fos.flush();
- code=code.substring(8);
- }
- //處理最后一個不為8的數
- int last=8-code.length();
- for (int i = 0; i <last; i++) {
- code+="0";
- }
- str1=code.substring(0, 8);
- int c=changeStringToInt(str1);
- fos.write(c);
- fos.flush();
- /**===============將數據寫入到文件中================*/
- //讀文件,並將對應的哈夫曼編碼串接成字符串
- int value=fis.read();
- String str = "";
- while(value!=-1){
- str+=HuffmCodes[value];
- // System.out.println((char)value+":"+str);
- value=fis.read();
- }
- System.out.println(str);
- fis.close();
- String s="";
- //將字符8位分割,對弈一個字節
- while(str.length()>=8){
- s=str.substring(0, 8);
- int b=changeStringToInt(s);
- // System.out.println(c);
- fos.write(b);
- fos.flush();
- str=str.substring(8);
- }
- //最后不夠8位添0
- int last1=8-str.length();
- for (int i = 0; i <last1; i++) {
- str+="0";
- }
- s=str.substring(0, 8);
- // System.out.println(s);
- int d=changeStringToInt(s);
- fos.write(d);
- //壓縮后不夠補0的個數暫
- fos.write(last1);
- fos.flush();
- fos.close();
- }
- //插入元素位置的索引
- public int getIndex(HuffmNode node) {
- for (int i = 0; i < list.size(); i++) {
- if(node.getData()<=list.get(i).getData()){
- return i;
- }
- }
- return list.size();
- }
- //將字符串轉換成整數
- public int changeStringToInt(String s){
- int v1=(s.charAt(0)-48)*128;
- int v2=(s.charAt(1)-48)*64;
- int v3=(s.charAt(2)-48)*32;
- int v4=(s.charAt(3)-48)*16;
- int v5=(s.charAt(4)-48)*8;
- int v6=(s.charAt(5)-48)*4;
- int v7=(s.charAt(6)-48)*2;
- int v8=(s.charAt(7)-48)*1;
- return v1+v2+v3+v4+v5+v6+v7+v8;
- }
- }
重新測試了一個文件,Test也做了一下更改:
- package com.huaxin.compress;
- public class Test {
- public static void main(String[] args) throws Exception {
- //創建壓縮對象
- Compress compress = new Compress();
- //統計文件中0-255出現的次數
- compress.countTimes("C:\\Users\\Administrator\\Desktop\\my.docx");
- //構造哈夫曼樹,並得到根節點
- HuffmNode root=compress.createTree();
- //得到哈夫曼編碼
- compress.getHuffmCode(root, "");
- //壓縮文件
- compress.compress("C:\\Users\\Administrator\\Desktop\\my.docx",
- "C:\\Users\\Administrator\\Desktop\\my.docx.zip");
- }
- }
解壓工程源代碼:
- package com.huaxin.decompress;
- import java.io.FileInputStream;
- import java.io.FileNotFoundException;
- import java.io.FileOutputStream;
- public class Decompress {
- //每個編碼的長度
- public int [] codelengths = new int [256];
- //對應的哈夫曼編碼值
- public String [] codeMap=new String[256];
- public static void main(String[] args) {
- Decompress d = new Decompress();
- d.decompress("C:\\Users\\Administrator\\Desktop\\my.docx.zip",
- "C:\\Users\\Administrator\\Desktop\\mydecompress.docx");
- }
- /*
- * 解壓思路:
- * 1、讀取文件里面的碼表
- * 2、得到碼表
- * 3、讀取數據
- * 4、還原數據
- */
- public void decompress(String srcpath,String destpath) {
- try {
- FileInputStream fis = new FileInputStream(srcpath);
- FileOutputStream fos = new FileOutputStream(destpath);
- int value;
- int codeLength=0;
- String code="";
- //還原碼表
- for (int i = 0; i < codelengths.length; i++) {
- value=fis.read();
- codelengths[i]=value;
- // System.out.println(times[i]);
- codeLength+=codelengths[i];
- }
- //得到總長度
- //將總長度除以8的到字節個數
- int len=codeLength/8;
- //如果不是8的倍數,則字節個數加1(對應壓縮補0的情況)
- if((codeLength)%8!=0){
- len++;
- }
- //讀取哈夫曼編碼
- // System.out.println("codeLength:"+len);
- for (int i = 0; i < len; i++) {
- //把讀到的整數轉換成二進制
- code+=changeIntToString(fis.read());
- }
- // System.out.println("哈夫曼編碼:"+code);
- for (int i = 0; i < codeMap.length; i++) {
- //如果第i個位置不為0 ,則說明第i個位置存儲有哈夫曼編碼
- if(codelengths[i]!=0){
- //將得到的一串哈夫曼編碼按照長度分割分割
- String ss=code.substring(0, codelengths[i]);
- codeMap[i]=ss;
- code=code.substring(codelengths[i]);
- }else{
- //為0則沒有對應的哈夫曼編碼
- codeMap[i]="";
- }
- }
- //讀取壓縮的文件內容
- String codeContent="";
- while(fis.available()>1){
- codeContent+=changeIntToString(fis.read());
- }
- //讀取最后一個
- value=fis.read();
- //把最后補的0給去掉
- codeContent=codeContent.substring(0, codeContent.length()-value);
- for (int i = 0; i < codeContent.length(); i++) {
- String codecontent=codeContent.substring(0, i+1);
- for (int j = 0; j < codeMap.length; j++) {
- if(codeMap[j].equals(codecontent)){
- // System.out.println("截取的字符串:"+codecontent);
- fos.write(j);
- fos.flush();
- codeContent=codeContent.substring(i+1);
- // System.out.println("截取后剩余編碼長度:"+codeContent.length());
- // count=1;
- i=-1;
- break;
- }
- }
- }
- // }
- fos.close();
- fis.close();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- //十進制轉二進制字符串
- public String changeIntToString(int value) {
- String s="";
- for (int i = 0; i < 8; i++) {
- s=value%2+s;
- value=value/2;
- }
- return s;
- }
- }
五、運行結果:字節大小相同
打開文件后文件內容相同
問題:為什么壓縮后的文件反而更大了?
答:因為我們寫了很多與該問價解壓的內容進去;平時的壓縮一般是針對大文件的;舉個簡單的例子,你用系統的壓縮軟件,壓縮一個只要幾個字節的文本,你會發現,壓縮文件也變得比原文件更大了;
public class HuffmanCompress { // 壓縮函數 public void compress(File inputFile, File outputFile) { Compare cmp = new Compare(); PriorityQueue<HufTree> queue = new PriorityQueue<HufTree>(12, cmp); // 映射字節及其對應的哈夫曼編碼 HashMap<Byte, String> map = new HashMap<Byte, String>(); // 文件中含有的字符的種類數 int i, char_kinds = 0; int char_temp, file_len = 0; FileInputStream fis = null; FileOutputStream fos = null; ObjectOutputStream oos = null; // 哈夫曼樹節點個數 int node_num; HufTree[] huf_tree = null; String code_buf = null; // 臨時存儲字符頻度的數組 TmpNode[] tmp_nodes = new TmpNode[256]; for (i = 0; i < 256; ++i) { tmp_nodes[i] = new TmpNode(); tmp_nodes[i].weight = 0; tmp_nodes[i].uch = (byte) i; } try { fis = new FileInputStream(inputFile); fos = new FileOutputStream(outputFile); oos = new ObjectOutputStream(fos); // 統計字符頻度,計算文件長度 while ((char_temp = fis.read()) != -1) { ++tmp_nodes[char_temp].weight; ++file_len; } fis.close(); Arrays.sort(tmp_nodes); // 排序后就會將頻度為0的字節放在數組最后,從而去除頻度為0的字節 // 同時計算出字節的種類 for (i = 0; i < 256; ++i) { if (tmp_nodes[i].weight == 0) break; } char_kinds = i; // 只有一種字節的情況 if (char_kinds == 1) { oos.writeInt(char_kinds); oos.writeByte(tmp_nodes[0].uch); oos.writeInt(tmp_nodes[0].weight); // 字節多於一種的情況 } else { node_num = 2 * char_kinds - 1;// 計算哈夫曼樹所有節點個數 huf_tree = new HufTree[node_num]; for (i = 0; i < char_kinds; ++i) { huf_tree[i] = new HufTree(); huf_tree[i].uch = tmp_nodes[i].uch; huf_tree[i].weight = tmp_nodes[i].weight; huf_tree[i].parent = 0; huf_tree[i].index = i; queue.add(huf_tree[i]); } tmp_nodes = null; for (; i < node_num; ++i) { huf_tree[i] = new HufTree(); huf_tree[i].parent = 0; } // 創建哈夫曼樹 createTree(huf_tree, char_kinds, node_num, queue); // 生成哈夫曼編碼 hufCode(huf_tree, char_kinds); // 寫入字節種類 oos.writeInt(char_kinds); for (i = 0; i < char_kinds; ++i) { oos.writeByte(huf_tree[i].uch); oos.writeInt(huf_tree[i].weight); map.put(huf_tree[i].uch, huf_tree[i].code); } oos.writeInt(file_len); fis = new FileInputStream(inputFile); code_buf = ""; // 將讀出的字節對應的哈夫曼編碼轉化為二進制存入文件 while ((char_temp = fis.read()) != -1) { code_buf += map.get((byte) char_temp); while (code_buf.length() >= 8) { char_temp = 0; for (i = 0; i < 8; ++i) { char_temp <<= 1; if (code_buf.charAt(i) == '1') char_temp |= 1; } oos.writeByte((byte) char_temp); code_buf = code_buf.substring(8); } } // 最后編碼長度不夠8位的時候,用0補齊 if (code_buf.length() > 0) { char_temp = 0; for (i = 0; i < code_buf.length(); ++i) { char_temp <<= 1; if (code_buf.charAt(i) == '1') char_temp |= 1; } char_temp <<= (8 - code_buf.length()); oos.writeByte((byte) char_temp); } } oos.close(); fis.close(); } catch (Exception e) { e.printStackTrace(); } } // 解壓函數 public void extract(File inputFile, File outputFile) { Compare cmp = new Compare(); PriorityQueue<HufTree> queue = new PriorityQueue<HufTree>(12, cmp); int i; int file_len = 0; int writen_len = 0; FileInputStream fis = null; FileOutputStream fos = null; ObjectInputStream ois = null; int char_kinds = 0; int node_num; HufTree[] huf_tree = null; byte code_temp; int root; try { fis = new FileInputStream(inputFile); ois = new ObjectInputStream(fis); fos = new FileOutputStream(outputFile); char_kinds = ois.readInt(); // 字節只有一種的情況 if (char_kinds == 1) { code_temp = ois.readByte(); file_len = ois.readInt(); while ((file_len--) != 0) { fos.write(code_temp); } // 字節多於一種的情況 } else { node_num = 2 * char_kinds - 1; // 計算哈夫曼樹所有節點個數 huf_tree = new HufTree[node_num]; for (i = 0; i < char_kinds; ++i) { huf_tree[i] = new HufTree(); huf_tree[i].uch = ois.readByte(); huf_tree[i].weight = ois.readInt(); huf_tree[i].parent = 0; huf_tree[i].index = i; queue.add(huf_tree[i]); } for (; i < node_num; ++i) { huf_tree[i] = new HufTree(); huf_tree[i].parent = 0; } createTree(huf_tree, char_kinds, node_num, queue); file_len = ois.readInt(); root = node_num - 1; while (true) { code_temp = ois.readByte(); for (i = 0; i < 8; ++i) { if ((code_temp & 128) == 128) { root = huf_tree[root].rchild; } else { root = huf_tree[root].lchild; } if (root < char_kinds) { fos.write(huf_tree[root].uch); ++writen_len; if (writen_len == file_len) break; root = node_num - 1; // 恢復為根節點的下標,匹配下一個字節 } code_temp <<= 1; } // 在壓縮的時候如果最后一個哈夫曼編碼位數不足八位則補0 // 在解壓的時候,補上的0之前的那些編碼肯定是可以正常匹配到和他對應的字節 // 所以一旦匹配完補的0之前的那些編碼,寫入解壓文件的文件長度就和壓縮之前的文件長度是相等的 // 所以不需要計算補的0的個數 if (writen_len == file_len) break; } } fis.close(); fos.close(); } catch (Exception e) { e.printStackTrace(); } } // 構建哈夫曼樹 public void createTree(HufTree[] huf_tree, int char_kinds, int node_num, PriorityQueue<HufTree> queue) { int i; int[] arr = new int[2]; for (i = char_kinds; i < node_num; ++i) { arr[0] = queue.poll().index; arr[1] = queue.poll().index; huf_tree[arr[0]].parent = huf_tree[arr[1]].parent = i; huf_tree[i].lchild = arr[0]; huf_tree[i].rchild = arr[1]; huf_tree[i].weight = huf_tree[arr[0]].weight + huf_tree[arr[1]].weight; huf_tree[i].index = i; queue.add(huf_tree[i]); } } // 獲取哈夫曼編碼 public void hufCode(HufTree[] huf_tree, int char_kinds) { int i; int cur, next; for (i = 0; i < char_kinds; ++i) { String code_tmp = ""; for (cur = i, next = huf_tree[i].parent; next != 0; cur = next, next = huf_tree[next].parent) { if (huf_tree[next].lchild == cur) code_tmp += "0"; else code_tmp += "1"; } huf_tree[i].code = (new StringBuilder(code_tmp)).reverse().toString(); } } }
