【贪心算法】哈夫曼编码问题

2023-10-29

问题描述

哈夫曼编码是广泛地用于数据文件压缩的十分有效的编码方法。其压缩率通常在20%~90%之间。哈夫曼编码算法用字符在文件中出现的频率表来建立一个用0,1串表示各字符的最优表示方式。一个包含100,000个字符的文件,各字符出现频率不同,设某信源产生a,b,c,d和f 6种符号,其频率见下表,单位为千次。
在这里插入图片描述
从表中可以看出a字符出现的频率最高——45千次,f出现的频率最低——5千次。
要区分6种不同的字符需要长度为3的0-1字符串表示,因此很自然的想到将a编码为000,b编码为001,依此类推。这种一视同仁的编码就是定长码编码。
在这里插入图片描述
定长码编码的好处就是译码简单,因为所有编码长度都相等于3,因此在在译码的时候,按照长度3译码,那么就是正确的。
比如收到的是000 001 000字符串,按照长度3分别断开,因此000译码为a,001译码为b,按此规则译码就得到了我们所需要的信息。

问题目标

给定编码字符集C及任一字符c的出现频率f©,定长编码的平均码长。
在这里插入图片描述

求解目标

找到使平均码长达到最小的编码方案。

定长码
在这里插入图片描述
定长码的平均码长:
(45+13+12+16+9+6)* 3 =300
那么定长码方案是字符集的最优编码吗?不是

在1951年,哈夫曼在MIT信息理论课程,需要完成学期报告:寻找最有效的二进制编码。
1952年,根据香农(Shannon)在1948年和范若(Fano)在1949年阐述的编码思想提出了一种不定长编码的方法,也称哈夫曼编码,即最优编码。

在定长码方案中,没有考虑带字符频率的差异性。

对刚才的例子,对字符集中的6个字符,有的人给出了如下的编码方法
在这里插入图片描述
看起来没有什么问题。那么这种编码方案是否能够保证译码的正确性呢?
假设我们收到字符串为 0 1 0 1 1 1 0 1,根据编码方案进行译码工作:

第一个字符为0,译码为a,后面第二个字符为1,译码b,还是0
后面的1 0 字符一起译码为字符 c ?还是101一起译码为 f 呢? 显然这种编码方案不能保证正确的译码。 原因第一个变长码为0,直接译码为a字符,这是因为 0 不是其余编码的前缀,到了后面的编码1 0 1 为什么不能正确编码了?我们可以看到在这种编码方案中,b的编码 1 他是字符 c 编码 1 0,字符 f 编码1 0 1的前缀,因此就导致了译码的多样性。
这就说明,不定长编码除了考虑字符的频率之外,还需要考虑每个字符的编码不能使其余字符的前缀,因此引入前缀码的概念:

前缀码: 对每一个字符规定一个0,1串作为其代码,并要求任一字符的代码都不是其他字符代码的前缀。这种编码称为前缀码。编码的前缀性质可以使译码方法非常简单;例如001011101可以唯一的分解为0,0,101,1101,因而其译码为aabe。

那么前缀码就能保证译码的确定性?举例
对字符集的编码如下
在这里插入图片描述
上图的编码方案正是前缀码,任何一个字符的代码都不是其他字符代码的前缀,下面对字符串进行译码 0 1 0 1 0 0 0 1
有且只有这种唯一的译码结果
在这里插入图片描述
平均码长:
定长码的平均码长在前求得是 300,那么对于这种不定长编码方案其平均码长为:451 +133 123 +163 +9*4 + 5 *4 =224
显然不定长编码方案比定长编码的平均码小,压缩率达到25%

分析到了这里,对于给定的字符集,哈夫曼编码问题就转换为:寻找使平均码长达到最小的前缀编码的问题。前缀编码应该怎么找?

对于平均码长为224的编码方案,将其组织为一棵二叉树,在如下的二叉树中,有6个叶子结点,在二叉树中,习惯将父节点到左儿子的路标记为0,到右儿子的路标记为1,观察从树根100,到每个树叶的路径,到字符a的路径为0,到b的路径为1 0 1
刚好对应有6个字符的前缀码方案
在这里插入图片描述观察这棵树,出了叶子结点,其余的结点都有2个儿子,因此它是一棵二元完全树,那么问题来了,为什么在二元完全树中,树根到树叶的路径为什么是前缀码? 因为是树叶,因此任何一个字符的编码,都不是其他字符编码的前缀。

