数据结构基础-哈夫曼树

在学习哈夫曼树之前,我们先要先了解一些概念,再从哈夫曼树的定义入手。

小引

什么是节点的路径长度?

按定义来讲,是指从根节点到该节点的路径上的连接数

按树的结构来想象一下,如果由A到G有且仅有一条路径,途中还经过了B、C、D、E、F,那么从A到G的这段路径,每两个节点之间的路径累加起来

A->B 1条 B->C 1条 C->D 1条 D->E 1条 E->F 1条 F->G 1条

累计有6条,那么由A->G的这条路径的路径长度为6(该引例与下图无关)

那么上述树结构图形,按照路径长度定义红色区域由A到K(看不太清,该树的定义就是按字母顺序来的,主要看路径长度就行)的路径长度就是3

什么是树的路径长度之和?

是指树中每个叶子结点路径长度之和。没有什么讲究的,单纯记忆就好

什么是节点的权值?

本节的这些概念和后续的图部分有一定重复的部分,提到节点的权值,我们可以很朴素的将数学概念带入理解

在数学领域,权值指加权平均数中的每个数的频数,也称为权数或权重。

那么我们简单理解的话就是其在事件中的重要程度,并赋给节点相应的值以展示其重要性

什么是节点的带权路径长度?

那么了解了节点的权值后,我们就可以定义带权路径长度

其是指结点的路径长度和节点权值的乘积

我们假设上图中K节点的权值为5,并可得知K节点的路径长度为3,那么K节点的带权路径长度为3*5=15

什么是树的带权路径长度?

就是树的所有叶子结点的带权路径长度之和,可简写为WPL,这个也是在哈夫曼树中最为重要的一种计算

而且通过类比也可知,凡是有关树的整体路径或带权路径概念,我们都需要联想到的是树的叶子节点

综上就是小引中的基本概念,下面我们引入哈夫曼树

构造一棵WPL最小的二叉树

但是构造该树的前提是有效节点只能做叶子结点

那就是变相的出了一道题,题目明确给了n个节点,但是都必须定义为树的叶子结点,还要保证树的带权路径是最小的,也就是这几个节点的带权路径之和最小,我们如何解决这个问题?

哈夫曼树就是解决方案

哈夫曼树又被称为最优二叉树

每次选两个最小权值的树,合并成一棵树,经历过n-1次循环后,得到唯一的一棵树

那么怎么在代码中合并呢?构造一个空节点用以连接,将两棵最小权值的树作为该空节点的左右孩子即可,而且我们可以将该树的权值(即所有叶子结点的权值和)赋值给该空节点,以便于其作为一个新树加入整个循环的比较队列

可以看到上图,不带颜色的节点为经比较后构造出的新树的根节点,经过一次一次的比较得到了一棵权值最小的树,该树的WPL值一定,但是树的长相不一定

这里有一个关于哈夫曼树的性质,但依然以问题形式做出引例:

n个固定的叶子结点创建出的哈夫曼树,总结点个数是多少?

一起分析一下吧,上述过程中,要求构造一棵WPL最小的二叉树,我们一开始得到了n个节点,但是构造该树的前提是n个节点只能做叶子结点,我们想到的方法是每次寻找树中权值最小的两个,组合起来,我们可想而知,n个节点,需要组合n-1次才可以,那么在此过程中就会出现n-1个将最小权值树连接起来的节点,那么加上一开始的n个节点,会有多少个呢?

n + n-1 = 2n-1

所以一个哈夫曼树中,有2n-1个节点

且由此过程可知,一棵哈夫曼树中,一定只存在度为0和度为2的节点

度为0的节点有n个(已知),度为2的节点有n-1个

其实我们还可以根据二叉树的性质来推导一下,n0=n2+1

那自然节点度为2的值为n-1

哈夫曼树的应用场景:哈夫曼编码

小引

定长编码

像ASCII编码、Unicode编码。

