当前位置:编程学习 > C#/ASP.NET >>

如何实现压缩文本,高分

RT,不考虑中文的问题,一个字符占2个字节,转换成编码后是类似于0,10,11111110,这样的串,那么到底把这些串保存为何类型可以实现真正的压缩呢?如果是BYTE,一个BYTE是八位,可是加入文本中含有26个字母那么最长的编码是1..1(25个1),那么就需要3个BYTE,而事实上可能远不止26个字符。这样如何能实现压缩呢?
--------------------编程问答-------------------- 1. 原理部分:
  有两种形式的重复存在于计算机数据中,zip 就是对这两种重复进行了压缩。
  一种是短语形式的重复,即三个字节以上的重复,对于这种重复,zip用两个数字:1.重复位置距当前压缩位置的距离;2.重复的长度,来表示这个重复,假设这两个数字各占一个字节,于是数据便得到了压缩,这很容易理解。
  一个字节有 0 - 255 共 256 种可能的取值,三个字节有 256 * 256 * 256 共一千六百多万种可能的情况,更长的短语取值的可能情况以指数方式增长,出现重复的概率似乎极低,实则不然,各种类型的数据都有出现重复的倾向,一篇论文中,为数不多的术语倾向于重复出现;一篇小说,人名和地名会重复出现;一张上下渐变的背景图片,水平方向上的像素会重复出现;程序的源文件中,语法关键字会重复出现(我们写程序时,多少次前后copy、paste?),以几十 K 为单位的非压缩格式的数据中,倾向于大量出现短语式的重复。经过上面提到的方式进行压缩后,短语式重复的倾向被完全破坏,所以在压缩的结果上进行第二次短语式压缩一般是没有效果的。
  第二种重复为单字节的重复,一个字节只有256种可能的取值,所以这种重复是必然的。其中,某些字节出现次数可能较多,另一些则较少,在统计上有分布不均匀的倾向,这是容易理解的,比如一个 ASCII 文本文件中,某些符号可能很少用到,而字母和数字则使用较多,各字母的使用频率也是不一样的,据说字母 e 的使用概率最高;许多图片呈现深色调或浅色调,深色(或浅色)的像素使用较多(这里顺便提一下:png 图片格式是一种无损压缩,其核心算法就是 zip 算法,它和 zip 格式的文件的主要区别在于:作为一种图片格式,它在文件头处存放了图片的大小、使用的颜色数等信息);上面提到的短语式压缩的结果也有这种倾向:重复倾向于出现在离当前压缩位置较近的地方,重复长度倾向于比较短(20字节以内)。这样,就有了压缩的可能:给 256 种字节取值重新编码,使出现较多的字节使用较短的编码,出现较少的字节使用较长的编码,这样一来,变短的字节相对于变长的字节更多,文件的总长度就会减少,并且,字节使用比例越不均匀,压缩比例就越大。
  在进一步讨论编码的要求以及办法前,先提一下:编码式压缩必须在短语式压缩之后进行,因为编码式压缩后,原先八位二进制值的字节就被破坏了,这样文件中短语式重复的倾向也会被破坏(除非先进行解码)。另外,短语式压缩后的结果:那些剩下的未被匹配的单、双字节和得到匹配的距离、长度值仍然具有取值分布不均匀性,因此,两种压缩方式的顺序不能变。
  在编码式压缩后,以连续的八位作为一个字节,原先未压缩文件中所具有的字节取值不均匀的倾向被彻底破坏,成为随机性取值,根据统计学知识,随机性取值具有均匀性的倾向(比如抛硬币试验,抛一千次,正反面朝上的次数都接近于 500 次)。因此,编码式压缩后的结果无法再进行编码式压缩。
  短语式压缩和编码式压缩是目前计算机科学界研究出的仅有的两种无损压缩方法,它们都无法重复进行,所以,压缩文件无法再次压缩(实际上,能反复进行的压缩算法是不可想象的,因为最终会压缩到 0 字节)。
  短语式重复的倾向和字节取值分布不均匀的倾向是可以压缩的基础,两种压缩的顺序不能互换的原因也说了,下面我们来看编码式压缩的要求及方法:
