哈夫曼树
*定义:给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树.
由来
大概于1951年间,大卫·a·霍夫曼(David a . Huffman)和他在麻省理工学院(MIT)信息理论的同学们被要求在学期论文或期末考试中做出选择。罗伯特·m·法诺(Robert M. Fano)教授布置了一篇学期论文,题目是如何找到最有效的二进制代码。霍夫曼,无法证明任何代码是最有效的,正要放弃并开始为期末考试做准备时,他突然想到了使用频率排序二叉树的想法,并很快证明了这种方法是最有效的——原文出处
解析
文中所提的最有效的二进制代码简单解析:让出现机率高的字母使用较短的编码,同时让出现机率低的则使用较长的编码,如此便使编码之后的字符串的平均长度缩短,以达到无损压缩数据的目的。
以上都是说不带权值的情况下,如果带了权值呢?
权值
定义:于二叉树中,权值=节点值*层次
层次定义:权值是树或者图中两个结点路径上的值(直接就简单理解成该结点到根结点有几根线相连就行).
例如:
A:层次为0 B:层次为1 D:层次为2 H:层次为3 J:层次为4
该二叉树的路径长度: 9 (当然如果为带权值的,请参照下面的介绍!)
带权值的树路径长度为:WPL=(W1L1+W2L2+W3L3+…+WnLn)
WPL(Weighted Path Length of Tree)是记号,W1、W2等是权值,L1、L2等是层次
哈夫曼树的构造原理
①于给定的n个权值{W1,W2,W3,W4……Wn}构成n颗只有根节点的二叉树(其实也就是需要弄个类似数组的东西把它给存储喽!)(这些最后都是叶子节点)
②于上述二叉树中,找到权值依次最小的两个根节点,分别作为左子树和右子树(新的二叉树的根节点的权值为这两个权值的和大小)
③于原{W1,W2,W3,W4……Wn}中删除在②中已作为别二叉树的左右子树的二叉树,并将②中新形成的二叉树加入到{W1,W2,W3,W4……Wn}中
④不断重复②、③,一直到{W1,W2,W3,W4……Wn}中只剩一颗二叉树为止。当然,这颗二叉树就是我们所需要的哈夫曼树.
哈夫曼算法的实现
哈夫曼树类型定义:
//此二叉树拥有权值
//下面有个perent,其实就是双亲节点,注意双亲不是两个,是一个
#define MAX 65982 //哈夫曼树最大的权值
typedef struct {
unsigned int weight;//记录权值用
unsigned int parent,lchild,rchild;//无双亲节点的将其(perent)记为0
}HTNode,*HuffmanTree;//HuffmanTree建树用
void CrtHuffmanTree(HuffmanTree ht,int w[],int n)
{
//ht(哈夫曼树,即ht[])ht就为树所在数组的头指针,w[](存放叶子节点的权值)
//将以序号对应的思维方式解决问题
int i;
for(i=1; i<=n; ++i)
ht[i]=w[i];//w[]应是已经储存好了所有开始阶段叶子节点权值的数组,1~n号数组单元的初始化
for(i=n+1; i<2*n-1; ++i)
ht[i]={0};//等价于 ht[i]=0; 预备将n+i~2*n-1号数组单元储存哈夫曼树的非叶子节点
for(i=n+1; i<2*n-1; ++i)
select(ht,i-1,&s1,&s2);//&s1、&s2是引用(传的直接是实参)(c++特用),与起别名类似,i-1是因为本就是从n+1开始,减一后本质上就是从n开始了
ht[i].weigth = ht[s1].weight + ht[s2].weight;//权值相加
ht[s1].parent = s1; ht[s2].parent = s2;//双亲节点指向确认
ht[i].lchild = s1;//左孩子
ht[i].rchild = s2;//右孩子
}
void select(HuffmanTree &ht,int n,int *s1,int *s2)
{
//ht指针自然指向是公用的哈夫曼树的存储单元,n为允许查找的最大序号,(以第一次为例,i-1就为n,也就是说从单元1~n中查找出最小的那两个,没毛病!!!)
int i;
int p1=MAX, P2=MAX;//用于记录最小的那两个的权值,自然定义的时候就需要是个很大的值啦!(默认p1<p2来设计)
int pn1=0, pn2=0;//用来记录两个最小叶子节点的序号(即存储单元)
for(i=1; i<=n; ++i)
{
//作用:找到权值最小的那两个
if(ht[i].weight<p1 && ht[i].parent == 0 )
{
//权值小于p1就相当于小于最小的(p1<p2),双亲节点等于0说明还未运用过
pn2 = pn1; p2 = p1; pn1 = i; p1 = ht[i].weight;
}
else if(ht[i].weight<p2 && ht[i].parent == 0)
{
//
pn2 = i; p2 = ht[i].weigth;
}
}
*s1 = pn1;
*s2 = pn2;
}
解释以上的函数定义为什么可以直接写成HuffmanTree ht:
HuffmanTree ht;
/* 前面在哈夫曼树类型定义的时候写了,typedef struct (此处省略了本该出现的结构体名) *HuffmanTree; */
/* 所以已经为“ struct 结构体名 * ” 起了别名 HuffmanTree ,所以HuffmanTree ht 就
相当于 struct 结构体名 *ht; */
/* 看似只是定义了一个指针,但在运用的时候将会发现并非如此,而是一个数组! */
/* 其实平时我们在定义数组的时候: char str[10]; 就相当于 char (*str)[10]; 这也是为什么后面总是说一维数组名就是首地址的原因 */
解读上面的select函数中的for循环为什么起作用
//死记住:p1、p2双权值,pn1、pn2双序号,p1<p2为默认(逻辑所通之需!)
for(i=1; i<=n; ++i)
{
if(ht[i].weight<p1 && ht[i].parent == 0 )
{
pn2 = pn1; p2 = p1; pn1 = i; p1 = ht[i].weight;
//pn2=pn1作用、p2=p1作用、pn1=i等请看解释①
}
else if(ht[i].weight<p2 && ht[i].parent == 0)
{
pn2 = i; p2 = ht[i].weigth;
//pn2=i;作用请看解释①
}
}
//for循环出来后,请看解释②
解释①(有点长,懂的跳过,不懂的真的可以耐心看一看,本人觉得已经解释的很到位了!)
:pn1注定是储存最小的那个序号(p1<p2),而且还是先放,比如现在程序进入for循环,假设第一个ht[1]的权值不是MAX,那么它就得进入if判断(条件一:满足,权值时小于最大值 条件二:双亲节点的确为0,毕竟还没成二叉树呢!还是为开始的初始值0的),这if一满足就进入了!pn2=pn1;看似第一回合没有说明用,但是我们快进,第二回合进入for循环,假设此时的ht[2]还不是最大的权值,而且权值比ht[1]要大,那么自然是要进入else if判断的,毕竟第一回合时已经将p1变为ht[1].weight了,满足不了第一个if的条件二,那么再看看else if的两个条件呗!(条件一:此时的p2还是最大值,自然满足,条件二:和上面说的一样也是满足的!)那么p2此时才变为ht[2].weight,再看第三回合,假设ht[3].weight大于ht[1].weight,但小于ht[2].weight,自然满足不了if的条件一了,再看else if的条件(全满足),p2自然就要更新为暂且倒数第二小的ht[3]了。第四回合,进入for循环,假设此时ht[4]小于h[1].weight了,将进入if语句,pn2=pn1作用出来了,将本该为第二小的pn2不在保留为ht[3].weight,而是进化为保留本该为第一小的ht[1].weight,而将暂时最小的ht[4].weight给了p1了。
解释②
那么此时呢,p1和p2中已经储存好了序号1~n的权值最小的两个,pn1、pn2为它们对应的序号
那么说会开头,说过了哈夫曼树的目的,那么一般都会匹配一个计算字符出现的次数的函数:
//ASCII码值 A:65 a:97
int Count(char *s,int cnt[],char str[])
{
//默认都是大写字母存入
char *p;
int i, j , k;
int temp[26];//26个字母
for(i=0; i<26; ++i)
temp[i]=0;//记录26个字母相应的个数
for(p=s; *p!='\0'; ++p)
if(*p>='A' && *p<='Z')
{
k=(*p)-65;
temp[k]++;//对应位加一
}
for(i=0, j=0; i<26; ++i)
if(temp[i]!=0)
{
str[j]=i+65;//变会原本的大写字符存储到str[]中
cnt[j]=temp[i];//cnt[]就是将26个字母中有的总和到一个数组里面
j++;//统计有多少中字符种类
}
str[j]='\0';//此时j已经加了一,到了没有存储字符的位置了,加上'\0'更方便后序的输出查操作
return j;//返回字符种类个数
}
债见~