ASCII编码每一个字符使用8个bit,能够编码256Unicode编码每个字符占16个bit,能够编码65536个字符,包含所有ASCII编码的字符。 假设我们要使用定长编码对由符号A,B,C,D和E构造的消息进行编码,对每个字符编码需要多少位呢? 至少需要3位,2个bit位不够表示五个字符,只能表示4个字符。 如果对DEAACAAAAABA进行编码呢? 总共12个字符,每个字符需要3bit,总共需要36位。

但显而易见的是,定长编码的空间浪费过于严重

对于仅包含ASCI字符的纯文本消息,Unicode使用的空间是ASCI的两倍,两种方式都会造成空间浪费;字符”a"和“e”的出现频率比””和“”的出现频率高,但是他们都占用了相同位数的空间。要解决定长编码的缺陷,便有了下面的变长编码。

但好处就是,它是统一的规范,是稳定的,不会因为外部原因发生改变,编译时好编译

变长编码

单个编码的长度不一样,可以根据整体出现的频率来调节,出现的频率越高,编码长度越短。 变长编码优于定长编码的是,变长编码可以将短编码赋予平均出现频率较高的字符,同一消息的编码长度小于定长编码。 这个时候又有一个问题,字符有长有短,我们怎么知道一个字符从哪里开始,又从哪里结束呢?如果位数固定,就没这个问题了。

前缀属性

字符集当中的一个字符编码不是其他字符编码的前缀,则这个字符编码具有前缀属性。所谓前缀,一个编码可以被解码为多个字符,表示不唯一。比如,abcd需要编码表示,设a = 0,b = 10,c = 110,d = 11。那么110可以表示c,也可以表示da。

所以我们进行字符编码时,不能将一个字符的编码设置为另一字符编码的前缀,否则译码工作无法进行

而我们今天介绍的哈夫曼编码就是满足该特征的,其编码永远不会是别的字符的前缀,因为在哈夫曼树的构造完成后,我们可以对其进行左1右0的路径标识,在标识好对应的每个字符后,其最后一位前面的编码都不属于任何一个字符,更不用说其本身了,所以因此保证了其变长编码稳定编译的特征

哈夫曼树的相关特征

  1. 哈夫曼编码树是一颗二叉树

每片叶子结点都包含一个字符 ​ 从结点到其左孩子的路径上标记0 ​ 从结点到其右孩子的路径上标记1

  1. 从根结点到包含字符的叶子结点的路径上获得的叶结点的编码
  2. 编码均具有前缀属性

每一个叶结点不能出现在到另一个叶结点的路径上

注意:定长编码可以由完整的哈夫曼树表示,并且显然具有前缀属性有了哈夫曼编码树的特性,我们也知道了哈夫曼树的构造过程,最后我们再来看一个哈夫曼编码和哈夫曼树构造的完整过程。

实战演练

给定一些字符n,告诉这些字符出现的频率,请对这些字符进行哈夫曼编码,并输出每个字符的编码串。

编程思路:

  1. 根据频率(权重),让这些字符做叶子结点,构造哈夫曼树

    1. 结构体数组存树,仍然是一个树节点的存储形式

    2. 每次选两个最小的根节点

    3. 数组大小选定,2n,因为我们需要的节点一共是2n-1个,不使用下标为0的节点那么从1开始正好2n个对应着需要的节点

  2. 对哈夫曼树的边进行编码赋值

  3. 找根叶子结点之间的路径,得到每个叶子节点的编码

    1. 倒着找编码,从叶子去找根节点

存储单元代码:

typedef struct node{    
int weight;//权重    
int lchild,rchild;//左右孩子节点的下标    
int parent;//父亲节点的下标
}HuffmanNode,*HuffmanTree;

构建哈夫曼树:

