數據結構 - 赫夫曼樹詳解與模擬壓縮(C++)

赫夫曼(Huffman)樹,由發明它的人物命名,又稱最優樹,是一類帶權路徑最短的二叉樹,主要用於數據壓縮傳輸。node

赫夫曼樹的構造過程相對比較簡單,要理解赫夫曼樹,要先了解赫夫曼編碼。算法

對一組出現頻率不一樣的字符進行01編碼,若是設計等長的編碼方法,不會出現混淆的方法,根據規定長度的編碼進行翻譯,有且只有一個字符與之對應。好比設計兩位編碼的方法,A,B,C,D字符能夠用00-11來表示,接收方只要依次取兩位編碼進行翻譯就能夠得出原數據,但若是原數據只由n個A組成的,那發出的編碼就是2n個0組成,這樣的壓縮效率並非十分理想(兩位編碼只是舉例,實際上表示全部ASCII字符須要255個編碼,則須要使用八位編碼方式,這樣若是原數據字符重複率較高的話,效果天然不理想)。若是設計的編碼分別爲:0,00,1和01,這樣我發送了n個A組成的數據,這樣一來只須要發送n個0,雖然會節省不少的數據傳輸量,但若是我傳送了「0000」,則有多種翻譯方法,能夠翻譯爲「AAAA」,「ABB」,也不是可行的解決方案。若是有一種設計方式,用較短的編碼表示出現頻率較高的字符,用相對較長的編碼表示出現頻率相對較高的字符,而每一個字符的解碼不產生混淆,效果將會十分理想,而赫夫曼編碼就是知足這種要求的編碼方式,這種編碼方式也叫前綴編碼。數據結構

以一顆二叉樹做爲基礎,規定左分支位0,右分支爲1,以每一個出現的字符做爲各葉子節點的權值構造一顆赫夫曼樹。在一顆赫夫曼樹之中,從根節點出發,每通過一個節點(經過左子樹或者右子樹),解碼出來的字符也隨之一步步肯定,由於從一個節點開始,經過左子樹和右子樹的結果是互斥的。併發

如左圖所示,若是一開始從根節點開始,往右子樹探索,則結果不可能會出現AB兩種可能,再接下去往左子樹探索就會獲得C,往右子樹就會獲得D,保證在獲得某一個字符以前的前綴編碼不會是另外一個字符的編碼。回到前一段,若是咱們把權值(出現頻率)較高的節點放在深度較低的葉子節點上,權值較低的節點由於構造二叉樹須要放的相對深一些,則可構造一顆赫夫曼數,具體方法以下:函數

  1)根據給定的n個權值{w1,w2,...,wn}構成n棵二叉樹的集合F = {T1,T2,...,Tn},其中每棵二叉樹Ti中只有一個帶權位wi的根節點,其中左右子樹均爲空。工具

  2)在F中選取兩顆根節點的權值最小的樹做爲左右子樹構造一棵新的二叉樹,且置新的二叉樹的根節點的權值爲其左,右子樹上根節點的權值之和。優化

  3)在F中刪除這兩棵樹,同時將新的二叉樹加入F中ui

  4)重複(2)和(3)的過程,直到F只含一棵樹爲止。這棵樹即是赫夫曼樹。編碼

文字描述可能有點費解,如下用一些示意圖表示,整個過程可表示爲:加密

整個過程下來可以保證權值較小的葉子節點比較先被選擇到,從而保證一套編碼下來權值較大的葉子節點編碼較短(由於深度比較低),而權值較小的葉子節點編碼相對就長一點。由於權值小的字符出現機率低,因此平均下來這種編碼方式可以達到較高的壓縮率。

