Haffman编码(算法导论)

news2024/12/24 2:47:53

上次算法导论课讲到了Haffman树,笔者惊叹于Haffman编码的压缩效果,故想自己亲自动手尝试写一个极简的Haffman压缩程序。

首先,我们来了解一下什么是Haffman编码

Haffman编码

        赫夫曼编码可以很有效地压缩数据:通常可以节省20%~90%的空间,具体压缩率依赖于数据的特性。我们将待压缩数据看做字符序列。根据每个字符的出现频率,赫夫曼贪心算法构造出字符的最优二进制表示。我们考虑一种二进制字符编码的方法,每个字符用一个唯一的二进制串表示,称为码字。如果使用定长编码,需要使用3位来表示6个字符:a=000,b=001,…,f=101。这种方法需要300000个二进制位来编码文件。是否有更好的编码方案呢?

         变长编码(variable-length code)可以达到比定长编码好得多的压缩率,其思想是赋予高频字符短码字,赋予低频字符长码字。下图显示了本例的一种变长编码:1位的串0表示a,4位的串1100表示f。因此,这种编码表示此文件共需(45·1+13·3+12·3+16·3+9·4+5·4)· 1000=224000位,与定长编码相比节约了25%的空间。实际上,我们将看到,这是此文件的最优字符编码。

前缀码 

        我们这里只考虑所谓前缀码(prefix code),即没有任何码字是其他码字的前缀。虽然我们这里不会证明,但与任何字符编码相比,前缀码确实可以保证达到最优数据压缩率,因此我们只关注前缀码,不会丧失一般性。

        任何二进制字符码的编码过程都很简单,只要将表示每个字符的码字连接起来即可完成文件压缩。例如,使用上图所示的变长前缀码,我们可以将3个字符的文件 abc编码为0·101·100-0101100, “·”表示连结操作。

        前缀码的作用是简化解码过程。由于没有码字是其他码字的前缀,编码文件的开始码字是无歧义的。我们可以简单地识别出开始码字,将其转换回原字符,然后对编码文件剩余部分重复这种解码过程。在我们的例子中,二进制串001011101可以唯一地解析为0·0·101 · 1101,解码为aabe。

        解码过程需要前缀码的一种方便的表示形式,以便我们可以容易地截取开始码字。一种二叉树表示可以满足这种需求,其叶结点为给定的字符。字符的二进制码字用从根结点到该字符叶结点的简单路径表示,其中0意味着“转向左孩子”,1意味着“转向右孩子”。下图给出了两个编码示例的二叉树表示。注意,编码树并不是二叉搜索树,因为叶结点并未有序排列,而内部结点并不包含字符关键字。

        上图为图1中编码方案的二叉树表示。每个叶结点标记了一个字符及其出现频率。每个内部结点标记了其子树中叶结点的频率之和。(a)对应定长编码a=000,…,f=101的二叉树。(b)对应最优前缀码a=0,b=101,…,f=1100的二叉树

        给定一棵对应前缀码的树T,我们可以容易地计算出编码一个文件需要多少个二进制位。对于字母表C中的每个字符c,令属性c. freq表示c在文件中出现的频率,令dT(c)表示c的叶结点在树中的深度。注意,dT(c)也是字符c的码字的长度。则编码文件需要

个二进制位,我们将 B(T)定义为T的代价。

Haffman树构造过程

        我们假定C是一个n个字符的集合,而其中每个字符c∈C都是一个对象,其属性c. freq给出了字符的出现频率。算法自底向上地构造出对应最优编码的二叉树T。它从|C|个叶结点开始,执行|C|-1个“合并”操作创建出最终的二叉树。算法使用一个以属性 freq为关键字最小优先队列Q,以识别两个最低频率的对象将其合并。当合并两个对象时,得到的新对象的频率设置为原来两个对象的频率之和。

        对前文给出的例子,赫夫曼算法的执行过程如下图所示。由于字母表包含6个字母,初始队列大小为n=6,需要5个合并步骤构造二叉树。最终的二叉树表示最优前缀码。一个字母的码字为根结点到该字母叶结点的简单路径上边标签的序列。

代码实现

哈夫曼编码压缩文件的主要思路:

 1. 读取某个文本文件, 统计文件中各个字符出现的次数作为权重

 2. 构建哈夫曼树, 生成每个字符对应的编码, 然后将编码保存到压缩文件中

 3. 文件解压缩实际就是将压缩文件翻译过来再保存到解压缩文件的过程

