09--二叉树应用之哈夫曼编码

297 阅读5分钟

一、哈夫曼介绍

image.png

二、哈夫曼思考

1.我们在对学生的成绩进行统计,得出各成绩对应的评价时,一般会通过下面的方式来实现 image.png

  • 1.这样的方式存在一个问题,当大部分是良好和优秀时,比较的时候需要从不及格开始比较,一直比较到良好或优秀为止,导致比较的次数较多,效率低
  • 2.如果我们知道良好和优秀的人数所占的比例较大,那么我们可以从是否优秀或者良好开始比较,这样的情况下,相同人数的情况下,比较的次数就会少很多了。

2.当我们对学生成绩的分布情况在各阶段的比例时,我们可以针对这个比例设计一个更高较的算法

image.png

3.首先来看一下下图中D成绩良好所需要的比较次数?

image.png

4.可以知道,比较到D成绩良好需要从开始比较4次

image.png

5.如果我们根据学生的成绩情况分布图,按下面的方式来比较,那D的比较次数是多少次呢?

image.png

6.可以知道,这种方式比较到D成绩良好只需要2次

image.png

7.由此我们可以知道,我们可以根据事件发生的概率,来设计一个高效的算法,使得工作的效率得到提升。

8.在网络中传输出数据时,也可以应用这种思想,使得传输更加高效

image.png

  • 1.假设蓝框中A B C D E F对应的编码分别对应蓝框下面一行的编码数据
  • 2.则黄框中传递BADCADFEED时,需要黄框下面一行这么多的编码数据,总共需要30位

9.假设我们知道A B C D E在传输过程中出现的概率

image.png

经过排序后得到了升序的顺序为:FBCDAE

10.根据这些数据,结合二叉树来设计一个高效的二叉树编码算法,使得字符在传输中的效率最高

image.png

  • 1.由小到大比较,来逐渐生成如上图的二叉树;
  • 2.58组合,5在左,8在右,生成了13;
  • 3.此时13比15小,13在左,15在右,可以生成28;
  • 4.因为28比1527都大,于是1527一左一右,可以生成42;
  • 5.又因为28比30小,一左一右生成了58;
  • 6.最后42比58小,它们一左一右,生成了最终的根结点。于是就得到了最终的二叉树。

11.如果使用上面生成的二叉树来表示编码呢?

image.png

把二叉树上的数值去掉,左子树用0表示,右子树用1表示,最终得到了上图A B C D E F在传输时的编码。

12.比较编码前后同一数据传输所占的位数

image.png

此时总共需要25位,比前面的30位少了5位,这样就可以提高传输效率。当服务器收到这25位编码数据时,使用解码算法即可以得到原始数据。这就是哈夫曼编码

三、哈夫曼编码

1.定义一些数据常量

const int MaxValue = 10000;//初始设定的权值最大值

const int MaxBit = 4;//初始设定的最大编码位数

const int MaxN = 10;//初始设定的最大结点个数

2.哈夫曼树结点设计

typedef struct HaffNode{

    int weight;//权值

    int flag;//标记:0表示未添加到哈夫曼树中,1表示已经添加到哈夫曼树中

    int parent;//双亲结点下标

    int leftChild;//左孩子下标

    int rightChild;//右孩子下标

}HaffNode;

typedef struct Code//存放哈夫曼编码的数据元素结构
{
    int bit[MaxBit];//数组

    int start;  //编码的起始下标

    int weight;//字符的权值

}Code;

3.根据权重值构建哈夫曼树