接下來是算法代碼的實現,通過以上的理論,咱們要完成的任務有:

  1)用一個初始的字符串去構建一棵赫夫曼樹(統計每一個字符的出現次數做爲權值),前提是這個字符串中字符出現的機率必須和數據交換時候字符出現的機率大體匹配。

  2)完成赫夫曼樹的構建(經常使用的字符葉子結點深度較淺,不經常使用的葉子結點深度較深),並求出每一個字符對應的編碼,存在一個Map數據結構中,這個Map暫稱編碼表

  3)A要把一段數據傳輸給B,A對B說,我給你一棵赫夫曼樹,到時候你就用這棵樹對我發給你的內容進行解碼,而後根據編碼表求出原數據的編碼併發給B

  4)B根據收到的編碼以赫夫曼樹爲基礎進行解碼,最後得出原數據內容

  首先是樹節點的數據結構:

class Node{
public:
    Type val;
    int weight;
    Node *parent,*left,*right;
    Node(Type v,int w):val(v),weight(w),parent(NULL),left(NULL),right(NULL){}
    Node(int w):weight(w),parent(NULL),left(NULL),right(NULL){}
};

  val爲初始化字符的每一個字符,weight,權值,即出現次數,left和right分別指向節點的左右子樹,parent指向父節點。

  構建赫夫曼樹的核心代碼爲:

void builTree(){
        int n = nodes.size()*2-1;
        for(int i = nodes.size();i<n;i++){
            vector<int> ret = selMin2(nodes);
            nodes.push_back(new Node(nodes[ret[0]]->weight+nodes[ret[1]]->weight));
            nodes[ret[0]]->parent = nodes.back();
            nodes[ret[1]]->parent = nodes.back();
            nodes.back()->left = nodes[ret[0]];
            nodes.back()->right = nodes[ret[1]];
        }
        root = nodes.back();
    }

  初始字符串通過統計以後存在nodes容器中,定義爲 vector<Node*> nodes ,由於開始構建的時候nodes存儲的全爲葉子節點,因此預先求出整棵赫夫曼樹總的節點數爲 int n = nodes.size()*2-1; (一棵徹底二叉樹的總節點個數爲全部葉子節點個數 x 2 - 1)其中selMin2()函數求出權值最小的且沒有父節點的兩個節點,返回這兩個節點的下表,我本身寫的複雜度爲O(n),沒有特別的優化過程。

  完成赫夫曼樹的構建後,從根節點開始求出每一個葉子節點(字符)的編碼:

void getcode(){
        //encoding all the character recursively
        if(root != NULL)
            Recur(root,"");
    }
    void Recur(Node *node,string c){
        if(!node->left && !node->right){
            //leaf node
            code[node->val] = c;
        }
        else{
            Recur(node->left,c+'0');
            Recur(node->right,c+'1');
        }
    }

  對應的編碼和解碼過程代碼以下:

string encoding(string plain){
        string res;
        for(auto elem : plain)
            res+=code[elem];
        return res;
    }
string decoding(string enci){
        string res;
        if(NULL != root){
            int i= 0;
            Node *n = root;
            while(i<enci.size()){
                if(enci[i] == '0')
                    n = n->left;
                else
                    n = n->right;
                if(n && !n->left && !n->right){
                    res.push_back(n->val);
                    n = root;
                }
                i++;
            }
        }
        return res;
    }

編碼過程當中的code爲unordered_map類型,即爲前文所提到的編碼表。

  一開始用」1111111111222222222333333334444444555555666667777888990」做爲初始化字符串來構建赫夫曼樹,寫兩個工具分別用於編碼和解碼,效果以下圖所示:

   

  此處只是一個模擬過程,因此編碼結果我在代碼中只是用字符串表示,實際上在傳輸過程當中是用bit表示(本來8個字符32位如今只須要22位bit),因此個人代碼省略了A把編碼轉爲bit表示的編碼,而後B根據編碼信息求出01字符串。

  以上爲本人對赫夫曼編碼壓縮(我的認爲還能起到加密做用,此時赫夫曼樹做爲密鑰)的拙見,文中的錯誤和不妥之處歡迎你們指出斧正。

 

 

 

 

  尊重知識產權,轉載引用請通知做者並註明出處!

相關文章
相關標籤/搜索