演算法作業三-哈夫曼編碼
實驗三 哈夫曼編碼
問題描述與實驗目的:
給定n個字母(或字)在文件中出現的頻率序列X=<x1,x2,…,xn>,求出這n個字母的Huffman編碼。為方便起見,以下將頻率用字母出現的次數(或稱權值)w1,w2,…,wn代替。
輸入
輸入檔案中的開始行上有一個整數T,(0<T<=20),表示有T組測試資料。
接下來是T行測試資料的描述,每組測試資料有2行。測試資料的第1行上是一個正整數n,(n<50),表示序列的長度。第2行是n個字母出現的權值序列w1,w2,…,wn,它們均為正整數,相鄰的兩個整數之間用空格隔開。
輸入直到檔案結束。
輸出
對輸入中的每組有n個權值的資料,應輸出n+1行:先在一行上輸出“Case #”,其中“#”是測試資料的組號(從1開始);接下來輸出n行,其第1行到第n行上依次輸出第i個字母出現的次數和相應的Huffman編碼,格式如下:
wi Huffman編碼。
每組測試資料對應的輸出最後結束時加一個空行,以便區分。
為保證Huffman編碼的唯一性,在構造Huffman樹的過程中,我們約定:
1.左兒子標記為0,右兒子標記為1;
2.左兒子的權值>=右兒子的權值;
3.相同權值w的兩個字母x、y,先輸入權值的字母x的Huffman編碼長度不超過後輸入權值的字母y的Huffman編碼長度。
4.合併兩個節點後新的權值應從右到左搜尋、插入到相應的位置。
例如:輸入權值序列8 9 3 4 1 2,其Huffman編碼求解過程如下,參考圖A-J:
注意,如圖C中權值1、2對應的節點合併後得權值為3的新節點,它插入到權值為3的原有節點的右邊。
輸入樣例
2
6
9 8 3 4 1 2
8
60 20 5 5 3 3 3 1
輸出
Case 1
9 00
8 01
3 100
4 11
1 1011
2 1010
Case 2
60 0
20 10
5 1101
5 1110
3 11000
3 11001
3 11110
1 11111
分析:
哈夫曼編碼本質是個貪心的演算法。將每個字元視作一個帶權結點(子樹)。
每次優先選擇權值最小的兩個子樹,將二者合併,權值相加,權值小的作為右子結點(1),權值大的作左子結點(0),形成一個新的子樹。再將這棵子樹放回優先佇列中,重複以上的操作。
本題要輸出每個字元對應的哈夫曼編碼,資料n<50較小,所以不考慮用資料結構建樹,用直接儲存路徑的方法實現。自定義一個結構體,內部記錄子樹的權值和所有葉子結點。
用優先佇列儲存所有子樹,因為堆排序是一種不穩定排序,可能導致相同權值的子樹順序顛倒,所以過載比較運算子,使用兩個關鍵字排序。
兩個子樹合併的時候,將左子樹記錄的葉子結點編碼全部加上''0',右子樹記錄的結點編碼全部加'1'。當佇列中結點數為1時,演算法結束。
最後根據讀入的順序輸出0-1字串即可。
執行結果:
OJ測評結果:
原始碼:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 105;
string res[55];
struct node{
int val,id;
vector<int> dp;
bool operator < (const node & rhs) const{ //雙關鍵字排序
if(val==rhs.val) return id < rhs.id;
return val > rhs.val;
}
}vz[maxn];
int main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
int T,cas=1; scanf("%d",&T);
while(T--){
int n;
scanf("%d",&n);
for(int i=0;i<=n;++i) res[i].clear();
priority_queue<node> Q;
for(int i=1,w;i<=n;++i){
node tmp;
scanf("%d",&w);
vz[i].val = w;
tmp.val = w;
tmp.id = i;
tmp.dp.push_back(i);
Q.push(tmp); //初始將每個結點視作子樹
}
int cnt = n+1;
while(!Q.empty()){
node x = Q.top(); Q.pop();
if(Q.empty()) break;
node y = Q.top(); Q.pop(); //取出權值最小的兩個子樹
node t;
t.val = x.val + y.val; //合併權值
t.id = cnt++;
for(int i=0,sz = x.dp.size() ;i < sz ;++i){ //左子樹
int id = x.dp[i];
res[id].push_back('1');
t.dp.push_back(id); //合併葉子結點
}
for(int i=0,sz = y.dp.size();i < sz; ++i){ //右子樹
int id = y.dp[i];
res[id].push_back('0');
t.dp.push_back(id);
}
Q.push(t);
}
printf("Case %d\n",cas++);
for(int i=1;i<=n;++i){
reverse(res[i].begin(),res[i].end());
cout<<vz[i].val<<" "<<res[i]<<endl;
}
puts("");
}
return 0;
}