1. 程式人生 > >演算法筆記 (七)PriorityQueue 實現 Huffman Tree and code

演算法筆記 (七)PriorityQueue 實現 Huffman Tree and code

背景

       在看word2vec訓練模型時發現它在優化cbow模型時採用了哈夫曼編碼,不禁勾起了以前的回憶,趁著模糊的記憶,梳理一下哈夫曼樹相關內容,在以前的文章介紹了線性表、圖等結構,這次我們正好來介紹下樹結構。

       先來熟悉一下樹有關的概念,它其實也只代表了每個資料節點之間的邏輯關係,每個節點中儲存的是資料,資料型別可以多種多樣,樹代表了資料節點之間的邏輯關係。
葉子節點:

  • 根節點
    顧名思義,根節點正如樹的樹根,一棵樹只有一個樹根,向四面八方開散開來,長出來各種子樹

  • 內部節點
    除去根節點、葉子結點以後的節點,內部結點既有孩子節點又有父節點

  • 二叉樹
    在電腦科學中,二叉樹是每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。

  • 滿二叉樹:即每個非葉子節點都有兩個孩子的樹結構

  • 完全二叉樹:每一層自上而下自左向右編號可以連起來(除了最後一層)

  • 路徑
    在一個樹中假設有兩點A 、B,由A到B可能有很多種邊連線順這邊到達B,經過的這些邊連起來就是一條路徑

  • 路徑長度
    經過的邊的個數即路徑長度,如經過了3條邊那麼路徑長度為3

  • 帶權路徑長度
    一個節點的帶權路徑長度等於該節點權重乘以根節點到這個節點的路徑長度乘機,在哈夫曼編碼中我們將每個編碼放在了葉子節點,所以,要計算所有葉子節點的帶權路徑長度。想一下為什麼都要放在葉子節點?

什麼是哈夫曼樹

       哈夫曼樹也屬於二叉樹的一種,只不過其具有一些特定性質,俗話說擁有了特殊性其價值也就出來了,二叉樹到哈夫曼樹可以說是普通到特殊的演化過程,它是不斷優化整棵樹的帶權路徑長度,當wpl達到最優的時候我們就稱為最優帶權路徑二叉樹,因為其實哈夫曼發明的也就有人稱為哈夫曼樹,如下圖所示樹的帶權路徑構成的樹有很多種,其中帶權路徑長度最小的那個即哈夫曼樹。
在這裡插入圖片描述

構建

       構建過程也簡單,核心思想是貪婪演算法,貪婪的是什麼要搞清楚,即在每一步解決子問題的時候都是找當前剩餘問題中權重之和最小的兩個子樹結合,如下圖:
在這裡插入圖片描述

結合上圖歸納如下:
1.確定n個帶有權重的節點,每個節點權重為wi,構成了一個只有單個根節點的森林
2.在森林中找到兩個權重最小的組成一個二叉樹,並且從森林中刪除這兩節點
3.重複步驟二,知道節點組成一個二叉樹為止

應用

通訊編碼

       作為通訊編碼的兩個條件
