最优二叉树 (哈夫曼树) 的构建及编码

参考:

https://www.bilibili.com/video/BV18t411U7Tb?from=search&seid=13776480377358559786

https://www.bilibili.com/video/BV18t411U7eD?from=search&seid=13776480377358559786

https://baike.baidu.com/item/%E5%93%88%E5%A4%AB%E6%9B%BC%E6%A0%91/2305769?fr=aladdin

https://baike.baidu.com/item/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95/5411800?fr=aladdin

数据结构教程(第五版)李春葆主编

 

 

一,概述

1,概念

  结点的带权路径长度:

    从根节点到该结点之间的路径长度与该结点上权的乘积。

  树的带权路径长度:

    树中所有叶结点的带权路径长度之和。

2,哈夫曼树(Huffman Tree)

  给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,则称这样的二叉树为最优二叉树,也称为哈夫曼树。

  哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

 

 

二,哈夫曼树的构建

 1,思考

  要实现哈夫曼树首先有个问题摆在眼前,那就是哈夫曼树用什么数据结构表示 ?

  首先,我们想到的肯定数组了,因为数组是最简单和方便的。用数组表示二叉树有两种方法:

    第一种:适用于所有的树

      即利用树的每个结点最多只有一个父节点这种特性,用 p[ i ] 表示 i 结点的根节点,进而表示树。

      但这种方法是有缺陷的,权重的值需要另设一个数组表示;每次找子节点都要遍历一遍数组,十分浪费时间。

    第二种:只适用于二叉树

      即利用二叉树每个结点最多只有两个子节点的特点。从下标 0 开始表示根节点,编号为 i 结点即为 2 * i + 1 和 2 * i + 2,父节点为 ( i - 1) / 2,没有用到的空间用 -1  表示。

      但这种方法也有问题,即哈夫曼树是从叶结点自下往上构建的,一开始树叶的位置会因为无法确定自身的深度而无法确定,从而无法构造。

  既然如此,只能在第一种方法的基础上,用结构体存储一个结点的信息,即用结构体数组表示二叉树

typedef struct HTNode // 哈夫曼树结点
{
	double w; // 权重
	int p, lc, rc;
}htn;

 

2,算法思想

  感觉比较偏向于贪心,权重最小的叶子节点要离根节点越远,又因为我们是从叶子结点开始构造最优树的,所以肯定是从最远的结点开始构造,即权重最小的结点开始构造。

    所以先选择权重最小的两个结点,构造一棵小二叉树。

    然后那两个最小权值的结点因为已经构造完了,不会在用了,就不去考虑它了,将新生成的根节点作为新的叶子节加入剩下的叶子节点,又因为该根节点要能代表整个以它为根节点的二叉树的权重,所以其权值要为其所有子节点的权重之和。

    如此,继续选取权重最小的两个结点,循环上一步骤。

  这种,就是贪心的思想:

    先是求整个最优树的问题,此时我们找到两个权重最小的叶子结点,构造一棵小二叉树。此时为当前最优的情况。

    然后将分解成了 —— 求去除了两个权值最小的叶子节点的最优树的问题。该问题与原问题在本质上是一样的,所以继续找到两个权重最小的叶子结点,构造一棵小二叉树。此时仍然为当前最优的情况。

    如此往复,不断贪心,最后将所有结点整合在一起就是原问题的最优解了。

 

3,算法步骤 

① 构造森林全是根    

  这一步就是把这 n 个叶子节点放入结构体数组中:

    有 n 个结点的二叉树的结点个数为 2n-1,所以要用到的数组长度为 2n-1

② 选择两小造新树

  选择两小:在剩下没用过的叶子结点中找到最小的两个数,这些结点包括树叶也包括你上一波造的新树的根。

③ 删除两小添新人

  删除两小: 给找到的结点认好父亲,下次搜索时排除掉有父节点的结点。

  添新人:将选择的两个权值最小的结点与其父节点构建好关系。

