哈夫曼树以及应用(哈夫曼编码)概念和建立——附带实现代码以及习题

1 概念

1.1 哈夫曼树

  • 定义:n个带权叶子结点的二叉树中,带权路径长度最小的二叉树。
    • 权:树结点被赋予一个某种一亿的数值;
    • 带权路径长度:从树根结点到任意结点的路径长度(经过的边数)与该结点上权值的乘积。
  • 性质:
    • 1 哈夫曼树可以不唯一,但最小带权路径长度一定是唯一的;
    • 2 哈夫曼树只有度为1的结点;
    • 3 权越小的结点,离根结点越远。

1.2 哈夫曼编码

  • 前缀码:任何一个字符的编码都不是另一个编码的前缀。
  • 前缀码存在意义:不产生混淆,让解码能正常进行。
  • 哈夫曼编码背景:
    • 对于一个给定字符串,有多种前缀码方式,为了信息传递的效率,尽量选择长度最短的编码方式。
    • 把每个字符出现的次数(频率)作为各自叶子结点的权值,那么字符编码成01串的长度为这颗树的带权路径长度。
    • 而带权路径长度最小的树为哈夫曼树。
  • 定义:由哈夫曼树产生的编码方式为哈夫曼编码。

2 实现

2.1 哈夫曼树

  • 构造思想:反复选择两个最小的元素,合并,直到只剩一个元素。
  • 实现方式:堆或优先队列

2.1.2 示例

合并果子
  在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
  例如有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。
所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。
输入
  输入包括两行,第一行是一个整数n(1<=n<=10000),表示果子的种类数。
  第二行包含n个整数,用空格分隔,第i个整数ai(1<=ai<=20000)是第i种果子的数目。
输出
  输出包括一行,这一行只包含一个整数,也就是最小的体力耗费值。输入数据保证这个值小于2^31。

样例输入 Copy
3
1 2 9
样例输出 Copy
15
  • 方法一(优先队列):
#include <cstdio>
#include <queue>

using std::vector;
using std::priority_queue;
using std::greater;

//小顶堆优先队列
priority_queue<long long, vector<long long>, greater<long long>> q;

int main(int argc, char const *argv[])
{
    int n;
    long long temp, x, y ,ans = 0;
    
    scanf("%d", &n);

    for (int i = 0; i < n; ++i)
    {
        scanf("%lld", &temp);
        q.push(temp);
    }

    while(q.size() > 1){
        x = q.top();
        q.pop();

        y = q.top();
        q.pop();

        q.push(x + y);
        ans += x + y;
    }

    printf("%lld\n", ans);
    return 0;
}
  • 方法二:小顶堆
#include <cstdio>
#include <algorithm>

using std::swap;

const int MAXN = 20010;
long long heap[MAXN];
int n;

void downAdjust(int low, int high){
    int i = low, j = i * 2;
    while(j <= high){
        if(j + 1 <= high && heap[j + 1] < heap[j]){
            j = j + 1;
        }

        if(heap[j] < heap[i]){
            swap(heap[j], heap[i]);
            i = j;
            j = i * 2;
        }else{
            break;
        }
    }
}

void createHeap(){
    for (int i = n / 2; i >= 1; ++i)
    {
        downAdjust(i, n);
    }
}

void deleteTop(){
    heap[1] = heap[n--];
    downAdjust(1, n);
}

void upAdjust(int low, int high){
    int i = high, j = i / 2;
    while(j >= low){
        if(heap[j] > heap[i]){
            swap(heap[j], heap[i]);
            i = j;
            j = i / 2;
        }else{
            break;
        }
    }

}

void Insert(int x){
    heap[++n] = x;
    upAdjust(1, n);
}

