结构

压缩软件的核心在于压缩算法。基于Huffman编码的压缩算法思路:

  1. 二进制方式读取源文件,按照每8bits作为一个字符;
  2. 统计每个字符的出现频率即为叶子结点的权值,按照Huffman算法得到每个叶子的编码;
  3. 对源文件的每个字符,将新的编码组合为二进制流,按照每8bits一个单位写入压缩文件。

举例来看:

假设我们有待压缩源文件helloh的ASCII码为01101000,同理可得整个文件的二进制形式0110100001100101011011000110110001101111,共5B,40bits。

根据Huffman算法:得到h的编码为00,同理可得整个文件的Huffman编码为0001111110,末尾不够8bits,采用补0的方法可得0001111110000000,按照每8bits一个单位,写入压缩文件的是31255对应的字符,共2B,16bits。

解压缩流程是压缩的逆过程:

  1. 二进制方式读取压缩文件;
  2. 每次取1bit,从Huffman树的根结点出发,找到某个叶子即为源字符。

效果

做一个简单的比较:

压缩软件 测试文件 压缩率 测试文件 压缩率
CompressIt txt(840B) 70.4% png(282KB) 101%
WinRaR txt(840B) 14.4% png(282KB) 100%

压缩率和压缩时间和专业软件没法比。之所以出现压缩文件大于源文件,是因为压缩文件中还存储了Huffman树等信息,为解压所需。

对于不同内容的文件,得到的压缩文件大小也不尽相同,这主要与Huffman编码的性质有关。

理论分析

Huffman编码依赖于信源的统计特征,其背后的原理在于为出现频率高的字符分配尽可能短的码长,这样就可以降低平均码长:

\[L=\Sigma p_il_i
\]

使得\(L\)最短的编码就是最优编码,可以证明Huffman编码是一种最优编码。

同时Huffman编码还是前缀码,简化了解码过程。

假设一种理想情况:源文件长\(len\)很大,共有\(m\)种不同字符,每个字符用8bits表示,并且每种字符出现频率\(\frac{len}{m}\)相同,忽略掉存储Huffman树等信息所需的空间。

这棵完全二叉树共有结点\(n=2*m-1\)个,那么树深度为\(h=1+\lfloor log_2n \rfloor\),每个字符的压缩长度为\(h-1=\lfloor log_2n \rfloor\),故压缩后的串长度为\(\frac{(h-1)*len}{8}\),可得压缩率\(\frac{h-1}{8}\),即:

\[\alpha=\frac{\lfloor log_2(2*m-1) \rfloor}{8}
\]

源文件中不同字符种类\(m\)越小,即源文件分布越集中,压缩效果越好。

如果和定长编码比较,可以得到压缩率:

\[\alpha=\frac{\lfloor log_2(2*m-1) \rfloor}{\lceil log_2(m) \rceil}
\]

\(m\)取值256时,Huffman树是一棵满二叉树,压缩率为100%,并不比8位固定长度编码更高效。

收获

  • EOFfeof()

    EOF是一个定义在cstdio头文件中的宏,一般为-1:
#define EOF (-1)

但是如果按照二进制读取文件,对于文件中的-1又该如何处理?

阮一峰的博客说:

在Linux系统之中,EOF根本不是一个字符,而是当系统读取到文件结尾,所返回的一个信号值(也就是-1)。至于系统怎么知道文件的结尾,资料上说是通过比较文件的长度。

我们通常会写出下面程序来读取文件:

int ch;
while ((ch = fgetc(fp)) != EOF) {
// your code here
}

但是fgetc()在到达文件结尾和发生读取错误的情况下都会返回EOF,所以上述代码不严谨,采用feof()函数来判断文件结尾:

int ch;
while (!feof(fp)) {
ch = fgetc(fp);
// your code here
}

但是采用feof()也有一个问题:读取最后一个字符后,feof()仍然返回0,进入循环,fgetc()再向后读取一个字符,feof()才返回1,这样程序会多循环一次。

所以比较安全的写法是:

int ch = fgetc(fp);
while (ch != EOF) {
// your code here
ch = fgetc(fp);
}
if (feof(fp))
puts("End-of-File reached.");
else
puts("Something went wrong.");
  • 虚析构函数

    基类的析构函数一般写成虚函数,做个测试:
