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 相关文章推荐
在线短消息收发的程序,不用数据库
Oct 09 PHP
PHP生成excel时单元格内换行问题的解决方法
Aug 26 PHP
php数组去重实例及分析
Nov 26 PHP
thinkphp的URL路由规则与配置实例
Nov 26 PHP
php使用explode()函数将字符串拆分成数组的方法
Feb 17 PHP
php校验表单检测字段是否为空的方法
Mar 20 PHP
php可扩展的验证类实例(可对邮件、手机号、URL等验证)
Jul 09 PHP
PHP截取IE浏览器并缩小原图的方法
Mar 04 PHP
PHP/ThinkPHP实现批量打包下载文件的方法示例
Jul 31 PHP
Laravel模型事件的实现原理详解
Mar 14 PHP
PHP后期静态绑定之self::限制实例分析
Dec 21 PHP
Memcached介绍及php-memcache扩展安装
Apr 01 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
对squid中refresh_pattern的一些理解和建议
2009/04/17 PHP
PHP动态输出JavaScript代码实例
2015/02/12 PHP
Ajax中的JSON格式与php传输过程全面解析
2017/11/14 PHP
PHP join()函数用法与实例讲解
2019/03/11 PHP
如何用javascript去掉字符串里的所有空格
2007/02/08 Javascript
JS类中定义原型方法的两种实现的区别
2007/03/08 Javascript
JavaScript性能优化 创建文档碎片(document.createDocumentFragment)
2010/07/13 Javascript
JQuery for与each性能比较分析
2013/05/14 Javascript
简单实用的反馈表单无刷新提交带验证
2013/11/15 Javascript
解析offsetHeight,clientHeight,scrollHeight之间的区别
2013/11/20 Javascript
JS中判断JSON数据是否存在某字段的方法
2014/03/07 Javascript
Angular Js文件上传之form-data
2015/08/28 Javascript
JavaScript Split()方法
2015/12/18 Javascript
Bootstrap学习系列之使用 Bootstrap Typeahead 组件实现百度下拉效果
2016/07/07 Javascript
AngularJS基础 ng-value 指令简单示例
2016/08/03 Javascript
AngularJs 动态加载模块和依赖
2016/09/15 Javascript
微信小程序开发之录音机 音频播放 动画实例 (真机可用)
2016/12/08 Javascript
jQueryMobile之窗体长内容的缺陷与解决方法实例分析
2017/09/20 jQuery
layui的table单击行勾选checkbox功能方法
2018/08/14 Javascript
微信小程序上传多图到服务器并获取返回的路径
2019/05/05 Javascript
详解js实时获取并显示当前时间的方法
2019/05/10 Javascript
什么时候不能在 Node.js 中使用 Lock Files
2019/06/24 Javascript
小程序如何获取多个formId实现详解
2019/09/20 Javascript
JS实现简单移动端鼠标拖拽
2020/07/23 Javascript
[46:00]DOTA2上海特级锦标赛主赛事日 - 2 胜者组第一轮#4EG VS Fnatic第一局
2016/03/03 DOTA
浅谈Python中用datetime包进行对时间的一些操作
2016/06/23 Python
python实现解数独程序代码
2017/04/12 Python
python3中property使用方法详解
2019/04/23 Python
TensorFlow通过文件名/文件夹名获取标签,并加入队列的实现
2020/02/17 Python
简述Linux文件系统通过i节点把文件的逻辑结构和物理结构转换的工作过程
2012/04/17 面试题
最新销售员个人自荐信
2013/09/21 职场文书
教育系毕业生中文求职信范文
2013/10/06 职场文书
个人委托书格式
2014/04/04 职场文书
坚守艰苦奋斗精神坚决反对享乐主义整改措施
2014/09/17 职场文书
党支部班子“四风”问题自我剖析材料
2014/09/28 职场文书
浅谈python中的多态
2021/06/15 Python