int main(int argc, char const *argv[])
{
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
    {
        scanf("%lld", &heap[i]);
    }

    createHeap();

    long long ans = 0;
    long long x, y;
    while(n > 1){
        x = heap[1];
        deleteTop();
        y = heap[1];
        deleteTop();

        Insert(x + y);
        ans += x + y;
    }

    printf("%lld\n", ans);
    return 0;
}

2.2 哈夫编码

2.2.1 从叶子到根的逆向顺序构造哈夫曼编码

#include <cstdio>
#include <cstring>
#include <limits.h>
#include <algorithm>

using std::swap;
using  std::strcpy;

typedef char *HuffmanCode;

const int MAXN = 110;

typedef struct{
    int weight;
    int lchild, rchild, parent;
}HuffmanNode, *HuffmanTree;

void selectMin(HuffmanTree HT, int n, int &s1, int &s2){
    int min = INT_MAX;
    for (int i = 1; i <= n; ++i) {
        if(HT[i].parent == 0 && min > HT[i].weight){
            min = HT[i].weight;
            s1 = i;
        }
    }

    min = INT_MAX;
    for (int j = 1; j <= n; ++j) {
        if(HT[j].parent == 0 && min > HT[j].weight && j != s1){
            min = HT[j].weight;
            s2 = j;
        }
    }

    if(s1 > s2){
        swap(s1, s2);
    }

}

void HuffmanCoding(HuffmanTree &HT, HuffmanCode *&HC,int w[],int n){
    // w存放n个字符的权值(均>0),构造哈夫曼树HT,并求出n个字符的哈夫曼树编码HC
    if(n <= 1) return;
    int m = 2 * n -1;//树的所有结点个数
    //0号单元未用
    HT = new HuffmanNode[m + 1];

    //初始化叶子结点
    for (int i = 1; i <= n; ++i) {
        HT[i].weight = w[i];
        HT[i].lchild = HT[i].rchild = HT[i].parent = 0;
    }
    //初始化非叶子结点
    for (int i = n + 1; i<= m; ++i) {
        HT[i].lchild = HT[i].rchild = HT[i].parent = 0;
    }


    for (int i = n + 1; i <= m; ++i)//建立哈夫曼树
    {
        int s1, s2;
        //在HT[1~i-1]中选择parent为0且weight最小的两个结点,其序号分别为s1,s2
        selectMin(HT, i - 1, s1, s2);
        HT[s1].parent = HT[s2].parent = i;
        HT[i].lchild = s1;
        HT[i].rchild = s2;
        HT[i].weight = HT[s1].weight + HT[s2].weight;

    }
    //从叶结点到根逆向求每个字符的哈夫曼编码
    HC = new HuffmanCode[n + 1];
    //分配n个字符编码的头指针向量([0]不用)
    char* cd = new char[n];//分配编码工作空间

    cd[n - 1] = '\0';//编码结束符

    for (int i = 1; i <= n; ++i)//逐个字符求哈夫曼编码
    {
        int star = n - 1;//编码结束位置
        for (int c = i, f = HT[i].parent;f != 0; c = f, f = HT[f].parent)
        {
            //从叶结点到根逆向求编码
            if(HT[f].lchild == c){
                cd[--star] = '0';
            }else{
                cd[--star] = '1';
            }
        }
            //为第i个字符串编码分配空间,最后一个空间作为结束符
            HC[i] = new char[n - star];
            //从cd复制编码(串)到HC
            strcpy(HC[i], cd + star);
    }
    delete []cd;//释放工作空间
}

int main(){
    HuffmanTree HT;
    HuffmanCode *HC;
    int n;
    int data[MAXN];

    while(scanf("%d", &n) != EOF){
        for (int i = 1; i <= n; ++i) {
            scanf("%d", &data[i]);
        }

        HuffmanCoding(HT, HC, data, n);

        for (int j = 1; j <= n; ++j) {
            printf("%s\n", HC[j]);
        }

        delete(HT);
        delete(HC);
    }

    return 0;
}
/*
哈夫曼的层序遍历为:100,42,58,23,19,29,29,11,8,14,15,5,3,7,8
Input:
8
5 29 7 8 14 23 3 11
Output:
0110
10
1110
1111
110
00
0111
010
 */

