1. 問題
通常的編碼方法有固定長度編碼和不等長度編碼兩種。這是一個設計最優編碼方案的問題,目的是使總碼長度最短。這個問題利用字符的使用頻率來編碼,是不等長編碼方法,使得經常使用的字符編碼較短,不常使用的字符編碼較長。如果采用等長的編碼方案,假設所有字符的編碼都等長,則表示 n 個不同的字符需要 log n 位。例如,3 個不同的字符 a、b、c,至少需要 2 位二進制數表示,a 為 00,b 為 01,c 為 10。如果每個字符的使用頻率相等,固定長度編碼是空間效率最高的方法。
不等長編碼方法需要解決兩個關鍵問題:
(1)編碼盡可能短
我們可以讓使用頻率高的字符編碼較短,使用頻率低的編碼較長,這種方法可以提高壓縮率,節省空間,也能提高運算和通信速度。即頻率越高,編碼越短。
(2)不能有二義性
例如,ABCD 四個字符如果編碼如下。
A:0。B:1。C:01。D:10。
那么現在有一列數 0110,該怎樣翻譯呢?是翻譯為 ABBA,ABD,CBA,還是 CD?那么如何消除二義性呢?解決的辦法是:任何一個字符的編碼不能是另一個字符編碼的前綴,即前綴碼特性。1952 年,數學家 D.A.Huffman 提出了根據字符在文件中出現的頻率,用 0、1 的數字串表示各字符的最佳編碼方式,稱為哈夫曼(Huffman)編碼。哈夫曼編碼很好地解決了上述兩個關鍵問題,被廣泛應用於數據壓縮,尤其是遠距離通信和大容量數據存儲方面,常用的JPEG 圖片就是采用哈夫曼編碼壓縮的。
2. 解析
哈夫曼編碼的基本思想是以字符的使用頻率作為權構建一棵哈夫曼樹,然后利用哈夫曼樹對字符進行編碼。構造一棵哈夫曼樹,是將所要編碼的字符作為葉子結點,該字符在文件中的使用頻率作為葉子結點的權值,以自底向上的方式,通過 n−1 次的“合並”運算后構造出的一棵樹,核心思想是權值越大的葉子離根越近。
哈夫曼算法采取的貪心策略是每次從樹的集合中取出沒有雙親且權值最小的兩棵樹作為左右子樹
3. 分析
n + (n + 1) + (n + 2) + ... +(n + (n -2)) = (n -1) *(3n - 2) /2=O(N²)
4. 源碼
/* project name:哈夫曼樹 Time Complexity: O(n²) */ #include <iostream>
using namespace std; //最大字符編碼數組長度
#define MAXCODELEN 100
//最大哈夫曼節點結構體數組個數
#define MAXHAFF 100
//最大哈夫曼編碼結構體數組的個數
#define MAXCODE 100
#define MAXWEIGHT 10000; typedef struct Haffman { //權重
int weight; //字符
char ch; //父節點
int parent; //左兒子節點
int leftChild; //右兒子節點
int rightChild; } HaffmaNode; typedef struct Code { //字符的哈夫曼編碼的存儲
int code[MAXCODELEN]; //從哪個位置開始
int start; } HaffmaCode; HaffmaNode haffman[MAXHAFF]; HaffmaCode code[MAXCODE]; void buildHaffman(int all) { //哈夫曼節點的初始化之前的工作, weight為0,parent,leftChile,rightChile都為-1
for (int i = 0; i < all * 2 - 1; ++i) { haffman[i].weight = 0; haffman[i].parent = -1; haffman[i].leftChild = -1; haffman[i].rightChild = -1; } std::cout << "請輸入需要哈夫曼編碼的字符和權重大小" << std::endl; for (int i = 0; i < all; i++) { std::cout << "請分別輸入第個" << i << "哈夫曼字符和權重" << std::endl; std::cin >> haffman[i].ch; std::cin >> haffman[i].weight; } //每次找出最小的權重的節點,生成新的節點,需要all - 1 次合並
int x1, x2, w1, w2; for (int i = 0; i < all - 1; ++i) { x1 = x2 = -1; w1 = w2 = MAXWEIGHT; //注意這里每次是all + i次里面便利
for (int j = 0; j < all + i; ++j) { //得到最小權重的節點
if (haffman[j].parent == -1 && haffman[j].weight < w1) { //如果每次最小的更新了,那么需要把上次最小的給第二個最小的
w2 = w1; x2 = x1; x1 = j; w1 = haffman[j].weight; } //這里用else if而不是if,是因為它們每次只選1個就可以了。
else if (haffman[j].parent == -1 && haffman[j].weight < w2) { x2 = j; w2 = haffman[j].weight; } } //么次找到最小的兩個節點后要記得合並成一個新的節點
haffman[all + i].leftChild = x1; haffman[all + i].rightChild = x2; haffman[all + i].weight = w1 + w2; haffman[x1].parent = all + i; haffman[x2].parent = all + i; std::cout << "x1 is" << x1 << " x1 parent is" << haffman[x1].parent << " x2 is" << x2 << " x2 parent is " << haffman[x2].parent << " new Node is " << all + i << "new weight is"
<< haffman[all + i].weight << std::endl; } } //打印每個字符的哈夫曼編碼
void printCode(int all) { //保存當前葉子節點的字符編碼
HaffmaCode hCode; //當前父節點
int curParent; //下標和葉子節點的編號
int c; //遍歷的總次數
for (int i = 0; i < all; ++i) { hCode.start = all - 1; c = i; curParent = haffman[i].parent; //遍歷的條件是父節點不等於-1
while (curParent != -1) { //我們先拿到父節點,然后判斷左節點是否為當前值,如果是取節點0 //否則取節點1,這里的c會變動,所以不要用i表示,我們用c保存當前變量i
if (haffman[curParent].leftChild == c) { hCode.code[hCode.start] = 0; std::cout << "hCode.code[" << hCode.start << "] = 0"
<< std::endl; } else { hCode.code[hCode.start] = 1; std::cout << "hCode.code[" << hCode.start << "] = 1"
<< std::endl; } hCode.start--; c = curParent; curParent = haffman[c].parent; } //把當前的葉子節點信息保存到編碼結構體里面
for (int j = hCode.start + 1; j < all; ++j) { code[i].code[j] = hCode.code[j]; } code[i].start = hCode.start; } } int main() { std::cout << "請輸入有多少個哈夫曼字符" << std::endl; int all = 0; std::cin >> all; if (all <= 0) { std::cout << "您輸入的個數有誤" << std::endl; return -1; } buildHaffman(all); printCode(all); for (int i = 0; i < all; ++i) { std::cout << haffman[i].ch << ": Haffman Code is:"; for (int j = code[i].start + 1; j < all; ++j) { std::cout << code[i].code[j]; } std::cout << std::endl; } return 0; }
