1. 程式人生 > >Huffman編碼實現壓縮解壓縮

Huffman編碼實現壓縮解壓縮

這是我們的課程中佈置的作業,找一些資料將作業完成,順便將其寫到部落格,以後看起來也方便。

原理介紹

  • 什麼是Huffman壓縮

    Huffman( 哈夫曼 ) 演算法在上世紀五十年代初提出來了,它是一種無失真壓縮方法,在壓縮過程中不會丟失資訊熵,而且可以證明 Huffman 演算法在無失真壓縮演算法中是最優的。 Huffman 原理簡單,實現起來也不困難,在現在的主流壓縮軟體得到了廣泛的應用。對應用程式、重要資料等絕對不允許資訊丟失的壓縮場合, Huffman 演算法是非常好的選擇。

  • 怎麼實現Huffman壓縮
    哈夫曼壓縮是個無損的壓縮演算法,一般用來壓縮文字和程式檔案。哈夫曼壓縮屬於可變程式碼長度演算法一族。意思是個體符號(例如,文字檔案中的字元)用一個特定長度的位序列替代。因此,在檔案中出現頻率高的符號,使用短的位序列,而那些很少出現的符號,則用較長的位序列。

    1. 二叉樹
      在電腦科學中,二叉樹是每個結點最多有兩個子樹的有序樹。通常子樹的根被稱作 “ 左子樹 ” ( left subtree )和 “ 右子樹 ” ( right subtree )。
      二叉樹
    2. 哈夫曼編碼 (Huffman Coding)
      哈夫曼編碼是一種編碼方式,哈夫曼編碼是可變字長編碼 (VLC) 的一種。 uffman 於 1952 年提出一種編碼方法,該方法完全依據字元出現概率來構造異字頭的平均長 度最短的碼字,有時稱之為最佳編碼,一般就叫作 Huffman 編碼。
  • Huffman編碼生成步驟
    1. 掃描要壓縮的檔案,對字元出現的頻率進行計算。
    2. 把字元按出現的頻率進行排序,組成一個佇列。
    3. 把出現頻率最低(權值)的兩個字元作為葉子節點,它們的權值之和為根節點組成一棵樹。
    4. 把上面葉子節點的兩個字元從佇列中移除,並把它們組成的根節點加入到佇列。
    5. 把佇列重新進行排序。重複步驟 3、4、5 直到佇列中只有一個節點為止。
    6. 把這棵樹上的根節點定義為 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 三個二進位制位。
    效果顯而易見。

編碼實現

  1. 流程圖
    編碼流程
Created with Raphaël 2.1.0開始讀入待壓縮檔案,計算檔案中各字元的權重根據權重構建Huffman樹根據Huffman樹獲得各個字元的HUffman編碼,並建立Huffman編碼的HashTable將字元總數、字元種數,以及Huffman樹寫入壓縮檔案檔案頭再次讀入待壓縮檔案,根據其內容和coding hash table 將壓縮後的資料寫入檔案結束
  1. 資料結構
    CharacterWeight:記錄字元值,以及其在待壓縮檔案中的權重。
public class CharacterCode {
    private int weight;//字元值  
    private char character;//字元值 
    private String code;//其對應huffman編碼 
    } 

HuffmanNode:huffman樹中的節點資訊。

public class HuffmanNode {
    private int parent;//父節點
    private int lChild;//左子
    private int rChild;//右子
    private int weight;//權重
    }
  1. 程式關鍵步驟
    • Huffman樹的構建
      Huffman樹的變數:ArrayList list;
      流程圖
Created with Raphaël 2.1.0開始i=0 n=字元的種數迴圈遍歷查詢列表中權重最小的兩個node建立一個新的節點作為找到的兩個權重最小的節點的父節點,並將該父節點的權重置為權重最小的兩節點的權重和,將該節點加入陣列中。i++i<n-1結束yesno

程式碼