哈夫曼编码

在这里插入图片描述求解目标:找到使平均码长达到最小的前缀码方案。

贪心算法如何求解哈夫曼编码问题

贪心策略:频率小的字符,优先入队

步骤
·、按照字符频繁f©,非减序排列,存入队列Q
2、从队列Q中选择频率最小的两棵树将其合并,将新频率插入队列Q直到得到n个结点的二元完全树。

将哈夫曼编码归结为两小无猜的求解思想。两小就是指当前最小的2个频率,无猜是指无条件的贪心选择他们合并

上述算法的主要计算量在于安装频率排序,故时间复杂度为O(nlog
).

举例

字符集C={a,b,c,d,e} , F={7,16,12,9,8}
解:按照频率排序得到当前的队列为:
7 8 9 12 16
将队列中7,8出队合并频率为15的新结点
在这里插入图片描述
得到新的队列:9 ,12, 15, 16
然后队列中9 和12出队合并为频率为21的新结点
在这里插入图片描述
依次合并,得到如下的二叉树
在这里插入图片描述下面找出该二元完全二叉树所对应的前缀码方案

字符 频率
a 100
b 101
c 00
d 01
e 11

平均码长B(T)=37 +38 +29 +212 +2*16 =119

说明
路标习惯上:0指示某结点到左儿子,1指示某结点到右儿子
同一父节点的2个儿子结点顺序是任意的,哈夫曼编码不唯一
最优编码的平均码长是一致的。

废话少说上代码

    include <iostream>
    using namespace std;
     
    //最大字符编码数组长度
    #define MAXCODELEN 100
    //最大哈夫曼节点结构体数组个数
    #define MAXHAFF 100
    //最大哈夫曼编码结构体数组的个数
    #define MAXCODE 100
    #define MAXWEIGHT  10000;
     
     
    typedef struct Haffman
    {
        //权重
        int weight;
        //字符
        char ch;
        //父节点
        int parent;
        //左儿子节点
        int leftChild;
        //右儿子节点
        int rightChild;
    }HaffmaNode;
     
    typedef struct Code
    {
        //字符的哈夫曼编码的存储
        int code[MAXCODELEN];
        //从哪个位置开始
        int start;
    }HaffmaCode;
     
    HaffmaNode haffman[MAXHAFF];
    HaffmaCode code[MAXCODE];
     
    void buildHaffman(int all)
    {
        //哈夫曼节点的初始化之前的工作, weight为0,parent,leftChile,rightChile都为-1
        for (int i = 0; i < all * 2 - 1; ++i)
        {
            haffman[i].weight = 0;
            haffman[i].parent = -1;
            haffman[i].leftChild = -1;
            haffman[i].rightChild = -1;
        }
        std::cout << "请输入需要哈夫曼编码的字符和权重大小" << std::endl;
        for (int i = 0; i < all; i++)
        {
            std::cout << "请分别输入第个" << i << "哈夫曼字符和权重" << std::endl;
            std::cin >> haffman[i].ch;
            std::cin >> haffman[i].weight;
        }
        //每次找出最小的权重的节点,生成新的节点,需要all - 1 次合并
        int x1, x2, w1, w2;
        for (int i = 0; i < all - 1; ++i)
        {
            x1 = x2 = -1;
            w1 = w2 = MAXWEIGHT;
            //注意这里每次是all + i次里面便利
            for (int j = 0; j < all + i; ++j)
            {
                //得到最小权重的节点
                if (haffman[j].parent == -1 && haffman[j].weight < w1)
                {
                    //如果每次最小的更新了,那么需要把上次最小的给第二个最小的
                    w2 = w1;
                    x2 = x1;
                    x1 = j;
                    w1 = haffman[j].weight;
                }
                //这里用else if而不是if,是因为它们每次只选1个就可以了。
                else if(haffman[j].parent == -1 && haffman[j].weight < w2)
                {
                    x2 = j;
                    w2 = haffman[j].weight;
                }
            }
            //么次找到最小的两个节点后要记得合并成一个新的节点
            haffman[all + i].leftChild = x1;
            haffman[all + i].rightChild = x2;
            haffman[all + i].weight = w1 + w2;
            haffman[x1].parent = all + i;
            haffman[x2].parent = all + i;
            std::cout << "x1 is" << x1 <<" x1 parent is"<<haffman[x1].parent<< " x2 is" << x2 <<" x2 parent is "<< haffman[x2].parent<< " new Node is " << all + i << "new weight is" << haffman[all + i].weight << std::endl;
        }
    }
    //打印每个字符的哈夫曼编码
    void printCode(int all)
    {
        //保存当前叶子节点的字符编码
        HaffmaCode hCode;
        //当前父节点
        int curParent;
        //下标和叶子节点的编号
        int c;
        //遍历的总次数
        for (int i = 0; i < all; ++i)
        {
            hCode.start = all - 1;
            c = i;
            curParent = haffman[i].parent;
            //遍历的条件是父节点不等于-1
            while (curParent != -1)
            {
                //我们先拿到父节点,然后判断左节点是否为当前值,如果是取节点0
                //否则取节点1,这里的c会变动,所以不要用i表示,我们用c保存当前变量i
                if (haffman[curParent].leftChild == c)
                {
                    hCode.code[hCode.start] = 0;
                    std::cout << "hCode.code[" << hCode.start << "] = 0" << std::endl;
                }
                else
                {
                    hCode.code[hCode.start] = 1;
                    std::cout << "hCode.code[" << hCode.start << "] = 1" << std::endl;
                }
                hCode.start--;
                c = curParent;
                curParent = haffman[c].parent;
            }
            //把当前的叶子节点信息保存到编码结构体里面
            for (int j = hCode.start + 1; j < all; ++j)
            {
                code[i].code[j] = hCode.code[j];
            }
            code[i].start = hCode.start;
        }
    }
    int main()
    {
        std::cout << "请输入有多少个哈夫曼字符" << std::endl;
        int all = 0;
        std::cin >> all;
        if (all <= 0)
        {
            std::cout << "您输入的个数有误" << std::endl;
            return -1;
        }
        buildHaffman(all);
        printCode(all);
        for (int i = 0; i < all; ++i)
        {
            std::cout << haffman[i].ch << ": Haffman Code is:";
            for (int j = code[i].start + 1; j < all; ++j)
            {
                std::cout << code[i].code[j];
            }
            std::cout << std::endl;
        }
        return 0;
    }



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

