try ai
科普
编辑
分享
反馈
  • LZW 算法

LZW 算法

SciencePedia玻尔百科
核心要点
  • LZW 算法是一种自适应、基于字典的压缩方法,它在处理数据的过程中动态学习并编码重复的数据序列。
  • 它的独特之处在于,解码器仅凭编码序列就能完美重建数据和字典,而无需显式地接收字典。
  • LZW 在压缩结构化、重复性数据方面表现出色,但对随机数据无效,甚至可能因编码开销而增大文件体积。
  • 其在 GIF 格式等实际应用中,通过适应不同的数据结构(如展平的二维图像),展示了其多功能性。

引言

在浩瀚的数字信息世界里,对高效存储和传输的需求至关重要。数据压缩为此提供了解决方案,但我们如何能在不丢失信息的前提下压缩数据呢?Lempel-Ziv-Welch (LZW) 算法作为一种优雅而强大的无损压缩方法脱颖而出。它通过扮演一个通用学习器的角色,在处理任何给定数据流的同时为其创建一套自定义的“速记法”,从而解决了发现并编码模式的挑战。本文将揭开 LZW 算法的神秘面纱,引导您从其核心逻辑走向其广泛应用。

接下来的章节将首先深入探讨 LZW 的​​原理与机制​​,剖析编码器和解码器之间精妙的协作关系。您将学习到该算法如何从零开始构建字典,以及为何它对重复性数据如此有效,却对随机噪声束手无策。随后,关于​​应用与跨学科联系​​的章节将探索 LZW 在现实世界中的闪光点——从压缩简单的文本和源代码,到其在 GIF 图像格式中的标志性角色——揭示一个单一的算法思想如何能够跨越迥然不同的领域。

原理与机制

Lempel-Ziv-Welch (LZW) 算法的核心,是一个在实践中学习的优美范例。想象一下,你正在阅读一份长篇手稿,并注意到作者反复使用“现实的基本性质”这个短语。在读到第十遍时,你可能会在页边空白处潦草地写下“XJBJXZ”,并以此作为速记。LZW 算法对数字数据所做的正是如此。它是一种​​自适应​​的、基于字典的方法,无需事先了解数据的统计特性。它在处理过程中构建自己的“速记法”,使其成为一种通用的压缩工具。这与像霍夫曼编码这样的静态方法有根本的不同,后者需要对文本中每个字符的频率进行一次性分析,创建一个固定的码本,然后永久使用。霍夫曼编码可以为字母“E”赋予一个短编码,但它无法为单词“the”或短语“to be or not to be”创建一个特殊的短编码。LZW 的强大之处在于它能够动态地为整个字符串和短语创造新词条。

编码器的旅程:从零开始构建一种语言

让我们跟随​​编码器​​,踏上它穿越数据流的旅程。这个过程始于一个简单且预先商定的基础:​​字典​​。

初始字典

编码器和解码器都从一个相同的、基本的字典开始。这个初始字典包含了它们可能遇到的字母表中的所有单个“字母”。对于一个标准的文本文件,这通常是 ASCII 字符集中的 256 个字符。每个字符都被分配一个编码。按照惯例,字节值为 kkk 的字符被赋予编码 kkk。这意味着初始字典填充了从 0 到 255 的编码。当算法需要添加第一个新的、多字符的字符串时,它将直接使用下一个可用的编码。因此,算法学到的第一个新短语将被分配编码 256。

贪心算法的实际运作

LZW 编码器的核心是一个极其简单且“贪心”的循环。它总是试图从输入中“咬下”它能匹配到的最大一块。其工作原理如下:

  1. 从输入流中读取,并找到当前在字典中的​​最长字符串​​。我们称这个字符串为​​前缀​​,或 PPP。
  2. 输出 PPP 的字典编码。
  3. 从输入中获取下一个字符,我们称之为 CCC。
  4. 通过连接 PPP 和 CCC (写作 P+CP+CP+C) 创建一个新字符串。将这个新字符串以下一个可用编码添加到字典中。
  5. 从字符 CCC 开始,进行下一次搜索。

