PHP实现Huffman编码/解码的示例代码


Posted in PHP onApril 20, 2018

Huffman 编码是一种数据压缩算法。我们常用的 zip 压缩,其核心就是 Huffman 编码,还有在 HTTP/2 中,Huffman 编码被用于 HTTP 头部的压缩。

本文就来用 PHP 来实践一下 Huffman 编码和解码。

1. 编码

字数统计

Huffman编码的第一步就是要统计文档中每个字符出现的次数,PHP的内置函数 count_chars() 就可以做到:

$input = file_get_contents('input.txt');
$stat = count_chars($input, 1);

构造Huffman树

接下来根据统计结果构造Huffman树,构造方法在 Wikipedia 有详细的描述。这里用PHP写了一个简易版的:

$huffmanTree = [];
foreach ($stat as $char => $count) {
  $huffmanTree[] = [
    'k' => chr($char),
    'v' => $count,
    'left' => null,
    'right' => null,
  ];
}

// 构造树的层级关系,思想见wiki:https://zh.wikipedia.org/wiki/%E9%9C%8D%E5%A4%AB%E6%9B%BC%E7%BC%96%E7%A0%81
$size = count($huffmanTree);
for ($i = 0; $i !== $size - 1; $i++) {
  uasort($huffmanTree, function ($a, $b) {
    if ($a['v'] === $b['v']) {
      return 0;
    }
    return $a['v'] < $b['v'] ? -1 : 1;
  });
  $a = array_shift($huffmanTree);
  $b = array_shift($huffmanTree);
  $huffmanTree[] = [
    'v' => $a['v'] + $b['v'],
    'left' => $b,
    'right' => $a,
  ];
}
$root = current($huffmanTree);

经过计算之后,$root 就会指向 Huffman 树的根节点

根据Huffman树生成编码字典

有了 Huffman 树,就可以生成用于编码的字典:

function buildDict($elem, $code = '', &$dict) {
  if (isset($elem['k'])) {
    $dict[$elem['k']] = $code;
  } else {
    buildDict($elem['left'], $code.'0', $dict);
    buildDict($elem['right'], $code.'1', $dict);
  }
}
$dict = [];
buildDict($root, '', $dict);

写文件

运用字典将文件内容进行编码,并写入文件。将Huffman编码写入文件的有几个注意的地方:

将编码字典和编码内容一起写入文件后,就没法区分他们的边界了,因此需要在文件开始写入他们各自占用的字节数

PHP提供的 fwrite() 函数一次能写入 8-bit(一个字节)或者是 8的整数倍个bit。但Huffman编码中,一个字符可能只使用 1-bit 表示,PHP不支持只往文件中写入 1-bit 这种操作。所以需要我们自行对编码进行拼接,每凑齐 8-bit 才写入文件。

PHP实现Huffman编码/解码的示例代码

每凑齐8-bit才写入

与第二条类似,最终形成的文件大小一定是 8-bit 的整数倍。所以如果整个编码的大小是 8001-bit的话,还要在末尾补上 7个 0

$dictString = serialize($dict);
// 写入字典和编码各自占用的字节数
$header = pack('VV', strlen($dictString), strlen($input));
fwrite($outFile, $header);
// 写入字典本身
fwrite($outFile, $dictString);

// 写入编码的内容
$buffer = '';
$i = 0;
while (isset($input[$i])) {
  $buffer .= $dict[$input[$i]];
  while (isset($buffer[7])) {
    $char = bindec(substr($buffer, 0, 8));
    fwrite($outFile, chr($char));
    $buffer = substr($buffer, 8);
  }
  $i++;
}
// 末尾的内容如果没有凑齐 8-bit,需要自行补齐
if (!empty($buffer)) {
  $char = bindec(str_pad($buffer, 8, '0'));
  fwrite($outFile, chr($char));
}
fclose($outFile);

解码

Huffman编码的解码相对简单:先读取编码字典,然后根据字典解码出原始字符。

解码过程有个问题需要注意:由于我们在编码过程中,在文件末尾补齐了几个0-bit,如果这些 0-bit 在字典中恰巧是某个字符的编码时,就会造成错误的解码。

所以解码过程中,当已解码的字符数达到文档长度时,就要停止解码。

<?php
$content = file_get_contents('a.out');

// 读出字典长度和编码内容长度
$header = unpack('VdictLen/VcontentLen', $content);
$dict = unserialize(substr($content, 8, $header['dictLen']));
$dict = array_flip($dict);

$bin = substr($content, 8 + $header['dictLen']);
$output = '';
$key = '';
$decodedLen = 0;
$i = 0;
while (isset($bin[$i]) && $decodedLen !== $header['contentLen']) {
  $bits = decbin(ord($bin[$i]));
  $bits = str_pad($bits, 8, '0', STR_PAD_LEFT);
  for ($j = 0; $j !== 8; $j++) {
    // 每拼接上 1-bit,就去与字典比对是否能解码出字符
    $key .= $bits[$j];
    if (isset($dict[$key])) {
      $output .= $dict[$key];
      $key = '';
      $decodedLen++;
      if ($decodedLen === $header['contentLen']) {
        break;
      }
    }
  }
  $i++;
}
echo $output;

