数据结构与算法 -- 哈夫曼树&哈夫曼编码

前言

  上一篇了解学习了线索化二叉树的一些知识,这一篇,对哈夫曼树哈夫曼编码来做一个了解学习。

首先,我们先看一个经典的问题,

等级 优秀 良好 中等 及格 不及格
考分 90 <= 分数 <= 100 80 <= 分数 < 90 70 <= 分数 < 80 60 <= 分数 < 70 0<=分数<60

一般我们会这样判断:

那么,在二叉树中是这样的

一般情况下,每个班的成绩及格不及格有一定的分布概率

那这样就会造成对中等良好的成绩判断时,会经过很多步,当成绩比较多时,就会出现效率问题。

那么怎么能优化解决一下这个问题呢?其实前辈们已经解决了这个问题,我们接下来学习一下

1. 哈夫曼树

在上面的成绩算法,

节点D的路径为4

树的总路径为:1 + 1 + 2 + 2 + 3 + 3 + 4 + 4 = 20

根据上面成绩的分布这棵树的WPL为:WPL = 1 * 5 + 2 * 15 + 3 * 40 + 4 * 30 + 4 * 10 = 315

假如我们对成绩的判断顺序进行如下的调整:

那么节点D的路径长度为2

树的总路径为1 + 2 + 3 + 3 + 2 + 1 + 2 + 2 = 16

根据上面成绩的分布这棵树的WPL为:WPL = 5 * 3 + 3 * 15 + 2 * 40 + 2 * 30 + 2 * 10 = 220

第二种情况,明显比第一种情况更优。

那么怎么构建一个最优的树呢,我们接下来看一下哈夫曼树的构建。

假设:A B C D E的权重如下:

A B C D E
5 15 40 30 10

首先,找到权重最小的两个AE,组成二叉树:

此时N1的权重相当于 15,依次找到 最小的 B,组建二叉树:

此时N2的权重相当于 30,依次找到 最小的 D,组建二叉树:

此时N3的权重相当于 60,依次找到 最小的 C,组建二叉树(小的在左子树位置):

该二叉树的WPL40 * 1 + 30 * 2 + 15 * 3 + 10 * 4 + 5 * 4 = 205

这种排列组成的二叉树的WPL,明显小于上面两种,以这种方式排列的二叉树即为哈夫曼树

小结:
先对给出的节点的权重排序
依次找出最小的两个节点,组成二叉树,小的在左子树位置

2. 哈夫曼编码

假设如下:

字符 A B C D E F
权重 27 8 15 15 30 5
编码 000 001 010 011 100 101

如上表中,每个字符的编码位数都是一样的,但是每个字符出现的权重是不同的,这样就会造成权重低的字符的空间浪费

我们可以利用 哈夫曼树 找到最佳的二叉树排列,然后生成 哈夫曼编码 来解决这个问题。

首先,对上面的字符生成哈夫曼树

  • 找到权重最小的 BF,组成二叉树(小的在左边)

  • 找到 C,组成二叉树(小的在左边)

  • 依次查找

最终的 哈夫曼树 如下:

然后,开始哈夫曼编码

哈夫曼编码的原则:左子树上的权重,替换为 0 ,右子树权重替换为 1

然后从根节点,到各个叶子节点,组成每个叶子节点的编码,如下:

字符 A B C D E F
权重 27 8 15 15 30 5
编码 01 1001 101 00 11 1000

那么对于同一个字符串BADCADFEED的新旧二进制编码如下:

旧编码: 001 000 011 010 000 011 101 100 100 011 (共30个字符)

新编码: 1001 01 00 101 01 00 1001 11 11 00 (共25个字符)

可以明显看出,新编码(哈夫曼编码)比旧编码,有更高的存储利用率。

小结:
先生成哈夫曼树
从根节点开始,左子树的权值为0,右子树的权值 为 1
从根节点开始,到叶子节点,组成叶子节点的编码

3. 哈夫曼树 & 哈夫曼编码 的实现

可以定义哈夫曼树(顺序)的节点如下:

const int MaxValue = 10000;//初始设定的权值最大值
const int MaxBit = 4;//初始设定的最大编码位数
const int MaxN = 10;//初始设定的最大结点个数

// 定义节点
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;

// 构建哈夫曼树
void Haffman(int weight[],int n,HaffNode *haffTree){
    int j, m1, m2, x1, x2;
    // 哈夫曼树初始化
    // 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;         // 无双亲,下标为0
        haffTree[i].flag       = 0;         // 未合并到哈夫曼树,为0
        haffTree[i].leftChild  = -1;        // 无左右孩子,给特殊在-1
        haffTree[i].rightChild = -1;
    }
    // 构建哈夫曼树
    for (int i = 0; i < n - 1; i++) {
        m1 = m2 = MaxValue;
        x1 = x2 = 0;
        // 循环找到所有权重中最小的两个
        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;
            }
        }
        
        // 将找到权重最小的两个子树合并为一个子树
        haffTree[x1].parent = n + i;
        haffTree[x2].parent = n + i;
        
        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;
    }
}

// 哈夫曼编码
void HaffmanCode(HaffNode haffTree[], int n, Code haffCode[])
{
    // 创建一个结点
    Code *cd = (Code *)malloc(sizeof(Code));
    
    int child, parent;
    
    // 求n个结点的哈夫曼编码
    for (int i = 0; i < n; i++) {
        // 从0开始计数
        cd->start = 0;
        // 取得编码对应权值的字符
        cd->weight = haffTree[i].weight;
        // 当前叶子节点为 i
        child = i;
        // 根据叶子节点找到双亲节点
        parent = haffTree[i].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;
        }
        
        // 此时的编码是倒序的,翻转 011 start = 3
        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;
    }
}

// 调用
void show{
        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);
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章