--------------------编程问答-------------------- 首先,为了使用不定长的编码表示单个字符,编码必须符合“前缀编码”的要求,即较短的编码决不能是较长编码的前缀,反过来说就是,任何一个字符的编码,都不是由另一个字符的编码加上若干位 0 或 1 组成,否则解压缩程序将无法解码。
看一下前缀编码的一个最简单的例子:


符号 编码
A 0
B 10
C 110
D 1110
E 11110

有了上面的码表,你一定可以轻松地从下面这串二进制流中分辨出真正的信息内容了:

1110010101110110111100010 - DABBDCEAAB

要构造符合这一要求的二进制编码体系,二叉树是最理想的选择。考察下面这棵二叉树:

        根(root)
       0  |   1
       +-------+--------+
    0  | 1   0  |  1
    +-----+------+ +----+----+
    |     | |     |
    a      | d     e
     0  |  1
     +-----+-----+
     |     |
     b     c

要编码的字符总是出现在树叶上,假定从根向树叶行走的过程中,左转为0,右转为1,则一个字符的编码就是从根走到该字符所在树叶的路径。正因为字符只能出现在树叶上,任何一个字符的路径都不会是另一字符路径的前缀路径,符合要求的前缀编码也就构造成功了:

a - 00 b - 010 c - 011 d - 10 e - 11


接下来来看编码式压缩的过程:
为了简化问题,假定一个文件中只出现了 a,b,c,d ,e四种字符,它们的出现次数分别是
a : 6次
b : 15次
c : 2次
d : 9次
e : 1次
如果用定长的编码方式为这四种字符编码: a : 000 b : 001 c : 010 d : 011 e : 100
那么整个文件的长度是 3*6 + 3*15 + 3*2 + 3*9 + 3*1 = 99

用二叉树表示这四种编码(其中叶子节点上的数字是其使用次数,非叶子节点上的数字是其左右孩子使用次数之和):

          根
           |
      +---------33---------+
      |        |
   +----32---+     +----1---+
   |    |     |    |
+-21-+   +-11-+    +--1--+   
|   |   |   |    |   |
6  15  2  9    1   

(如果某个节点只有一个子节点,可以去掉这个子节点。)

         根
         |
        +------33------+
       |     |
    +-----32----+     1
    |      |
  +--21--+  +--11--+
  |   |  |   |
  6   15 2    9

现在的编码是: a : 000 b : 001 c : 010 d : 011 e : 1 仍然符合“前缀编码”的要求。

第一步:如果发现下层节点的数字大于上层节点的数字,就交换它们的位置,并重新计算非叶子节点的值。
先交换11和1,由于11个字节缩短了一位,1个字节增长了一位,总文件缩短了10位。

           根
            |
       +----------33---------+
       |        |
   +-----22----+     +----11----+
   |      |     |     |
+--21--+    1      2     9
|     |
6   15

再交换15和1、6和2,最终得到这样的树:

           根
            |
       +----------33---------+
       |        |
     +-----18----+    +----15----+
    |      |    |     |
  +--3--+    15   6     9
  |   |
  2   1

这时所有上层节点的数值都大于下层节点的数值,似乎无法再进一步压缩了。但是我们把每一层的最小的两个节点结合起来,常会发现仍有压缩余地。

第二步:把每一层的最小的两个节点结合起来,重新计算相关节点的值。

在上面的树中,第一、二、四三层都只有一或二个节点,无法重新组合,但第三层上有四个节点,我们把最小的3和6结合起来,并重新计算相关节点的值,成为下面这棵树。

           根
            |
       +----------33---------+
       |         |
    +------9-----+    +----24----+
    |      |    |     |
   +--3--+    6   15    9
   |   |
  2  1

然后,再重复做第一步。
这时第二层的9小于第三层的15,于是可以互换,有9个字节增长了一位,15个字节缩短了一位,文件总长度又缩短了6位。然后重新计算相关节点的值。

           根
            |
       +----------33---------+
       |        |
       15     +----18----+ 
            |    |
         +------9-----+   9
         |      |
         +--3--+   6
         |   |
         2  1