试验

我们将Huffman编码Wiki页 的HTML代码保存到本地,进行Huffman编码测试,试验结果:

编码前: 418,504 字节

编码后: 280,127 字节

空间节省了 33%,如果原文的重复内容较多,Huffman编码节省的空间可以达到 50% 以上.

除了文本内容,我们再尝试将一个二进制文件进行Huffman编码,比如 f.lux的安装程序 ,试验结果如下:

编码前: 770,384 字节

编码后: 773,076 字节

编码后反而占用了更大的空间,一方面是由于我们存储字典时,并没有做额外的处理,占用了不少空间。另一方面,二进制文件中,各个字符出现的概率相对比较平均,无法发挥Huffman编码的优势。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
在PHP中利用XML技术构造远程服务(下)
Oct 09 PHP
Godaddy空间Zend Optimizer升级方法
May 10 PHP
PHP的变量总结 新手推荐
Apr 18 PHP
PHP高级对象构建 多个构造函数的使用
Feb 05 PHP
php使用异或实现的加密解密实例
Sep 04 PHP
php面向对象中static静态属性和静态方法的调用
Feb 08 PHP
PHP多文件上传类实例
Mar 07 PHP
PHP YII框架开发小技巧之模型(models)中rules自定义验证规则
Nov 16 PHP
PHP缩略图生成和图片水印制作
Jan 07 PHP
使用PHPStorm+XDebug搭建单步调试环境
Nov 19 PHP
PHP抽象类和接口用法实例详解
Jul 20 PHP
php和js实现根据子网掩码和ip计算子网功能示例
Nov 09 PHP
PHP排序算法之希尔排序(Shell Sort)实例分析
Apr 20 #PHP
PHP排序算法之直接插入排序(Straight Insertion Sort)实例分析
Apr 20 #PHP
PHP排序算法之简单选择排序(Simple Selection Sort)实例分析
Apr 20 #PHP
PHP排序算法之冒泡排序(Bubble Sort)实现方法详解
Apr 20 #PHP
PHP实现二叉树深度优先遍历(前序、中序、后序)和广度优先遍历(层次)实例详解
Apr 20 #PHP
PHP SPL 被遗落的宝石【SPL应用浅析】
Apr 20 #PHP
Laravel 加载第三方类库的方法
Apr 20 #PHP
You might like
php学习之function的用法
2012/07/14 PHP
Window 7/XP 安装Apache 2.4与PHP 5.4 的过程详解
2013/06/02 PHP
php实现连接access数据库并转txt写入的方法
2017/02/08 PHP
JavaScript面向对象之Prototypes和继承
2012/07/12 Javascript
JS写的贪吃蛇游戏(个人练习)
2013/07/08 Javascript
Js点击弹出下拉菜单效果实例
2013/08/12 Javascript
javascript中全局对象的isNaN()方法使用介绍
2013/12/19 Javascript
JQuery的$命名冲突详细解析
2013/12/28 Javascript
javascript实现tab切换特效
2015/11/12 Javascript
js实现文本框输入文字个数限制代码
2015/12/25 Javascript
jQuery 限制输入字符串长度
2016/06/20 Javascript
JS闭包用法实例分析
2017/03/27 Javascript
Vue服务端渲染和Vue浏览器端渲染的性能对比(实例PK )
2017/03/31 Javascript
JS简单生成随机数(随机密码)的方法
2017/05/11 Javascript
vue绑定的点击事件阻止冒泡的实例
2018/02/08 Javascript
解决Mac node版本升级失败的问题
2018/05/16 Javascript
利用angular自动编译andriod APK的绕坑经历分享
2019/03/08 Javascript
详解Vue中使用插槽(slot)、聚类插槽
2019/04/12 Javascript
echarts大屏字体自适应的方法步骤
2019/07/12 Javascript
layui-table获得当前行的上/下一行数据的例子
2019/09/24 Javascript
微信小程序接入腾讯云验证码的方法步骤
2020/01/07 Javascript
[01:42:49]DOTA2-DPC中国联赛 正赛 iG vs PSG.LGD BO3 第一场 2月26日
2021/03/11 DOTA
python画出三角形外接圆和内切圆的方法
2018/01/25 Python
Python判断对象是否相等及eq函数的讲解
2019/02/25 Python
解决pycharm下os.system执行命令返回有中文乱码的问题
2019/07/07 Python
详解python3中用HTMLTestRunner.py报ImportError: No module named 'StringIO'如何解决
2019/08/27 Python
3种python调用其他脚本的方法
2020/01/06 Python
python中的错误如何查看
2020/07/08 Python
彻底搞懂python 迭代器和生成器
2020/09/07 Python
HTML5时代CSS设置漂亮字体取代图片
2014/09/04 HTML / CSS
汽车专业大学生职业生涯规划范文
2014/01/07 职场文书
大学生毕业鉴定
2014/01/31 职场文书
蟋蟀的住宅教学反思
2014/04/26 职场文书
护理目标管理责任书
2014/07/25 职场文书
2015年医生个人工作总结
2015/04/25 职场文书
JavaScript控制台的更多功能
2021/04/28 Javascript