简介:LZ77 算法是一种经典的数据压缩技术,通过查找源文本中的重复模式来实现压缩。本项目提供了一个简化的 LZ77 算法实现,包含滑动窗口、最长匹配查找、编码输出等关键步骤。通过实践,你可以深入理解 LZ77 算法的工作原理,并将其应用于实际的数据压缩场景中。
1. LZ77算法概述
LZ77算法是一种无损数据压缩算法,它通过查找和替换重复的数据序列来减少文件大小。算法的工作原理是将输入数据分成滑动窗口和查找缓冲区。滑动窗口存储最近处理的数据,而查找缓冲区存储先前处理的数据。当在滑动窗口中找到与查找缓冲区中数据匹配的序列时,算法会生成一个指向查找缓冲区中匹配序列的指针和匹配序列的长度。这种指针和长度对称为LZ77码。通过替换重复序列的原始数据,LZ77码可以显著减少文件大小。
2.1 滑动窗口的概念
滑动窗口是一种数据结构,用于存储最近处理过的数据项。在 LZ77 算法中,滑动窗口用于存储已经处理过的输入字符串,并随着算法的进行而滑动。
滑动窗口的大小由一个参数 w
指定,它表示窗口中可以存储的最大字符数。窗口的左侧称为 头部 ,右侧称为 尾部 。
当算法处理输入字符串时,它将每个新字符添加到窗口的尾部。如果窗口已满,则将窗口头部移动一个字符,并将头部字符从窗口中删除。
滑动窗口的主要目的是提供一个局部上下文,以便算法可以查找输入字符串中重复出现的模式。通过将最近处理过的字符存储在窗口中,算法可以快速识别重复的子串,并使用它们进行压缩。
2.2 滑动窗口的维护
维护滑动窗口涉及以下步骤:
- 初始化: 在算法开始时,创建一个大小为
w
的空滑动窗口。 - 添加字符: 当算法处理输入字符串时,将每个新字符添加到窗口的尾部。
- 滑动窗口: 如果窗口已满,则将窗口头部移动一个字符,并将头部字符从窗口中删除。
- 查找匹配: 算法使用滑动窗口中的字符来查找输入字符串中重复出现的模式。
以下代码展示了如何使用数组实现滑动窗口:
class SlidingWindow:
def __init__(self, w):
self.window = [None] * w
self.head = 0
self.tail = 0
def add_char(self, char):
self.window[self.tail] = char
self.tail = (self.tail + 1) % len(self.window)
if self.tail == self.head:
self.head = (self.head + 1) % len(self.window)
def get_window(self):
return self.window[self.head:self.tail]
这个类提供了以下方法:
-
__init__(w)
:初始化滑动窗口,大小为w
。 -
add_char(char)
:将一个字符添加到窗口的尾部。 -
get_window()
:返回窗口中当前存储的字符列表。
3. 最长匹配查找算法
3.1 朴素查找算法
朴素查找算法是最简单、最直接的查找算法。它从输入字符串的第一个字符开始,逐个字符地与滑动窗口中的字符进行比较,直到找到一个匹配的子串。如果找到匹配,则更新滑动窗口的起始位置和长度,并继续比较下一个字符。如果未找到匹配,则将滑动窗口向后移动一个字符,并从头开始比较。
def朴素查找算法(输入字符串, 滑动窗口):
匹配长度 = 0
匹配起始位置 = 0
for i in range(len(输入字符串)):
for j in range(len(滑动窗口)):
if 输入字符串[i+j] != 滑动窗口[j]:
break
if j+1 > 匹配长度:
匹配长度 = j+1
匹配起始位置 = i
return 匹配长度, 匹配起始位置
参数说明:
- 输入字符串:需要查找匹配子串的字符串。
- 滑动窗口:要查找的子串。
代码逻辑:
- 遍历输入字符串,逐个字符与滑动窗口中的字符比较。
- 如果找到匹配,则更新匹配长度和匹配起始位置。
- 如果未找到匹配,则将滑动窗口向后移动一个字符,并从头开始比较。
- 返回匹配长度和匹配起始位置。
分析:
朴素查找算法的复杂度为 O(mn),其中 m 为输入字符串的长度,n 为滑动窗口的长度。由于需要逐个字符比较,因此时间复杂度较高。
3.2 哈希查找算法
哈希查找算法利用哈希函数将输入字符串中的子串映射到一个固定长度的哈希值,然后通过比较哈希值来判断子串是否匹配。哈希函数的目的是将不同的子串映射到不同的哈希值,从而减少比较次数。
def哈希查找算法(输入字符串, 滑动窗口):
滑动窗口_哈希值 = 哈希(滑动窗口)
最长匹配长度 = 0
最长匹配起始位置 = 0
for i in range(len(输入字符串) - len(滑动窗口) + 1):
输入字符串_哈希值 = 哈希(输入字符串[i:i+len(滑动窗口)])
if 输入字符串_哈希值 == 滑动窗口_哈希值:
匹配长度 = len(滑动窗口)
匹配起始位置 = i
if 匹配长度 > 最长匹配长度:
最长匹配长度 = 匹配长度
最长匹配起始位置 = 匹配起始位置
return 最长匹配长度, 最长匹配起始位置
参数说明:
- 输入字符串:需要查找匹配子串的字符串。
- 滑动窗口:要查找的子串。
代码逻辑:
- 计算滑动窗口的哈希值。
- 遍历输入字符串,逐个字符计算哈希值。
- 如果输入字符串的哈希值与滑动窗口的哈希值相等,则比较子串是否匹配。
- 如果子串匹配,则更新最长匹配长度和最长匹配起始位置。
- 返回最长匹配长度和最长匹配起始位置。
分析:
哈希查找算法的复杂度为 O(m+n),其中 m 为输入字符串的长度,n 为滑动窗口的长度。由于哈希函数可以快速计算哈希值,因此时间复杂度较低。
3.3 字典树查找算法
字典树查找算法利用字典树的数据结构来存储输入字符串中的所有子串。字典树的每个节点代表一个字符,子节点代表该字符的后续字符。通过遍历字典树,可以快速找到匹配的子串。
class 字典树:
def __init__(self):
self.子节点 = {}
self.是叶子节点 = False
def字典树查找算法(输入字符串, 滑动窗口):
根节点 = 字典树()
for 子串 in 输入字符串:
当前节点 = 根节点
for 字符 in 子串:
if 字符 not in 当前节点.子节点:
当前节点.子节点[字符] = 字典树()
当前节点 = 当前节点.子节点[字符]
当前节点.是叶子节点 = True
匹配长度 = 0
匹配起始位置 = 0
当前节点 = 根节点
for 字符 in 滑动窗口:
if 字符 not in 当前节点.子节点:
break
当前节点 = 当前节点.子节点[字符]
匹配长度 += 1
if 当前节点.是叶子节点:
匹配起始位置 = len(滑动窗口) - 匹配长度
return 匹配长度, 匹配起始位置
参数说明:
- 输入字符串:需要查找匹配子串的字符串。
- 滑动窗口:要查找的子串。
代码逻辑:
- 构建字典树,将输入字符串中的所有子串存储在字典树中。
- 遍历滑动窗口,逐个字符在字典树中查找匹配的子串。
- 如果找到匹配的子串,则更新匹配长度和匹配起始位置。
- 返回匹配长度和匹配起始位置。
分析:
字典树查找算法的复杂度为 O(m+n),其中 m 为输入字符串的长度,n 为滑动窗口的长度。由于字典树可以快速查找匹配的子串,因此时间复杂度较低。
4. 编码输出生成
4.1 字典和代码表
在 LZ77 算法中,字典用于存储已经匹配过的字符串,代码表用于存储字典中每个字符串对应的代码。
字典
- 字典是一个滑动窗口,大小通常为 4KB 到 64KB。
- 字典中存储着最近匹配过的字符串,这些字符串按照匹配顺序排列。
- 当一个新的字符串被匹配时,它会被添加到字典的末尾,同时字典中的最老的字符串会被删除。
代码表
- 代码表是一个数组,大小与字典相同。
- 代码表中存储着字典中每个字符串对应的代码。
- 代码通常是 9 到 16 位的二进制数。
4.2 编码输出格式
LZ77 算法的编码输出格式如下:
<长度, 偏移量>
其中:
- 长度 :匹配字符串的长度。
- 偏移量 :匹配字符串在字典中的偏移量。
如果匹配字符串未在字典中找到,则输出一个单字符编码,格式为:
0, 字符
其中:
- 0 :表示单字符编码。
- 字符 :未匹配的字符。
示例
假设字典中存储着以下字符串:
"ABCD"
"EFGH"
"IJKL"
要编码字符串 "ABCDEFGHI",编码输出如下:
<4, 0>
<2, 2>
<3, 4>
其中:
-
<4, 0>
表示匹配字符串 "ABCD",长度为 4,偏移量为 0。 -
<2, 2>
表示匹配字符串 "EF",长度为 2,偏移量为 2。 -
<3, 4>
表示匹配字符串 "GHI",长度为 3,偏移量为 4。
5. 未匹配字符处理
5.1 单字符编码
当滑动窗口中没有找到匹配的子串时,需要对未匹配的字符进行处理。最简单的方法是直接对未匹配的字符进行单字符编码。
步骤:
- 将未匹配的字符添加到字典中,并分配一个新的代码。
- 输出该字符的代码。
代码示例:
def encode_unmatched_char(char):
"""对未匹配的字符进行单字符编码。
Args:
char: 未匹配的字符。
Returns:
未匹配字符的代码。
"""
# 将未匹配的字符添加到字典中
dictionary[char] = len(dictionary)
# 输出该字符的代码
return dictionary[char]
逻辑分析:
-
encode_unmatched_char
函数接收一个未匹配的字符char
作为参数。 - 函数首先将
char
添加到字典dictionary
中,并分配一个新的代码。 - 然后,函数返回
char
的代码。
5.2 字典扩展
当滑动窗口中没有找到匹配的子串,并且字典中也没有未匹配的字符时,需要对字典进行扩展。
步骤:
- 将未匹配的字符添加到字典中,并分配一个新的代码。
- 将滑动窗口中与未匹配字符相邻的字符添加到字典中,并分配一个新的代码。
- 将滑动窗口中与未匹配字符相邻的字符的代码添加到输出中。
代码示例:
def extend_dictionary(char, window):
"""对字典进行扩展。
Args:
char: 未匹配的字符。
window: 滑动窗口。
Returns:
None。
"""
# 将未匹配的字符添加到字典中
dictionary[char] = len(dictionary)
# 将滑动窗口中与未匹配字符相邻的字符添加到字典中
for i in range(len(window)):
if window[i] not in dictionary:
dictionary[window[i]] = len(dictionary)
# 将滑动窗口中与未匹配字符相邻的字符的代码添加到输出中
for i in range(len(window)):
if window[i] == char:
output.append(dictionary[window[i - 1]])
逻辑分析:
-
extend_dictionary
函数接收一个未匹配的字符char
和滑动窗口window
作为参数。 - 函数首先将
char
添加到字典dictionary
中,并分配一个新的代码。 - 然后,函数遍历滑动窗口
window
,将与char
相邻的字符添加到字典dictionary
中,并分配一个新的代码。 - 最后,函数将滑动窗口中与
char
相邻的字符的代码添加到输出output
中。
6. 压缩和解压缩流程
6.1 压缩流程
LZ77 算法的压缩流程可以总结为以下步骤:
- 初始化滑动窗口和字典: 滑动窗口大小为 W,字典大小为 N。
- 读入输入数据: 将输入数据读入滑动窗口。
- 查找最长匹配: 在滑动窗口中查找与当前输入字符匹配的最长字符串。
- 生成编码: 将匹配的字符串表示为一对 (偏移量, 长度) 的编码。偏移量表示匹配字符串在滑动窗口中的起始位置,长度表示匹配字符串的长度。
- 更新滑动窗口: 将匹配字符串从滑动窗口中移除,并将当前输入字符添加到滑动窗口。
- 更新字典: 如果匹配字符串不在字典中,则将其添加到字典。
- 重复步骤 2-6: 直到输入数据全部处理完毕。
示例:
假设输入数据为 "ABABABAB",滑动窗口大小为 3,字典大小为 2。
| 步骤 | 滑动窗口 | 字典 | 编码 | |---|---|---|---| | 1 | 空 | 空 | | | 2 | A | 空 | | | 3 | AB | 空 | | | 4 | ABA | 空 | | | 5 | ABAB | 空 | | | 6 | ABABA | A | (0, 1) | | 7 | ABABAB | A, B | (0, 2) | | 8 | ABABABA | A, B | (1, 2) |
6.2 解压缩流程
LZ77 算法的解压缩流程可以总结为以下步骤:
- 初始化滑动窗口和字典: 滑动窗口大小为 W,字典大小为 N。
- 读入编码: 从压缩数据中读入一对 (偏移量, 长度) 的编码。
- 查找匹配字符串: 在滑动窗口中查找与偏移量和长度匹配的字符串。
- 输出匹配字符串: 将匹配字符串输出到解压缩数据。
- 更新滑动窗口: 将匹配字符串添加到滑动窗口。
- 更新字典: 如果匹配字符串不在字典中,则将其添加到字典。
- 重复步骤 2-6: 直到所有编码都处理完毕。
示例:
假设压缩数据为 "0, 1, 0, 2, 1, 2",滑动窗口大小为 3,字典大小为 2。
| 步骤 | 滑动窗口 | 字典 | 输出 | |---|---|---|---| | 1 | 空 | 空 | | | 2 | A | 空 | A | | 3 | AB | 空 | AB | | 4 | ABA | 空 | ABA | | 5 | ABAB | 空 | ABAB | | 6 | ABABA | A | ABAB | | 7 | ABABAB | A, B | ABABA |
7.1 LZ77算法的优化
LZ77算法的优化主要集中在提高查找效率和减少编码长度方面。
查找效率优化:
- 哈希查找: 使用哈希表存储滑动窗口中的数据,通过计算哈希值快速查找匹配串。
- 前缀树查找: 构建一棵前缀树,将滑动窗口中的数据组织成树形结构,通过前缀匹配快速查找匹配串。
编码长度优化:
- 自适应编码: 根据匹配串的长度和频率动态调整编码长度,短的匹配串使用较短的编码,长的匹配串使用较长的编码。
- 哈夫曼编码: 对编码符号的出现频率进行统计,并根据频率生成哈夫曼树,使用较短的编码表示出现频率较高的符号。
7.2 LZ77算法的变种
LZ77算法的变种主要有:
- LZSS算法: Sliding Window Size Shrinking,将滑动窗口大小动态调整,以适应不同长度的数据。
- LZW算法: Lempel-Ziv-Welch,使用字典树存储匹配串,并使用字典索引进行编码,具有更高的压缩率。
- DEFLATE算法: LZ77算法和哈夫曼编码的结合,广泛应用于ZIP、PNG等文件格式的压缩。
简介:LZ77 算法是一种经典的数据压缩技术,通过查找源文本中的重复模式来实现压缩。本项目提供了一个简化的 LZ77 算法实现,包含滑动窗口、最长匹配查找、编码输出等关键步骤。通过实践,你可以深入理解 LZ77 算法的工作原理,并将其应用于实际的数据压缩场景中。