void Haffman(int weight[],int n,HaffNode *haffTree){

    int j,m1,m2,x1,x2;

    //1.哈夫曼树初始化
    //n个叶子结点,共有2n-1结点
    for(int i = 0; i < 2*n-1;i++){
        if(i<n)
        {//叶子结点
            haffTree[i].weight = weight[i];
        }
        else
        {//非叶子结点
            haffTree[i].weight = 0;
        }

        haffTree[i].parent = 0;
        haffTree[i].flag = 0;
        haffTree[i].leftChild = -1;
        haffTree[i].rightChild = -1;
    }

    //2.构造哈夫曼树haffTree的n-1个非叶结点
    for (int i = 0; i< n - 1; i++){
         m1 = m2 = MaxValue;
         x1 = x2 = 0;

        //2,4,5,7
        //循环找出所有权重中,最小的两个值--morgan
        for (j = 0; j< n + i; j++)
        {
            if(haffTree[j].weight < m1 && haffTree[j].flag == 0)
            {
                m2 = m1;
                x2 = x1;

                m1 = haffTree[j].weight;
                x1 = j;
            } else if(haffTree[j].weight<m2 && haffTree[j].flag == 0)
            {
                m2 = haffTree[j].weight;
                x2 = j;
            }
        }

        //3.将找出的两棵权值最小的子树合并为一棵子树
        haffTree[x1].parent = n + i;
        haffTree[x2].parent = n + i;

        //将2个结点的flag 标记为1,表示已经加入到哈夫曼树中
        haffTree[x1].flag = 1;
        haffTree[x2].flag = 1;

        //修改n+i结点的权值
        haffTree[n + i].weight = haffTree[x1].weight + haffTree[x2].weight;

        //修改n+i的左右孩子的值
        haffTree[n + i].leftChild = x1;
        haffTree[n + i].rightChild = x2;
    }
}

4.由哈夫曼树构建哈夫曼编码

void HaffmanCode(HaffNode haffTree[], int n, Code haffCode[])
{
    //1.创建一个结点cd
    Code *cd = (Code * )malloc(sizeof(Code));

    int child, parent;

    //2.求n个叶子结点的哈夫曼编码
    for(int i = 0; i<n; i++)
    {
        //从0开始计数
        cd->start = 0;

        //取得编码对应权值的字符
        cd->weight = haffTree[i].weight;

        //当叶子结点i 为孩子结点.
        child = i;

        //找到child 的双亲结点;
        parent = haffTree[child].parent;

        //由叶子结点向上直到根结点
        while(parent != 0)
        {
            if(haffTree[parent].leftChild == child){
                cd->bit[cd->start] = 0;//左孩子结点编码0
            }
            else{
                cd->bit[cd->start] = 1;//右孩子结点编码1
            }

            //编码自增
            cd->start++;

            //当前双亲结点成为孩子结点
            child = parent;

            //找到双亲结点
            parent = haffTree[child].parent;
        }

         int temp = 0;
        //编码倒过来
        for(int j = cd->start - 1; j >= 0; j--){
            temp = cd->start-j-1;
            haffCode[i].bit[temp] = cd->bit[j];
        }

        //把cd中的数据赋值到haffCode[i]中.
        //保存好haffCode 的起始位以及权值;
        haffCode[i].start = cd->start;

        //保存编码对应的权值
        haffCode[i].weight = cd->weight;
    }
}

5.调试代码

    printf("Hello, 哈夫曼编码!\n");
    int i, j, n = 4, m = 0;

    //权值
    int weight[] = {2,4,5,7};

    //初始化哈夫曼树, 哈夫曼编码
    HaffNode *myHaffTree = malloc(sizeof(HaffNode)*2*n-1);

    Code *myHaffCode = malloc(sizeof(Code)*n);

    //当前n > MaxN,表示超界. 无法处理.
    if (n>MaxN)
    {
        printf("定义的n越界,修改MaxN!");
        exit(0);
    }

    //1. 构建哈夫曼树
    Haffman(weight, n, myHaffTree);

    //2.根据哈夫曼树得到哈夫曼编码
    HaffmanCode(myHaffTree, n, myHaffCode);

    //3.
    for (i = 0; i<n; i++)
    {
        printf("Weight = %d\n",myHaffCode[i].weight);

        for(j = 0; j<myHaffCode[i].start; j++){
            printf("%d",myHaffCode[i].bit[j]);
        }

        m = m + myHaffCode[i].weight*myHaffCode[i].start;
        printf("\n");

    }
    printf("Huffman's WPS is:%d\n",m);

四、总结

哈夫曼树的构建过程就是一个寻求平衡的过程,针对权值越平衡的哈夫曼树,最终得到的哈夫曼编码就越高效