說到文件壓縮大家很容易想到的就是 rar,zip 等我們常見的壓縮格式。然而,還有一種就是大家在學習數據結構最常見到的哈夫曼樹的數據結構,以前還不知道他又什么用,其實他最大的用途就是用來做壓縮,也是一些 rar,zip 壓縮的祖先,稱為哈弗曼壓縮(什么你不知道誰是哈弗曼,也不知道哈弗曼壓縮,不急等下介紹)。
隨著網絡與多媒體技術的興起,人們需要存儲和傳輸的數據越來越多,數據量越來越大,以前帶寬有限的傳輸網絡和容量有限的存儲介質難以滿足用戶的需求。
特別是聲音、圖像和視頻等媒體在人們的日常生活和工作中的地位日益突出,這個問題越發顯得嚴重和迫切。如今,數據壓縮技術早已是多媒體領域中的關鍵技術之一。
一、什么是哈弗曼壓縮
Huffman( 哈夫曼 ) 算法在上世紀五十年代初提出來了,它是一種無損壓縮方法,在壓縮過程中不會丟失信息熵,而且可以證明Huffman 算法在無損壓縮算法中是最優的。 Huffman 原理簡單,實現起來也不困難,在現在的主流壓縮軟件得到了廣泛的應用。對應用程序、重要資料等絕對不允許信息丟失的壓縮場合, Huffman 算法是非常好的選擇。
二、怎么實現哈弗曼壓縮
哈夫曼壓縮是個無損的壓縮算法,一般用來壓縮文本和程序文件。哈夫曼壓縮屬于可變代碼長度算法一族。意思是個體符號(例如,文本文件中的字符)用一個特定長度的位序列替代。因此,在文件中出現頻率高的符號,使用短的位序列,而那些很少出現的符號,則用較長的位序列。
故我們得了解幾個概念:
1 、二叉樹 : 在計算機科學中,二叉樹是每個結點最多有兩個子樹的有序樹。通常子樹的根被稱作 “ 左子樹 ” ( left subtree )和 “右子樹 ” ( right subtree )。
2 、哈夫曼編碼 (Huffman Coding) :是一種編碼方式,哈夫曼編碼是可變字長編碼 (VLC) 的一種。 uffman 于 1952 年提出一種編碼方法,該方法完全依據字符出現概率來構造異字頭的平均長 度最短的碼字,有時稱之為最佳編碼,一般就叫作 Huffman 編碼。
三、哈夫曼編碼生成步驟:
① 掃描要壓縮的文件,對字符出現的頻率進行計算。
② 把字符按出現的頻率進行排序,組成一個隊列。
③ 把出現頻率最低(權值)的兩個字符作為葉子節點,它們的權值之和為根節點組成一棵樹。
④ 把上面葉子節點的兩個字符從隊列中移除,并把它們組成的根節點加入到隊列。
⑤ 把隊列重新進行排序。重復步驟 ③④⑤ 直到隊列中只有一個節點為止。
⑥ 把這棵樹上的根節點定義為 0 (可自行定義 0 或 1 )左邊為 0 ,右邊為 1 。這樣就可以得到每個葉子節點的哈夫曼編碼了。
既如 (a) 、 (b) 、 (c) 、 (d) 幾個圖,就可以將離散型的數據轉化為樹型的了。
如果假設樹的左邊用0 表示右邊用 1 表示,則每一個數可以用一個 01 串表示出來。
則可以得到對應的編碼如下:
1-->110
2-->111
3-->10
4-->0
每一個01 串,既為每一個數字的哈弗曼編碼。
為什么能壓縮:
壓縮的時候當我們遇到了文本中的1 、 2 、 3 、 4 幾個字符的時候,我們不用原來的存儲,而是轉化為用它們的 01 串來存儲不久是能減小了空間占用了嗎。(什么 01 串不是比原來的字符還多了嗎?怎么減少?)大家應該知道的,計算機中我們存儲一個 int 型數據的時候一般式占用了 2^32-1 個 01 位,因為計算機中所有的數據都是最后轉化為二進制位去存儲的。所以,想想我們的編碼不就是只含有 0 和 1 嘛,因此我們就直接將編碼按照計算機的存儲規則用位的方法寫入進去就能實現壓縮了。
比如:
1這個數字,用整數寫進計算機硬盤去存儲,占用了 2^32-1 個二進制位
而如果用它的哈弗曼編碼去存儲,只有110 三個二進制位。
效果顯而易見。
開始寫程序:
開始些程序之前,必須想好自己的文件存儲格式,和存儲規則是什么
為了簡便,我自定義存儲的基本信息,格式如下:
SaveCode[i].n int型 // 每一個字節的編碼長度 i:0~256
B yte[] byte數組型 // 每一個字節的編碼 SaveCode[i].node 中 String 的 01 串轉化而來。
Byte[] byte數組型 // 對文件中每一個 byte 的重新編碼的哈夫曼編碼寫入。
有了定義好的格式我們的讀寫就非常的方便了,
創建步驟:
①讀入文件中的所有字節:
- // 創建文件輸入流
- java.io.FileInputStream fis = new java.io.FileInputStream(path);
- //讀入所有的文件字節
- while(fis.available()>0){
- int i=fis.read();
- byteCount[i]++;
- }
②構建哈夫曼樹:
- /**
- * 使用優先隊列構建哈弗曼樹
- */
- public void createTree(){
- //優先隊列
- PriorityQueue<hfmNode> nodeQueue = new PriorityQueue<hfmNode>();
- //把所有的節點都加入到 隊列里面去
- for (int i=0;i<256;i++){
- if(byteCount[i]!=0){
- hfmNode node = new hfmNode(i,byteCount[i]);
- nodeQueue.add(node);//加入節點
- }
- }
- //構建哈弗曼樹
- while(nodeQueue.size()>1)
- {
- hfmNode min1 = nodeQueue.poll();//獲取隊列頭
- hfmNode min2 = nodeQueue.poll();
- hfmNode result = new hfmNode(0,min1.times+min2.times);
- result.lChild=min1;
- result.rChild=min2;
- nodeQueue.add(result);//加入合并節點
- }
- root=nodeQueue.peek(); //得到根節點
- }
這里我采用了優先隊列的方法,因為優先隊列比較符合哈夫曼的構造方法,形式上也非常的相似。
③取得每一個葉子節點的哈夫曼編碼:
- /**
- * 獲得葉子節點的哈弗曼編碼
- * @param root 根節點
- * @param s
- */
- public void getMB(hfmNode root,String s){
- if ((root.lChild==null)&&(root.rChild==null)){
- Code hc=new Code();
- hc.node=s;
- hc.n=s.length();
- System.out.println("節點"+root.data+"編碼"+hc.node);
- SaveCode[root.data]=hc;//保存字節 root.data 的編碼 HC
- }
- if (root.lChild!=null){//左0 右 1
- getMB(root.lChild,s+'0');
- }
- if (root.rChild!=null){
- getMB(root.rChild,s+'1');
- }
- }
如此一來我們的哈夫曼樹就建好了。
下面就是按照我們之前定義的文件存儲格式直接寫進文件就可以了。
壓縮文件的寫入:
① 寫出0~256 每一個字節對應編碼的長度
- //寫出每一個編碼的長度
- for (int i=0;i<256;i++){
- fos.write(SaveCode[i].n);
- }
② 寫出每一個字節所對應的編碼
這一步較為復雜,因為JAVA 中沒有自己定義的二進制為寫入,我們就不得不將每 8 個 01 String 轉化為一個byte 再將 byte 寫入。但是,問題又來了不是所有的 01String 都是 8 的整數倍,所以就又得在不是 8 整數倍的String 后面補上 0
- /////寫入每一個字節所對應的 String編碼
- int count=0;//記錄中轉的字符個數
- int i=0;//第i個字節
- String writes ="";
- String tranString="";//中轉字符串
- String waiteString;//保存所轉化的所有字符串
- while((i<256)||(count>=8)){
- //如果緩沖區的等待寫入字符大于8
- if (count>=8){
- waiteString="";//清空要轉化的的碼
- for (int t=0;t<8;t++){
- waiteString=waiteString+writes.charAt(t);
- }
- //將writes前八位刪掉
- if (writes.length()>8){
- tranString="";
- for (int t=8;t<writes.length();t++){
- tranString=tranString+writes.charAt(t);
- }
- writes="";
- writes=tranString;
- }else{
- writes="";
- }
- count=count-8;//寫出一個8位的字節
- int intw=changeString(waiteString);//得到String轉化為int
- //寫入文件
- fos.write(intw);
- }else{
- //得到地i個字節的編碼信息,等待寫入
- count=count+SaveCode[i].n;
- writes=writes+SaveCode[i].node;
- i++;
- }
- }
- //把所有編碼沒有足夠8的整數倍的String補0使得足夠8的整數倍,再寫入
- if (count>0){
- waiteString="";//清空要轉化的的碼
- for (int t=0;t<8;t++){
- if (t<writes.length()){
- waiteString=waiteString+writes.charAt(t);
- }else{
- waiteString=waiteString+'0';
- }
- }
- fos.write(changeString(waiteString));//寫入
- System.out.println("寫入了->"+waiteString);
- }
- /**
- * 將一個八位的字符串轉成一個整數
- * @param s
- * @return
- */
- public int changeString(String s){
- return ((int)s.charAt(0)-48)*128+((int)s.charAt(1)-48)*64+((int)s.charAt(2)-48)*32
- +((int)s.charAt(3)-48)*16+((int)s.charAt(4)-48)*8+((int)s.charAt(5)-48)*4
- +((int)s.charAt(6)-48)*2+((int)s.charAt(7)-48);
- }
③ 將源文件中的所有byte 轉化為 01 哈夫曼編碼,寫入壓縮文件
這一步也比較復雜,原理同上一步,在SaveCode 中查找一個 byte 所對應的哈夫曼編碼,不夠 8 的整數倍的就不足,再寫入。
值得注意的事,最后一定要寫一個byte 表示,補了多少個 0 方便解壓縮時的刪除 0
- //再次讀入文件的信息,對應每一個字節寫入編碼
- count=0;
- writes ="";
- tranString="";
- int idata=fis.read();
- //文件沒有讀完的時候
- while ((fis.available()>0)||(count>=8)){
- //如果緩沖區的等待寫入字符大于8
- if (count>=8){
- waiteString="";//清空要轉化的的碼
- for (int t=0;t<8;t++){
- waiteString=waiteString+writes.charAt(t);
- }
- //將writes前八位刪掉
- if (writes.length()>8){
- tranString="";
- for (int t=8;t<writes.length();t++){
- tranString=tranString+writes.charAt(t);
- }
- writes="";
- writes=tranString;
- }else{
- writes="";
- }
- count=count-8;//寫出一個8位的字節
- int intw=changeString(waiteString);
- fos.write(intw);//寫到文件中區
- }else{
- //讀入idata字節,對應編碼寫出信息
- count=count+SaveCode[idata].n;
- writes=writes+SaveCode[idata].node;
- idata=fis.read();
- }
- }
- count=count+SaveCode[idata].n;
- writes=writes+SaveCode[idata].node;
- //把count剩下的寫入
- int endsint=0;
- if (count>0){
- waiteString="";//清空要轉化的的碼
- for (int t=0;t<8;t++){
- if (t<writes.length()){
- waiteString=waiteString+writes.charAt(t);
- }else{
- waiteString=waiteString+'0';
- endsint++;
- }
- }
- fos.write(changeString(waiteString));//寫入所補的0
如此一來,整個的壓縮過程就完畢了。
解壓縮過程:
與壓縮過程剛好是反著來進行的,知道的存儲的方式我們就反著來讀取看看。
① 讀入所有字節所對應的編碼長度:
- //讀入文件中的每一個編碼的長度
- for (int i=0;i<256;i++){
- Code hC=new Code();
- hC.n=fis.read();
- hC.node="";
- SaveCode[i]=hC;
- }
② 讀入每一個字節所對應的編碼
- int i=0;
- int count=0;
- String coms="";
- //讀SaveCode中0~256字節的的數據
- while (i<256){
- //當緩存區的編碼長度比編碼[i]的長度長的時候
- if (coms.length()>=SaveCode[i].n){
- for (int t=0;t<SaveCode[i].n;t++){//SaveCode[i].node取得該編碼
- SaveCode[i].node=SaveCode[i].node+coms.charAt(t);
- }
- //把coms前這幾位去掉
- String coms2="";
- for (int t=SaveCode[i].n;t<coms.length();t++){
- coms2=coms2+coms.charAt(t);
- }
- coms="";
- coms=coms2;//累計的下一個編碼,繼續的使用
- i++;
- }else{
- //如果已經沒有寫出的話,再度入一個byte轉化為01 String
- coms=coms+IntToString(fis.read());
- }
- }
③ 讀入文件的哈夫曼編碼
得注意的事,前面我們又補0 的位,所以在我們讀取的時候,記得把之前所補得東西,給刪除掉就 OK 了。
- String rsrg;//存轉換成的Sting
- String compString="";//存要比較的字符串
- //讀入文件,寫出文件
- while(fis.available()>1){
- if (search(compString)>=0){
- int cint=search(compString);
- fos.write(cint);
- //刪掉前n個數據
- String compString2="";
- for (int t=SaveCode[cint].n;t<compString.length();t++){
- compString2=compString2+compString.charAt(t);
- }
- compString="";
- compString=compString2;
- }else{//繼續讀入
- compString=compString+IntToString(fis.read());
- }
- }
- //處理最后一個字節
- int cint=fis.read();
- String compString2="";
- for (int t=0;t<compString.length()-cint;t++){
- compString2=compString2+compString.charAt(t);
- }
- compString=compString2;
- //刪掉前n個數據
- while (compString.length()>0){
- int ccint=search(compString);
- fos.write(ccint);
- compString2="";
- for (int t=SaveCode[ccint].n;t<compString.length();t++){
- compString2=compString2+compString.charAt(t);
- }
- compString="";
- compString=compString2;
- }