让我们来看一个实际例子。假设我们的字母表只有 {A, B, W},初始字典为 {A:1, B:2, W:3}。新的编码将从 4 开始。我们的输入是 WABBABW。

  • ​​第 1 步:​​ 编码器从头开始。最长的匹配是 W (编码 3)。下一个字符是 A。编码器输出 3,并将 WA 作为编码 4 添加到字典中。处理过程从 A 处恢复。
  • ​​第 2 步:​​ 当前字符串是 A。最长的匹配是 A (编码 1)。下一个字符是 B。编码器输出 1,将 AB 作为编码 5 添加,并从 B 处恢复。
  • ​​第 3 步:​​ 最长的匹配是 B (编码 2)。下一个字符也是 B。编码器输出 2,将 BB 作为编码 6 添加,并从第二个 B 处恢复。
  • ​​第 4 步:​​ 最长的匹配是 B (编码 2)。下一个字符是 A。编码器输出 2,将 BA 作为编码 7 添加,并从 A 处恢复。
  • ​​第 5 步:​​ 现在事情变得有趣了!编码器看到 A。下一个字符是 B。AB 在字典里吗?是的,我们刚刚把它作为编码 5 添加进去了!算法是贪心的,所以它继续匹配。当前字符串现在是 AB。下一个字符是 W。ABW 在字典里吗?不在。所以,最长的匹配是 AB。编码器输出其编码 5,并将 ABW 作为编码 8 添加到字典中。它从 W 处恢复。
  • ​​第 6 步:​​ 只剩下 W 了。最长的匹配是 W。编码器输出其编码 3。

WABBABW 的压缩输出是编码序列:3, 1, 2, 2, 5, 3。原始的 7 个字符被转换成了 6 个编码。在这个微小的例子中,我们没有实现多少压缩,但你可以看到这个引擎在工作中,它边处理边学习像 WA、AB、BB 和 BA 这样的新“词”。

解码器的秘密:完美的思维同步

现在到了真正精妙的部分。编码器只发送了编码序列 (3, 1, 2, 2, 5, 3)。它没有发送它所构建的字典。那么,​​解码器​​怎么可能知道编码 5 是什么意思呢?似乎关键信息——用于构成每个新条目的字符 CCC——已经永远丢失了。

然而,解码器却能完美地重建字典。这不是魔法,而是一段优美的逻辑。关键的洞见在于,解码器总能推断出缺失的字符,因为​​它需要的那个字符是它将要解码的下一个字符串的第一个字母​​。

让我们用序列 3, 1, 2, 2, 5, 3 来追踪解码器。它从相同的初始字典开始:{A:1, B:2, W:3}。

  • ​​编码 3:​​ 解码器查找编码 3,得到 W。它输出 W。我们把这个记为 previous_string。
  • ​​编码 1:​​ 解码器查找编码 1,得到 A。它输出 A。现在,解码器施展它的技巧。它知道前一个字符串是 W,当前字符串是 A。它需要用来构建编码器第一个新条目的字符,是当前字符串的第一个字符,也就是 A。所以,它将 previous_string + first_char(current_string) = W + A = WA 作为编码 4 添加到自己的字典中。此时,它与编码器完全同步。previous_string 现在变为 A。
  • ​​编码 2:​​ 解码器查找编码 2,得到 B。它输出 B。它将 previous_string + first_char(current_string) = A + B = AB 作为编码 5 添加到字典中。previous_string 变为 B。
  • ​​编码 2:​​ 解码器查找编码 2,得到 B。它输出 B。它将 B + B = BB 作为编码 6 添加。previous_string 变为 B。
  • ​​编码 5:​​ 解码器查找编码 5,得到 AB。它输出 AB。它将 B + A = BA 作为编码 7 添加。previous_string 变为 AB。
  • ​​编码 3:​​ 解码器查找编码 3,得到 W。它输出 W。它将 AB + W = ABW 作为编码 8 添加。

解码后的输出是 WABBABW。解码器分毫不差地重建了原始数据和编码器的字典。

KWKWK 的奇特情况

