哈夫曼編碼的實現

哈夫曼樹及哈夫曼編碼

  • 哈夫曼樹是斷定過程最優的決策樹,又稱最優二叉樹。
  • 哈夫曼樹的每一個結點有權值,一個結點的權值實際上就是這個結點子樹在整個樹中所佔的比例,一般指字符對應的二進制編碼出現的機率。權值大的結點距離根結點近。
  • 樹的帶權路徑長度(WPL):若是樹中每一個葉子上都帶有一個權值,則把樹中全部葉子的帶權路徑長度之和稱爲樹的帶權路徑長度。哈弗曼樹就是帶權路徑長度最小的二叉樹。設某二叉樹有n個帶權值的葉子結點,則該二叉樹的帶權路徑長度記爲(Wk爲第k個葉子結點的權值;Lk爲該結點的路徑長度):
  • 哈弗曼樹是自底向上構建的,二叉樹的左子樹編碼爲0,右子樹編碼爲1,而後從根到每一個葉結點依次寫出葉結點的編碼,即哈夫曼編碼。
  • 哈夫曼編碼是已知的最佳無損壓縮算法,而且知足前綴碼的性質,能夠隨時解碼。
  • 具體過程:
    • 給定字符集設爲S={a,b,c,d,e,f,g,h},計算各個字符出現的機率,即權值。
    • 將字符轉換爲樹結點並根據權值大小將字符由小到大排序
    • 依次將權值最小的兩個字符的權值相加,獲得新的權值,爲那兩個字符的父結點,刪除那兩個字符,並將造成的樹添加到排序列表中,父結點的權值按大小排序,在序列中找到合適的位置,而後再將最小的兩個字符進行以上操做...
    • 最終只剩一個結點,並造成了一棵樹,即哈弗曼樹。
  • 編碼:在哈夫曼樹中規定左分支表示符號0,右分支表示符號1。對於每一個葉子結點,從根結點到此葉子結點造成的編碼就是這個結點表示字符的哈夫曼編碼。
  • 解碼:譯碼過程是分解、識別各個字符,還原數據的過程。從字符串頭開始掃描到尾,依次去匹配。java

  • 圖示過程
    • 首先是帶權值的數
    • 第二步將最小的1和2組合相加造成父結點,並刪除1和2
    • 依次進行

    • 這是隻剩下一個結點,同時也造成了一棵樹,這就是哈夫曼樹。