这时发现所有的上层节点都大于下层节点,每一层上最小的两个节点被并在了一起,也不可能再产生比同层其他节点更小的父节点了。

这时整个文件的长度是 3*6 + 1*15 + 4*2 + 2*9 + 4*1 = 63

这时可以看出编码式压缩的一个基本前提:各节点之间的值要相差比较悬殊,以使某两个节点的和小于同层或下层的另一个节点,这样,交换节点才有利益。
所以归根结底,原始文件中的字节使用频率必须相差较大,否则将没有两个节点的频率之和小于同层或下层其他节点的频率,也就无法压缩。反之,相差得越悬殊,两个节点的频率之和比同层或下层节点的频率小得越多,交换节点之后的利益也越大。

在这个例子中,经过上面两步不断重复,得到了最优的二叉树,但不能保证在所有情况下,都能通过这两步的重复得到最优二叉树,下面来看另一个例子:

                         根
                         |
              +---------19--------+
              |                   |
      +------12------+            7
      |              |
  +---5---+      +---7---+
  |       |      |       |
+-2-+   +-3-+  +-3-+   +-4-+
|   |   |   |  |   |   |   |
1   1   1   2  1   2   2   2

这个例子中,所有上层节点都大于等于下层节点,每一层最小的两个节点结合在了一起,但仍然可以进一步优化:


                         根
                         |
              +---------19--------+
              |                   |
      +------12------+            7
      |              |
  +---4---+      +---8---+
  |       |      |       |
+-2-+   +-2-+  +-4-+   +-4-+
|   |   |   |  |   |   |   |
1   1   1   1  2   2   2   2

通过最低一层的第4第5个节点对换,第3层的8大于第2层的7。
到这里,我们得出这样一个结论:一棵最优二叉编码树(所有上层节点都无法和下层节点交换),必须符合这样两个条件:
1.所有上层节点都大于等于下层节点。
2.某节点,设其较大的子节点为m,较小的子节点为n,m下的任一层的所有节点都应大于等于n下的该层的所有节点。

当符合这两个条件时,任一层都无法产生更小的节点去和下层节点交换,也无法产生更大的节点去和上层节点交换。

上面的两个例子是比较简单的,实际的文件中,一个字节有256种可能的取值,所以二叉树的叶子节点多达256个,需要不断的调整树形,最终的树形可能非常复杂,有一种非常精巧的算法可以快速地建起一棵最优二叉树,这种算法由D.Huffman(戴·霍夫曼)提出,下面我们先来介绍霍夫曼算法的步骤,然后再来证明通过这么简单的步骤得出的树形确实是一棵最优二叉树。
--------------------编程问答-------------------- 霍夫曼算法的步骤是这样的:

·从各个节点中找出最小的两个节点,给它们建一个父节点,值为这两个节点之和。
·然后从节点序列中去除这两个节点,加入它们的父节点到序列中。

重复上面两个步骤,直到节点序列中只剩下唯一一个节点。这时一棵最优二叉树就已经建成了,它的根就是剩下的这个节点。

仍以上面的例子来看霍夫曼树的建立过程。
最初的节点序列是这样的:
a(6)  b(15)  c(2)  d(9)  e(1)

把最小的c和e结合起来
                   | (3)
a(6)   b(15)   d(9)   +------+------+
              |      |
              c     e

不断重复,最终得到的树是这样的:

       根
        |
   +-----33-----+
   |     |
   15   +----18----+    
       |       |
       9  +------9-----+
          |       |
         6     +--3--+
              |   |
              2  1

这时各个字符的编码长度和前面我们说过的方法得到的编码长度是相同的,因而文件的总长度也是相同的: 3*6 + 1*15 + 4*2 + 2*9 + 4*1 = 63

考察霍夫曼树的建立过程中的每一步的节点序列的变化:

6  15 2 9 1
6  15 9 3
15 9  9
15 18
33

