【数据结构与算法】-哈夫曼树(Huffman Tree)与哈夫曼编码

2023-05-16

超详细讲解哈夫曼树(Huffman Tree)以及哈夫曼编码的构造原理、方法,并用代码实现。

1哈夫曼树基本概念

路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径。

结点的路径长度:两结点间路径上的分支数

树的路径长度:从树根到每一个结点的路径长度之和。记作: TL 

 

权(weight)又称权重:将树中结点赋给一个有着某种含义的数值,(具体的意义根据树使用的场合确定)则这个数值称为该结点的权。比如之前提到的判断树中5%表示对应分数段人在总人数中的比例
结点的带权路径长度:从根结点到该结点之间的路径长度与结点上权的乘积

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

树的路径长度:从树根到每一个结点的路径长度之和。

 哈夫曼树:最优树,带权路径长度(WPL)最短的树

“带权路径长度最短”是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树之称。

哈夫曼树:最优二叉树,带权路径长度(WPL)最短的二叉树,因为构造这种树的算法是由哈夫曼教授于1952年提出的,所以被称为哈夫曼树,相应的算法称为哈夫曼算法。

2.哈夫曼树构造算法

哈夫曼算法(构造哈夫曼树的方法)

(1)根据n个给定的权值(W1,W2,..., Wn)构成n棵二叉树的森林F=(T1, T2,.., Tn),其中Ti只有一个带权为Wi;的根结点。

构造森林全是根

(2)在F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。

选用两小造新树

(3)在F中删除这两棵树,同时将新得到的二叉树加入森林中。

删除两小添新人