2.2.2 根出发直到叶子构造赫夫曼编码

#include <cstdio>
#include <limits.h>
#include <algorithm>
#include <cstring>

using std::swap;
using std::strcpy;

typedef char *HuffmanCode;
const int MAXN = 110;

typedef struct
{
    int parent, lchild, rchild;
    int weight;
}HuffmanNode, *HuffmanTree;

void selectMin(HuffmanTree HT, int n, int &s1, int &s2){
    int min = INT_MAX;
    for (int i = 1; i <= n; ++i) {
       if(HT[i].parent == 0 && min > HT[i].weight){
           min = HT[i].weight;
           s1 = i;
       }
    }

    min = INT_MAX;
    for (int i = 1; i <= n; ++i) {
        if(HT[i].parent == 0 && min > HT[i].weight && i != s1){
            min = HT[i].weight;
            s2 = i;
        }
    }

    if(s1 > s2){
        swap(s1, s2);
    }
}

//无栈非递归遍历哈夫曼树,求哈夫曼编码
void HuffmanCoding(HuffmanTree &HT, HuffmanCode *&HC, int w[], int n){
    if(n <= 1) return;
    int m = 2 * n - 1;

    HT = new HuffmanNode[m + 1];
    //分配n个字符编码的头指针向量[0]不用

    for (int i = 1; i <= n; ++i) {
        HT[i].weight = w[i];
        HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
    }

    for (int i = n + 1; i <= m; ++i) {
        HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
    }

    //构建哈夫曼树
    for (int i = n + 1; i <= m; ++i) {
        int s1, s2;
        selectMin(HT, i - 1, s1, s2);
        HT[i].lchild = s1;
        HT[i].rchild = s2;
        HT[s1].parent = HT[s2].parent = i;
        HT[i].weight = HT[s1].weight + HT[s2].weight;
    }

    HC = new HuffmanCode[n + 1];
    char* cd = new char[n];
    int c = m;
    int cdlen = 0;
    cd[n - 1] = '\0';
    for (int i = 1; i <= m; ++i)
    {
        HT[i].weight = 0;//遍历哈夫曼树作结点状态标志
    }

    while(c) {
        if (HT[c].weight == 0) {//向左
            HT[c].weight = 1;
            if (HT[c].lchild != 0) {
                c = HT[c].lchild;
                cd[cdlen++] = '0';
            } else if (HT[c].rchild == 0) {//登记叶子结点的编码
                HC[c] = new char[cdlen + 1];
                cd[cdlen] = '\0';
                strcpy(HC[c], cd);//复制编码(串)
            }
        } else if (HT[c].weight == 1) {//向右
            HT[c].weight = 2;
            if (HT[c].rchild != 0) {
                c = HT[c].rchild;
                cd[cdlen++] = '1';
            }
        } else {//HT[c].weight == 2, 退回
            HT[c].weight = 0;
            c = HT[c].parent;
            --cdlen;//退回父结点,编码长度减一
        }

    }
    delete []cd;
}




int main(int argc, char const *argv[])
{
    HuffmanTree HT;
    HuffmanCode *HC;
    int n;
    int data[MAXN];

    while(scanf("%d", &n) != EOF){
        for (int i = 1; i <= n; ++i) {
            scanf("%d", &data[i]);
        }

        HuffmanCoding(HT, HC, data, n);
        for (int i = 1; i <= n; ++i) {
            printf("%s\n", HC[i]);
        }
        delete(HC);
        delete(HT);
    }

    return 0;
}
/*
Input:
8
5 29 7 8 14 23 3 11
Output:
0110
10
1110
1111
110
00
0111
010
 */
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

繁星蓝雨

如果觉得文章不错,可以请喝咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值