下面我们用逆推法来证明对于各种不同的节点序列,用霍夫曼算法建立起来的树总是一棵最优二叉树:

对霍夫曼树的建立过程运用逆推法:
当这个过程中的节点序列只有两个节点时(比如前例中的15和18),肯定是一棵最优二叉树,一个编码为0,另一个编码为1,无法再进一步优化。
然后往前步进,节点序列中不断地减少一个节点,增加两个节点,在步进过程中将始终保持是一棵最优二叉树,这是因为:
1.按照霍夫曼树的建立过程,新增的两个节点是当前节点序列中最小的两个,其他的任何两个节点的父节点都大于(或等于)这两个节点的父节点,只要前一步是最优二叉树,其他的任何两个节点的父节点就一定都处在它们的父节点的上层或同层,所以这两个节点一定处在当前二叉树的最低一层。
2.这两个新增的节点是最小的,所以无法和其他上层节点对换。符合我们前面说的最优二叉树的第一个条件。
3.只要前一步是最优二叉树,由于这两个新增的节点是最小的,即使同层有其他节点,也无法和同层其他节点重新结合,产生比它们的父节点更小的上层节点来和同层的其他节点对换。它们的父节点小于其他节点的父节点,它们又小于其他所有节点,只要前一步符合最优二叉树的第二个条件,到这一步仍将符合。

这样一步步逆推下去,在这个过程中霍夫曼树每一步都始终保持着是一棵最优二叉树。

由于每一步都从节点序列中删除两个节点,新增一个节点,霍夫曼树的建立过程共需 (原始节点数 - 1) 步,所以霍夫曼算法不失为一种精巧的编码式压缩算法。


附:对于 huffman 树,《计算机程序设计艺术》中有完全不同的证明,大意是这样的:
1.二叉编码树的内部节点(非叶子节点)数等于外部节点(叶子节点)数减1。
2.二叉编码树的外部节点的加权路径长度(值乘以路径长度)之和,等于所有内部节点值之和。(这两条都可以通过对节点数运用数学归纳法来证明,留给大家做练习。)
3.对 huffman 树的建立过程运用逆推,当只有一个内部节点时,肯定是一棵最优二叉树。
4.往前步进,新增两个最小的外部节点,它们结合在一起产生一个新的内部节点,当且仅当原先的内部节点集合是极小化的,加入这个新的内部节点后仍是极小化的。(因为最小的两个节点结合在一起,并处于最低层,相对于它们分别和其他同层或上层节点结合在一起,至少不会增加加权路径长度。)
5.随着内部节点数逐个增加,内部节点集合总维持极小化。




2.实现部分
  如果世界上从没有一个压缩程序,我们看了前面的压缩原理,将有信心一定能作出一个可以压缩大多数格式、内容的数据的程序,当我们着手要做这样一个程序的时候,会发现有很多的难题需要我们去一个个解决,下面将逐个描述这些难题,并详细分析 zip 算法是如何解决这些难题的,其中很多问题带有普遍意义,比如查找匹配,比如数组排序等等,这些都是说不尽的话题,让我们深入其中,做一番思考。
--------------------编程问答-------------------- 我们前面说过,对于短语式重复,我们用“重复距当前位置的距离”和“重复的长度”这两个数字来表示这一段重复,以实现压缩,现在问题来了,一个字节能表示的数字大小为 0 -255,然而重复出现的位置和重复的长度都可能超过 255,事实上,二进制数的位数确定下来后,所能表示的数字大小的范围是有限的,n位的二进制数能表示的最大值是2的n次方减1,如果位数取得太大,对于大量的短匹配,可能不但起不到压缩作用,反而增大了最终的结果。针对这种情况,有两种不同的算法来解决这个问题,它们是两种不同的思路。一种称为 lz77 算法,这是一种很自然的思路:限制这两个数字的大小,以取得折衷的压缩效果。例如距离取 15 位,长度取 8 位,这样,距离的最大取值为 32 k - 1,长度的最大取值为 255,这两个数字占 23 位,比三个字节少一位,是符合压缩的要求的。让我们在头脑中想象一下 lz77 算法压缩进行时的情况,会出现有意思的模型:

   最远匹配位置->          当前处理位置->