代碼實現

  • 首先構造結點類,每一個結點須要有哈夫曼編碼,字符自己,權值,以及左結點和右結點。node

    public class TreeNode  {
        public String code = "";// 節點的哈夫曼編碼
        public String data = "";// 節點的字符
        public int count;// 節點的權值
        public TreeNode lChild;//左結點
        public TreeNode rChild;//右結點
        public TreeNode(String data, int count) {
            this.data = data;
            this.count = count;
        }
        public TreeNode(int count, TreeNode lChild, TreeNode rChild) {
            this.count = count;
            this.lChild = lChild;
            this.rChild = rChild;
        }
    }
  • 計算每一個字符的權重,flag用來斷定取出的字符串是否存在,若不存在爲true,若是存在那麼就在出現次數統計上加一,即權重加一,若是沒有出現過,就將它加入到存儲字符的鏈表中。git

    // 統計出現的字符及出現次數
    private void getCharNum(String str) {
        for (int i = 0; i < str.length(); i++) {
            char ch = str.charAt(i); // 從字符串中取出字符
            flag = true;
            for (int j = 0; j < charList.size(); j++) {
                CharData data = charList.get(j);
                // 遍歷字符鏈表,如有相同字符則將個數加1
                if(ch == data.chardata){
                    data.num++;
                    flag = false;
                    totalcount++;
                    break;
                }
            }
            // 字符對象鏈表中沒有相同字符則將其加到鏈表中
            if(flag){
                charList.add(new CharData(ch));
                totalcount++;
            }
        }
    }
  • 將字符鏈表中的字符構建爲結點,利用循環將字符鏈表中的字符和其權值建立爲TreeNode結點,並加入到結點鏈表中。算法

    //將字符建立爲結點
    private void creatNodes() {
        for (int i = 0; i < charList.size(); i++) {
            String data = charList.get(i).chardata + "";
            int count = charList.get(i).num;//權值
            TreeNode node = new TreeNode(data, count); // 建立節點對象
            NodeList.add(node); // 加入到節點鏈表
        }
    }
  • 構建哈弗曼樹,根據上文提到的哈夫曼編碼的規則,將最小的兩個節點分別設置爲左右子結點,左邊編碼爲0,右邊爲1,而後依據父結點建立新的TreeNode結點,將新的結點放到鏈表首位並從新排序。一次循環進行,直到結點鏈表中的節點數不大於1,這時樹就造成了。this

    //構建哈夫曼樹
    private void creatTree() {
        // 當節點數目大於一時,將權值最小的兩個節點生成一棵新樹,父結點爲二者之和,並刪除兩個結點將新樹放到列表中
        while (NodeList.size() > 1) {
            TreeNode left = NodeList.poll();
            TreeNode right = NodeList.poll();
            // 設置各個結點的哈夫曼編碼
            left.code = "0";
            right.code = "1";
            setCode(left);
            setCode(right);
            int parentWeight = left.count + right.count;// 父節點權值等於子節點權值之和
            TreeNode parent = new TreeNode(parentWeight, left, right);
            NodeList.addFirst(parent); // 暫時將父節點置於首位
            Sort(NodeList); // 從新排序將父結點放到合適位置
        }
    }
  • 在構建哈夫曼樹的過程當中每次將最小的兩個相加構造新結點時,須要對新的鏈表進行排序,因此用到升序排序法,將鏈表從左開始依次與其右邊的全部字符比較權值,將權值小的字符放到左邊,這樣就造成升序鏈表。編碼

    //升序排序
    private void Sort(LinkedList<TreeNode> nodelist) {
        for (int i = 0; i < nodelist.size() - 1; i++) {
            for (int j = i + 1; j < nodelist.size(); j++) {
                TreeNode temp;
                if (nodelist.get(i).count > nodelist.get(j).count) {
                    temp = nodelist.get(i);
                    nodelist.set(i, nodelist.get(j));
                    nodelist.set(j, temp);
                }
            }
        }
    }
  • 設置每一個結點的哈夫曼編碼,從根結點開始,分別對作孩子和右孩子的編碼添加0/1,左結點爲0,右結點爲1。這樣每一個葉子節點都有獨一無二的01編碼。.net

    //設置結點的哈夫曼編碼
    private void setCode(TreeNode root) {
        if (root.lChild != null) {
            root.lChild.code = root.code + "0";
            setCode(root.lChild);
        }
        if (root.rChild != null) {
            root.rChild.code = root.code + "1";
            setCode(root.rChild);
        }
    }
  • 利用以上方法,根據哈夫曼編碼的規則構建哈夫曼樹code

    //構建哈夫曼樹
    public void creatHuffmanTree(String str) {
        this.str = str;
        NodeList = new LinkedList<TreeNode>();
        charList = new LinkedList<CharData>();
        // 統計字符串中字符以及字符的出現次數
        getCharNum(str);
        // 建立節點
        creatNodes();
        // 對節點升序排序
        Sort(NodeList);
        // 將權值最小的兩個節點相加生成一個新的父節點並刪除權值最小的兩個節點,將父節點存放到列表中,造成樹
        creatTree();
        // 將最後的一個節點賦給根節點
        root = NodeList.get(0);
    }
  • 遍歷節點輸出字符的編碼,經過判斷左右孩子是否爲空的狀況,找到葉子結點,即字符,而後輸出其編碼和出現次數。對象

    //遍歷結點
    private void output(TreeNode node) {
        if (node.lChild == null && node.rChild == null) {
            System.out.println(node.data + " 的編碼爲:" + node.code+ "   出現次數爲:"+ node.count + "    出現機率爲:"+ (node.count)/totalcount);
        }
        if (node.lChild != null) {
            output(node.lChild);
        }
        if (node.rChild != null) {
            output(node.rChild);
        }
    }
  • 編碼,定義字符串hfmCodeStr,將葉子結點的編碼依次添加到編碼字符串中,返回hfmCodeStr字符串。blog

    //編碼
    public String creHufmCode(String str) {
        for (int i = 0; i < str.length(); i++) {
            String string = str.charAt(i) + "";
            search(root, string);
        }
        return hfmCodeStr;
    }
    //找到葉子結點,添加其編碼
    private void search(TreeNode root, String c) {
        if (root.lChild == null && root.rChild == null) {
            if (c.equals(root.data)) {
                hfmCodeStr += root.code;
            }
        }
        if (root.lChild != null) {
            search(root.lChild, c);
        }
        if (root.rChild != null) {
            search(root.rChild, c);
        }
    }
  • 解碼,經過遍歷葉子結點找到相匹配的字符編碼,定義編碼後的字符串從第一個數字開始,用substring方法截取字符串,若是找到相匹配的葉子結點編碼,就進行解碼,當解碼失敗時,說明編碼不匹配,end向後移,當解碼成功時,first向後移,對下一段字符進行解碼,最後輸出解碼後的字符串。

    //解碼
    public String decode(String codeStr) {
        int first = 0;
        int end = 1;
        while(end <= codeStr.length()){
            target = false;
            String s = codeStr.substring(first, end);
            matchCode(root, s); // 解碼
            // 每解碼一個字符,first向後移
            if(target){
                first = end;
            }
            end++;
        }
        return result;
    }
    //匹配字符哈夫曼編碼,找到對應的字符
    private void matchCode(TreeNode root, String code){
        if (root.lChild == null && root.rChild == null) {
            if (code.equals(root.code)) {
                result += root.data; // 找到對應的字符,拼接到解碼字符串後
                target = true; // 標誌爲true
            }
        }
        if (root.lChild != null) {
            matchCode(root.lChild, code);
        }
        if (root.rChild != null) {
            matchCode(root.rChild, code);
        }
    }
  • 讀取文件中的字符串,並構造哈夫曼樹

    BufferedReader a = new BufferedReader(new FileReader("wen.txt"));
        String data = a.readLine();
        huff.creatHuffmanTree(data);// 構造樹
  • 將編碼解碼後的內容寫入文件

    //寫入文件
        File file = new File("wen2");
        Writer write = new FileWriter(file);
        write.write(hufmCode);
        write.flush();
        write.write(huff.decode(hufmCode));
        write.close(
  • 運行結果
  • 英文文件
  • 將編碼解碼後的結果存入文件

碼雲連接

https://gitee.com/CS-IMIS-23/20172314/blob/master/src/新第十四周/Huffman.java

參考資料

相關文章
相關標籤/搜索