//构建哈夫曼树
HuffmanTree createHuffmanTree(int *w,int n){
    int m = 2*n;
    HuffmanTree tree = (HuffmanTree)malloc(sizeof(HuffmanNode)*m);//先给2n个节点设置好空间
    //对数组进行初始化
    for(int i=1;i<=n;i++){
        //对前n个节点赋权值
        tree[i].weight = w[i];
        //初始化其节点均为0,因为目前还 都是一个一个独立的根节点,未进行双亲结点的查找,而且左右孩子对于叶节点来说一定是0
        tree[i].lchild = tree[i].parent = tree[i].rchild = 0;
    }
    for(int i=n+1;i<m;i++){
        //剩余的节点都是度为2的节点,道理与上方类似,待一一寻找
        tree[i].weight = 0;
        tree[i].lchild = tree[i].parent = tree[i].rchild = 0;
    }
    //建立哈夫曼树
    int s1,s2;
    //开始为赋值做准备,从后n个新建节点开始为其找合适的孩子(每轮查找都是两者和最小的)
    for(int i=n+1;i<m;i++){
        find(tree,i-1,&s1,&s2);
    }
    
}

main函数:

int main()
{
    //在main函数中定义好权值和对应字符,用于测试
    int w[] = {5,29,7,8,14,23,3,11};
    char show[] = {'A','B','C','D','E','F','G','H'};
    //初始化哈夫曼树
    HuffmanTree tree = createHuffmanTree(w,sizeof(w)/sizeof(w[0]));
    
    
}

find函数,用于查找1-x中两个最小权值节点并直接返回

void find(HuffmanTree tree,int x,int *s1,int *s2){
    int min = 0;//设置最小根节点下标
    //先找父节点为0的(证明没有被选过),对min进行初始化,此时找的是第一个s1的值
    for(int i=1;i<=x;i++){
        if(tree[i].parent == 0){
            min = i;
            break;
        }
    }
    //找最小值
    //printf("第一个s1遍历过的值:");
    for(int i=min+1;i<=x;i++){
        if(tree[i].parent == 0 && tree[i].weight < tree[min].weight){
            min = i;
            //printf("%d ",min);
        }
    }
    //printf("\n");
    *s1 = min;
    //找第二个s2的值
    for(int i=1;i<=x;i++){
        if(tree[i].parent == 0){
            min = i;
            break;
        }
    }
    //找最小值
    //printf("第二个s2遍历过的值:");
    for(int i=min+1;i<=x;i++){
        if(tree[i].parent == 0 && tree[i].weight < tree[min].weight && i!=*s1){
            //多增加一个下标不相同的条件,权值可以相同但是位置一定不能相同
            min = i;
            //printf("%d ",min);
        }
    }
    //printf("\n");
    *s2 = min;
    //printf("最小的两个权值为:tree[%d]=%d tree[%d]=%d\n",*s1,tree[*s1].weight,*s2,tree[*s2].weight);
}

回来我们继续构造哈夫曼树:

//构建哈夫曼树
HuffmanTree createHuffmanTree(int *w,int n){
    int m = 2*n;
    HuffmanTree tree = (HuffmanTree)malloc(sizeof(HuffmanNode)*m);//先给2n个节点设置好空间
    //对数组进行初始化
    for(int i=1;i<=n;i++){
        //对前n个节点赋权值
        tree[i].weight = w[i];
        //初始化其节点均为0,因为目前还 都是一个一个独立的根节点,未进行双亲结点的查找,而且左右孩子对于叶节点来说一定是0
        tree[i].lchild = tree[i].parent = tree[i].rchild = 0;
    }
    for(int i=n+1;i<m;i++){
        //剩余的节点都是度为2的节点,道理与上方类似,待一一寻找
        tree[i].weight = 0;
        tree[i].lchild = tree[i].parent = tree[i].rchild = 0;
    }
    //建立哈夫曼树
    int s1,s2;
    //开始为赋值做准备,从后n个新建节点开始为其找合适的孩子(每轮查找都是两者和最小的)
    for(int i=n+1;i<m;i++){
        find(tree,i-1,&s1,&s2);
        //我们是对n后的每一个节点进行其左右孩子最小值的查找,所以依次对这些进行赋值
        tree[s1].parent = tree[s2].parent = i;
        tree[i].lchild = s1;//左小
        tree[i].rchild = s2;//右大
        //可以将s1和s2的位置互换一下,只是最后的出来的树不一样
        tree[i].weight = tree[s1].weight + tree[s2].weight;
    }
    return tree;
}

