1. 程式人生 > >【貪心演算法】Huffman編碼

【貪心演算法】Huffman編碼

問題描述

有一組字符集{c1, c2, …, cn},在使用這組字符集的過程中,通過統計發現每個字元都有其相應的出現頻率,假設對應的頻率為{f1, f2, …, fn}。現在需要對這些字元進行二進位制編碼,我們希望的編碼結果如下:每個字元都有其獨一無二的編碼;編碼長度是變長的,頻率大的字元使用更少的二進位制位進行編碼,頻率小的字元則使用比較多的二進位制位進行編碼,使得最終的平均編碼長度達到最短;每個字元的編碼都有特定的字首,一個字元的編碼不可能會是另一個字元的字首,這樣我們可以在讀取編碼時,當讀取的二進位制位可以對應一個字元時,就讀取出該字元。舉個例子,假如我們有字符集{‘a’, ‘b’, ‘c’},字元’a’的編碼為001,字元’b’的編碼為010,那麼此時c的編碼不能為00或者01,這樣我們才能識別’a’和’c’或者’b’和’c’。

演算法描述

上述問題可以使用Huffman編碼來解決,Huffman編碼實際上是一個貪心演算法。在這個演算法中,使用二叉樹來表示字首碼,每個字元都是樹的葉子結點,非葉子結點則不代表任何字元。將每個字元構造成結點形成結點集S,每次都從結點集S中選出頻率最低的兩個結點x和y作為子節點進行建樹,為這兩個子結點構造一個父節點,父節點不儲存任何字元,父節點的頻率為兩個子節點頻率之和,將兩個子節點從S中移走,將父節點加入S中。不斷迭代下去,直到S只剩一個結點時,這個結點就是樹的根節點。這樣我們就得到了一棵Huffman樹,整個過程就是一個自底向上的建樹過程。由於從根節點到每個葉子節點有且僅有一條路徑,所以,每個葉子的路徑都是不一樣的,唯一的。我們把從根節點到葉子節點的路徑記錄下來,便可作為葉子節點上字元的編碼。初始化編碼為空,從根節點開始,往左走則編碼加0,往右走則編碼加1,具體展示圖如下所示:

哈弗曼樹

建樹過程的虛擬碼如下:

給定字符集C={c1,c2,...,cn},每個字元ci都有相應的頻率fi
根據字符集構建結點集S={s1,s2,...,sn},每個結點si儲存有字元ci和頻率fi的資訊
while |S| != 1 do
  取出S中頻率最小的兩個結點x和y;
  構造父節點z;
  z.f = x.f + y.f;
  z.c = undefined;
  z.left = x;
  z.right = y;
  將x和y從S中移走,將z加入S;
endWhile
此時S[0]就是根節點,返回根節點

最後整個Huffman編碼過程的C++實現如下(建樹+編碼):

#include <iostream>
#include <vector> #include <string> #include <map> using namespace std; /* Huffman樹的節點 */ struct Node { Node() {} Node(int frequency, char ch, Node* left, Node* right) { this->frequency = frequency; this->ch = ch; this->left = left; this->right = right; } int frequency; char ch; Node* left; Node* right; }; class HuffmanCode { public: HuffmanCode() {} ~HuffmanCode() { if (nvec.size() > 0) clear(nvec[0]); } /* 建樹 */ void buildTree(const char* ch, const int* fq, const int& size) { for (int i = 0; i < size; ++i) { Node* node = new Node(fq[i], ch[i], NULL, NULL); nvec.push_back(node); } while (nvec.size() != 1) { Node* x = getMinNodeAndRemoveIt(); Node* y = getMinNodeAndRemoveIt(); Node* z = new Node(x->frequency + y->frequency, '\0', x, y); nvec.push_back(z); } } /* 編碼 */ void buildCode() { buildCodeByDFS(nvec[0], ""); } /* 獲取特定字元的編碼 */ string getCode(char ch) { return code[ch]; } private: /* 清空Huffman樹,釋放資源 */ void clear(Node* root) { if (root != NULL) { clear(root->left); clear(root->right); delete root; } } /* 獲取結點集中頻率最小的結點並將其移出結點集 */ Node* getMinNodeAndRemoveIt() { int min = 0; for (int i = 1; i < nvec.size(); ++i) { if (nvec[i]->frequency < nvec[min]->frequency) { min = i; } } Node* tmp = nvec[nvec.size() - 1]; nvec[nvec.size() - 1] = nvec[min]; nvec[min] = tmp; tmp = nvec[nvec.size() - 1]; nvec.pop_back(); return tmp; } /* 遍歷Huffman樹進行編碼 */ void buildCodeByDFS(Node* r, string str) { if (r->left == NULL && r->right == NULL) code[r->ch] = str; if (r->left != NULL) buildCodeByDFS(r->left, str + "0"); if (r->right != NULL) buildCodeByDFS(r->right, str + "1"); } vector<Node*> nvec; // 結點集 map<char, string> code; // 字元編碼 }; int main() { char ch[100]; int fq[100], size; cin >> size; if (size <= 0 || size > 100) { cout << "字符集大小不合適" << endl; return -1; } for (int i = 0; i < size; ++i) { cin >> ch[i] >> fq[i]; } HuffmanCode hfmc; hfmc.buildTree(ch, fq, size); hfmc.buildCode(); string code; for (int i = 0; i < size; ++i) { code = hfmc.getCode(ch[i]); cout << ch[i] << ": " << code << endl; } return 0; }