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 相关文章推荐
WINDOWS 2000下使用ISAPI方式安装PHP
Sep 05 PHP
并发下常见的加锁及锁的PHP具体实现代码
Oct 12 PHP
巧用php中的array_filter()函数去掉多维空值的代码分享
Sep 07 PHP
php中cookie的使用方法
Mar 29 PHP
PHP里8个鲜为人知的安全函数分析
Dec 09 PHP
Thinkphp单字母函数使用指南
May 08 PHP
zen cart实现订单中增加paypal中预留电话的方法
Jul 12 PHP
PHP socket 模拟POST 请求实例代码
Jul 18 PHP
php文件上传 你真的掌握了吗
Nov 28 PHP
yii2利用自带UploadedFile实现上传图片的示例
Feb 16 PHP
PHP注释语法规范与命名规范详解篇
Jan 21 PHP
用PHP的反射实现委托模式的讲解
Mar 22 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
asp和php下textarea提交大量数据发生丢失的解决方法
2008/01/20 PHP
为PHP初学者的8点有效建议
2010/11/20 PHP
PHP实现手机归属地查询API接口实现代码
2012/08/27 PHP
PHP获取指定函数定义在哪个文件中以及其所在的行号实例
2014/05/08 PHP
PHP实现对站点内容外部链接的过滤方法
2014/09/10 PHP
php htmlentities()函数的定义和用法
2016/05/13 PHP
jQuery针对各类元素操作基础教程
2014/08/29 Javascript
js实现飞入星星特效代码
2014/10/17 Javascript
java中String类型变量的赋值问题介绍
2016/03/23 Javascript
AngularJS在IE下取数据总是缓存问题的解决方法
2016/08/05 Javascript
nodejs更改项目端口号的方法
2018/05/13 NodeJs
详解JS函数stack size计算方法
2018/06/18 Javascript
微信小程序实现即时通信聊天功能的实例代码
2018/08/17 Javascript
jQuery实现鼠标移入移出事件切换功能示例
2018/09/06 jQuery
layui 富文本赋值,取值,取纯文本值的实例
2019/09/18 Javascript
如何在微信小程序中存setStorage
2019/12/13 Javascript
[01:04:39]OG vs Mineski 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/18 DOTA
python自动化测试之从命令行运行测试用例with verbosity
2014/09/28 Python
用十张图详解TensorFlow数据读取机制(附代码)
2018/02/06 Python
Python代码块批量添加Tab缩进的方法
2018/06/25 Python
python 列表,数组和矩阵sum的用法及区别介绍
2018/06/28 Python
对python 多线程中的守护线程与join的用法详解
2019/02/18 Python
基于keras输出中间层结果的2种实现方式
2020/01/24 Python
解决python和pycharm安装gmpy2 出现ERROR的问题
2020/08/28 Python
python re模块常见用法例举
2021/03/01 Python
CSS3 Notes: -webkit-box-reflect实现倒影的实例
2016/12/08 HTML / CSS
html5新增的定时器requestAnimationFrame实现进度条功能
2018/12/13 HTML / CSS
Canvas 文本转粒子效果的实现代码
2019/02/14 HTML / CSS
美国照明、家居装饰和家具购物网站:Bellacor
2017/09/20 全球购物
新西兰网上购物,折扣店:BestDeals.co.nz
2019/03/20 全球购物
股东授权委托书
2014/10/15 职场文书
学校拾金不昧表扬信
2015/01/16 职场文书
承诺保证书格式
2015/02/28 职场文书
护士自荐信怎么写
2015/03/06 职场文书
回门宴新娘答谢词
2015/09/29 职场文书
浅谈MySQL之浅入深出页原理
2021/06/23 MySQL