下一步就是构造二位字符数组来存储编码,因为不同的字符对应的是不同的编码,一层用来存数据,一层用来存编码

//构造哈夫曼编码
char **createHuffmanCode(HuffmanTree tree,int n){
    char *temp = (char*)malloc(sizeof(char)*n);//用于暂存每一个字符的编码
    char **codes = (char**)malloc(sizeof(char*)*n);//最后返回的二位编码数组
    memset(codes,0,sizeof(codes));//对二维数组空间内的数据全部清除
    
    int start,p,pos;
    for(int i=1;i<=n;i++){
        start = n-1;//是temp数组中的最后一个位置
        temp[start] = '\0';
        pos = i;//前n个节点依次遍历
        p = tree[i].parent;//p是指i节点的父亲节点,一直往上找
        while(p!=0){
            //进入循环的思路和上面相同,直到父节点p=0,才算找到根节点
            start--;
            if(tree[p].lchild==pos){
                temp[start] = '1';
            }
            else{
                temp[start] = '0';
            }
            pos = p;
            p = tree[p].parent;
        }
        //前n个节点每一个都是如此,需要通过循环进行编码赋值
        //然后将得到的temp赋值给codes
        codes[i-1] = (char*)malloc(sizeof(char)*(n-start));
        strcpy(codes[i-1],&temp[start]);
    }
    
    return codes;
}

我们返回main函数继续编写:

int main()
{
    //在main函数中定义好权值和对应字符,用于测试
    int w[] = {5,29,7,8,14,23,3,11};
    char show[] = {'A','B','C','D','E','F','G','H'};
    //初始化哈夫曼树
    HuffmanTree tree = createHuffmanTree(w,sizeof(w)/sizeof(w[0]));
    
    char **codes = createHuffmanCode(tree,sizeof(w)/sizeof(w[0]));
    
    //输出我们得到的码值
    for(int i=0;i<sizeof(w)/sizeof(w[0]);i++){
        printf("%c的哈夫曼编码为:%-4s\n",show[i],codes[i]);
    }
    
    return 0;
}

以上代码总结为:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct node{
    int weight;//权重
    int lchild,rchild;//左右孩子节点的下标
    int parent;//父亲节点的下标
}HuffmanNode,*HuffmanTree;

void find(HuffmanTree tree,int x,int *s1,int *s2){
    int min = 0;//设置最小根节点下标
    //先找父节点为0的(证明没有被选过),对min进行初始化,此时找的是第一个s1的值
    for(int i=1;i<=x;i++){
        if(tree[i].parent == 0){
            min = i;
            break;
        }
    }
    //找最小值
    //printf("第一个s1遍历过的值:");
    for(int i=min+1;i<=x;i++){
        if(tree[i].parent == 0 && tree[i].weight < tree[min].weight){
            min = i;
            //printf("%d ",min);
        }
    }
    //printf("\n");
    *s1 = min;
    //找第二个s2的值
    for(int i=1;i<=x;i++){
        if(tree[i].parent == 0){
            min = i;
            break;
        }
    }
    //找最小值
    //printf("第二个s2遍历过的值:");
    for(int i=min+1;i<=x;i++){
        if(tree[i].parent == 0 && tree[i].weight < tree[min].weight && i!=*s1){
            //多增加一个下标不相同的条件,权值可以相同但是位置一定不能相同
            min = i;
            //printf("%d ",min);
        }
    }
    //printf("\n");
    *s2 = min;
    //printf("最小的两个权值为:tree[%d]=%d tree[%d]=%d\n",*s1,tree[*s1].weight,*s2,tree[*s2].weight);
}