有一种特殊情况,上述逻辑似乎会失效。如果编码器输出一个解码器尚未创建的编码,会发生什么?这不是一个错误,而是一种特殊、可预测的情况,它发生在当一个字符串的形式是 string + first_character_of_string 时。例如,像 BLABLA 这样的字符串。编码器可能在其字典中找到 BLA,看到下一个字符是 B,于是将 BLAB 添加到字典。然后,在紧接着的下一步,它可能立即找到并输出了 BLAB 的编码。

解码器由于慢一步,会在有机会构建 BLAB 之前就收到了它的编码。当解码器收到一个它不认识的编码时,规则很简单:这个未知的字符串就是​​它刚解码的上一个字符串,加上那个上一个字符串的第一个字符​​。

例如,如果解码器刚刚输出了 L,然后收到了一个它没有的编码 258,它就知道 258 必然代表 L + L = LL。这个巧妙的例外处理了编码器唯一可能领先于解码器的情况,确保了同步永远不会被真正破坏。

压缩的艺术:LZW 的优势与劣势

理解了其工作机制,我们就能预测 LZW 在何处表现出色,又在何处会遇到困难。

重复的力量

LZW 在重复性数据上表现优异。考虑一个大型源代码文件。它充满了重复的关键字(function、return、if)、变量名和函数调用。起初,LZW 会逐字符地编码这些内容。但很快,它的字典就会被这些常用短语填满。if 变成一个单独的编码。return 变成一个单独的编码。一个像 document.getElementById 这样长的重复字符串最终可能被一个单一的、简短的编码所代表。每当这个长字符串出现时,它就被一个编码所取代。这正是巨大压缩收益的来源。数据越是冗余和结构化,LZW 的自适应字典就越强大。像 XYZXYZXYZXYZ 这样的字符串是 LZW 的完美游乐场。它会学会 XY,然后是 Z,然后是 XYZ,很快它就能用单个编码来吞噬大块的输入。

压缩混乱的徒劳

与重复、结构化数据相反的是什么?随机性。想象一个由唯一的、不重复的字符组成的字符串,或者一个纯粹的随机噪声文件。当 LZW 试图处理这个时,它能在字典中找到的“最长匹配”永远都只是单个字符。它将为输入中的每一个字符输出一个编码。

但问题在于:输入的字符可能是 8 比特的。但随着字典的增长,编码需要更多的比特来表示。一旦字典的条目超过 28=2562^8=25628=256 个,输出的编码将至少需要 9 比特。如果字典增长到超过 211=20482^{11}=2048211=2048 个条目,编码将需要 12 比特。如果你为每一个 8 比特的字符输出一个 12 比特的编码,你不是在压缩数据,而是在将其扩大 50%!

这引出了一个至关重要的洞见:你无法压缩随机数据。事实上,试图压缩一个已经被压缩过的文件通常是徒劳的。一个被良好压缩的文件,其模式和冗余已被移除,使其看起来更随机。将这样的文件再次输入 LZW,结果将是数据膨胀,而不是进一步压缩。

现实世界的约束:有限的字典

我们之前的讨论假设字典可以无限增长。在实践中,内存是有限的。现实世界的 LZW 实现,比如用于 GIF 图像格式的实现,都使用固定大小的字典。当字典满了会发生什么?

一旦字典达到其最大容量(例如 4096 个条目,需要 12 比特编码),就必须做出决定。一个常见的策略是简单地停止添加新条目,并使用现有的字典继续压缩文件的其余部分。另一种方法是完全重置字典,从头开始学习。这使得算法能够适应数据中不断变化的模式,但代价是“忘记”了它已经学到的有用短语。在现实世界中应用这个优雅的算法时,这种在内存、适应性和性能之间的权衡是一个关键的工程考量。

应用与跨学科联系