───┸─────────────────╂─────────────>压缩进行方向
   已压缩部分             ┃    未压缩部分

  在最远匹配位置和当前处理位置之间是可以用来查找匹配的“字典”区域,随着压缩的进行,“字典”区域从待压缩文件的头部不断地向后滑动,直到达到文件的尾部,短语式压缩也就结束了。
  解压缩也非常简单:

         ┎────────拷贝────────┒
 匹配位置    ┃          当前处理位置  ┃
   ┃<──匹配长度──>┃       ┠─────∨────┨
───┸──────────┸───────╂──────────┸─>解压进行方向
   已解压部分              ┃    未解压部分

  不断地从压缩文件中读出匹配位置值和匹配长度值,把已解压部分的匹配内容拷贝到解压文件尾部,遇到压缩文件中那些压缩时未能得到匹配,而是直接保存的单、双字节,解压时只要依次直接拷贝到文件尾部即可,直到整个压缩文件处理完毕。
  lz77算法模型也被称为“滑动字典”模型或“滑动窗口”模型,由于它限制匹配的最大长度,对于某些存在大量的极长匹配的文件来说,这种折衷算法显出了缺陷。另有一种lzw算法对待压缩文件中存在大量极长匹配的情况进行了完全不同的算法设计,并且只用一个数字来表示一段短语,下面来描述一下lzw的压缩解压过程,然后来综合比较两者的适用情况。
  lzw的压缩过程:
1) 初始化一个指定大小的字典,把 256 种字节取值加入字典。
2) 在待压缩文件的当前处理位置寻找在字典中出现的最长匹配,输出该匹配在字典中的序号。
3) 如果字典没有达到最大容量,把该匹配加上它在待压缩文件中的下一个字节加入字典。
4) 把当前处理位置移到该匹配后。
5) 重复 2、3、4 直到文件输出完毕。

  lzw 的解压过程:
1) 初始化一个指定大小的字典,把 256 种字节取值加入字典。
2) 从压缩文件中顺序读出一个字典序号,根据该序号,把字典中相应的数据拷贝到解压文件尾部。
3) 如果字典没有达到最大容量,把前一个匹配内容加上当前匹配的第一个字节加入字典。
4) 重复 2、3 两步直到压缩文件处理完毕。

  从 lzw 的压缩过程,我们可以归纳出它不同于 lz77 算法的一些主要特点:
1) 对于一段短语,它只输出一个数字,即字典中的序号。(这个数字的位数决定了字典的最大容量,当它的位数取得太大时,比如 24 位以上,对于短匹配占多数的情况,压缩率可能很低。取得太小时,比如 8 位,字典的容量受到限制。所以同样需要取舍。)
2) 对于一个短语,比如 abcd ,当它在待压缩文件中第一次出现时,ab 被加入字典,第二次出现时,abc 被加入字典,第三次出现时,abcd 才会被加入字典,对于一些长匹配,它必须高频率地出现,并且字典有较大的容量,才会被最终完整地加入字典。相应地,lz77 只要匹配在“字典区域”中存在,马上就可以直接使用。
3) 一个长匹配被加入字典的过程,是从两个字节开始,逐次增长一个字节,确定了字典的最大容量,也就间接确定了匹配的可能的最大长度。相对于 lz77 用两个数字来表示一个短语,lzw 只用一个数字来表示一个短语,因此,“字典序号”的位数可以取得多一点(二进制数多一位,意味着数值大一倍),也就是说最长匹配可以比 lz77 更长,当某些超长匹配高频率地出现,直到被完整地加入字典后,lzw将开始弥补初期的低效,逐渐显出自己的优势。
  可以看出,在多数情况下,lz77 拥有更高的压缩率,而在待压缩文件中占绝大多数的是些超长匹配,并且相同的超长匹配高频率地反复出现时,lzw 更具优势,GIF 就是采用了 lzw 算法来压缩背景单一、图形简单的图片。zip 是用来压缩通用文件的,这就是它采用对大多数文件有更高压缩率的 lz77 算法的原因。

  接下来 zip 算法将要解决在“字典区域”中如何高速查找最长匹配的问题。