1.編碼傳送方和解碼接收方內容必須一樣,不能有歧義
2.編碼長度儘可能短
常用的編碼方式有兩種

  • 等長編碼
           設字符集為A、B、C、D四個字元,二進位制編碼分別為00、01、10、11,假設有電文AABCCCCD要為其編碼為0000011010101011,解碼時兩位擷取一次即可,此種方法看似比較簡單,缺點是編碼長度不是最小,發文內容越長佔用頻寬越大,電報以前都是按文字收費還是挺貴的,為了節省成本還有不等長編碼
  • 不等長編碼
           不等長編碼的思想是按傳送電文儘可能短,如果想在等長編碼基礎上改進思考一下可以想到讓頻率出現太高的字元短編碼即可,如上面可以讓C編碼變為一位0或1,不過可能出現問題因為在解碼時並不知道哪個字元是幾位,很容易造成歧義解碼錯誤的情況發生,這與編碼的唯一性條件不符合,既要減少編碼的長度又要保持編碼唯一性,唯一性的另一種理解就是任何一個字元的編碼不能是另一個編碼的字首,可以叫做非字首編碼,其實叫字首編碼。
           樹結構哈夫曼樹正好可以解決類似問題,讓每個字元為葉子結點,從根節點到每個葉子節點都是隻有一條路徑,滿足編碼唯一性要求,同時葉子節點還有權重可以表示每個葉子節點的重要程度,這裡可以是每個字元出現的次數或概率。
    例題:
           假設一個文字檔案TFile中只包含7個字元{A,B,C,D,E,F,G},這7個字元在文字中出現的次數為{5,24,7,17,34,5,13}
    利用哈夫曼樹可以為檔案TFile構造出符合字首編碼要求的不等長編碼
    具體做法:
  1. 將上面7個字元都作為葉子結點,每個字元出現次數作為該葉子結點的權值
  2. 規定哈夫曼樹中所有左分支表示字元0,所有右分支表示字元1,將依次從根結點到每個葉子結點所經過的分支的二進位制位的序列作為該
    結點對應的字元編碼
  3. 由於從根結點到任何一個葉子結點都不可能經過其他葉子,這種編碼一定是字首編碼,哈夫曼樹的帶權路徑長度正好是檔案TFile編碼
    的總長度

例子

程式碼實現

       下面是簡單實現的哈夫曼編碼以及解碼過程,利用有序的優先佇列實現,優先佇列不用自己實現,java.uitl中有,當然也可以自己實現,原理是利用了最大堆最小堆,程式碼如下:

package sort.huffman;

public class BinaryTreeNode {

    /**
     * 節點的哈夫曼編碼
     */
    public String code = "";
    /**
     * 節點的資料
     */
    public String data = "";
    /**
     * 節點權值
     */
    public int count;
    public BinaryTreeNode lChild;
    public BinaryTreeNode rChild;

    public BinaryTreeNode(String data, int count) {
        this.data = data;
        this.count = count;
    }

    public BinaryTreeNode(int count, BinaryTreeNode lChild, BinaryTreeNode rChild) {
        this.count = count;
        this.lChild = lChild;
        this.rChild = rChild;
    }
    
}

下面為哈夫曼實現過程

package sort.huffman;

import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.PriorityQueue;

public class HuffmanTree {

    /**
     * 壓縮檔案 可以是檔案路徑或網路檔案
     */
    private String sourceFile;
    /**
     * 二叉樹的根節點
     */
    private BinaryTreeNode root;
    /**
     * 儲存不同字元以及權重
     */
    private LinkedList<CharData> charList;
    /**
     * 優先佇列儲存huffman樹節點
     */
    private PriorityQueue<BinaryTreeNode> huffmanNodeQueue;

    private class CharData {
        int num = 1;
        char c;
        public CharData(char ch) {
            c = ch;
        }
    }

    /**
     * 構建哈夫曼樹
     * @param sourceFile
     */
    public void creatHfmTree(String sourceFile) {
        try {
            this.sourceFile = sourceFile;

            charList = new LinkedList<CharData>();

            getCharNum(sourceFile);

            huffmanNodeQueue =new PriorityQueue<BinaryTreeNode>(charList.size(),
                    new Comparator<BinaryTreeNode>() {
                        @Override
                        public int compare(BinaryTreeNode o1, BinaryTreeNode o2) {
                            return o1.count - o2.count;
                        }
                    });

            creatNodes();

            creatTree();
            root = huffmanNodeQueue.peek();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 統計出現的字元及其頻率
     * @param sourceFile
     */
    private void getCharNum(String sourceFile) {
        boolean flag;
        for (int i = 0; i < sourceFile.length(); i++) {
            char ch = sourceFile.charAt(i); // 從給定的字串中取出字元
            flag = true;

            for (int j = 0; j < charList.size(); j++) {
                CharData data = charList.get(j);

                if (ch == data.c) {
                    data.num++;
                    flag = false;
                    break;
                }
            }

            if (flag) {
                charList.add(new CharData(ch));
            }

        }

    }

    /**
     * 將出現的字元建立成單個的結點物件
     */
    private void creatNodes() {
        for (int i = 0; i < charList.size(); i++) {
            String data = charList.get(i).c + "";
            int count = charList.get(i).num;
            BinaryTreeNode node = new BinaryTreeNode(data, count); // 建立節點物件
            huffmanNodeQueue.add(node);
        }

    }

    /**
     * 構建哈夫曼樹
     */
    private void creatTree() {

        while (huffmanNodeQueue.size() > 1) {
            BinaryTreeNode left = huffmanNodeQueue.poll();
            BinaryTreeNode right = huffmanNodeQueue.poll();

            left.code = "0";
            right.code = "1";
            setCode(left);
            setCode(right);

            int parentWeight = left.count + right.count;
            BinaryTreeNode parent = new BinaryTreeNode(parentWeight, left, right);

            huffmanNodeQueue.add(parent);
        }
    }

    /**
     * 設定結點的哈夫曼編碼
     *
     * @param root
     */
    private void setCode(BinaryTreeNode 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);
        }
    }