到目前为止,我们已经拆解了 Lempel-Ziv-Welch (LZW) 算法这部精巧的机器。我们看到了它的齿轮和杠杆:字典、贪心匹配,以及它基于自身知识进行构建的巧妙方式。但只有当我们看到一台机器在运转时,才能真正理解它。现在,让我们把这个奇妙的装置带出去兜一圈。我们会发现,它不仅仅是一个压缩工具;它是一个通用学习器,一个揭示数据秘密语言的侦探,无论这语言是简单的节奏、小说的文本,甚至是图画中的色彩。它的应用不仅是实用的——它们还是窥探信息本质的窗口。

简洁之美:捕捉重复

让我们从最简单的“秘密语言”开始:纯粹的重复。想象一个信号,它只是同一个字符重复数千次,比如“A”。我们的 LZW 侦探如何处理这种情况?起初,它的字典只包含单个字符“A”。它看到第一个“A”,向前看到第二个,然后意识到:“啊哈!我以前没见过‘AA’这个字符串。” 于是它输出它找到的最长匹配(“A”)的编码,并将“AA”添加到它的已知短语字典中。在下一步,它发现“AA”现在是一个已知短语。再往前看下一个“A”,它发现“AAA”是一个新的、未知的短语。于是,它输出“AA”的编码,并将“AAA”添加到它的字典中。

你可以看到这里的模式!算法学习的短语长度不断增加:1、2、3、4,以此类推。它总共消耗的字符数量以一种优美且可预测的方式增长,遵循著名的三角数(1,1+2,1+2+3,…1, 1+2, 1+2+3, \dots1,1+2,1+2+3,…)。这是一种描述单调性极其高效的方式,而它必须学习的新短语数量可以用一个与字符串总长度相关的优雅数学公式来描述。这不仅仅是一个假设性的练习;许多现实世界的数据类型,从传感器读数中的静默期到简单的图形元素,都包含长段的、单调的序列,LZW 能够极其有效地压缩它们。

当然,大多数数据更像是一种节奏,而不是单调的嗡鸣。考虑一个像 ABACABACABAC... 这样的字符串。在这里,LZW 很快学会了像 AB、BA 和 AC 这样的短语。但一旦这些短语进入它的字典,它并不会停止。在下一次遍历中,它可能会看到已知的短语 AB 后面跟着字符 A,从而让它学会了新的、更长的短语 ABA。它以自身知识为基础进行扩展,将小的、已知的模式转变为更大、更强大的模式。这种发现并编码重复*子序列*的能力是其力量的核心。

从编码到比特:压缩的实用艺术

我们的 LZW 侦探输出一个编码列表,如 [1, 2, 1, 3, 4, 6, ...]。但我们如何传输这个列表呢?在计算机的世界里,一切都必须转换成比特——零和一。像 65('A' 的 ASCII 值)这样的编码可能会作为一个 8 比特的字节发送。但我们新创造的编码,比如说数字 257,该怎么办呢?我们不能再用 8 比特了,因为 282^828 只有 256。我们需要更多的比特!

这引出了算法在实际实现中固有的一个优美的权衡。随着字典的增长,它变得更加强大,能够用一个编码描述更长、更复杂的模式。但这种强大是有代价的:编码本身需要更多的比特来表示。一个常见且聪明的策略是使用可变数量的比特。当字典包含 NNN 个条目时,我们可以为每个编码使用 ⌈log⁡2(N)⌉\lceil \log_2(N) \rceil⌈log2​(N)⌉ 个比特。所以,当字典从 256 个条目增长到 257 个时,编码长度会突然从 8 比特跳到 9 比特。我们实现的实际压缩率——最终压缩大小与原始大小的比率——取决于在找到长模式和编码其标签成本之间的这种微妙平衡。一个使用固定步长编码宽度(例如,初始字符用 8 比特,所有新条目用 9 比特)的实现更简单,但可能不如一个随着字典增长而动态调整编码大小的实现高效。

数据的“化石记录”:作为故事的字典

LZW 字典并非用后即弃之物。它是一个故事——是输入数据历程的“化石记录”。通过检查字典的最终状态,我们可以推断出算法发现了哪些类型的模式。例如,如果我们被告知字典按顺序包含了 256: XY、257: YZ 和 258: ZY 这几个条目,我们自己也可以扮演侦探。首先创建 XY 的唯一方式是输入以序列 XY... 开始。接下来创建 YZ 的唯一方式是输入继续为 XYZ...。而之后要创建 ZY,输入至少必须是 XYZY。字典的内容是输入结构的直接结果,是一部可读的、关于所遇到模式的历史。