for(int i=0;i<list.size()-1;i++){  
            //w1 : the first min weight w2: the second min weight   
            //i1 : the first min weight index, i2: the second min weight index  
            int w1 = MAX_VALUE, w2=MAX_VALUE;   
            int i1 = 0, i2 = 0;  
            // find the two node with the minimum weight  
            for(int j=0;j<tree.size();j++){  
                HuffmanNode node = tree.get(j);  
                if(node.getWeight()< w1 && node.getParent()==-1){  
                    w2 = w1;  
                    w1 = node.getWeight();  
                    i2 = i1;  
                    i1 = j;  
                }  
                else if(node.getWeight()<w2 && node.getParent()==-1){  
                    w2 = node.getWeight();  
                    i2 = j;  
                }  
            }  
            //set the two node to be the children of a new node, and add the new node to the tree  
            HuffmanNode pNode = new HuffmanNode(w1+w2);  
            pNode.setlChild(i1);  
            pNode.setrChild(i2);  
            tree.add(pNode);  
            tree.get(i1).setParent(tree.indexOf(pNode));  
            tree.get(i2).setParent(tree.indexOf(pNode));}  
  • 根據Huffman 樹獲得Huffman編碼
    從葉子節點開始網上遍歷Huffman樹,直到到達根節點,根據當前節點為其父節點的左兒子還是右兒子確定這一位值是0還是1。最後將依次獲得的0,1字串反轉獲得Huffman編碼。
for(int i=0;i<list.size();i++){  
            HuffmanNode node = tree.get(i);  
            HuffmanNode pNode = tree.get(node.getParent());  
            String code ="";  
            while(true){  
                if(pNode.getlChild()==tree.indexOf(node)){  
                    code = "0"+code;  
                }  
                else if(pNode.getrChild() == tree.indexOf(node)){  
                    code = "1"+code;  
                }  
                else {  
                    System.out.println("Tree Node Error!!!");  
                    return null;  
                }  
                node=pNode;  
                if(node.getParent()!=-1)  
                    pNode=tree.get(node.getParent());  
                else   
                    break;  
            }  
            list.get(i).setCode(new String(code));  
        }  
  • 標頭檔案設計

    編碼 型別 位元組數
    字元總數 Int 4
    字元種類數 Short 2
    葉子節點 char字元 short 父節點 3
    非葉子節點 Short 左兒子 short 右兒子 short父節點 6

    檔案頭長度(單位: byte)
    l= 9n
    其中n 為字元種類數。

    • 檔案內容的編碼和寫入
Created with Raphaël 2.1.0開始將待壓縮檔案讀入字元陣列根據coding hash table 獲得huffman編碼字串,並將該字串新增到buff中檢視buff,如果字元數大於8則將字串轉換為Short型別變數並寫入檔案將寫入的字元從buff中刪除是否到達檔案尾?結束yesno

程式碼

while((temp=reader.read())!=-1){ //!= EOF     
            // get the code from the code table  
            String code = codeTable.get((char)temp);  
            c++;  
            if(c>=count/96){  
                System.out.print("=");  
                c=0;  
            }  
            try{  
                StringBuilder codeString = new StringBuilder(code);  
                outputStringBuffer.append(codeString);  
                while(outputStringBuffer.length()>8){  
                    out.write(Short.parseShort(outputStringBuffer.substring(0, 8),2));  
                    outputStringBuffer.delete(0, 8);  
                }  
            } catch(Exception e){  
                e.printStackTrace();  
            }  

        }  

解碼實現

  • 流程圖
Created with Raphaël 2.1.0開始讀壓縮檔案,讀入檔案頭,獲得字元總數,字元種數以及huffman表資訊,重建huffman樹讀入正文,根據重建得到的huffman樹獲得原本的字元,將字元寫入解壓縮後的檔案是否到達檔案尾部?結束yesno
  • 資料結構
    HuffmanNode:huffman樹中的節點資訊。