HaffmanTree.h

#ifndef HAFFMANTREE_H_INCLUDED
#define HAFFMANTREE_H_INCLUDED

/**
 *  Haffman树的顺序存储
 *  哈夫曼编码压缩文件的主要思路:
 *  1. 读取某个文本文件, 统计文件中各个字符出现的次数作为权重
 *  2. 构建哈夫曼树, 生成每个字符对应的编码, 然后将编码保存到压缩文件中
 *  3. 文件解压缩实际就是将压缩文件翻译过来再保存到解压缩文件的过程
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_SIZE 256
#define HALF_MAX MAX_SIZE / 2
#define ASCII_SIZE 128              // ASCII码的数量0~127个字符

/** 哈夫曼树的结点 */
typedef struct haffNode
{
    char data;                      // 用来存放结点字符的数据域
    int weight;                     // 权重
    struct haffNode * leftChild;
    struct haffNode * rightChild;
} HaffNode;

/** 以顺序结构存储的树节点 - 编码解码的字符映射表 */
HaffNode node[MAX_SIZE];
/** 用来保存所有左孩子节点的数组*/
HaffNode left[HALF_MAX];
/** 用来保存所有右孩子节点的数组*/
HaffNode right[HALF_MAX];

/** 使用二维数组来存储字符的哈夫曼编码, 其中第一维的下标就是这个字符的ASCII码*/
char code[MAX_SIZE][HALF_MAX];
// char code[][] = {
//      "00", "10", "1101", ......
// };

/**
 *  构造哈夫曼树
 *  @param node 结点数组
 *  @param length 节点数组的长度
 */
void CreateHaffmanTree(HaffNode * node, int length);

/** 冒泡排序, 默认以权值大小降序排列*/
void SortHaffmanNode(HaffNode * node, int length);

/**
 *  编码过程(压缩)
 *  @param node 结点数组
 *  @param tempCode 编码后的字符数组(keepCode)
 *  @param index 当前字符数组下标
 */
void Coding(HaffNode * node, char * tempCode, int index);

/** 解码过程 flag - 0/1 标志 */
HaffNode * unzip(HaffNode * node, int flag);

#endif // HAFFMANTREE_H_INCLUDED

HaffmanTree.c 

#include "HaffmanTree.h"

/**
 *  构造哈夫曼树
 *  @param node 哈夫曼树的(根)节点
 *  @param length 节点数组的长度
 */
void CreateHaffmanTree(HaffNode * node, int length)
{
    if(length <= 1)
    {
        return ;
    }
    SortHaffmanNode(node, length);
    // 构建一个以node数组最后两个结点组成的父节点
    HaffNode parent;        // 声明一个父节点
    left[length] = node[length - 1];        // 排序后, length-1就是权重最小的结点
    right[length] = node[length - 2];       // length-2就是权重次小的结点
    parent.weight = left[length].weight + right[length].weight;
    parent.leftChild = &left[length];
    parent.rightChild = &right[length];
    // parent 结点的data不用赋值
    // 将倒数第二位替换为parent结点, 数组长度减一, 递归创建哈夫曼树
    node[length - 2] = parent;
    CreateHaffmanTree(node, length - 1);
}

/**
 *  编码过程(压缩)
 *  @param node 结点数组
 *  @param tempCode 编码后的字符数组(keepCode)
 *  @param index 当前字符数组下标
 */
void Coding(HaffNode * node, char * tempCode, int index)
{
    if(!node) return ;
    // 处理叶节点 - 所有的字符结点都是叶子结点
    if(node->leftChild == NULL || node->rightChild == NULL)
    {
        // 将编码赋值到编码数组中去
        tempCode[index] = '\0';
        strcpy(code[node->data - 0], tempCode);
        return ;
    }

    // 左分支编码为'0', 右分支编码为'1'
    tempCode[index] = '0';
    Coding(node->leftChild, tempCode, index + 1);
    tempCode[index] = '1';
    Coding(node->rightChild, tempCode, index + 1);
}

/** 解码过程 flag - 0/1 标志 */
HaffNode * unzip(HaffNode * node, int flag)
{
    if(flag == 0)
    {
        return node->leftChild;
    }
    else if(flag == 1)
    {
        return node->rightChild;
    }
    return NULL;
}

