【DSA】哈弗曼树详解-原理+代码(是什么?怎么构造?在哪里用?)

什么是哈弗曼树

  • 百度百科的定义
    给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
    哈弗曼树

上述定义很学术,是很严谨的表达,但是看起来总不是那么好理解。在这里,我们不说理论,直接来看一颗哈弗曼树是如何构建的,通过具象事物来理解抽象的概念。

怎样构建哈弗曼树

下边,我们使用一个哈夫曼编码的例子来理解哈弗曼树。

  • 背景问题
    在这里,我们通过一个示例来说明。
    现在,需要将一篇英文文章从A发送给B,要求是编码的长度最短。英文字母一共26个,大小写如果不同,那就是52个。那么我们需要6位二进制来进行编码(2^5 < 52 < 2 ^6=64)。如果这篇文章有字母10000个,那么编码长度就是10000*6。我们知道,在文章中,每一个字母出现的频率是不同的,思考:对不同频率的字母使用不通长度的编码位数,也就是出现频率最高的字母最短的编码长度,给频率最低的字母最长的编码长度。 这样就能使得整篇文章的编码总长度降到最低。提高传输效率。

实现方式如下:
字符频率表

字母 A B C D E F G H
频率 80 30 20 75 40 8 55 60

根据上述表格,我们知道,A的编码是最短的,F的编码是最长的。这里的频率是我任意指定的,实际字母出现的概率在密码学中有统计(可以参考字母频率)。

  • 构建
    第一步:选出其中频率最小的两个字母F和C,用这连个字母组成二叉树,小的为左孩子,大的为右孩子。并且将F和C的频率之和作为根节点,返回给频率表。
    【DSA】哈弗曼树详解-原理+代码(是什么?怎么构造?在哪里用?)_第1张图片
    第二步:连续重复上述操作。
    F+C 小于 B,所以28在左孩子的位置。
字母 A B D E FC G H
频率 80 30 75 40 28 55 60

【DSA】哈弗曼树详解-原理+代码(是什么?怎么构造?在哪里用?)_第2张图片
发现此时最小的是 40 和 55 (E和G)

字母 A D E FCB G H
频率 80 75 40 58 55 60

【DSA】哈弗曼树详解-原理+代码(是什么?怎么构造?在哪里用?)_第3张图片
此时最小的是58 和60 (EG和H)

字母 A D FCB EG H
频率 80 75 58 95 60

【DSA】哈弗曼树详解-原理+代码(是什么?怎么构造?在哪里用?)_第4张图片

字母 A D FCBH EG
频率 80 75 118 95

【DSA】哈弗曼树详解-原理+代码(是什么?怎么构造?在哪里用?)_第5张图片

字母 AD FCBH EG
频率 155 118 95

【DSA】哈弗曼树详解-原理+代码(是什么?怎么构造?在哪里用?)_第6张图片

字母 AD FCBHEG
频率 155 213

【DSA】哈弗曼树详解-原理+代码(是什么?怎么构造?在哪里用?)_第7张图片
至此,哈弗曼树构造完成了,那么前面说的编码是怎么实现的呢?按照二进制,输的左边标0,右边标1.沿着树的方向直至字母所在的叶子节点的0和1的序列即为该字母的哈夫曼编码。如下如:
【DSA】哈弗曼树详解-原理+代码(是什么?怎么构造?在哪里用?)_第8张图片
下表是所有字母的最终编码

字母 编码
A 01
B 1101
C 11001
D 00
E 100
F 11000
G 101
H 111

假如我要传送ABC四个字母,那么编码就是0 111111 1111101,一共14位,如果按照最开始的一个字母6位编码,那么长度就是18了。

哈弗曼树的代码实现

构建哈夫曼树时,需要每次根据各个结点的权重值,筛选出其中值最小的两个结点,然后构建二叉树。
查找权重值最小的两个结点的思想是:从树组起始位置开始,首先找到两个无父结点的结点(说明还未使用其构建成树),然后和后续无父结点的结点依次做比较,有两种情况需要考虑:

  • 如果比两个结点中较小的那个还小,就保留这个结点,删除原来较大的结点;
  • 如果介于两个结点权重值之间,替换原来较大的结点;

哈夫曼树的结构数据结构

// 哈夫曼树结点结构
typedef int Type;

typedef struct HuffmanNode_
{
    Type  weight; // 节点权重
    Type  parent, left, right; //父结点、左孩子、右孩子在数组中的位置下标

}Node, *HuffmanTree; 
// 选中频率最小的两个数据
// HT数组中存放的哈夫曼树,end表示HT数组中存放结点的最终位置,s1和s2传递的是HT数组中权重值最小的两个结点在数组中的位置