    /**
     * 遍歷
     *
     * @param node 節點
     */
    private void output(BinaryTreeNode node) {

        if (node.lChild == null && node.rChild == null) {
            System.out.println(node.data + ": " + node.code);
        }
        if (node.lChild != null) {
            output(node.lChild);
        }
        if (node.rChild != null) {
            output(node.rChild);
        }
    }

    /**
     * 輸出結果字元的哈夫曼編碼
     */
    public void output() {
        output(root);
    }


    private String hfmCodeStr = "";// 哈夫曼編碼連線成的字串

    /**
     * 編碼
     *
     * @param str
     * @return
     */
    public String toHufmCode(String str) {

        for (int i = 0; i < str.length(); i++) {
            String c = str.charAt(i) + "";
            search(root, c);
        }

        return hfmCodeStr;
    }

    /**
     * @param root 哈夫曼樹根節點
     * @param c    需要生成編碼的字元
     */
    private void search(BinaryTreeNode 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);
        }
    }

    // 儲存解碼的字串
    String result = "";
    boolean target = false; // 解碼標記

    /**
     * 解碼
     *
     * @param codeStr
     * @return
     */
    public String CodeToString(String codeStr) {

        int start = 0;
        int end = 1;

        while (end <= codeStr.length()) {
            target = false;
            String s = codeStr.substring(start, end);
            matchCode(root, s); // 解碼
            // 每解碼一個字元,start向後移
            if (target) {
                start = end;
            }
            end++;
        }

        return result;
    }

    /**
     * 匹配字元哈夫曼編碼,找到對應的字元
     * @param root 哈夫曼樹根節點
     * @param code 需要解碼的二進位制字串
     */
    private void matchCode(BinaryTreeNode 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);
        }

    }


    public static void main(String[] args) {

        HuffmanTree huffman = new HuffmanTree();// 建立哈弗曼物件

        String data = "aaaaaaaabbbbccddd";
        huffman.creatHfmTree(data);// 構造樹

        huffman.output(); // 顯示字元的哈夫曼編碼

        // 將目標字串利用生成好的哈夫曼編碼生成對應的二進位制編碼
        String hufmCode = huffman.toHufmCode(data);
        System.out.println("編碼:" + hufmCode);

        // 將上述二進位制編碼再翻譯成字串
        System.out.println("解碼:" + huffman.CodeToString(hufmCode));
    }

}

       上面是否還記得我們寫的一個例子 a(8) b(4) c(2) d(3) ,我們對這個字元編碼看看結果和我們畫的圖是一致的。
在這裡插入圖片描述

貪心

       哈夫曼編碼是一種資料壓縮技術,也可以理解為應用了貪心思想,再想一下利用貪心的圖問題,如prim演算法加權連通圖最小生成樹,它主要解決問題是以一種代價最低的方式連線n個點,可應用於通訊網路、計算機網路等;另外還有一個Kruskal演算法也是用來生成最小樹,但是它的思路是不一樣的,它是從邊的維度出發,prim是從頂點的維度出發,他們的相同點是都應用了貪心思想,可見在演算法領域裡面貪心演算法的思想還是非常有用的。

問題

除了哈夫曼編碼還有其它編碼問題嗎?
huffman在word2vec cbow中的應用?

參考

https://www.cnblogs.com/tomhawk/p/7471133.html
https://www.cnblogs.com/13224ACMer/p/4706174.html