【贪心算法】哈夫曼编码问题 的相关文章

随机推荐

  • Linux查看文件及文件夹大小

    du sh 查看当前目录下各个文件及目录占用空间大小 du sh 查看当前目录的总大小 df h 查看系统中文件的使用情况 Size 分割区总容量 Used 已使用的大小 Avail 剩下的大小 Use 使用的百分比 Mounted on
  • Uiautomator2

    https github com openatx uiautomator2 官方文档 第一步 先准备一台开启了开发者选项的安卓手机 连接上电脑 确保执行adb devices可以看到连接上的设备 不要开启charles 否则会导致下载失败
  • Window触发器和Delta触发器在大数据处理中的应用

    大数据处理是指处理海量数据的技术和方法 在大数据处理中 窗口触发器 Window Trigger 和Delta触发器 Delta Trigger 是常用的工具 用于按照一定的规则触发数据处理操作 本文将介绍这两种触发器的概念 应用场景 并给
  • 使用java实现http多线程下载

    下载工具我想没有几个人不会用的吧 前段时间比较无聊 花了点时间用java写了个简单的http多线程下载程序 纯粹是无聊才写的 只实现了几个简单的功能 而且也没写界面 今天正好也是一个无聊日 就拿来写篇文章 班门弄斧一下 觉得好给个掌声 不好
  • linux下通过V4L2驱动USB摄像头

    目录 文章目录 目录 前言 v4l2 解析 v4l2 介绍 应用程序通过 V4L2 接口采集视频数据步骤 相关结构体解析 总结 参考链接 前言 在移植罗技C270摄像头到6818的过程中 内核已经检测到了USB摄像头 但是直接用OpenCV
  • /proc/sys/kernel/hung_task_timeout_secs问题

    具体的问题如下 判定是磁盘写入的问题 正在找照成文件卷hung的原因
  • 一维码和二位码主要原理

    1 条码主要分类 Code39码 标准39码 Codabar码 库德巴码 Code25码 标准25码 ITF25码 交叉25码 Matrix25码 矩阵 25码 UPC A码 UPC E码 EAN 13码 EAN 13国际商品条码 EAN
  • EEPROM读写测试实验

    EEPROM是一种用于计算机系统的非易失性存储器 也常在嵌入式领域中作为数据的存储设备 在物联网及可穿戴设备等需要存储少量数据的场景中也有广泛应用 实验任务 本节的实验任务是先向EEPROM AT24C64 的存储器地址0至255分别写入数
  • MongoDB 使用总结

    简介 java系列技术分享 持续更新中 初衷 一起学习 一起进步 坚持不懈 如果文章内容有误与您的想法不一致 欢迎大家在评论区指正 希望这篇文章对你有所帮助 欢迎点赞 收藏 留言 更多文章请点击 文章目录 一 MongoDB简介 二 Mon
  • 臻识科技用全智能相机,把智慧城市的交通/安防/工业制造做到极致

    俨然 智慧城市已经是一个技术密集 资本密集 巨头密集 关注度密集的大热门领域 从技术层面来看 智慧城市对当下热门技术进行了综合 Cloud Big Data AI AR VR 5G IoT Quantum Computing Edge Co
  • 极域课堂管理系统软件V6.0 2016 豪华版

    百度网盘链接地址 https pan baidu com s 1ZXClL84 iFl8klR3Kme5 w 地址链接失效请及时联系本人 QQ 395648542
  • 超实用!深度比较Python对象之间的差异

    本文完整示例代码及文件已上传至Github仓库https github com CNFeffery PythonPracticalSkills 很多情况下我们需要对两条数据之间的差异进行比较 如果仅仅是针对数值型对象 那么两者的差值就是所谓
  • 面试经:一线城市搬砖,又面软件测试岗,5000就知足了...

    今天有个大专生来我公司面试软件测试 他说在 地下城 64开搬砖 一个月能赚7万多 就在上星期 所有的号全被封了 所以来公司上班了 目前有一年多软件测试工作经验 来面试的这个大专生他的自我介绍是这样的 他说 学历大专 大专学的专业是软件技术
  • 《数学建模实战攻略》

    专栏策划 一 目标受众 数学建模实战攻略 面向数学建模初学者 参加数学建模竞赛的学生以及对数学建模有兴趣的研究者和开发者 二 专栏目录 引言 专栏简介与目标 数学建模的重要性及应用领域 数学建模基本概念与方法论 问题抽象与建模过程 常见数学
  • Linux中断原理、上半部和下半部、硬中断和软中断

    目录 1 中断简介 1 1 作用 1 2 物理实现 1 3 中断请求线IRQ 1 4 异常 2 中断处理程序 2 1 作用 2 2 上半部和下半部 2 3 中断上下文 3 中断系统 3 1 中断机制的实现 3 2 中断控制 4 下半部和软中
  • python skimage图像处理(一)

    本文转自 python数字图像处理 基于python脚本语言开发的数字图片处理包 比如PIL Pillow opencv scikit image等 PIL和Pillow只提供最基础的数字图像处理 功能有限 opencv实际上是一个c 库
  • mipi dsi接口_Camera MIPI接口详解2

    简介 上一篇文章中 我们简单的介绍了camera接口的类型 有串口和并口和LVDS接口 以及MIPI接口一些电气特性的一些简单的技术探讨 那么我们现在常用的都是mipi接口 需要深入一点去理解MIPI接口的电气特性 有助于我们接下来理解MI
  • 类脑导航的机理、算法、实现与展望

    类脑导航 CBN 是一种新型的导航方式 其机理基于对大脑和动物行为的理解 与传统导航系统不同的是 CBN借鉴了大脑神经元与突触的工作原理 通过人工神经网络学习和模拟动物的行为 使导航过程更加具有灵活性和适应性 CBN涉及到的算法主要是基于机
  • 区分接口继承和实现继承——条款34

    表面上直截了当的public继承概念 经过严密的检查之后 发现它由两部分组成 函数接口 function interfaces 继承和函数实现 function implementations 继承 这两种继承的差异 很像本书导读所讨论的函
  • 【贪心算法】哈夫曼编码问题

    问题描述 哈夫曼编码是广泛地用于数据文件压缩的十分有效的编码方法 其压缩率通常在20 90 之间 哈夫曼编码算法用字符在文件中出现的频率表来建立一个用0 1串表示各字符的最优表示方式 一个包含100 000个字符的文件 各字符出现频率不同