(4)重复(2)和(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树。

重复2、3剩单根

 

总结 

1、在哈夫曼算法中,初始时有n棵二叉树,要经过n-1次合并最终形成哈夫曼树。

2、经过n-1次合并产生n-1个新结点,且这n-1个新结点都是具有两个孩子的分支结点。

可见:哈夫曼树中共有n+n-1 =2n-1个结点,且其所有的分支结点的度均不为1。

3.构建哈夫曼树代码实现

3.1哈弗曼树中结点结构

构建哈夫曼树时,首先需要确定树中结点的构成。

由于哈夫曼树的构建是从叶子结点开始,不断地构建新的父结点,直至树根,所以结点中应包含指向父结点的指针。但是在使用哈夫曼树时是从树根开始,根据需求遍历树中的结点,因此每个结点需要有指向其左孩子和右孩子的指针。

//哈夫曼树结点结构
typedef struct {
    int weight;//结点权重
    int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
}HTNode, *HuffmanTree;

 

3.2哈弗曼树中的查找算法

构建哈夫曼树时,需要每次根据各个结点的权重值,筛选出其中值最小的两个结点,然后构建二叉树。

查找权重值最小的两个结点的思想是:从树组起始位置开始,首先找到两个无父结点的结点(说明还未使用其构建成树),然后和后续无父结点的结点依次做比较,有两种情况需要考虑:

  • 如果比两个结点中较小的那个还小,就保留这个结点,删除原来较大的结点;
  • 如果介于两个结点权重值之间,替换原来较大的结点;
//HT数组中存放的哈夫曼树,end表示HT数组中存放结点的最终位置,s1和s2传递的是HT数组中权重值最小的两个结点在数组中的位置
void Select(HuffmanTree HT, int end, int *s1, int *s2)
{
    int min1, min2;
    //遍历数组初始下标为 1
    int i = 1;
    //找到还没构建树的结点
    while(HT[i].parent != 0 && i <= end){
        i++;
    }
    min1 = HT[i].weight;
    *s1 = i;
   
    i++;
    while(HT[i].parent != 0 && i <= end){
        i++;
    }
    //对找到的两个结点比较大小,min2为大的,min1为小的
    if(HT[i].weight < min1){
        min2 = min1;
        *s2 = *s1;
        min1 = HT[i].weight;
        *s1 = i;
    }else{
        min2 = HT[i].weight;
        *s2 = i;
    }
    //两个结点和后续的所有未构建成树的结点做比较
    for(int j=i+1; j <= end; j++)
    {
        //如果有父结点,直接跳过,进行下一个
        if(HT[j].parent != 0){
            continue;
        }
        //如果比最小的还小,将min2=min1,min1赋值新的结点的下标
        if(HT[j].weight < min1){
            min2 = min1;
            min1 = HT[j].weight;
            *s2 = *s1;
            *s1 = j;
        }
        //如果介于两者之间,min2赋值为新的结点的位置下标
        else if(HT[j].weight >= min1 && HT[j].weight < min2){
            min2 = HT[j].weight;
            *s2 = j;
        }
    }
}

3.3 构建算法实现

//HT为地址传递的存储哈夫曼树的数组,w为存储结点权重值的数组,n为结点个数
void CreateHuffmanTree(HuffmanTree *HT, int *w, int n)
{
    if(n<=1) return; // 如果只有一个编码就相当于0
    int m = 2*n-1; // 哈夫曼树总节点数,n就是叶子结点
    *HT = (HuffmanTree) malloc((m+1) * sizeof(HTNode)); // 0号位置不用
    HuffmanTree p = *HT;
    // 初始化哈夫曼树中的所有结点
    for(int i = 1; i <= n; i++)
    {
        (p+i)->weight = *(w+i-1);
        (p+i)->parent = 0;
        (p+i)->left = 0;
        (p+i)->right = 0;
    }
    //从树组的下标 n+1 开始初始化哈夫曼树中除叶子结点外的结点
    for(int i = n+1; i <= m; i++)
    {
        (p+i)->weight = 0;
        (p+i)->parent = 0;
        (p+i)->left = 0;
        (p+i)->right = 0;
    }
    //构建哈夫曼树
    for(int i = n+1; i <= m; i++)
    {
        int s1, s2;
        Select(*HT, i-1, &s1, &s2);
        (*HT)[s1].parent = (*HT)[s2].parent = i;
        (*HT)[i].left = s1;
        (*HT)[i].right = s2;
        (*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
    }
}

4.哈夫曼编码

4.1哈夫曼编码基本概念

在远程通讯中,要将待传字符转换成由二进制的字符串 

若将编码设计为长度不等的二进制编码,即让待传字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少。

关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀,这种编码称做前缀编码

问题:什么样的前缀码能使得电文总长最短?

哈夫曼编码方法:

1、统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)

2、利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。

3、在哈夫曼树的每个分支上标上0或1:

结点的左分支标0,右分支标1

把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。

 两个问题:

1.为什么哈夫曼编码能够保证是前缀编码?

因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀。(字符都是叶子结点,根到一个字符不会路过另一个字符T)

2.为什么哈夫曼编码能够保证字符编码总长最短?

因为哈夫曼树的带权路径长度最短,故字符编码的总长最短。

性质1哈夫曼编码是前缀码

性质2哈夫曼编码是最优前缀码

4.2哈夫曼编码代码实现

使用程序求哈夫曼编码有两种方法:

  1. 从叶子结点一直找到根结点,逆向记录途中经过的标记。例如,图 3 中字符 c 的哈夫曼编码从结点 c 开始一直找到根结点,结果为:0 1 1 ,所以字符 c 的哈夫曼编码为:1 1 0(逆序输出)。
  2. 从根结点出发,一直到叶子结点,记录途中经过的标记。例如,求图 3 中字符 c 的哈夫曼编码,就从根结点开始,依次为:1 1 0。

采用方法 1 的实现代码为:

//HT为哈夫曼树,HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数
void HuffmanCoding(HuffmanTree HT, HuffmanCode *HC,int n){
    *HC = (HuffmanCode) malloc((n+1) * sizeof(char *));
    char *cd = (char *)malloc(n*sizeof(char)); //存放结点哈夫曼编码的字符串数组
    cd[n-1] = '\0';//字符串结束符
   
    for(int i=1; i<=n; i++){
        //从叶子结点出发,得到的哈夫曼编码是逆序的,需要在字符串数组中逆序存放
        int start = n-1;
        //当前结点在数组中的位置
        int c = i;
        //当前结点的父结点在数组中的位置
        int j = HT[i].parent;
        // 一直寻找到根结点
        while(j != 0){
            // 如果该结点是父结点的左孩子则对应路径编码为0,否则为右孩子编码为1
            if(HT[j].left == c)
                cd[--start] = '0';
            else
                cd[--start] = '1';
            //以父结点为孩子结点,继续朝树根的方向遍历
            c = j;
            j = HT[j].parent;
        }
        //跳出循环后,cd数组中从下标 start 开始,存放的就是该结点的哈夫曼编码
        (*HC)[i] = (char *)malloc((n-start)*sizeof(char));
        strcpy((*HC)[i], &cd[start]);
    }
    //使用malloc申请的cd动态数组需要手动释放
    free(cd);
}

采用第二种算法的实现代码为:

//HT为哈夫曼树,HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数
void HuffmanCoding(HuffmanTree HT, HuffmanCode *HC,int n){
    *HC = (HuffmanCode) malloc((n+1) * sizeof(char *));
    int m=2*n-1;
    int p=m;
    int cdlen=0;
    char *cd = (char *)malloc(n*sizeof(char));
    //将各个结点的权重用于记录访问结点的次数,首先初始化为0
    for (int i=1; i<=m; i++) {
        HT[i].weight=0;
    }
    //一开始 p 初始化为 m,也就是从树根开始。一直到p为0
    while (p) {
        //如果当前结点一次没有访问,进入这个if语句
        if (HT[p].weight==0) {
            HT[p].weight=1;//重置访问次数为1
            //如果有左孩子,则访问左孩子,并且存储走过的标记为0
            if (HT[p].left!=0) {
                p=HT[p].left;
                cd[cdlen++]='0';
            }
            //当前结点没有左孩子,也没有右孩子,说明为叶子结点,直接记录哈夫曼编码
            else if(HT[p].right==0){
                (*HC)[p]=(char*)malloc((cdlen+1)*sizeof(char));
                cd[cdlen]='\0';
                strcpy((*HC)[p], cd);
            }
        }
        //如果weight为1,说明访问过一次,即是从其左孩子返回的
        else if(HT[p].weight==1){
            HT[p].weight=2;//设置访问次数为2
            //如果有右孩子,遍历右孩子,记录标记值 1
            if (HT[p].right!=0) {
                p=HT[p].right;
                cd[cdlen++]='1';
            }
        }
        //如果访问次数为 2,说明左右孩子都遍历完了,返回父结点
        else{
            HT[p].weight=0;
            p=HT[p].parent;
            --cdlen;
        }
    }
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【数据结构与算法】-哈夫曼树(Huffman Tree)与哈夫曼编码 的相关文章

  • 基于ROS平台的移动机器人-8-使用Kinect2导航

    基于ROS平台的移动机器人 8 使用Kinect2导航 ready 终于到写最后一篇了 不是经常写博文的老司机果然伤不起 xff01 在这一篇教程就是利用KinectV2来导航啦 go 1 安装一下所需的包 xff08 1 xff09 cd
  • kali linux学习——安装WingIDE(libqt4-webkit软件依靠问题)

    kali linux 中安装wingide xff08 libqt4 webkit软件依靠问题 xff09 走过的坑缺失的libqt4 webkit成功安装WingIDE 走过的坑 在kali linux上利用命令 dpkg i wingi
  • 华为的OD,值得去吗?

    最近有不少小伙伴接到了华为OD的面试邀约 xff0c 但搞不清楚OD到底怎么回事儿 xff0c 要不要去 所以今天来说说华为的OD到底是怎么回事儿 xff0c 怎么判断是否值得去 1 华为的OD是怎么回事儿 OD xff0c 是Outsou
  • 第01课:技术成长的三阶段模型

    引言 作为整个系统课程的第一部分 xff0c 我想先跟大家分享的是如何选择技术方向 xff0c 我将结合技术成长的三阶段模型 xff0c 讨论在入行 构建技能树 技术转型 团队技术方案选型等常见场景中如何选择适合自己的技术 努力只有在方向正
  • 开篇词 | 程序员的成长课

    大家好 xff0c 我是安晓辉 xff0c 做过开发工程师 研发经理 技术总监等岗位 xff0c 现在自由职业 xff0c 专注写作和开发者生涯咨询 出版过 程序员的成长课 Qt Quick 核心编程 你好哇 xff0c 程序员 解忧程序员
  • const与define的区别

    1 define是预编译指令 xff0c const是普通变量的定义 xff0c define定义的宏是在预处理阶段展开的 xff0c 而const定义的只读变量是在编译运行阶段使用的 2 const定义的是变量 xff0c 而define
  • 如何摆脱CRUD等打杂状态,从事更高价值工作

    每个月都会有十来个来询者向我抱怨工作低端 xff0c 程序员说自己每天CRUD xff0c 重复 枯燥 没技术含量 xff0c 销售助理说自己天天搜集客户信息 打印资料 帮老大带饭 xff0c 繁琐 无聊 不重要 xff0c 他们都说自己整
  • Windows下Qt 5.2 for Android开发入门

    Qt 5 2 发布了 xff0c 支持 Android 平台 xff0c 太好了 之前公司项目 xff0c 为了移植一个依赖 Qt 的程序到安卓平台上 xff0c 我自己交叉编译了 Qt Embedded 4 5 2 xff0c 费了老大劲
  • Qt Quick 之 QML 与 C++ 混合编程详解

    Qt Quick 技术的引入 xff0c 使得你能够快速构建 UI xff0c 具有动画 各种绚丽效果的 UI 都不在话下 但它不是万能的 xff0c 也有很多局限性 xff0c 原来 Qt 的一些技术 xff0c 比如低阶的网络编程如 Q
  • 漫谈程序员系列:一张图道尽程序员的出路

    推背图 相传由唐太宗时期的司天监李淳风和袁天罡合著 xff08 此两人其实是超级武学高手 xff0c 参见小椴的 开唐 xff09 xff0c 推算大唐以后中国两千多年的国运盛衰 xff0c 在中国七大预言书中居首 xff0c 是当之无愧的
  • 漫谈程序员系列:咦,你也在混日子啊

    戳你一下 xff0c 疼吗 xff1f 混日子的定义 来自百度百科的定义 xff1a 生活等方面过得不怎么好 xff0c 无目标 xff0c 混混沌沌 混日子 xff1a 即没有理想 xff0c 没有抱负 xff0c 糊里糊涂地生活 也指工
  • QtAndroid详解(1):QAndroidJniObject

    Qt 5 3之后 xff0c 新增了 QtAndroid 名字空间 xff0c 内有下列四个方法 xff1a QAndroidJniObject AndroidActivity int androidSdkVersion void star
  • freeSWITCH安装、配置与局域网测试

    这次来说说 freeSWITCH 的安装和配置 1 安装 freeSWITCH 下载页面 xff1a https freeswitch org confluence display FREESWITCH Installation 我们 Wi
  • 就 3 点,提升工作效率

    要想提高工作效率 xff0c 不论你看什么书 xff0c 看什么文章 xff0c 用什么工具 xff0c 只有下面这三点最重要 xff1a 动力剖析自己 xff0c 找到改善的切入点付诸行动并且坚持 目标驱动 有目标才能高效 我们爬山 xf
  • Python3 下 ROS 的使用 cv_bridge

    Python 3 下 ROSmsg 转 cv2 项目中用到的 Tensorflow2 4 的环境 xff0c 该环境只支持python3 版本 xff0c 项目中遇到不少需要和 ROS 交互的地方 xff0c 所以不断探索 python3
  • 深度图和RGB图对齐

    深度图 canny RGB canny Alignment xff1a code span class token function import span cv2 span class token function import span
  • 认识romfs文件系统

    1 1 什么是romfs romfs 是一个只读文件系统 xff0c 主要用在 mainly for initial RAM disks of installation disks 使用romfs 文件系统可以构造出一个最小的内核 xff0
  • Livox Lidar 特征提取方式总结

    传统的 旋转式雷达 xff0c 激光固定在雷达的旋转部件上 xff0c 依靠转子的转动而获得360的旋转视野 xff0c 由于旋转部件的存在 xff0c 为了获得更加精准的数据 xff0c 需要跟多的人工校准和更复杂的设计 xff0c 也因
  • C++ 菜鸟之路 (四) boost::thread 多线程全解析

    boost thread 的一般用法 boost thread的几个函数 锁 lock 函数多线程函数的限制 官方解释 xff1a http www cplusplus com reference thread thread joinabl
  • ROS 之 advertise 详解

    在学习过程中接触到如下的一段话 span class hljs comment ROS handles span ros span class hljs tag NodeHandle span node tf span class hljs

随机推荐

  • Linux 下 openMP 效率并未提升的解决方案

    OpenMP 正确观察计算时间OpenMP 经验总结 xff08 1 xff09 openmp 线程使用范围 xff08 2 xff09 openmp 多层嵌套的问题 OpenMP 正确观察计算时间 在使用 openmp的过程中 xff0c
  • C++ Yaml文件解析安装及使用 yaml-cpp

    C 43 43 Yaml文件解析安装及使用 安装 yaml cpp克隆官方库编译 yaml cpp 示例代码robot cpprobot yaml编译 robot cpp运行结果 难点分析与总结什么是 a 与 so 文件静态链接库 a 与动
  • 点云数据格式解析 sensor_msgs::PointCloud2

    在使用多线激光的时候需要总是会碰到点云数据 xff0c 这里简单的接受一下点云数据 xff0c 并堆数据结构进行分析 xff0c 方便自己后期对点云特征数据进行处理 文章目录 Rviz中的点云数据点云数据结构分析点云数据 python 解析
  • Arduino 读取GPS 数据发送解析并发布ROS topic(二)

    Arduino 读取GPS 数据发送解析并发布ROS topic 一 https blog csdn net Fourier Legend article details 84107494 概述 本部分将主要讲将串口接受到的数据 xff0c
  • LOAM进行点云地图创建

    3D激光点云数据处理入门 xff08 一 xff09 使用LOAM进行点云地图创建 LOAM 原理简述topic关系算法分析算法伪代码 LOAM 建图实践创建你的 ROS Workspace下载LOAM Package下载数据包运行 LOA
  • ROS - teb_local_planner 参数总结

    参考官方教程 http wiki ros org teb local planner Tutorials 全英文看着有点累 在此总结一下调试的过程和小小的经验 安装 teb local planner sudo apt get instal
  • 路径规划算法(2) - A*寻路算法 python实现及解析

    代码 span class token comment coding 61 utf 8 span span class token keyword import span math span class token comment 启发距离
  • 使用FSMC驱动LCD以及数据线偏移的问题

    FSMC的理解 使用FSMC功能将8080接口的LCD当外部RAM来使用 xff08 数据传给LCD时没经过内部SRAM xff0c 所以一帧图片很大也可以直接传 xff09 xff0c 根据STM的地址分配图可以看出外部RAM的地址由0x
  • C++调用HTTP实现方式

    转自 xff1a http blog 163 com lyz sea blog static 11558670720118245052189 Http访问有两种方式 xff0c GET和POST xff0c 就编程来说GET方式相对简单点
  • 跟我学c++中级篇——STL智能指针再述

    一 智能指针 xff08 smart pointer xff09 在前面的文章分析中对智能指针分析的还是比较多的 xff0c 这里把一具体的遗漏以及一些新的感悟再总结一下 xff0c 以之为鉴 什么是智能指针 xff1f 在C C 43 4
  • 从CAN到CANOpen——准入门大全(二)

    第二节 关于CAN ID xff1a 一个标准帧的ID共11位 xff0c 一个扩展帧的ID共29位 扩展帧的ID分11位和18位的两段 xff0c 如下图所示 RTR显性 xff1a 数据帧 xff1b 隐性 xff1a 远程帧 SRR隐
  • vscode使用clang-format格式化C++代码

    1 安装c c 43 43 插件 2 在首选项 设置中搜索format xff0c 设置Editor Default Formatter为ms vscode cpptools 3 在扩展C C 43 43 中设置 Clang format
  • EC20 GPS RMC格式数据转化

    文章目录 目录 前言 一 RMC是什么 xff1f 二 EC20 输出的RMC解析 1 EC20返回的RMC报文 2 RMC报文解析 3 NMEA数据ddmm mmmm转换成dd ddddd 4 RMC UTC时间转化成北京时间 总结 前言
  • C++ 第三方常用网络库

    From xff1a https www cnblogs com aitantianderuangutou p 11416902 html 1 ACE 庞大 复杂 xff0c 适合大型项目 开源 免费 xff0c 不依赖第三方库 xff0c
  • Postman 使用方法详解

    From xff1a https zhuanlan zhihu com p 534078123 Postman V9 16 绿色版汉化 xff1a https www cr173 com soft 1497202 html Postman是
  • 串口波形分析

    本文使用逻辑分析仪 xff0c 抓取串口波形 xff0c 进而分析串口数据 串口配置为115200波特率 xff0c 8个数据位 xff0c 1个停止位 xff0c 无校验方式 字符1的波形如下图 xff1a 从图中可以看到8个数据位 xf
  • 电影资源 BT PT下载的电影命名 规则 资源 详解

    初识 一般来说 xff0c 正规压制组压制的电影 xff0c 都采用 0day 命名方式 xff0c 即 xff1a 英文名称 版本说明 年份 片源 分辨率 视频编码 音频格式 压制小组 例如文件名 xff1a Jumanji The Ne
  • 对于51单片机的RAM内存分配(包含栈的分配)

    对于51单片机的RAM内存分配 xff08 包含栈的分配 xff09 我使用的是SH79F3283 xff0c 内部RAM有256字节 xff0c 由常规寄存器 静态存储区和堆栈组成的 xff0c 创建一个新的程序默认占用9个字节RAM x
  • 忘记windows密码解决办法(用户密码或SYSKEY)

    64 TOC 还有朋友问第一层的BIOS密码怎么解 xff1a 拔电池就能解 xff0c 断电 xff0c 打开电脑主板 xff0c 找到一个纽扣电池 xff0c 拔掉 xff0c 长按电源键 xff0c 再插电 xff0c 第一层就没了
  • 【数据结构与算法】-哈夫曼树(Huffman Tree)与哈夫曼编码

    超详细讲解哈夫曼树 Huffman Tree 以及哈夫曼编码的构造原理 方法 xff0c 并用代码实现 1哈夫曼树基本概念 路径 从树中一个结点到另一个结点之间的分支构成这两个结点间的路径 结点的路径长度 两结点间路径上的分支数 树的路径长