/** 冒泡排序, 默认以权值大小降序排列*/
void SortHaffmanNode(HaffNode * node, int length)
{
    HaffNode tempNode;
    for(int i = 0; i < length - 1; ++ i)
    {
        for(int j = 0; j < length - i - 1; ++ j)
        {
            if(node[j].weight < node[j + 1].weight)
            {
                tempNode = node[j];
                node[j] = node[j + 1];
                node[j + 1] = tempNode;
            }
        }
    }
}

注:此处没有按照算法导论上说的那样,使用优先队列(实际上就是堆嘛),笔者使用了最简单的冒泡排序,每次将最小的两个数合并之后,调用一次冒泡排序,读者可在此处进行扩充,改用堆排序或直接使用C++ STL中的优先队列hh~ 

main.c 

#include <stdio.h>
#include <stdlib.h>
#include "HaffmanTree.h"

int main()
{
    unsigned char saveChar = 0;     // 保存到二进制文件的无符号字符
    unsigned char tempChar;
    printf("使用哈夫曼树实现文本文件的压缩: (暂时只支持英文文件)\n");
    FILE * inputFile = fopen("pride-and-prejudice.txt", "r");     // 待解码文件
    FILE * zipedFile = fopen("ziped.txt", "wb");    // 编码压缩后的文件

    int fileLength = 0;             // 文件中存放的字符个数
    // 存放0-127个字符出现的次数 - 权数组
    int asciiCount[ASCII_SIZE] = {0};
    // 读取待编码的文件, 统计各字符出现的次数
    char readChar;
    // 逐字符读取文件
    while((readChar = fgetc(inputFile)) != EOF)
    {
        fileLength ++;
        // 读取到的字符就作为asciiCount数组的下标
        asciiCount[readChar - 0] ++;
    }
    int num = 0;        // 节点数量(计数器)
    for(int i = 0; i < ASCII_SIZE; ++ i)
    {
        if(asciiCount[i] != 0)
        {
            node[num].data = i;
            node[num].weight = asciiCount[i];
            num ++;
        }
    }

    // 创建哈夫曼树
    CreateHaffmanTree(node, num);
    // 进行哈夫曼编码
    char tempCode[HALF_MAX];
    Coding(node, tempCode, 0);

    // 逐位将编码保存到文件zipedFile中
    num = 0;
    fseek(inputFile, 0L, 0);    // 文件指针复位
    int zipedLength = 0;        // 压缩后的字符数量
    // 遍历读取到的这个字符编码("10", "111", "1101" ......)
    while((readChar = fgetc(inputFile)) != EOF)
    {
        for(int i = 0; i < strlen(code[readChar - 0]); ++ i)
        {
            saveChar |= code[(int)readChar][i] - '0';
            num ++;
            if(num == 8)
            {   // 每8位写入一次文件
                fwrite(&saveChar, sizeof(unsigned char), 1, zipedFile);
                zipedLength ++;
                num = 0;
                saveChar = 0;
            }
            else
            {
                saveChar <<= 1;
            }
        }
    }
    // 如果最后剩余的编码不足8位, 就移动到最左端, 凑够8位
    if(num < 8)
    {
        saveChar = saveChar << (8 - num);
        fwrite(&saveChar, sizeof(unsigned char), 1, zipedFile);
        zipedLength ++;
        saveChar = 0;
    }
    fclose(inputFile);
    fclose(zipedFile);
    printf("压缩成功\n压缩前字符个数: %d\t压缩后字符个数: %d\n", fileLength, zipedLength);
    printf("压缩比: %.2f%%\n", (double)zipedLength / fileLength * 100);

    printf("\n使用哈夫曼树实现解压缩: \n");
    zipedFile = fopen("ziped.txt", "rb");
    FILE * resultFile = fopen("result.txt", "w");
    num = 0;    // 计数器清零
    HaffNode * currNode = &node[0];
    while(fread(&readChar, sizeof(unsigned char), 1, zipedFile))
    {
        if(fileLength == num)   break;
        // 遍历readChar中的每个二进制数字
        for(int i = 0; i < 8; ++ i)
        {
            tempChar = readChar & 128;  // 取readChar得最高位
            tempChar >>= 7;
            readChar <<= 1;             // 因为最高位已经被取, 所以左移1位
            currNode = unzip(currNode, tempChar - 0);
            // 判断叶节点
            if(currNode->leftChild == NULL || currNode->rightChild == NULL)
            {
                fprintf(resultFile, "%c", currNode->data);
                num ++;
                currNode = &node[0];
            }
        }
    }

    fclose(zipedFile);
    fclose(resultFile);
    printf("解压缩完成, 请查看文件: result.txt\n");

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/477386.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UNIX环境高级编程——进程关系

9.1 引言 本章详细说明进程组以及会话的概念&#xff0c;还将介绍登录shell&#xff08;登录时所调用的&#xff09;和所有从登录shell启动的进程之间的关系。 9.2 终端登录 9.3 网络登录 9.4 进程组 每个进程除了有一进程ID之外&#xff0c;还属于一个进程组&#xff0c;进…

一曲微茫度余生 ——川剧《李亚仙》唱响香港西九戏曲中心

2023年4月28日晚&#xff0c;香港西九戏曲中心灯火辉煌。重庆市川剧院携手成都市川剧研究院带来的川剧《李亚仙》首场演出在这个为戏曲而设的世界级舞台重磅上演。 此次访演受香港西九戏曲文化中心的邀请&#xff0c;原重庆市文化和旅游发展委员会党委书记、主任刘旗带队&…

『LogDashboard』.NET开源的日志面板——登录授权页面扩展

&#x1f4e3;读完这篇文章里你能收获到 了解LogDashboard扩展开源项目——LogDashboard.Authorization掌握LogDashboard扩展内置登录页的使用方式 文章目录 一、LogDashbaord介绍1. 功能支持2. 快速开始 二、LogDashboard.Authorization1. 功能介绍2. 项目接入3. 更多示例 一…

Python语法学习

目录 Openmv用micro python开发的原因 print函数 列表 元组 判断 if...else... if...elif...else 循环 for循环 while循环 强制类型转换 点灯之路 点个不同颜色的闪烁LED 本文章仅作为个人的Openmv学习笔记&#xff0c;有问题欢迎指正~ Openmv用micro python开发…

【MPC|云储能】基于模型预测控制(MPC)的微电网调度优化的研究(matlab代码)

目录 1 主要内容 2 程序难点及问题说明 3 部分程序 4 下载链接 1 主要内容 该程序分为两部分&#xff0c;日前优化部分——该程序首先根据《电力系统云储能研究框架与基础模型》上面方法&#xff0c;根据每个居民的实际需要得到响应储能充放电功率&#xff0c;优化得到整体…

性能测评:阿里云服务器ECS通用型g8i实例CPU内存安全存储

阿里云服务器ECS通用型实例规格族g8i采用2.7 GHz主频的Intel Xeon(Sapphire Rapids) Platinum 8475B处理器&#xff0c;3.2 GHz睿频&#xff0c;g8i实例采用阿里云全新CIPU架构&#xff0c;可提供稳定的算力输出、更强劲的I/O引擎以及芯片级的安全加固。阿里云百科分享阿里云服…

真题详解(DNS)-软件设计(六十三)

真题详解&#xff08;有向图&#xff09;-软件设计&#xff08;六十二)https://blog.csdn.net/ke1ying/article/details/130443040 顺序存储&#xff1a;元素和存储空间相对位置来表示数据元素之间逻辑关系。 RFB&#xff1a;远程访问图形用户界面的简单协议。 在ISO/IEC9126软…

【五一创作】跑alpaca-lora语言模型的常见问题(心得)

训练部署alpaca-lora语言模型常见问题 Alpaca-Lora是一个开源的自然语言处理框架&#xff0c;使用深度学习技术构建了一个端到端的语言模型。在训练和部署alpaca-lora语言模型时&#xff0c;可能会遇到一些常见问题。本文将介绍一些这些问题及其解决方法。 1. bitsandbytes版…

计算机视觉毕业后找不到工作怎么办?怒刷leetcode,还是另寻他路?

文章目录 一、计算机视觉毕业后找不到工作怎么办&#xff1f;二、大环境&#xff1a;前两年的泡沫太大三、还是要把自己的基本功搞扎实&#xff0c;真正的人才什么时候都紧缺四、转换思路&#xff0c;另投他坑五、要有毅力&#xff0c;心态放平六、最后的建议 一、计算机视觉毕…

Python毕业设计之django社区报修维修预约上门服务系统

开发语言&#xff1a;Python 框架&#xff1a;django Python版本&#xff1a;python3.7.7 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat 开发软件&#xff1a;PyCharm 目 录 摘 要 I Pick to II 1绪论 1 1.1项目研究的背景 1 1.2开发意义 1 1.3项…

「数据架构」介绍下一代主数据管理(MDM)

主数据管理是旨在创建和维护权威、可靠、可持续、准确、及时和安全的环境的过程和技术框架。这个环境代表了一个单一版本的事实&#xff0c;作为跨不同的系统、业务单元和用户社区的可接受的记录系统。 尽管MDM不是新的&#xff0c;但是最近人们对开发MDM解决方案的兴趣大增。这…

正则表达式 Regular Expression

情景引入改代码查找文件词法分析器网站注册密码信息爬取 简介在线测试工具RegulexRegExr 语法普通字符非打印字符特殊字符限定符定位符修饰符元字符 实例匹配邮箱 情景引入 改代码 修改代码格式问题&#xff0c;或者重命名代码里的某个变量等&#xff0c;都可以使用 VS Code …

Mysql 管理

目录 0 课程视频 1 系统数据库 -> 安装完mysql ->自带四个数据库 2 常用工具 -> 写脚本用 2.1 mysql 客户端工具 2.2 mysqladmin 2.3 mysqlbinlog -> 二进制日志 -> 运维讲解 2.4 mysqlshow 2.5 mysqldump 备份用 ->导出 2.6 mysqlimport/source -…

一篇带你了解大厂都在用的DDD领域驱动设计

一、DDD到底是什么 DDD全称Domain Driven Design&#xff0c;领域驱动设计。 为了解决快速变化、复杂系统的设计问题的 领域驱动设计是Eric Evans在2004年发表的Domain Driven Design&#xff08;领域驱动设计&#xff0c;DDD)著作中提出的一种从系统分析到软件建模的一套方…

K8s基础2——部署单Master节点K8s集群、切换containerd容器运行时

文章目录 一、部署K8S集群方式二、kubeadm工具搭建K8s集群2.1 资源配置2.2 服务器规划2.3 搭建流程2.3.1 操作系统初始化2.3.2 使用docker容器引擎2.3.3 安装cri-dockerd2.3.4 安装kubeadm&#xff0c;kubelet和kubectl2.3.5 master节点初始化2.3.6 加入node节点2.3.7 部署容器…

redhat 7.9 安装oracle 11g-11.2.0.4

redhat 7.9 安装oracle 11g-11.2.0.4 1、数据库下载和安装文档1.1、查看oracle 11g 适合安装的linux版本1.2、安装文档1.3、license种类解释&#xff08; XE版 标准本 个人版 企业版&#xff09;1.4、在安装完oracle后再创建数据库1.5、DBA的文档1.6、Automatic Storage Manage…

2.4 等比数列

学习步骤&#xff1a; 如果我要学习等比数列&#xff0c;我会按照以下步骤进行学习&#xff1a; 定义和性质&#xff1a;首先了解等比数列的定义和性质&#xff0c;包括公比、首项、通项公式、求和公式等。 例题练习&#xff1a;通过练习一些简单的例题来理解等比数列的概念和…

BMS的菊花链技术和AFE

文章目录 菊花链在BMS中的位置菊花链拓扑菊花链通信AFE&#xff08;Analog Front End&#xff09;AFE均衡电路菊花链应用示例MC33665AMC33665A SPI通信 MC33775AMC33775A 硬件资源 文章参考 菊花链在BMS中的位置 如下图&#xff0c;AFE在从板中&#xff0c;用来采集电池电压和…

ChatGPT在语音识别技术领域的应用

第一章&#xff1a;引言 近年来&#xff0c;随着深度学习技术的飞速发展&#xff0c;语音识别技术已经成为了人工智能领域中备受关注的重要领域之一。在语音识别技术的应用中&#xff0c;ChatGPT作为一款先进的语言模型&#xff0c;可以发挥其强大的文本生成和自然语言处理能力…

#详细介绍!!! 文件系统的一点相关知识

本文主要是介绍了一些计算机文件相关的基础知识&#xff0c;帮助读者更好的认识文件 目录 1.内存和外存 内存 特性1&#xff1a;读写速度快 特性2&#xff1a;内存中的数据不能永久存储 特性3&#xff1a;容量小&#xff0c;价格贵 外存 内存和外存对比总结 2.认识文件 …