这个“化石记录”也异常敏感。想象一下给算法输入两个几乎相同的字符串:ABCDEFGHIJ 和 ABCDEXFGHIJ。在最初的几个字符中,构建的字典将是相同的,学习像 AB、BC 和 CD 这样的短语。但在第五个字符处,一切都变了。对于第一个字符串,算法看到 E 后面跟着 F,于是将新短语 EF 添加到字典中。而对于第二个字符串,它看到 E 后面跟着 X,于是添加了 EX。从那时起,两个字典——以及整个后续的压缩历史——开始分道扬镳。一个单一字符的改变会涟漪般地影响整个过程,这表明 LZW 不仅仅是在计算字符频率;它是在学习输入数据精确的、有序的“语法”。

跨学科应用:LZW 的实际应用

当我们将 LZW 应用于不同领域的问题时,它真正的才华才得以展现,揭示了其统一的力量和多功能性。

为语言提供先机

考虑压缩英文文本。我们可以让 LZW 从零开始学习单词 THE。它会先看到 T,然后是 H,添加 TH,然后看到 TH 和 E,最后添加 THE。但我们知道 THE 非常常见!为什么不给算法一个先机呢?我们可以用常见的字母组合(如 TH、ER、ING)甚至整个单词(THE、AND)来预加载它的字典。这种领域特定的优化可以从一开始就显著提高压缩性能,因为算法可以立即匹配这些更长的、预加载的模式,而无需费力地去发现它们。这是信息论和语言学的美妙结合,为特定任务量身定制通用工具。

展平世界:压缩图像

这或许是 LZW 最著名的应用之一,用于古老的 GIF 图像格式。但图像是像素的二维网格,而 LZW 是一种一维字符串算法。这是如何工作的呢?我们必须首先将图像“展平”成一维的像素流。但我们这样做的顺序至关重要!想象一幅带有垂直条纹的简单图像:一列'A'像素,然后一列'B'像素,再一列'C'像素,依此类推。如果我们逐行扫描图像(光栅扫描),我们的数据流将看起来像 ABCABCABC...。LZW 擅长压缩这个。但如果我们逐列扫描呢?数据流变成了 AAAAAAAAA...BBBBBBBBB...CCCCCCCCC...。这就是我们前面看到的“单调性”问题,LZW 对其处理效率惊人!对于具有强烈垂直模式的图像,列主序扫描将比光栅扫描产生好得多的压缩效果。扫描顺序的选择深刻地体现了我们关于在何处寻找冗余的假设,展示了算法与数据几何结构之间的深层联系。

通用适配器

如果数据的性质中途改变了怎么办?想象一个数据源先产生 ABABABA... 一段时间,然后突然切换到 BCBCBCB...。LZW 会失败吗?不会!这正是其“通用”性的体现。当它压缩第一部分时,它的字典里充满了像 AB 和 ABA 这样的模式。当数据切换到 BCBC... 时,这些旧模式就不再有用了。算法会短暂地退回到匹配单个字符(B、C),并且当它疯狂地学习 BC 和 BCB 的新“语言”时,它添加新字典条目的速率会激增。很快,它就适应了,学习速率再次稳定下来。LZW 不需要被告知变化;它自己发现并动态适应。它的字典增长率直接衡量了数据在任何给定时刻的“惊奇度”或“新颖度”。

从 AAAA... 的简单节拍到图像的复杂细节,LZW 算法不仅提供了一种压缩手段,更提供了一个理解结构的框架。它在文件格式、通信和数据分析中的应用,证明了一个简单、自适应思想的力量。它构建的字典不仅仅是一个查找表;它是一个故事,一个模型,一个关于它所见数据的动态生成的理论。通过研究 LZW 的工作原理,我们学会了观察存在于我们周围、存在于每一股信息流中的模式、节奏和隐藏的语言。