哈夫曼树是一种非常有用的数据结构,通常用于数据压缩。以下是使用C++构造哈夫曼树的步骤:
-
定义节点:哈夫曼树的节点包含三个属性,
left
,right
和weight
.left
和right
指向左孩子和右孩子,weight
存储节点的权重。
struct Node {
int weight;
Node* left;
Node* right;
};
-
创建节点:为每个输入的权重创建一个新的节点。
Node* createNode(int weight) {
Node* newNode = new Node();
newNode->weight = weight;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
-
构建优先队列:使用
weight
作为优先队列的优先级。这样,权重最小的节点将总是在队列的前面。
struct Compare {
bool operator()(Node* l, Node* r) {
return l->weight > r->weight;
}
};
std::priority_queue<Node*, std::vector<Node*>, Compare> minHeap;
-
构建哈夫曼树:将所有节点添加到优先队列中。然后,从队列中取出两个权重最小的节点(需要进行排序),创建一个新的节点作为这两个节点的父节点,这个新节点的权重是这两个节点的权重之和。然后将这两个节点从队列中删除,将新的父节点添加到队列中。重复这个过程,直到队列中只剩下一个节点,这个节点就是最终的哈夫曼树的根节点。
while (minHeap.size() > 1) {
Node* left = minHeap.top();
minHeap.pop();
Node* right = minHeap.top();
minHeap.pop();
Node* parent = createNode(left->weight + right->weight);
parent->left = left;
parent->right = right;
minHeap.push(parent);
}
- 从哈夫曼树中构造编码表:从根节点开始,向左孩子走就向编码表中添加
0
,向右孩子就向编码表中添加1
。这样,每个节点都会得到一个唯一的二进制编码。同时,记录每个节点到其父节点的距离。 - 使用编码表对数据进行编码:对于数据中的每一个字符,从哈夫曼树中找到相应的节点,然后使用从根节点到该节点的路径作为该字符的编码。
例题:基于哈夫曼树的数据压缩算法
任务描述
输入一串字符串,根据给定的字符串中字符出现的频率建立相应哈夫曼树,构造哈夫曼编码表,在此基础上可以对待压缩文件进行压缩(即编码),同时可以对压缩后的二进制编码文件进行解压(即译码)。
编程要求
输入
多组数据,每组数据一行,为一个字符串(只考虑26个小写字母即可)。当输入字符串为“0”时,输入结束。
输出
每组数据输出2n+4行(n为输入串中字符类别的个数)。第一行为统计出来的字符出现频率(只输出存在的字符,格式为:字符:频度),每两组字符之间用一个空格分隔,字符按照ASCII码从小到大的顺序排列。第二行至第2n行为哈夫曼树的存储结构的终态(形如教材139页表5.2(b),一行当中的数据用空格分隔)。第2n+2行为每个字符的哈夫曼编码(只输出存在的字符,格式为:字符:编码),每两组字符之间用一个空格分隔,字符按照ASCII码从小到大的顺序排列。第2n+3行为编码后的字符串,第2n+4行为解码后的字符串(与输入的字符串相同)。
#include<iostream>
#include<string.h>
#define MAXSIZE 100
using namespace std;
typedef struct
{//哈夫曼树结点的形式
int weight; //结点的权值
int parent,lchild,rchild; //结点的双亲、左孩子、右孩子的下标
}HTNode,*HuffmanTree; //动态分配数组存储哈夫曼树
typedef char **HuffmanCode; //定义编码表类型
int Search(char a[],char ch)
{//查找数组中字符ch所在的位置,返回数组下标,否则返回-1
for(int i=0;a[i]!='\0';i++)
{
if(a[i]==ch) return i;
}
return -1;
}
void Sort(char a[],int b[],int len)
{//按ASCII码冒泡排序
/**************begin************/
int i ,j,t2;
char t1;
for(i = 0; i < len - 1; i++){
for(j = 0; j < len - 1 - i; j++){
if(a[j] > a[j+1]){
t1=a[j];
a[j]=a[j+1];
a[j+1]=t1;
t2=b[j];
b[j]=b[j+1];
b[j+1]=t2;
}
}
}
/**************end************/
}
void Select_min(HuffmanTree HT,int n,int &s1,int &s2)
{// 在HT[k](1≤k≤i-1)中选择两个其双亲域为0且权值最小的结点,并返回它们在HT中的序号s1和s2
/**************begin************/
int min1=MAXSIZE,min2=MAXSIZE,i,min1_j,min2_j;
for(i=1;i<=n;i++)
{
if(HT[i].parent==0) //选择双亲域为0的结点
{
if(HT[i].weight<min1)
{
min1=HT[i].weight; //更新min1为当前最小值
min1_j=i; //记录相应下标i
}
}
}
s1=min1_j; //记录它在HT中的序号s1
for(i=1;i<=n;i++)
{
if(HT[i].parent==0&&i!=min1_j) //选择双亲域为0且不是刚才已选择的结点
{
if(HT[i].weight<min2)
{
min2=HT[i].weight;
min2_j=i;
}
}
}
s2=min2_j; //记录它在HT中的序号s2
/**************end************/
}
int m;
void CreateHuffmanTree(HuffmanTree &HT,int n,int b[])
{//构造哈夫曼树HT
/**************begin************/
if(n<=1) return;
int s1,s2;
m=2*n-1; //共有2n-1个结点
HT=new HTNode[m+1]; //0号单元未用,所以需要动态分配m+1单元,HT[m]表示根结点
int i;
for(i=1;i<=m;i++) //将1~m号单元中的双亲、左孩子、右孩子的下标都初始化为0
{
HT[i].parent=0;
HT[i].lchild=0;
HT[i].rchild=0;
}
for(i=1;i<=n;i++) //读入前n个单元中叶子结点的权值
HT[i].weight=b[i-1];
/*-------------------初始化工作结束,下面开始创建哈夫曼树------------------*/
for(i=n+1;i<=m;i++)
{//通过n-1次的选择、删除和合并来创建哈夫曼树
Select_min(HT,i-1,s1,s2);
// 在HT[k](1≤k≤i-1)中选择两个其双亲域为0且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent=i;
HT[s2].parent=i;
//得到新结点i,从森林中删除s1,s2,将s1和s2的双亲域有0改为1
HT[i].lchild=s1; //s1,s2分别作为i的左右孩子
HT[i].rchild=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight; //i的权值为左右孩子权值之和
}//for
/**************end************/
}
void CreateHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n)
{//从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
/**************begin************/
HC=new char*[n+1]; //分配存储n个字符编码的编码表空间
char* cd;
int i,c,f,start;
cd=new char[n]; //分配临时存放每个字符编码的动态分配空间
cd[n-1]='\0'; //编码结束符
for(i=1;i<=n;i++) //逐个字符求哈夫曼编码
{
start=n-1; //start开始时指向最后,即编码结束符位置
c=i;
f=HT[i].parent; //f指向结点c的双亲结点
while(f!=0) //从叶子结点开始向上回溯,直到根结点
{
--start;
if(HT[f].lchild==i) //结点c是f的左孩子,则生成代码0
cd[start]='0';
else //结点c是f的右孩子,则生成代码1
cd[start]='1';
c=f;
f=HT[c].parent; //继续向上回溯
} //求出第i个字符的编码
HC[i]=new char[n-start]; //为第i个字符编码分配空间
strcpy(HC[i],&cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}//for
delete cd; //释放临时空间
/**************end************/
}
void CharFrequency(char ch[],char a[],int b[],int &j)
{//统计词频
/**************begin************/
int i,pos;
for(i=0;ch[i]!='\0';i++)
{
pos=Search(a,ch[i]); //在数组a中查找字符ch[i]的位置
if(pos==-1) //如果在a中没有找到
{
a[j]=ch[i]; //将字符ch[i]加入数组a中
a[j+1]='\0'; //数组a的元素末位置为'\0'
b[j]++; //词频数组元素b[j]加1
j++; //后移指针j
}
else
{
b[pos]++; //如果在a中找到了字符ch[i]的位置,对应词频元素数值加1
}
}
/**************end************/
}
void PrintHT(HuffmanTree HT)
{//输出哈夫曼树的存储结构的终态
/**************begin************/
for(int i=1;i<=m;i++)
{
cout<<i<<" "<<HT[i].weight<<" "<<HT[i].parent<<" "<<HT[i].lchild<<" "<<HT[i].rchild<<endl;
}
/**************end************/
}
void PrintHC(HuffmanCode HC,char a[],int j)
{//输出每个字符的哈夫曼编码
/**************begin************/
for(int i=1;i<=j;i++)
{
if(i!=j)
cout<<a[i-1]<<":"<<HC[i]<<" ";
else
cout<<a[i-1]<<":"<<HC[i]<<endl;
}
/**************end************/
}
int main()
{
char ch[MAXSIZE];
int i,j;
while(cin>>ch)
{
if(ch[0]=='0') break;
HuffmanTree HT;
char a[MAXSIZE]={'\0'};
int b[MAXSIZE]={0};
j=0; //j统计不同字符的数量
CharFrequency(ch,a,b,j); //统计词频
Sort(a,b,j); //按ASCII码冒泡排序
for(i=0;a[i]!='\0';i++) //输出统计出来的字符和出现频率
{
if(a[i+1]!='\0')
cout<<a[i]<<":"<<b[i]<<" ";
else
cout<<a[i]<<":"<<b[i]<<endl;
}
//构造哈夫曼树
CreateHuffmanTree(HT,i,b); //构造哈夫曼树HT
PrintHT(HT); //输出哈夫曼树的存储结构的终态
//哈夫曼编码
HuffmanCode HC; //编码表HC
CreateHuffmanCode(HT,HC,j);
PrintHC(HC,a,j); //输出每个字符的哈夫曼编码
int k;
for(i=0;ch[i]!='\0';i++) //输出编码后的字符串
{
for(k=0;k<j;k++)
{
if(ch[i]==a[k])
cout<<HC[k+1];
}
}
cout<<endl;
cout<<ch<<endl;//输出解码后的字符串(与输入的字符串相同)
}
return 0;
}