哈夫曼编码(1)
注意
需要先看完这一视频哈夫曼树和哈夫曼编码, 看完秒懂!_哔哩哔哩_bilibili
背景
给一段文本编码,要求:解码无歧义;最小化总编码长度
核心
贪心+优先队列
性质
- 所有待编码的字符都放在哈夫曼树的叶子上
- 出现频率低的字符先编码,出现频率高的字符后编码
- 出现频率低的字符,所处层次越深(编码较长)
- 出现频率高的字符,所处层次越低(编码较短)
- \(文本总编码长度=∑length(i)*w[i]\)(length(i):字符i的编码长度,w[i]:字符i出现的频率)
- k叉哈夫曼树,要求最长编码尽可能短(追求平衡),需要补虚拟结点
练习题
题目:
给定n个字符及其出现的频率,构建哈夫曼树(左0右1),要求:
- 输出哈夫曼树的层序遍历结果(输出每个节点的权值)
- 输出每个字符的哈夫曼编码(按照输入顺序输出)
输入格式:
一个数n,表示有n个字符
接下来n行,每行包括一个字符及其出现的频率
输出格式:
- 层序遍历结果
- 每个字符的哈夫曼编码
输入样例:
4
a 5
b 9
c 12
d 13
输出样例:
39 14 25 5 9 12 13
a:00
b:01
c:10
d:11
样例解释:

代码结构
结点存储:
//二叉
struct node{int wi,idx;};
//k叉
struct node{int wi,hi,idx;};
wi:权重
idx:节点编号
优先队列:
生存期:优先队列在哈夫曼树构建完后,就几乎消失
struct cmp{
bool operator()(const node& a,const node& b) const {
return a.wi>b.wi;
}
};
priority_queue<node,vector<node>,cmp> pq;
小根堆:权值小的优先
其余存储结构:
生存期:长期存在,利于最终输出编码/总长度
//权值 左孩子 右孩子
int w[N],lch[N],rch[N];
//记录字符对应的编码
map<char,string> codemap;
输入:
cin>>n;
for(int i=1;i<=n;++i){
cin>>ch[i]>>w[i];
pq.push({w[i],i});
lch[i]=0; rch[i]=0;
}
补虚拟节点:
//只针对k叉
need=(k-1-(n-1)%(k-1))%(k-1);
cnt=n;
for(int i=1;i<=need;++i){
w[++cnt]=0;
ch[cnt]='\0';
h[cnt]=0;
pq.push({w[cnt],0,cnt});
}
构建哈夫曼树:
//二叉
cnt=n;
while(pq.size()>1){
lft=pq.top(); pq.pop();
rgt=pq.top(); pq.pop();
cnt++;
w[cnt]=lft.wi+rgt.wi; lch[cnt]=lft.idx; rch[cnt]=rgt.idx;
pq.push({w[cnt],cnt});
}
//k叉
while(pq.size()>1){
w[++cnt]=0;
for(int i=1;i<=k;++i){
auto head=pq.top(); pq.pop();
w[cnt]+=head.wi;
h[cnt]=max(h[cnt],head.hi);
sons[cnt].push_back(head.idx);
}
h[cnt]++;
pq.push({w[cnt],h[cnt],cnt});
}
root=pq.top();
层序遍历BFS:
核心:从当前结点扩展到子节点
注意:在取出head的时候输出w[head]
//二叉
void levelOrder(int r)
{
int head,lchild,rchild;
queue<int> q;
q.push(r);
while(!q.empty()){
head=q.front(); q.pop();
cout<<w[head]<<" ";
lchild=lch[head]; rchild=rch[head];
if(lchild){//左孩子
q.push(lchild);
}
if(rchild){//右孩子
q.push(rchild);
}
}
cout<<"\n";
}
//k叉
void levelOrder(int r)//层序遍历BFS
{
queue<int> q;
q.push(r);
while(!q.empty()){
auto head=q.front(); q.pop();
cout<<w[head]<<" ";
for(auto child:sons[head]){//各个孩子
q.push(child);
}
}
cout<<"\n";
}
编码获取DFS:
从根节点开始DFS,直到遇到树叶(结点编号<=n)
注意:只有一个结点时,path=“”,需要手动改为path="0"
//二叉
void getCode(int v,string path)
{
if(v<=n){//原始字符 叶子结点
if(path.size()==0) path="0";
codemap[ch[v]]=path;
return ;
}
if(lch[v]) getCode(lch[v],path+"0");//左孩子
if(rch[v]) getCode(rch[v],path+"1");//右孩子
}
//k叉
void getCode(int v,string path)
{
int i=0;
char tmp;
if(v<=n){
if(path.size()==0) path="0";
codemap[ch[v]]=path;
return ;
}
for(auto child:sons[v]){//各个孩子
tmp='0'+i;
getCode(child,path+tmp);
i++;
}
}
调用与输出:
levelOrder(root.idx);
getCode(root.idx,"");
for(int i=1;i<=n;++i){
cout<<ch[i]<<":"<<codemap[ch[i]]<<"\n";
}

浙公网安备 33010602011771号