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作的文本留言本的例子(六)
Oct 09 PHP
php for 循环语句使用方法详细说明
May 09 PHP
使用NetBeans + Xdebug调试PHP程序的方法
Apr 12 PHP
采集邮箱的php代码(抓取网页中的邮箱地址)
Jul 17 PHP
解析php开发中的中文编码问题
Aug 08 PHP
PHP实现的增强性mhash函数
May 27 PHP
解决nginx不支持thinkphp中pathinfo的问题
Jul 21 PHP
php加密解密字符串示例
Oct 13 PHP
yii2简单使用less代替css示例
Mar 10 PHP
PHP递归实现文件夹的复制、删除、查看大小操作示例
Aug 11 PHP
浅谈PHP接入(第三方登录)QQ登录 OAuth2.0 过程中遇到的坑
Oct 13 PHP
laravel 修改记住我功能的cookie保存时间的方法
Oct 14 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+iFrame实现页面无需刷新的异步文件上传
2014/09/16 PHP
如何使用纯PHP实现定时器任务(Timer)
2015/07/31 PHP
PHP框架性能测试报告
2016/05/08 PHP
PHP使用微信开发模式实现搜索已发送图文及匹配关键字回复的方法
2017/09/13 PHP
jquery attr 设定src中含有&amp;(宏)符号问题的解决方法
2011/07/26 Javascript
jQuery EasyUI API 中文文档 - Pagination分页
2011/09/29 Javascript
使用js修改客户端注册表的方法
2013/08/09 Javascript
jQuery右下角旋转环状菜单特效代码
2015/08/10 Javascript
js如何判断是否在iframe中及防止网页被别站用iframe嵌套
2017/01/11 Javascript
JavaScript基本语法_动力节点Java学院整理
2017/06/26 Javascript
Express使用html模板的详细代码
2017/09/18 Javascript
vue2.0项目实现路由跳转的方法详解
2018/06/21 Javascript
JS中使用cavas截图网页并解决跨域及模糊问题
2018/11/13 Javascript
实例讲解JS中pop使用方法
2019/01/27 Javascript
通过实践编写优雅的JavaScript代码
2019/05/30 Javascript
Element InfiniteScroll无限滚动的具体使用方法
2020/07/27 Javascript
python正则分组的应用
2013/11/10 Python
使用Nginx+uWsgi实现Python的Django框架站点动静分离
2016/03/21 Python
python利用有道翻译实现&quot;语言翻译器&quot;的功能实例
2017/11/14 Python
基于DataFrame改变列类型的方法
2018/07/25 Python
Python数据可视化:泊松分布详解
2019/12/07 Python
Python如何实现小程序 无限求和平均
2020/02/18 Python
解决django接口无法通过ip进行访问的问题
2020/03/27 Python
pycharm如何使用anaconda中的各种包(操作步骤)
2020/07/31 Python
详解css3 flex弹性盒自动铺满写法
2020/09/17 HTML / CSS
美国最大的存储市场:SpareFoot
2018/07/23 全球购物
乌克兰最大的家用电器和电子产品连锁店:Eldorado
2019/10/02 全球购物
配置管理计划的主要内容有哪些
2014/06/20 面试题
善意的谎言事例
2014/02/15 职场文书
研讨会主持词
2014/04/02 职场文书
比赛口号霸气押韵
2015/12/24 职场文书
2016关于读书活动的心得体会
2016/01/14 职场文书
使用numpy nonzero 找出非0元素
2021/05/14 Python
vue+springboot实现登录验证码
2021/05/27 Vue.js
Redis特殊数据类型HyperLogLog基数统计算法讲解
2022/06/01 Redis
jdbc中自带MySQL 连接池实践示例
2022/07/23 MySQL