④ 重复 ②,③操作

  结构体数组的前 n 个元素放叶子节点,把新生成的按顺序放在叶结点后面。

  于是,我们从下标为 n 开始循环,这样要找的结点全部都包含在循环变量 i 的前面,当然其中也包含了已经不需要考虑的结点。

 

 

三,哈夫曼树编码

1,概念

  哈夫曼编码的实质:使用频率越高的字符采用越短的编码。

 

2,算法思想

  如果我们只使用包含 0 和 1 字符串来编码的话,那么我们就可以使用哈夫曼树来解决哈夫曼编码的问题。

  哈夫曼编码要求频率越高的字符采用越短的编码,而哈夫曼树是权重越大的叶子节点离根节点越远。

    那么,如果我们用叶结点到根节点的路径来代表编码长度的话,不就符合哈夫曼编码的频率越高编码越短的要求吗?

    又因为哈夫曼树是二叉树,一个父节点只有两个子节点,所以每个编码的字符只能有两种区别,正好对应 0 和 1,

      我们可以用左节点代表 0,右节点代表 1。这样,我们走一遍叶结点到根节点的路径,该字符的编码就出来了。

        当然,从根节点到叶结点也可以,不过要统一使用,不能一个字符是从叶结点出发,另外一个是从根节点出发。

 

3,算法步骤

  ① 遍历每个叶结点,给每个叶结点进行编码。

  ② 编码过程

    Ⅰ首先是可以确定树高最高为为 n-1,因为这是一棵有 n 个叶子节点的二叉树,所以这样就可以用 n 给数组赋长了。

     Ⅱ我们这里的编码顺序是从根节点到叶结点,但实际的遍历顺序是从叶结点到根节点(因为这样可以根据是否),所以存编码的数组就要从后往前赋值。

      即使用下标为 0 的位置,将 n-1 位赋值为 '\0' ,然后从下标为 n -2 的位置往前遍历。

    Ⅲ 然后从树叶回溯到根,每一个结点判断一下是左节点还是右节点 ,就知道要当前编码位是 0 还是 1 了。

    

 4,哈夫曼编码的性质

  在一组字符的哈夫曼编码中,任一字符的哈夫曼编码不可能是另一字符哈夫曼编码的前缀。

  定性证明:

    观察哈夫曼树,根节点到不同叶结点的路径必不同。

    所以一个叶子节点不可能是另一叶子节点路径的某一部分,所以也不可能一个字符是另一字符哈夫曼编码的前缀。

 

 

