使用预先计算的字典在小块中进行无损压缩

时间:2009-07-02 03:03:37

标签: algorithm compression

我有一个应用程序,我正在阅读和写入数百万个小数据块(数百个字节)。我想基于示例数据文件生成压缩字典,并在读取和写入小块时永远使用该字典。我倾向于LZW压缩算法。维基百科页面(http://en.wikipedia.org/wiki/Lempel-Ziv-Welch)列出了压缩和解压缩的伪代码。修改它看起来相当简单,因此字典创建是一个单独的代码块。所以我有两个问题:

  1. 我是在正确的轨道还是有更好的方法?
  2. 为什么LZW算法在解压缩步骤中会添加到字典中?我可以省略它,还是会在字典中失去效率?
  3. 感谢。

    更新:现在我认为理想的情况是找到一个库,让我将字典与压缩数据分开存储。有什么相似的吗?

    更新:我最终在http://www.enusbaum.com/blog/2009/05/22/example-huffman-compression-routine-in-c处获取了代码并进行了调整。我是该页面评论中的克里斯。我通过电子邮件将我的mod发回给了博客作者,但我还没有收到回复。我用这个代码看到的压缩率并不令人印象深刻。也许这是由于8位树的大小。

    更新:我将其转换为16位,压缩效果更好。它也比原始代码快得多。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.IO;
    
    namespace Book.Core
    {
      public class Huffman16
      {
        private readonly double log2 = Math.Log(2);
    
        private List<Node> HuffmanTree = new List<Node>();
    
        internal class Node
        {
          public long Frequency { get; set; }
          public byte Uncoded0 { get; set; }
          public byte Uncoded1 { get; set; }
          public uint Coded { get; set; }
          public int CodeLength { get; set; }
          public Node Left { get; set; }
          public Node Right { get; set; }
    
          public bool IsLeaf
          {
            get { return Left == null; }
          }
    
          public override string ToString()
          {
            var coded = "00000000" + Convert.ToString(Coded, 2);
            return string.Format("Uncoded={0}, Coded={1}, Frequency={2}", (Uncoded1 << 8) | Uncoded0, coded.Substring(coded.Length - CodeLength), Frequency);
          }
        }
    
        public Huffman16(long[] frequencies)
        {
          if (frequencies.Length != ushort.MaxValue + 1)
          {
            throw new ArgumentException("frequencies.Length must equal " + ushort.MaxValue + 1);
          }
          BuildTree(frequencies);
          EncodeTree(HuffmanTree[HuffmanTree.Count - 1], 0, 0);
        }
    
        public static long[] GetFrequencies(byte[] sampleData, bool safe)
        {
          if (sampleData.Length % 2 != 0)
          {
            throw new ArgumentException("sampleData.Length must be a multiple of 2.");
          }
          var histogram = new long[ushort.MaxValue + 1];
          if (safe)
          {
            for (int i = 0; i <= ushort.MaxValue; i++)
            {
              histogram[i] = 1;
            }
          }
          for (int i = 0; i < sampleData.Length; i += 2)
          {
            histogram[(sampleData[i] << 8) | sampleData[i + 1]] += 1000;
          }
          return histogram;
        }
    
        public byte[] Encode(byte[] plainData)
        {
          if (plainData.Length % 2 != 0)
          {
            throw new ArgumentException("plainData.Length must be a multiple of 2.");
          }
    
          Int64 iBuffer = 0;
          int iBufferCount = 0;
    
          using (MemoryStream msEncodedOutput = new MemoryStream())
          {
            //Write Final Output Size 1st
            msEncodedOutput.Write(BitConverter.GetBytes(plainData.Length), 0, 4);
    
            //Begin Writing Encoded Data Stream
            iBuffer = 0;
            iBufferCount = 0;
            for (int i = 0; i < plainData.Length; i += 2)
            {
              Node FoundLeaf = HuffmanTree[(plainData[i] << 8) | plainData[i + 1]];
    
              //How many bits are we adding?
              iBufferCount += FoundLeaf.CodeLength;
    
              //Shift the buffer
              iBuffer = (iBuffer << FoundLeaf.CodeLength) | FoundLeaf.Coded;
    
              //Are there at least 8 bits in the buffer?
              while (iBufferCount > 7)
              {
                //Write to output
                int iBufferOutput = (int)(iBuffer >> (iBufferCount - 8));
                msEncodedOutput.WriteByte((byte)iBufferOutput);
                iBufferCount = iBufferCount - 8;
                iBufferOutput <<= iBufferCount;
                iBuffer ^= iBufferOutput;
              }
            }
    
            //Write remaining bits in buffer
            if (iBufferCount > 0)
            {
              iBuffer = iBuffer << (8 - iBufferCount);
              msEncodedOutput.WriteByte((byte)iBuffer);
            }
            return msEncodedOutput.ToArray();
          }
        }
    
        public byte[] Decode(byte[] bInput)
        {
          long iInputBuffer = 0;
          int iBytesWritten = 0;
    
          //Establish Output Buffer to write unencoded data to
          byte[] bDecodedOutput = new byte[BitConverter.ToInt32(bInput, 0)];
    
          var current = HuffmanTree[HuffmanTree.Count - 1];
    
          //Begin Looping through Input and Decoding
          iInputBuffer = 0;
          for (int i = 4; i < bInput.Length; i++)
          {
            iInputBuffer = bInput[i];
    
            for (int bit = 0; bit < 8; bit++)
            {
              if ((iInputBuffer & 128) == 0)
              {
                current = current.Left;
              }
              else
              {
                current = current.Right;
              }
              if (current.IsLeaf)
              {
                bDecodedOutput[iBytesWritten++] = current.Uncoded1;
                bDecodedOutput[iBytesWritten++] = current.Uncoded0;
                if (iBytesWritten == bDecodedOutput.Length)
                {
                  return bDecodedOutput;
                }
                current = HuffmanTree[HuffmanTree.Count - 1];
              }
              iInputBuffer <<= 1;
            }
          }
          throw new Exception();
        }
    
        private static void EncodeTree(Node node, int depth, uint value)
        {
          if (node != null)
          {
            if (node.IsLeaf)
            {
              node.CodeLength = depth;
              node.Coded = value;
            }
            else
            {
              depth++;
              value <<= 1;
              EncodeTree(node.Left, depth, value);
              EncodeTree(node.Right, depth, value | 1);
            }
          }
        }
    
        private void BuildTree(long[] frequencies)
        {
          var tiny = 0.1 / ushort.MaxValue;
          var fraction = 0.0;
    
          SortedDictionary<double, Node> trees = new SortedDictionary<double, Node>();
          for (int i = 0; i <= ushort.MaxValue; i++)
          {
            var leaf = new Node()
            {
              Uncoded1 = (byte)(i >> 8),
              Uncoded0 = (byte)(i & 255),
              Frequency = frequencies[i]
            };
            HuffmanTree.Add(leaf);
            if (leaf.Frequency > 0)
            {
              trees.Add(leaf.Frequency + (fraction += tiny), leaf);
            }
          }
    
          while (trees.Count > 1)
          {
            var e = trees.GetEnumerator();
            e.MoveNext();
            var first = e.Current;
            e.MoveNext();
            var second = e.Current;
    
            //Join smallest two nodes
            var NewParent = new Node();
            NewParent.Frequency = first.Value.Frequency + second.Value.Frequency;
            NewParent.Left = first.Value;
            NewParent.Right = second.Value;
    
            HuffmanTree.Add(NewParent);
    
            //Remove the two that just got joined into one
            trees.Remove(first.Key);
            trees.Remove(second.Key);
    
            trees.Add(NewParent.Frequency + (fraction += tiny), NewParent);
          }
        }
    
      }
    
    }
    

    用法示例:

    从样本数据创建字典:

    var freqs = Huffman16.GetFrequencies(File.ReadAllBytes(@"D:\nodes"), true);
    

    使用给定字典初始化编码器:

    var huff = new Huffman16(freqs);
    

    并做一些压缩:

    var encoded = huff.Encode(raw);
    

    减压:

    var raw = huff.Decode(encoded);
    

5 个答案:

答案 0 :(得分:3)

LZW在解压缩期间添加到字典中以确保它与压缩器具有相同的字典状态。否则解码将无法正常运行。

但是,如果您处于修复字典的状态,那么,是的,您不需要添加新代码。

您的方法将运行得相当好,并且很容易使用现有工具来制作原型并测量结果。也就是说,压缩示例文件,然后压缩示例并测试数据。后者的大小减去前者将是块的预期压缩大小。

LZW是一种聪明的方式,可以动态地建立字典并提供不错的结果。但是,对典型数据块进行更全面的分析可能会生成更高效的字典。

LZW表示压缩数据的方式也有改进的余地。例如,每个字典引用可以基于其使用的预期频率被霍夫曼编码为更接近最佳长度。为了真正优化,代码应该是算术编码的。

答案 1 :(得分:3)

我心中的难点在于你如何构建静态字典。您不想使用从示例数据构建的LZW字典。 LZW浪费了大量的时间学习,因为它无法比解压缩器更快地构建字典(令牌仅在压缩器第二次使用时才使用,因此解压缩程序可以在第一次看到时将其添加到字典中) 。另一方面,它是在字典中添加可能永远不会被使用的东西,以防字符串再次显示。 (例如,要获得'stackoverflow'的标记,你也会有'ac','ko','ve','rf'等条目......)

但是,查看LZ77算法的原始令牌流可能效果很好。你只能看到至少看过两次字符串的令牌。然后,您可以构建一个包含在字典中的最常见标记/字符串列表。

一旦你有一个静态字典,使用LZW没有字典更新似乎是一个简单的实现,但为了获得最佳压缩,我认为是静态Huffman表而不是传统的12位固定大小令牌(如乔治菲利普斯建议)。 LZW字典将为您可能永远不会实际编码的所有子字符串刻录令牌(例如,如果您可以编码'stackoverflow',则会有'st','sta','stac','stack','的标记' stacko'等。)。

此时它真的不是LZW - 是什么让LZW更聪明的是解压缩程序如何构建压缩器仅使用压缩数据流的相同字典。你将不会使用的东西。但是所有LZW实现都有一个字典已满并且不再更新的状态,这就是你如何将它与静态字典一起使用。

答案 2 :(得分:2)

我会查看您的数据,看看是否有一个明显的原因,它是如此容易压缩。你可以做一些比LZ78简单得多的事情。我已经完成了LZ77(回顾)和LZ78(字典)。

尝试在数据上运行LZ77。 LZ77没有字典,所以你可以使用一个没有改动的库。 Deflate是LZ77的实现。

您使用普通字典的想法很好,但很难知道这些文件是否彼此相似,或者只是自我相似而不进行某些测试。

答案 3 :(得分:0)

  1. 正确的方法是使用一个库 - 几乎每种现代语言都有一个压缩库。 C#,Python,Perl,Java,VB.net,无论你使用什么。

  2. LZW通过依赖先前输入的字典来节省一些空间。它有一个初始字典,当你解压缩一些东西时,你将它们添加到字典中 - 所以字典正在增长。 (我在这里省略了一些细节,但这是一般的想法)

  3. 您可以通过提供整个(完整)字典作为初始字典来省略此步骤。但这会花费一些空间。

答案 4 :(得分:0)

对于重复的日志条目以及我想要使用的内容,我发现这个方法非常有趣。

您是否可以共享针对您的用例使用此方法的压缩统计信息,以便将其与其他替代方案进行比较?

您是否考虑过让公共字典随着时间的推移而增长,或者这不是一个有效的选项?