数据结构和算法——常见的压缩算法

一、数据压缩

很多人甚至包括相当多的开发者可能对数据压缩都不是多么重视。可能就认为它只是用来压缩个文件什么的吧。但实际上,数据压缩相当重要。像每天在网上看的视频、图像以及云存储等等上应用都非常广泛。可以这样说,在现代的网络中,已经无法离开数据压缩了。
那么什么是数据压缩呢?其实就是用更少的数据空间大小来描述相同或类似的信息。从名字上也可以看出,它的目的是在尽量保证信息可用的前提下减少数据空间占用。
需要提醒的是,大家经常提到的压缩,其实指的是两个方面即压缩和解压。如果学习过通信相关的技术就会知道香农的名言:压缩的本质是消除冗余,而非创造信息。

二、有损压缩和无损压缩

在前面的学习和分析中,会发现在实际的应用场景中,大量的信息实际上是无效或者说非关键性的。也可以这样理解,在描述信息内容的过程中,真正起作用的,经常比较小。比如提到过的稀疏矩阵、图片及音视频中,有很多重复、冗余以及细枝末节的信息。所以就可以考虑如何删除或更好的处理它们。
在实际的压缩算法中,主要分成两大类即无损压缩和有损压缩。这个很好理解,无损压缩即在压缩的结果后,原始信息的数据完全不会丢失可以完整的重新恢复出来;而有损压缩则表示在压缩过程中丢失了一些细节来换取更高的压缩率,导致无法从压缩结果完整的恢复到原始数据。

三、无损压缩算法

常见的无损压缩算法包括以下几种:

  1. 哈夫曼编码
    哈夫曼编码是一种非常经典的无损压缩编码技术。它可以根据字符出现频率的不同来分配不同长度的编码,高频的使用短码来表示而低频的使用长码(如果感兴趣可以看一看哈夫曼树的相关资料)。它的优点是压缩比相对较高且和前缀无关,但压缩速度比较慢(包括解压)。一般应用在文件压缩等场景下
  2. LZW
    Lempel-Ziv-Welch。它通过动态构建字典,用短码映射的方式来代替重复出现的字符串,从而达到压缩的目的。它的原理和实现相对简单,但压缩率一般。比较适合一些图像如GIF等的压缩
  3. 算术编码
    Arithmetic Coding,它是一种高级的无损压缩算法。和哈夫曼算法不同的是,它不是为了每个符号分配一个表示码而将整个待编码的消息转为一个0~1间的小数。通过具体的概率实现来表示相关的信息,从而达到很高的压缩率。这种算法压缩率很高、适配必强,但压缩速度慢,特别是实现相对复杂且早期有专利限制。
    它主要适合于音视频和静态图像的压缩,如jpeg等图像压缩算法种类中就包含它
  4. LZ77
    之所以说明LZ77编码,是因为它是一种基础算法,现在一些主流的压缩算法,基本都是从它上面发展和完善而来的。它使用滑动窗口和字典通过<距离, 长度>来表示原始数据中出现的数据块,达到了数据压缩的目的。实现简单
  5. 游程编程
    游程编码相对好理解,它就是将连续的数据(比如信息中可能存在连续111…)替换为计数量标记或是数据值,达到压缩的目的。这对于一些特定情况如稀疏矩阵、原始音视频及图像等非常有效。它非常简单,编解码速度都很快,一般用在早期的图像压缩和传输。但对于复杂或多变的数据就不太适合了

当然,上面的无损压缩算法是基础的。其它还有不少,比如DEFLATE(应用于zip,gzip等)、LZMA系列(用于7z,xz等)、Zstandard(用于数据仓库、日志及容器等)、Snappy(用于数据库和消息队列)等。此处不再赘述,有兴趣可以查看相关的资料。

四、有损压缩算法

其实在大家常见的应用中,有损压缩遇到的频率更高。常见的有损压缩算法有:

  1. 图像压缩算法
    图像压缩是有损压缩的一个重要应用场景,常见的如JEPG、JEPG2000以及WebP和HEIC等。它们都在保证质量的前提下提供更高的压缩比
  2. 视频压缩算法
    视频压缩算法包括H.261/263/264/265系列。特别是AVC(H.264)是现在广泛应用的编码标准。另外还包括谷歌等推出的VP9和/AV1等编码标准,在保证视频质量的前提下,其压缩效率更高
  3. 音频压缩算法
    音频的相关压缩算法更为大家熟悉,如MP3,AAC,WMA及Opus等。特别是MP3和AAC,几乎普通的用户都知道

除了述的较为常见的有损压缩算法,其实还有一些如变换编码、预测编码、分形压缩及矢量量化等我种有损压缩算法,不过作为非专业人员,可以忽略。