class base {
public:
base() {};
virtual ~base() {
cout << "destructor in base" << endl;
}; virtual void f() {
cout << "f in base" << endl;
}
}; class derive :public base {
public:
derive() {};
~derive() {
cout << "destructor in derive" << endl;
}; void f() {
cout << "f in derive" << endl;
}
}; base* p = new derive;
p->f();
delete p;

输出:

f in derive
destructor in derive
destructor in base

如果基类的析构函数不是虚函数,输出:

f in derive
destructor in base

结果并没有调用派生类的析构函数,造成内存泄漏。

所以基类的虚析构函数的作用是:当一个基类指针删除一个派生类对象,确保调用派生类的析构函数

  • 二进制文件

    在压缩过程中,对于不同格式源文件的读取都是采用二进制方式rb

    实际上二进制文件和文本文件并没有本质区别,你所看到的内容取决于打开文件的软件对二进制流的解释方式,文件扩展名帮助计算机知道应该用哪种解释方式,通常的文本文件的解释方式有ASCII码和Unicode码。

CompressIt的更多相关文章

  1. 使用zlib来压缩文件-用delphi描述

    今天用到压缩文件的问题,找了一些网上的资料,后来发现了delphi自身所带的zlib单元,根据例子稍微改变了一些,使它能够符合所有的格式. 使用时,需要Zlib.pas和 Zlibconst.pas两 ...

  2. delphi 压缩

    DELPHI 通过ZLib来压缩文件夹 unit Unit1; interface uses ZLib, Windows, Messages, SysUtils, Variants, Classes, ...

随机推荐

  1. ssh秘钥免交互批量分发脚本

    将以下内容保存为.sh文件后运行即可,需根据各自情况修改ip_up和ip_arr #!/bin/bash #脚本功能:ssh秘钥免交互批量分发 #制 作 人:罗钢 联系方式:278554547@qqc ...

  2. Linux下修改efi启动项

    Linux下有一个efibootmgr工具可以编辑efi启动项,十分方便,简单介绍如下 直接运行efibootmgr会显示出当前所有efi启动项,每个启动项前都有相应编号, 可以使用efibootmg ...

  3. MongoDB查询mgov2的聚合方法

    1.多条表数据累计相加. respCount := struct { Rebatescore int64 //变量命名必须要和查询的参数一样.}{} o := bson.M{"$match& ...

  4. Java第三十五天,用JDBC操作MySQL数据库(一),基础入门

    一.JDBC的概念 Java DataBase Connectivity 从字面意思我们也不难理解,就是用Java语言连接数据库的意思 JDBC定义了Java语言操作所有关系型数据库的规则(接口).即 ...

  5. 痞子衡嵌入式:走进二维码(QR Code)的世界(2)- 初体验(PyQt5.11+MyQR2.3+ZXing+OpenCV4.2.0)

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是走进二维码(QR Code)的世界专题之初体验. 接上篇 <走进二维码(QR Code)的世界(1)- 引言> 继续更文,在 ...

  6. Java日志管理:Logger.getLogger()和LogFactory.getLog()的区别(详解Log4j)

    Java日志管理:Logger.getLogger()和LogFactory.getLog()的区别(详解Log4j) 博客分类: Java综合   第一.Logger.getLogger()和Log ...

  7. syncronized如何上锁

    上锁,根据操作系统所说的原则,对共享变量上锁,对临界区上锁.谁访问临界资源?就给谁上锁 同步监视器,它上锁的对象. 1.用关键字给方法上锁 2.用synchronized代码块上锁 默认上锁对象:th ...

  8. Python之利用jieba库做词频统计且制作词云图

    一.环境以及注意事项 1.windows10家庭版 python 3.7.1 2.需要使用到的库 wordcloud(词云),jieba(中文分词库),安装过程不展示 3.注意事项:由于wordclo ...

  9. 对于之间不平凡的我,为什么会选择IT!(上)

    我相信有很多小伙伴看了我发布的文章后,不知道对大家有无启发,在这里我都非常感谢大家的收看,因为现在收疫情影响,我也看到很多朋友私信我,看你经历这么多是经历了什么,如果大家在上一篇发现的时候会看见我父亲 ...

  10. JMeter分布式压测-常见问题之(Server failed to start: java.rmi.server.ExportException: Listen failed on port: 0; nested exception )

    问题描述: 在Linux环境启动jmeter-server时抛出了如下异常: 问题描述: 1.可能监听的端口被占用,修改端口号2.Server相关的rmi配置需要调整 解决方案: 在目录/apache ...