四,完整代码:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 666
#define inf 0x3f3f3f3f
typedef struct HTNode // 哈夫曼树结点
{
    double w; // 权重
    int p, lc, rc;
}htn;
htn ht[N];       // 哈夫曼树
char hcd[N][20]; // 存哈夫曼编码
void select(int i, int &c1, int &c2)  // 在 ht[0 ~ i-1] 中找权值最小的两个结点
{
    double min1 = inf, min2 = inf;
    for (int j = 0; j < i; j++)
    {
        if (ht[j].p != -1) // 在没有父节点的节点中查找,包括未参与的和新生成的结点
            continue;
        if (ht[j].w < min1) // 找到当前最小值
        {
            min2 = min1;    // 原先最小值 变成 第二小
            c2 = c1;
            min1 = ht[j].w; // 新找到的 变成 最小值
            c1 = j;
        }
        else if (ht[j].w < min2)// 找到当前第二小值
        {
            min2 = ht[j].w;
            c2 = j;
        }
    }
}
void CreateHT(int n) // 创建 Huffman tree
{
    // ① 构造森林全是根,即所有结点的初始值置为 -1
    for (int i = 0; i < 2 * n - 1; i++) // 
        ht[i].p = ht[i].lc = ht[i].rc = -1;

    // ④ 重复 ②,③操作
    for (int i = n; i < 2 * n - 1; i++)
    {
        // ② 选择两小造新树,其中 c1 权值最小,c2 第二小
        int c1 = -1, c2 = -1;
        select(i, c1, c2);

        // ③ 删除两小添新人
        ht[c1].p = ht[c2].p = i;
        ht[i].lc = c1, ht[i].rc = c2;
        ht[i].w = ht[c1].w + ht[c2].w;
    }

    // 输出 Huffman tree
    printf("\n该哈夫曼树为:\n");
    for (int i = 0; i < 2 * n - 1; i++)
        printf("结点 %2d:权值为 %.2lf,父节点:%2d,子节点:%2d,%2d\n", i, ht[i].w, ht[i].p, ht[i].lc, ht[i].rc);
    printf("( -1 代表不存在这个结点.)\n\n");
}
void HuffmanCoding(int n)  // 哈夫曼编码
{
    char code[20];
    for (int i = 0; i < n; i++)
    {
        code[n] = 0;
        int s = n - 1, c = i, p = ht[i].p; // s 指向当前编码位,c 是子节点,p 是父节点
        while (p != -1) // 循环直到根结点
        {
            if (ht[p].lc == c)
                code[s--] = '0';
            else
                code[s--] = '1';
            c = p;
            p = ht[c].p;
        }
        s++; // ++ 之后 s 指向当前编码的第一个字符
        strcpy(hcd[i], code + s);
    }

    printf("各权值所对应的哈夫曼编码如下:\n"); // 输出哈夫曼编码
    for (int i = 0; i < n; i++)
        printf("权值:%.2lf,哈夫曼编码:%s\n", ht[i].w, hcd[i]);
}
int main(void)
{
    int n; // 叶的个数
    while (scanf("%d", &n) != EOF)
    {
        for (int i = 0; i < n; i++) // 树叶的权重
            scanf("%lf", &ht[i].w);
        CreateHT(n);
        HuffmanCoding(n);
    }
    system("pause");
    return 0;
}
/*
输入数据:
8
0.05 0.29 0.07 0.08 0.14 0.23 0.03 0.11

该哈夫曼树为:
结点  0:权值为 0.05,父节点: 8,子节点:-1,-1
结点  1:权值为 0.29,父节点:13,子节点:-1,-1
结点  2:权值为 0.07,父节点: 9,子节点:-1,-1
结点  3:权值为 0.08,父节点: 9,子节点:-1,-1
结点  4:权值为 0.14,父节点:11,子节点:-1,-1
结点  5:权值为 0.23,父节点:12,子节点:-1,-1
结点  6:权值为 0.03,父节点: 8,子节点:-1,-1
结点  7:权值为 0.11,父节点:10,子节点:-1,-1
结点  8:权值为 0.08,父节点:10,子节点: 6, 0
结点  9:权值为 0.15,父节点:11,子节点: 2, 3
结点 10:权值为 0.19,父节点:12,子节点: 8, 7
结点 11:权值为 0.29,父节点:13,子节点: 4, 9
结点 12:权值为 0.42,父节点:14,子节点:10, 5
结点 13:权值为 0.58,父节点:14,子节点: 1,11
结点 14:权值为 1.00,父节点:-1,子节点:12,13
( -1 代表不存在这个结点.)

各权值所对应的哈夫曼编码如下:
权值:0.05,哈夫曼编码:0001
权值:0.29,哈夫曼编码:10
权值:0.07,哈夫曼编码:1110
权值:0.08,哈夫曼编码:1111
权值:0.14,哈夫曼编码:110
权值:0.23,哈夫曼编码:01
权值:0.03,哈夫曼编码:0000
权值:0.11,哈夫曼编码:001
*/
View Code

 

 

========== ========= ======== ======= ====== ===== ==== === == =

  鹊桥仙  秦观(宋)

纤云弄巧,飞星传恨,银汉迢迢暗度。金风玉露一相逢,便胜却人间无数。

柔情似水,佳期如梦,忍顾鹊桥归路。两情若是长久时,又岂在朝朝暮暮。

 

posted @ 2020-05-29 17:28  叫我妖道  阅读(3754)  评论(0编辑  收藏  举报
~~加载中~~