五、比较和分析

一般来说,有损压缩的整体效率即从压缩比、压缩/解压速度上都估于无损压缩。无损压缩强调的是数据的完整还原,但有损压缩则是在压缩比、速度及质量间的平衡。它一般都会经过从变化到量化再到编码的一个过程。
不管是有损压缩还是无损压缩,其底层都与数学有着密不可分的关系。学过哈夫曼树的同学可能立刻就能想到,它其实是一种概率论和编码理论的应用。而在前面分析稀疏矩阵时,对SVD(奇异值分解)也进行过分析。这是线性代数在压缩算法中的应用。另外在音视频的压缩中,其实是处理连续的信号。而通过通信相关的知识(如FFT等)中可以发现,微积分也是不可或缺的。随着压缩技术的发展,更多的数学知识如数论和离散数学(ANS、有限域等)也在压缩技术中崭露头角。
特别是随着AI和量子理论的发展,压缩技术一定会迎来更大的机遇和发展。
应用场景和策略
压缩技术对于现代的计算机应用来说,是一门基础的技术。它广泛应用于各类场景。从硬件内部到内核再到上层应用、数据库系统以及云服务等起着重要的作用。在实际的应用中,它其实一种对压缩效率、压缩/解压速度及资源需求的平衡。
针对压缩的实际需求,策略也各有不同。最核心的策略就是在保证质量的前提下最大可能提高压缩速度和压缩率。如果面对海量数据,可以通过分块策略来提高并行度,达到对并行设备的充分利用;而在音视频等的实际场景中,则可以采用动态预测和感知策略,用来在满足实际播放需求的前提下删除非必要数据;对于大数据也可以引入经典的分层与分列策略,进行相关的数据压缩。
策略是死的,但人是活的。需要不断的根据实际情况综合应用不同的策略。并且在这种策略的前提下,根据具体的场景如哪些硬件富余,哪些数据实时性要求高,从而确定具体的压缩算法。比如内存不足,就可以使用zstd,CPU很厉害就可以使用Snappy算法。如此种种。

六、例程

下面看一个例子:

#include <algorithm>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <queue>
#include <string>
#include <unordered_map>
#include <vector>

namespace huffman {

struct Node {
  char ch;
  int freq;
  Node *left;
  Node *right;

  Node(char c, int f) : ch(c), freq(f), left(nullptr), right(nullptr) {}
  Node(Node *l, Node *r) : ch('\0'), freq(l->freq + r->freq), left(l), right(r) {}
};

struct Compare {
  bool operator()(const Node *a, const Node *b) const { return a->freq > b->freq; }
};

class Codec {
public:
  std::string encode(const std::string &text) {
    table_.clear();
    nodes_.clear();
    root_ = nullptr;

    if (text.empty()) {
      return {};
    }

    std::unordered_map<char, int> freq;
    for (char ch : text) {
      ++freq[ch];
    }

    std::priority_queue<Node *, std::vector<Node *>, Compare> queue;
    for (const auto &[ch, count] : freq) {
      queue.push(makeNode(ch, count));
    }

    while (queue.size() > 1) {
      Node *left = queue.top();
      queue.pop();
      Node *right = queue.top();
      queue.pop();
      queue.push(makeNode(left, right));
    }

    root_ = queue.top();
    buildCode(root_, "");

    std::string bits;
    for (char ch : text) {
      bits += table_.at(ch);
    }
    return bits;
  }

  std::string decode(const std::string &bits) const {
    if (!root_) {
      return {};
    }

    if (!root_->left && !root_->right) {
      return std::string(bits.size(), root_->ch);
    }

    std::string result;
    const Node *current = root_;

    for (char bit : bits) {
      current = bit == '0' ? current->left : current->right;
      if (!current->left && !current->right) {
        result.push_back(current->ch);
        current = root_;
      }
    }

    return result;
  }

  const std::unordered_map<char, std::string> &table() const { return table_; }

private:
  std::vector<std::unique_ptr<Node>> nodes_;
  std::unordered_map<char, std::string> table_;
  Node *root_ = nullptr;

  Node *makeNode(char ch, int freq) {
    nodes_.push_back(std::make_unique<Node>(ch, freq));
    return nodes_.back().get();
  }

  Node *makeNode(Node *left, Node *right) {
    nodes_.push_back(std::make_unique<Node>(left, right));
    return nodes_.back().get();
  }

  void buildCode(const Node *node, const std::string &code) {
    if (!node) {
      return;
    }

    if (!node->left && !node->right) {
      table_[node->ch] = code.empty() ? "0" : code;
      return;
    }

    buildCode(node->left, code + "0");
    buildCode(node->right, code + "1");
  }
};

void demo() {
  const std::string text = "this is a huffman example";
  Codec codec;
  const std::string encoded = codec.encode(text);
  const std::string decoded = codec.decode(encoded);
  std::cout << "Original: " << text << std::endl;
  std::cout << "Encoded bits: " << encoded << std::endl;
  std::cout << "Decoded: " << decoded << std::endl;
}

} 