--------------------编程问答-------------------- 我已经晕了。。。lz慢慢看吧 --------------------编程问答-------------------- 你还不如直接给他链接 --------------------编程问答-------------------- 哈哈,帮顶 --------------------编程问答-------------------- up --------------------编程问答-------------------- 看了。。。十分感谢楼上的大哥,我只是请问产生的编码诸如a : 000 b : 001 c : 010 d : 011 e : 100以什么类型保存?整型、字符串、BYTE型?,文章中好像没有给出解释。。麻烦大哥你再给我解答一下 --------------------编程问答-------------------- 复杂!!! --------------------编程问答-------------------- 重新编码,重复次数多的用位数较少的编码表示----霍夫曼树 --------------------编程问答-------------------- 如果只是这个技术 是不可能实现 把3G多的3D游戏压缩到400多K的!~~~ 可是现实是已经存在这个技术了 但是压缩消耗CPU 和解压 消耗的 CPU 太大了~ --------------------编程问答-------------------- google 下一堆出来 --------------------编程问答-------------------- 学习中 --------------------编程问答-------------------- 目前想到的办法是把经过编码的文件保存在BYTE数组中,这个在各字符数频率分布正常的情况下有一定效率的,只是不稳定 --------------------编程问答-------------------- 我更晕 --------------------编程问答-------------------- 关注中...

------------------------
http://blog.cngator.com --------------------编程问答-------------------- 搞那么复杂的答案干什么啊!!! --------------------编程问答--------------------







汗。。。。。。。。。。
打击我幼小的心灵
让刚踏入编程大门的我感到了深深的恐惧
这是人干的事么
答案中带了些许的疑惑
编程的路途太艰难
但是也很有成就感
虽说付出的越多 收获的越多
但令我迷茫的是 
当快到收获的季节的时候,我是否早已老去。。。





--------------------编程问答-------------------- mark学习哈 --------------------编程问答-------------------- 跟着学习!
--------------------编程问答-------------------- 用net 3.0 的ZipPackage类 --------------------编程问答-------------------- 复杂... --------------------编程问答-------------------- 汗。。。。。。。。。。
打击我幼小的心灵
让刚踏入编程大门的我感到了深深的恐惧
这是人干的事么
答案中带了些许的疑惑
编程的路途太艰难
但是也很有成就感
虽说付出的越多 收获的越多
但令我迷茫的是 
当快到收获的季节的时候,我是否早已老去。。。

===========================

他们是变态。别理他们。。。 --------------------编程问答-------------------- 单纯用霍夫曼算法压缩文本是很难达到很高的压缩率的,一般是先用字典压缩算法
如LZ77,LZW算法进行压缩,再使用霍夫曼算法对字典压缩后的数据再进行一次压缩。
ZIP 压缩就是这样进行的。
--------------------编程问答-------------------- 学习了  就像楼上说的 打击我幼小的心灵 --------------------编程问答-------------------- 学习 --------------------编程问答-------------------- 都老高的人。学习
.net对字符串的压缩与解压缩 --------------------编程问答-------------------- 无语,人家说的很好啊,压缩算法压缩率和效率是最重要的。至于实现压缩后当然是二制流,如果010是数字,你自己想想,会有压缩效果吗?不过通常霍夫曼资源消耗不低。具体的数法跟实际使用环境有关,比如我们公司的工程师做的压缩算法,根据情况,把地区名直接编成号,这样压缩率极高。 --------------------编程问答-------------------- string 是可以序列化的

所以序列化(如果只有英文的话选择合适的编码,只占用1个字节)后用开源的压缩
再保存 --------------------编程问答-------------------- mark --------------------编程问答-------------------- 学习了 --------------------编程问答-------------------- 二进制存  哈夫曼树
补充:.NET技术 ,  C#
CopyRight © 2012 站长网 编程知识问答 www.zzzyk.com All Rights Reserved
部份技术文章来自网络,