最优二叉树:
定义:
路径:数的路径就是从书中的一个节点到树中的另一个节点的分支的个数长度,路径上的分支数目我们称之为长度
树的路径长度:从树根到每一个节点的长度之和(完全二叉树是一种树的路径最短的二叉树)
节点的带权路径长度:从根节点到该节点的路径的分支数目与节点的权值的乘积
树的带权路径的长度:树中的所有的节点的带权路径长度之和,并且,我们将改长度定义为WPL
最优二叉树(哈夫曼树):有n个权值的点,我们构造一颗有n个叶子节点的二叉树,WPL最小的二叉树我们称之为最优二叉树或者哈夫曼树
ps:首先我们要注意到,哈夫曼的压缩效果不是固定的,我们考虑的是整体的压缩效率而不是单次的压缩效果
pss:哈夫曼树或者最优二叉树的叶子节点才是我们的节点,中间的节点只是我们的合并过程中产生的新节点,不作为我们的考虑范围
哈夫曼树:
1.算法的步骤:
1.从森林中选出所有的节点中权值最小的两个节点并删除
2.合并两个最小的节点生成新的节点,插入到森林中
3.重复上述的1,2步骤直到森里中只有一颗树是我们变构建完了最优二叉树
2.WPL最优证明:
我们对于证明采用反证法:
首先,如上图,我们假设我们目前构建的二叉树
如果出现了A>C的情况话,我们很明显可以发现,如果我们交换C和A的位置的话,显然
这样的话我们重新交换生成的二叉树明显要比我们之前的要更优一些
所以说,吐过我们对所有的节点之间的大小关系和层次关系进行比较的话就会很明显的发现,
只要存在上述的情况,我们就可以通过交换来优化我们当前的二叉树WPL的值
因此我们全部的考虑所有的情况,到最后不出现上述可以交换得到最优的情况的时候,我们得到的WPL就是最优的,无法再次优化的最优二叉树了
这是后我们再反过来考虑一下这样我们得到的树的性质:
我们很明显的额发现,底层的节点的权值必定比高层次的节点的权值要小,并且如果是最有的话,这个性质是必须要每层之间的每个节点都要保证的,在我们构建哈夫曼树的算法过程中,底层的节点代表我们最先合并,而高层的节点代表我们最后合并,所以说,这样我们也就证明了哈夫曼树构建算法的正确性
实际上,我们仅仅考虑算法本身的话,我们发现哈夫曼构建的算法实际上就是贪心的策略
3.哈夫曼树的压缩性
我们比较哈夫曼树的压缩效率,我们是考虑哈夫曼树的压缩能力和平常的每个字符的三位码的码长进行比较的
虽然哈夫曼树因为可能不唯一(存在权值相同的节点,或者存在计算过程中新生的节点的权值相同的情况),但是我们整体的压缩效果是一样的
因为,就算哈夫曼树是不唯一的,但是我们要记住,同为最优二叉树,WPL只是相同的,换句话来说,我们的不同的的最优二叉树的压缩比是相同的
举个例子:
ABACCDA
A 3
B 1
C 2
D 1
构建的编码有两种情况分别是:
1.
A:0
B:110
C:10
D:111
2.
A:0
B:100
C:11
D:101
但是对于压缩比:
原码长:7*3=21
哈夫曼码长:1*3+3*1+2*2+3*1=13
压缩比为:13/21
4.哈夫曼编码的前缀编码
上述的哈夫曼编码虽然可以很明显的压缩我们的码长,但是我们现在来考虑,如果我们不给出你现在的这串码的各个字符的频率,那么我们是没有办法构建出哈夫曼树的,那么我们就无法解码
为了解码,我们必须要确定我们的每个字符对应的二进制编码是不会出现二义性的,也就是说,我们不能让一个字符的编码成为另一个字符编码的前缀,那么样的话,我们就无法确定到底是用那个字符进行解释
但是不要担心,对于哈夫曼树来说,我们构建出的哈夫曼树最后如果我们将左分支定义成0/1,有分支定义成1/0,那么我们便可以发现任何一个字符编码都不可能成为另一个个字符的前缀,很巧妙的解决了这个问题
参考代码
类封装:
测试用例:ABACCDA 编码结果:0111010101100 压缩比是:13/21;
存在问题,哈夫曼树是不唯一的,那么就会存在多种编码方式,实际中是如何唯一确定唯一编码方式的,是将相等的权值情况进行了统一定义吗,求解?
#include"iostream"
#include"cstdio"
#include"cstdlib"
#include"cstring"
#define MAXI 500
typedef struct node //空节点的c值是'#',count是-1;其余都是正常值;
{
char c;
int count;
struct node* left;
struct node* right;
struct node* parent;
}point;
typedef struct k
{
char c;
char code[100];
}bc;
using namespace std;
class haffman
{
public:
haffman()
{
memset(bookchar,0,sizeof(bookchar));
memset(prefile,0,sizeof(prefile));
memset(code,0,sizeof(code));
memset(file,0,sizeof(file));
memset(workspace,0,sizeof(workspace));
root=NULL;
help=NULL;
number=0;
worknumber=0;
}
haffman(char p[])
{
memset(bookchar,0,sizeof(bookchar));
memset(prefile,0,sizeof(prefile));
memset(code,0,sizeof(code));
memset(file,0,sizeof(file));
memset(workspace,0,sizeof(workspace));
root=NULL;
help=NULL;
number=0;
worknumber=0;
strcpy(prefile,p);
}
~haffman()
{
clear(root);
root=NULL;
number=0;
}
friend istream& operator>>(istream&,haffman&);
friend ostream& operator<<(ostream&,haffman&);
void creattree();
void select(point*&,int,point*);
void followfiletomakecode();
bc makecode(char);
void codewritefile();
void intobookchar(bc);
void findhelp(point*,char);
void clear(point*);
void scan(); //从头到尾扫描文本prefile,生成workspace
private:
bc bookchar[100]; //记录字符对应编码,方便下次读取
int booknumber;
char code[MAXI];
char stack[MAXI];
char file[MAXI]; //最终的翻译文本
char prefile[MAXI];
int filenumber;
point workspace[MAXI]; //哈夫曼算法需要的操作空间
int worknumber;
point* help;
point* root; //哈弗曼树根节点
int number; //字符总数
};
istream& operator>>(istream& in,haffman& x)
{
cout<<"请输入要转译的编码: ";
cin>>x.prefile;
return in;
}
ostream& operator<<(ostream& out,haffman& x)
{
cout<<"预处理电文:"<count>b->count)
{
workspace[worknumber].right=b;
workspace[worknumber].left=a;
}
else
{
workspace[worknumber].right=a;
workspace[worknumber].left=b;
}
workspace[worknumber].parent=NULL;
a->parent=&workspace[worknumber];
b->parent=&workspace[worknumber];
}
root=&workspace[worknumber];
}
void haffman::findhelp(point* p,char k)
{
if(p==NULL) return ;
else
{
if(p->c==k)
{
help=p;
return ;
}
findhelp(p->left,k);
findhelp(p->right,k);
}
}
bc haffman::makecode(char w)
{
int k=0;
while(help!=root)
{
if(help->parent->left==help) code[k++]='0';
else code[k++]='1';
help=help->parent;
}
bc p;
memset(p.code,0,sizeof(p.code));
p.c=w;
int j=0;
for(int i=k-1;i>=0;i--,j++) p.code[j]=code[i];
return p;
}
void haffman::intobookchar(bc p)
{
bookchar[++booknumber]=p;
}
void haffman::codewritefile()
{
for(int i=0;ileft);
clear(p->right);
delete p;
}
}
int main()
{
haffman my;
cin>>my;
my.scan();
my.creattree();
my.followfiletomakecode();
cout<
解码方法:
对于哈夫曼的解码方式我们采用了DFS的方式查找出个个字符对应的编码,我们直接对译码进行处理,从译码的排列触发DFS递归遍历哈夫曼树,直到找到叶子节点后就完成了一串的翻译工作,知道我们将整个串翻译完成:
下面的解码类继承了上面的类,直接写个深搜函数就可以了:
class rehuffman:public haffman
{
public:
rehuffman()
{
memset(code,0,sizeof(code));
num=0;
}
void set()
{
cout<<"输入要翻译的译码的长度:";cin>>num;
cout<<"输入你的译码:";
for(int i=1;i<=num;i++) scanf("%d",&code[i]);
}
void predfs()
{
for(int i=1;i<=num;i++)
{
i=dfs(root,i)-1;
}
cout<left==NULL&&root->right==NULL)
{
cout<c;
return i;
}
else
{
if(code[i]==0) return dfs(root->left,i+1);
else return dfs(root->right,i+1);
}
}
private:
int code[MAXI];
int num;
};
参考试题:
NYOJ801:本博主的解题报告链接地址 点开进入NYOJ801解题报告