namespace arithmetic {

struct Range {
  long double low;
  long double high;
};

class Codec {
public:
  std::pair<long double, std::map<char, Range>> encode(const std::string &text) const {
    std::map<char, int> freq;
    for (char ch : text) {
      ++freq[ch];
    }

    std::map<char, Range> ranges;
    long double start = 0.0L;
    for (const auto &[ch, count] : freq) {
      long double probability = static_cast<long double>(count) / text.size();
      ranges[ch] = {start, start + probability};
      start += probability;
    }

    long double low = 0.0L;
    long double high = 1.0L;

    for (char ch : text) {
      long double width = high - low;
      long double nextLow = low + width * ranges[ch].low;
      long double nextHigh = low + width * ranges[ch].high;
      low = nextLow;
      high = nextHigh;
    }

    return {(low + high) / 2.0L, ranges};
  }

  std::string decode(long double code, const std::map<char, Range> &ranges, std::size_t outputSize) const {
    std::string result;
    long double value = code;

    for (std::size_t i = 0; i < outputSize; ++i) {
      for (const auto &[ch, range] : ranges) {
        if (value >= range.low && value < range.high) {
          result.push_back(ch);
          value = (value - range.low) / (range.high - range.low);
          break;
        }
      }
    }

    return result;
  }
};

void demo() {
  const std::string text = "ABACABA";
  Codec codec;
  const auto [code, ranges] = codec.encode(text);
  const std::string decoded = codec.decode(code, ranges, text.size());

  std::cout << "\n=== Arithmetic Coding ===\n";
  std::cout << "Original: " << text << '\n';
  std::cout << "Code: " << static_cast<double>(code) << '\n';
  std::cout << "Decoded: " << decoded << '\n';
}

} 

namespace lz77 {

struct Token {
  int offset;
  int length;
  char next;
};

std::vector<Token> compress(const std::string &input, int windowSize) {
  std::vector<Token> tokens;
  int index = 0;

  while (index < static_cast<int>(input.size())) {
    int bestOffset = 0;
    int bestLength = 0;
    int windowStart = std::max(0, index - windowSize);

    for (int candidate = windowStart; candidate < index; ++candidate) {
      int length = 0;

      while (index + length < static_cast<int>(input.size()) && input[candidate + length] == input[index + length]) {
        ++length;
        if (candidate + length >= index) {
          break;
        }
      }

      if (length > bestLength) {
        bestLength = length;
        bestOffset = index - candidate;
      }
    }

    char next = '\0';
    if (index + bestLength < static_cast<int>(input.size())) {
      next = input[index + bestLength];
    }

    tokens.push_back({bestOffset, bestLength, next});
    index += bestLength + 1;
  }

  return tokens;
}

std::string decompress(const std::vector<Token> &tokens) {
  std::string output;

  for (const Token &token : tokens) {
    if (token.offset > 0 && token.length > 0) {
      const int start = static_cast<int>(output.size()) - token.offset;
      for (int i = 0; i < token.length; ++i) {
        output.push_back(output[start + i]);
      }
    }

    if (token.next != '\0') {
      output.push_back(token.next);
    }
  }

  return output;
}

void demo() {
  const std::string text = "ABABABAABABA";
  const std::vector<Token> tokens = compress(text, 6);
  const std::string decoded = decompress(tokens);

  std::cout << "Original: " << text << std::endl;
  std::cout << "Tokens:" << std::endl;
  for (const Token &token : tokens) {
    std::cout << "  (" << token.offset << ", " << token.length << ", ";
    if (token.next == '\0') {
      std::cout << "\\0";
    } else {
      std::cout << token.next;
    }
    std::cout << ")" << std::endl;
  }
  std::cout << "Decoded: " << decoded << std::endl;
}

} 

int main() {
  huffman::demo();
  arithmetic::demo();
  lz77::demo();

  return 0;
}

代码仅供示例。需要说明的是,对于无损压缩,往往更复杂,所以一般现在都是使用开源或第三方的库,基本很少有人再手写相关代码了。此处就不再提供相关的代码了。

七、总结

通过上面的分析,可以看到,压缩其实并不是一种多么精妙的技巧,而是数学和统计学的在计算机科学中的应用。就像文学和电报,为了表达同样的信息可能会产生巨大的文字量上的差异。与信息内容描述的无效数据和弱相关的数据无处不在,这也是数据压缩存在的基础。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值