void select(HuffmanTree HT, int *pos1, int *pos2, int end)
{
    int min1 = 0, min2 = 0;

    int i = 1; // 数组的 0 号元素作为根节点的位置所以不使用

    // 找到没有构建成树的第一个节点
    while (HT[i].parent != 0 && i <= end)
    {
        i++;
    }
    min1 = HT[i].weight;
    *pos1 = i;

    i++;
    // 找到没有构建成树的第二个节点
    while(HT[i].parent != 0 && i <= end) 
    {
        i++;
    }

    min2 = HT[i].weight;
    if (min2 < min1)
    {
        min2 = min1;
        *pos2 = *pos1;
        min1 = HT[i].weight;
        *pos1 = i;
    }
    else
    {
        *pos2 = i;
    }

    // 取得两个节点之后,跟之后所有没有构建成树的节点逐一比较,最终获取最小的两个节点
    for (int j = i+1; j <= end; ++j)
    {
        // 如果已经存在父节点,也就是已经被构建树了,则跳过
        if (HT[j].parent != 0)
        {
            continue;
        }

        // 如果比min1 还小,将min2 = 敏, min1修改为新的节点下标
        if (HT[j].weight < min1)
        {
            min2 = min1;
            min1 = HT[j].weight;
            *pos2 = *pos1;
            *pos1 = j;
        }
        else if (HT[j].weight < min2 && HT[j].weight > min1)
        {
            // 如果大于 min1 小于 min2
            min2 = HT[j].weight;
            *pos2 = j;
        }
    }
}
// 创建完整的哈夫曼树
// HT为地址传递的存储哈夫曼树的数组,w为存储结点权重值的数组,n为结点个数

HuffmanTree init_huffman_tree(Type *weight, int node_num)
{
    if (node_num <= 1)
    {
        // 只有一个节点那么编码就是 0
        return NULL;
    }

    int tree_node_num = node_num * 2 - 1; // 根节点不使用
    HuffmanTree p = (HuffmanTree)malloc((tree_node_num+1) * sizeof(Node));

    // 初始化哈夫曼数组中的所有节点
    for (int i = 1; i <= tree_node_num; ++i)
    {        
        if (i <= node_num)
        {
            (p+i)->weight = *(weight+i-1); // 第0个位置不使用
        }
        else
        {
            (p+i)->weight = 0;
        }

        (p+i)->parent = 0;
        (p+i)->left = 0;
        (p+i)->right = 0;
    }

    return p;
}

void close_huffman_tree(HuffmanTree HT)
{
    if (HT)
    {
        free(HT);
        HT = NULL;
    }
}

void create_huffman_tree(HuffmanTree HT, int node_num)
{
    if (NULL == HT || node_num <= 1)
    {
        return;
    }

    int tree_node_num = node_num * 2 - 1; // 根节点不使用
    for (int i = node_num + 1; i <= tree_node_num; ++i)
    {
        int pos1 = -1, pos2 = -1;
        // 找到频率最小的连个节点
        select(HT, &pos1, &pos2, i-1);
        printf("当前最小的两个节点 [%d %d]\n", HT[pos1].weight, HT[pos2].weight);
        // 这里使用下表来表示父子关系
        HT[pos1].parent = HT[pos2].parent = i; // pos1 位置的元素和pos2位置的元素 的父节点就是,第 i个位置的元素
        HT[i].left = pos1;  // 父节点的左后孩子赋值
        HT[i].right = pos2;
        HT[i].weight = HT[pos1].weight + HT[pos2].weight; // 父节点的权重等于 左右孩子权重的和
    }
}
  • 测试代码
void print(HuffmanTree HT, int node_num)
{
    if (NULL == HT)
    {
        printf("数组为空\n");
        return;
    }

    int tree_node_num;

    for (int i = 1; i < tree_node_num; ++i)
    {
        printf("%d 的父节点:%d 左孩子:%d 右孩子:%d\n", HT[i].weight, HT[HT[i].parent].weight, HT[i].left, HT[i].right);
    }

}

int main(int argc, char const *argv[])
{
    
    Type weight[8] = {80, 30, 20, 75, 40, 8, 55, 60};

    int node_num = sizeof(weight) / sizeof(Type);

    HuffmanTree HT = init_huffman_tree(weight, node_num);

    create_huffman_tree(HT, node_num);

    print(HT, node_num);

    close_huffman_tree(HT);

    return 0;
}
  • 测试结果
    !【DSA】哈弗曼树详解-原理+代码(是什么?怎么构造?在哪里用?)_第9张图片

为什么要设计哈弗曼树

  • 哈弗曼树主要用于哈夫曼编码,其主要作用就是利用频率属性进行编码,最终达到目的:让高频的数据拥有短编码,而低频的数据拥有长编码。
  • 哈夫曼编码并不是适用于所有场景,它更适用于频率变化多端的数据编码。

你可能感兴趣的:(数据结构与算法)