//构建哈夫曼树
HuffmanTree createHuffmanTree(int *w,int n){
    int m = 2*n;
    HuffmanTree tree = (HuffmanTree)malloc(sizeof(HuffmanNode)*m);//先给2n个节点设置好空间
    //对数组进行初始化
    for(int i=1;i<=n;i++){
        //对前n个节点赋权值
        tree[i].weight = w[i-1];
        //初始化其节点均为0,因为目前还 都是一个一个独立的根节点,未进行双亲结点的查找,而且左右孩子对于叶节点来说一定是0
        tree[i].lchild = tree[i].parent = tree[i].rchild = 0;
    }
    for(int i=n+1;i<m;i++){
        //剩余的节点都是度为2的节点,道理与上方类似,待一一寻找
        tree[i].weight = 0;
        tree[i].lchild = tree[i].parent = tree[i].rchild = 0;
    }
    //建立哈夫曼树
    int s1,s2;
    //开始为赋值做准备,从后n个新建节点开始为其找合适的孩子(每轮查找都是两者和最小的)
    for(int i=n+1;i<m;i++){
        find(tree,i-1,&s1,&s2);
        //我们是对n后的每一个节点进行其左右孩子最小值的查找,所以依次对这些进行赋值
        tree[s1].parent = tree[s2].parent = i;
        tree[i].lchild = s1;//左小
        tree[i].rchild = s2;//右大
        //可以将s1和s2的位置互换一下,只是最后的出来的树不一样
        tree[i].weight = tree[s1].weight + tree[s2].weight;
    }
    return tree;
}

//构造哈夫曼编码
char **createHuffmanCode(HuffmanTree tree,int n){
    char *temp = (char*)malloc(sizeof(char)*n);//用于暂存每一个字符的编码
    char **codes = (char**)malloc(sizeof(char*)*n);//最后返回的二位编码数组
    memset(codes,0,sizeof(codes));//对二维数组空间内的数据全部清除
    
    int start,p,pos;
    for(int i=1;i<=n;i++){
        start = n-1;//是temp数组中的最后一个位置
        temp[start] = '\0';
        pos = i;//前n个节点依次遍历
        p = tree[i].parent;//p是指i节点的父亲节点,一直往上找
        while(p!=0){
            //进入循环的思路和上面相同,直到父节点p=0,才算找到根节点
            start--;
            if(tree[p].lchild==pos){
                temp[start] = '1';
            }
            else{
                temp[start] = '0';
            }
            pos = p;
            p = tree[p].parent;
        }
        //前n个节点每一个都是如此,需要通过循环进行编码赋值
        //然后将得到的temp赋值给codes
        codes[i-1] = (char*)malloc(sizeof(char)*(n-start));
        strcpy(codes[i-1],&temp[start]);
    }
    
    return codes;
}

int main()
{
    //在main函数中定义好权值和对应字符,用于测试
    int w[] = {5,29,7,8,14,23,3,11};
    char show[] = {'A','B','C','D','E','F','G','H'};
    //初始化哈夫曼树
    HuffmanTree tree = createHuffmanTree(w,sizeof(w)/sizeof(w[0]));
    
    char **codes = createHuffmanCode(tree,sizeof(w)/sizeof(w[0]));
    
    //输出我们得到的码值
    for(int i=0;i<sizeof(w)/sizeof(w[0]);i++){
        printf("%c的哈夫曼编码为:%4s\n",show[i],codes[i]);
    }
    
    return 0;
}

输出结果为:

A的哈夫曼编码为: 110

B的哈夫曼编码为: 1

C的哈夫曼编码为: 101

D的哈夫曼编码为: 100

E的哈夫曼编码为: 11

F的哈夫曼编码为: 0

G的哈夫曼编码为: 111

H的哈夫曼编码为: 10

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值