本博客仅供学习交流使用,请勿用于作弊以及用于应付作业,珍惜每一个学习的机会
首先,明确要实现的功能
1. 构建哈夫曼树:根据给定的字符及其权重值,构建出对应的哈夫曼树。
2.计算编码长度:对于构建出的哈夫曼树,计算每个字符的编码长度,即根节点到该字符叶子节点的路径长度之和。路径长度定义为从根节点到某个节点的边的数量。
3.输出编码表:根据哈夫曼树,生成字符到对应编码的映射表。将每个字符与其对应的哈夫曼编码打印输出。
4.对字符串“pneumonoultramicroscopicsilicovolcanoconiosis”进行编码:利用步骤3中生成的编码表,对给定的字符串进行编码,将各个字符替换为其对应的哈夫曼编码。
然后分解任务
任务1:哈夫曼树的构建
我做了这些事:
首先,二十六个字母对应二十六个要编码的节点。每个节点包含了左孩子,右孩子,父节点和其权值。
于是我定义了节点的数据类型:
struct HNode//建立一个节点
{
float weight;//表示权重。注意要用float类型
int parent;//父节点的下标
int LChild;//左孩子的下标
int RChild;//右孩子的下标
};
然后,我定义了哈夫曼编码表,用来存储编码长度和编码:
struct CodeTableNode//建立编码表
{
char name;//像ABC这样的名字
int length;
string code;//哈夫曼编码
};
之后,我回想了一下哈夫曼树编码的流程:
1.先创建好各个节点,并且把他们设置成相互独立的,组成集合T
2.在所有节点中,先选两个权值最小的节点作为左右子树
3.将他们构建成一颗新的二叉树,这个新二叉树的根节点的权值=左节点权值+右节点权值。在T中删除这两颗树,并且把新的树加入T中
4.重复2,3步骤
实现:
1.先创建好各个节点,并且把他们设置成相互独立的,组成集合T
我写了个InitializeHuffmanTree()的函数,来对哈夫曼树初始化。
talk is cheep,show the code:
void InitializeHuffmanTree()//初始化哈夫曼树
{
//先将哈夫曼树初始化
float weight[26]={
0.0819,0.0147,0.0383,0.0391,0.1225,
0.0226,0.0171,0.0457,0.0710,0.0041,
0.0014,0.0377,0.0334,0.0706,0.0726,
0.0289,0.0009,0.0685,0.0636,0.0941,
0.0258,0.0109,0.0159,0.0021,0.0158,
0.0008
};//A到Z的权值(数据由老师提供)
for(int i=0;i<26;i++)
{
HTree[i].weight=weight[i];
}
for(int i=0;i<2*26-1;i++)
{
HTree[i].LChild=HTree[i].RChild=HTree[i].parent=-1;
//将父节点,左孩子,右孩子都设置成-1,来表示他们各个节点相互独立。
}
}
数组存储二十六个字母,然后初始化了他们的权值(数据来源是老师给的)。
之后把左孩子,右孩子,父节点的下标都设置为了-1,以表示他们相互独立。
2.在所有节点中,先选两个权值最小的节点作为左右子树
我几经修改后,选了一种较为简单实现的方法(修改过程见“碎碎念”模块)
由于同时选出两个最小的数不是很好选,于是我选择了“曲线救国”:
首先,我开了个visited数组,来标记每个数有没有被选为最小值过。
接着,我通过FindMin(int i)函数选出最小值,并且将visited数组中这个数标为1。
最后,在新数组中再选出最小值,就是“第二小”的了。
代码实现:
bool visited[26*2-1];
int FindMin(int i)//找到最小的数
{
//找到最小值
float min=1000000;
//注意这里一定要用float!(我第一次习惯性的用int,结果出bug了,找了好久才发现罪魁祸首
int pos=-1;
for(int j=0;j<i;j++)
{
if(HTree[j].weight<min&&HTree[j].parent==-1&&visited[j]==0)
{
min=HTree[j].weight;
pos=j;
}
}
visited[pos]=1;
return pos;
}
3.然后将他们构建成一颗新的二叉树,这个新二叉树的根节点的权值=左节点权值+右节点权值。在T中删除这两颗树,并且把新的树加入T中
选出后,构建子树的算法:
//建立哈夫曼树
HTree[Min1].parent=i;
HTree[Min2].parent=i;//这一步就相当于把原先最小的两棵树删除了
HTree[i].weight=HTree[Min1].weight+HTree[Min2].weight;
HTree[i].LChild=Min1;
HTree[i].RChild=Min2;
HTree[i].parent=-1;
4.重复2,3步骤
外层套个for循环就搞定了
构建整颗哈夫曼树完整的代码
void CreatHuffmanTree()//建立哈夫曼树
{
for(int i=26;i<26*2-1;i++)
{
//这里的查找算法我一开始用的是一遍找出最小的两个数。
//但是这样子容易出bug
//于是我就另外开了一个数组,记录某个数是否有被当过最小值。
//然后我先找出最小值,接着把最小值剔除,再在新的数组中找最小值。这就是第二小的值了。
int Min1=FindMin(i);//找到从0到i的最小值。
int Min2=FindMin(i);//找到从0到i第二小的值。
//建立哈夫曼树
HTree[Min1].parent=i;
HTree[Min2].parent=i;//这一步就相当于把原先最小的两棵树删除了
HTree[i].weight=HTree[Min1].weight+HTree[Min2].weight;
HTree[i].LChild=Min1;
HTree[i].RChild=Min2;
HTree[i].parent=-1;
}
}
哈夫曼树的可视化
又于时间不够,我就简单做了个表格。不然,我还可以用Graphviz来对哈夫曼树做可视化。
代码:
void DrawHuffmanTree()//画出哈夫曼树(以编码表的形式)
{
printf("|-------------Huffman Tree--------------|\n");
printf("|---------|--------|------|------|------|\n");
printf("|Character| Weight |LChild|RChild|Parent|\n");
printf("|---------|--------|------|------|------|\n");
for(int i=0;i<26;i++)
{
printf("| %c | %.4f | %02d | %02d | %02d |\n",(char)(65+i),HTree[i].weight,HTree[i].LChild,HTree[i].RChild,HTree[i].parent);
printf("|---------|--------|------|------|------|\n");
}
for(int i=26;i<26*2-1;i++)
{
printf("| %02d | %.4f | %02d | %02d | %02d |\n",i,HTree[i].weight,HTree[i].LChild,HTree[i].RChild,HTree[i].parent);
printf("|---------|--------|------|------|------|\n");
}
}
输出结果:
任务2.计算编码长度
要有编码长度表,就得定义表的数据类型。一个节点包含了:字母,编码长度,编码:
struct CodeTableNode//建立编码表
{
char name;//像ABC这样的名字
int length;
string code;//哈夫曼编码
};
计算编码长度
这边用的是递归函数求解:
void CountStep(int i,int num)//记录步数(用于计算编码长度的)
{
//通过递归来实现记录编码长度
if(HTree[i].LChild==-1)
{
CodeTable[i].length=num;
return;
}
CountStep(HTree[i].LChild,num+1);
CountStep(HTree[i].RChild,num+1);
}
确定编码
这边用的也是递归求解:
void Encode(int i,string code)//编码函数(用于创建哈夫曼编码的)
{
//通过递归来实现建立哈夫曼编码
if(HTree[i].LChild==-1)
{
CodeTable[i].code=code;
return;
}
//按照哈夫曼编码的规则,向左为0,向右为1
Encode(HTree[i].LChild,code+"0");
Encode(HTree[i].RChild,code+"1");
}
将数据写入数组
void CreatTable()//建立编码表(将数据写入数组)
{
for(int i=0;i<26;i++)
{
//利用ASCII码来写名字
CodeTable[i].name=(char)(65+i);
}
CountStep(26*2-2,0);
Encode(26*2-2,"");
}
任务3.输出编码表
打印表格
void PrintTable()//打印编码表
{
//用printf来格式化输出,比cout更加简单清晰
printf("|-----------Code Table-----------|\n");
printf("|---------|-----------|----------|\n");
printf("|Character|Code Length| Code |\n");
printf("|---------|-----------|----------|\n");
for(int i=0;i<26;i++)
{
printf("| %c | %02d |%-10s|\n",CodeTable[i].name,CodeTable[i].length,CodeTable[i].code.c_str());
printf("|---------|-----------|----------|\n");
}
}
输出结果
(下图是我用Syntax Tree Generator做的树可视化)
网站:Syntax Tree Generator (mshang.ca)
编辑树的源码:
[1.0000[0.4233[0.1826[0.0885[0.0428[0.0202[0.0093[J,0.0041][0.0052[X,0.0021][0.0031[K,0.0014][0.0017[Z,0.0008][Q,0.0009]]]]][V,0.0019]][F,0.0226]][H,0.0457]][T,0.0941]][0.2407[0.1182[0.0547[U,0.0258][P,0.0289]][0.0635[0.0305[B,0.0147][Y,0.0158]][0.0330[W,0.0159][G,0.0171]]]][E,0.1225]]][0.5767[0.2737[0.1321[R,0.0685][S,0.0636]][0.1416[N,0.0706][I,0.0710]]][0.3030[0.1437[0.0711[M,0.0334][L,0.0377]][O,0.0726]][0.1593[0.0774[C,0.0383][D,0.0391]][A,0.0819]
任务4.测试例的编码
void EncodeTheTestString()
{
cout<<"The Huffman Code of the string 'pneumonoultramicroscopicsilicovolcanoconiosis' is:"<<endl;
string str("pneumonoultramicroscopicsilicovolcanoconiosis");
int num=str.length();
for(int i=0;i<num;i++)
{
cout<<CodeTable[str[i]-'a'].code;
}
}
完整代码
#include<iostream>
#include<stdio.h>
#include<string>
#include<string.h>
#include<algorithm>
using namespace std;
struct HNode//建立一个节点
{
float weight;//表示权重。注意要用float类型
int parent;//父节点的下标
int LChild;//左孩子的下标
int RChild;//右孩子的下标
};
struct CodeTableNode//建立编码表
{
char name;//像ABC这样的名字
int length;
string code;//哈夫曼编码
};
//the function declare
void InitializeVisitedArray();//初始化数组
void InitializeHuffmanTree();//初始化哈夫曼树
void CreatHuffmanTree();//建立哈夫曼树
int FindMin(int i);//找到最小的数
void DrawHuffmanTree();//画出哈夫曼树(以编码表的形式)
void CountStep(int i,int num);//记录步数(用于计算编码长度的)
void Encode(int i,string code);//编码函数(用于创建哈夫曼编码的)
void CreatTable();//建立编码表(将数据写入数组)
void PrintTable();//打印编码表
void EncodeTheTestString();//将测试的语句编码
CodeTableNode CodeTable[26];
HNode HTree[26*2-1];//Huffman Tree is a triple fork tree,so at most have 2*N-1 nodes
bool visited[26*2-1];
int main()
{
InitializeVisitedArray();
InitializeHuffmanTree();
CreatHuffmanTree();
DrawHuffmanTree();
printf("\n\n\n");
CreatTable();
PrintTable();
printf("\n\n\n");
EncodeTheTestString();
printf("\n\n\n");
system("pause");
}
void InitializeVisitedArray()//初始化数组
{
for(int i=0;i<26*2-1;i++)
{
visited[i]=0;
}
}
void InitializeHuffmanTree()//初始化哈夫曼树
{
//先将哈夫曼树初始化
float weight[26]={
0.0819,0.0147,0.0383,0.0391,0.1225,
0.0226,0.0171,0.0457,0.0710,0.0041,
0.0014,0.0377,0.0334,0.0706,0.0726,
0.0289,0.0009,0.0685,0.0636,0.0941,
0.0258,0.0109,0.0159,0.0021,0.0158,
0.0008
};//A到Z的权值(数据由老师提供)
for(int i=0;i<26;i++)
{
HTree[i].weight=weight[i];
}
for(int i=0;i<2*26-1;i++)
{
HTree[i].LChild=HTree[i].RChild=HTree[i].parent=-1;
//将父节点,左孩子,右孩子都设置成-1,来表示他们各个节点相互独立。
}
}
int FindMin(int i)//找到最小的数
{
//找到最小值
float min=1000000;
//注意这里一定要用float!(我第一次习惯性的用int,结果出bug了,找了好久才发现罪魁祸首
int pos=-1;
for(int j=0;j<i;j++)
{
if(HTree[j].weight<min&&HTree[j].parent==-1&&visited[j]==0)
{
min=HTree[j].weight;
pos=j;
}
}
visited[pos]=1;
return pos;
}
void CreatHuffmanTree()//建立哈夫曼树
{
for(int i=26;i<26*2-1;i++)
{
//这里的查找算法我一开始用的是一遍找出最小的两个数。
//但是这样子容易出bug
//于是我就另外开了一个数组,记录某个数是否有被当过最小值。
//然后我先找出最小值,接着把最小值剔除,再在新的数组中找最小值。这就是第二小的值了。
int Min1=FindMin(i);//找到从0到i的最小值。
int Min2=FindMin(i);//找到从0到i第二小的值。
//建立哈夫曼树
HTree[Min1].parent=i;
HTree[Min2].parent=i;//这一步就相当于把原先最小的两棵树删除了
HTree[i].weight=HTree[Min1].weight+HTree[Min2].weight;
HTree[i].LChild=Min1;
HTree[i].RChild=Min2;
HTree[i].parent=-1;
}
}
void DrawHuffmanTree()//画出哈夫曼树(以编码表的形式)
{
printf("|-------------Huffman Tree--------------|\n");
printf("|---------|--------|------|------|------|\n");
printf("|Character| Weight |LChild|RChild|Parent|\n");
printf("|---------|--------|------|------|------|\n");
for(int i=0;i<26;i++)
{
printf("| %c | %.4f | %02d | %02d | %02d |\n",(char)(65+i),HTree[i].weight,HTree[i].LChild,HTree[i].RChild,HTree[i].parent);
printf("|---------|--------|------|------|------|\n");
}
for(int i=26;i<26*2-1;i++)
{
printf("| %02d | %.4f | %02d | %02d | %02d |\n",i,HTree[i].weight,HTree[i].LChild,HTree[i].RChild,HTree[i].parent);
printf("|---------|--------|------|------|------|\n");
}
}
void CountStep(int i,int num)//记录步数(用于计算编码长度的)
{
//通过递归来实现记录编码长度
if(HTree[i].LChild==-1)
{
CodeTable[i].length=num;
return;
}
CountStep(HTree[i].LChild,num+1);
CountStep(HTree[i].RChild,num+1);
}
void Encode(int i,string code)//编码函数(用于创建哈夫曼编码的)
{
//通过递归来实现建立哈夫曼编码
if(HTree[i].LChild==-1)
{
CodeTable[i].code=code;
return;
}
//按照哈夫曼编码的规则,向左为0,向右为1
Encode(HTree[i].LChild,code+"0");
Encode(HTree[i].RChild,code+"1");
}
void CreatTable()//建立编码表(将数据写入数组)
{
for(int i=0;i<26;i++)
{
//利用ASCII码来写名字
CodeTable[i].name=(char)(65+i);
}
CountStep(26*2-2,0);
Encode(26*2-2,"");
}
void PrintTable()//打印编码表
{
//用printf来格式化输出,比cout更加简单清晰
printf("|-----------Code Table-----------|\n");
printf("|---------|-----------|----------|\n");
printf("|Character|Code Length| Code |\n");
printf("|---------|-----------|----------|\n");
for(int i=0;i<26;i++)
{
printf("| %c | %02d |%-10s|\n",CodeTable[i].name,CodeTable[i].length,CodeTable[i].code.c_str());
printf("|---------|-----------|----------|\n");
}
}
void EncodeTheTestString()//将测试的语句编码
{
cout<<"The Huffman Code of the string 'pneumonoultramicroscopicsilicovolcanoconiosis' is:"<<endl;
string str("pneumonoultramicroscopicsilicovolcanoconiosis");
//利用string已有的函数获得字符串的长度
int num=str.length();
for(int i=0;i<num;i++)
{
//字符串也可以像数组一样进行一个个字符的操作
cout<<CodeTable[str[i]-'a'].code;
}
}
程序复杂度分析
这个程序主要的空间复杂度在于三个数组:
CodeTableNode CodeTable[26];
HNode HTree[26*2-1];
bool visited[26*2-1];
空间复杂度为O(n)。
时间复杂度主要在于构建哈夫曼树的代码:
int FindMin(int i)//找到最小的数
{
//找到最小值
float min=1000000;
//注意这里一定要用float!(我第一次习惯性的用int,结果出bug了,找了好久才发现罪魁祸首
int pos=-1;
for(int j=0;j<i;j++)
{
if(HTree[j].weight<min&&HTree[j].parent==-1&&visited[j]==0)
{
min=HTree[j].weight;
pos=j;
}
}
visited[pos]=1;
return pos;
}
void CreatHuffmanTree()//建立哈夫曼树
{
for(int i=26;i<26*2-1;i++)
{
//这里的查找算法我一开始用的是一遍找出最小的两个数。
//但是这样子容易出bug
//于是我就另外开了一个数组,记录某个数是否有被当过最小值。
//然后我先找出最小值,接着把最小值剔除,再在新的数组中找最小值。这就是第二小的值了。
int Min1=FindMin(i);//找到从0到i的最小值。
int Min2=FindMin(i);//找到从0到i第二小的值。
//建立哈夫曼树
HTree[Min1].parent=i;
HTree[Min2].parent=i;//这一步就相当于把原先最小的两棵树删除了
HTree[i].weight=HTree[Min1].weight+HTree[Min2].weight;
HTree[i].LChild=Min1;
HTree[i].RChild=Min2;
HTree[i].parent=-1;
}
}
n个节点中,对于每个节点要之前所有数值的最小值,找2n次,所以的时间复杂度为O(n^2)。
此外,构建编码表的时间复杂度一个节点是logn,一共有n个节点,所以是O(n*logn)。
综上,时间复杂度为O(n^2).
此外,关于查找两个最小的数时,用了两遍遍历,可以再进行优化。
碎碎念
1.因为用中文写注释要来回切换输入法,太麻烦了,所以我就用英文写注释了(主要是我懒orz)
2.代码运行的时候老是报错,最后发现原因都是少打了分号(python写多了是这样的hhh)
3.在输出编码长度的时候,一开始输出很奇怪
于是我开始debug。debug了一个多小时,发现是构建哈夫曼树的时候,在找到最小两个数字时,j标成i了。。。(吐血)
经验教训:内层循环是i还是j一定要写清楚!
后来调试,还是不行。又经过一个多小时debug,发现:
1.选取最小值的时候应该加上“父节点==-1”这个条件
2.min1和min2的初始化应该是要两个最大的数值。所以将第50个节点的权重设置为最大,并且将min1和min2都初始化为50
后来这种方法一直调不对,于是我放弃了。换另外一种方法:
每次只找最小的,不找第二小的。然后将找到的标记一下,下次不找了。之后再找一遍,就是第二小的。
这样虽然要多遍历一次,但是方法简单,不容易出错。
具体代码:
bool visited[26*2-1]={0};
int FindMin(int i)
{
//find the min:
int min=1000000;
int pos=-1;
for(int j=0;j<i;j++)
{
if(HTree[j].weight<min&&HTree[j].parent==-1&&visited[j]==0)
{
min=HTree[j].weight;
pos=j;
}
}
visited[pos]=1;
return pos;
}
然后还是不行,我找了好久,甚至开始怀疑人生。。。
最后,发现min要用float。。。(再次吐血)
修正后:
bool visited[26*2-1]={0};
int FindMin(int i)
{
//find the min:
float min=1000000;
int pos=-1;
for(int j=0;j<i;j++)
{
if(HTree[j].weight<min&&HTree[j].parent==-1&&visited[j]==0)
{
min=HTree[j].weight;
pos=j;
}
}
visited[pos]=1;
return pos;
}