public class HuffmanNode {
    private int parent;//父節點
    private int lChild;//左子
    private int rChild;//右子
    private int weight;//權重
    }
  • 程式關鍵步驟

    • 重建Huffman樹。在檔案頭中存放的原本就是Huffman樹的節點資訊。

      in = new DataInputStream(new FileInputStream(file));  
          count = in.readInt();  
          charNum = in.readShort();  
          nodeNum = 2*charNum -1;  
          //rebuild the huffman tree  
          for(int i=0;i<charNum;i++){  
              HuffmanNode node = new HuffmanNode((char)in.readByte());  
              int parent = in.readShort();  
              node.setParent(parent);  
              tree.add(node);  
          }  
      
          for(int i=charNum;i<nodeNum;i++){  
              HuffmanNode node = new HuffmanNode(' ');  
              int l = in.readShort();  
              int r = in.readShort();  
              int p = in.readShort();  
              node.setlChild(l);  
              node.setrChild(r);  
              node.setParent(p);  
              tree.add(node);  
          }  
    • 解碼
      流程圖

Created with Raphaël 2.1.0開始Buff.length<32從檔案中讀入整數將讀入的整數轉為二進位制字串,並將其加到buff中根據buff中的01字元從頂向下遍歷huffman樹,得到葉子節點、其對應的解碼值,將其寫入檔案,從buff中遍歷刪去已經遍歷過的字元字元數是否達到總數處理buff中剩餘內容結束yesnoyesno

程式碼

while(true){  
            while(buff.length()<32){  
                temp = in.readInt();  
                String codeString = Integer.toBinaryString(temp);  
                while(codeString.length()<32){  
                    codeString='0'+codeString;  
                }  
                buff.append(codeString);  
            }  
            node = tree.get(tree.size()-1);  
            dep = 0;  
            while(!(node.getlChild()==-1&&node.getrChild()==-1)){  
                if(dep>=buff.length()){  
                    System.out.println( "Buff overflow");  
                }  
                if(buff.charAt(dep)=='0'){  
                    node = tree.get(node.getlChild());  
                }  
                else if(buff.charAt(dep)=='1'){  
                    node = tree.get(node.getrChild());  
                }  
                else{  
                    System.out.println("Coding error");  
                }  
                dep++;  
            }  

            char c = node.getCH();  
            num++;  
            if(num>=n/99){  
                System.out.print("=");  
                num=0;  
            }  
            count++;  
            if(count>=n){  
                break;  
            }  
            charBuff+=c;  
            if(charBuff.length()>256){  
                writer.write(charBuff);  
                charBuff="";  
            }  
            buff.delete(0, dep);  

        }  

    } catch(EOFException e){  
        //just do nothing  
    }  
    catch(Exception e){  
        e.printStackTrace();  
    } finally{  
        //there may be data released in the buff and charbuff, so we need to process them  
        while(buff.length()>0){  
            node = tree.get(tree.size()-1);  
            dep = 0;  
            while(!(node.getlChild()==-1&&node.getrChild()==-1)){  
                if(dep>=buff.length()){  
                    break;  
                }  
                if(buff.charAt(dep)=='0'){  
                    node = tree.get(node.getlChild());  
                }  
                else if(buff.charAt(dep)=='1'){  
                    node = tree.get(node.getrChild());  
                }  
                else{  
                    System.out.println("Coding error");  
                    //return;  
                }  
                dep++;  
            }  
            char c = node.getCH();  
            num++;  
            if(num>=n/99){  
                System.out.print("=");  
                num=0;  
            }  
            count++;  
            if(count>=n){  
                break;  
            }  
            charBuff+=c;  
            if(charBuff.length()>256){  
                try {  
                    writer.write(charBuff);  
                } catch (IOException e1) {  
                    // TODO Auto-generated catch block  
                    e1.printStackTrace();  
                }  
                charBuff="";  
            }  
            buff.delete(0, dep);  
        }  

        try {  
            writer.write(charBuff);  
            writer.close();  
        } catch (IOException e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        }   
    }  
    try{  
        writer.close();  
    } catch(IOException e){  
        throw e;  
    }  

專案原始碼
留坑回頭放上