Categories
Tags
API API文档 CAS CI-CD DevOps Docker ElasticSearch Everything Git GitHub GitHub Actions HTTP客户端 Java JWT Micrometer MyBatis-Plus Prometheus Python Redis RSS Spring Spring Boot Supplier Typora uni-app Vue Web Web应用 Web开发 中间件 代码生成 任务执行 任务管理 会话管理 内存模型 分布式 前端开发 协议 后端开发 图床 字符串匹配 安全认证 容器化 局域网 工具 工具类 并发容器 并发编程 开源 微服务 搜索引擎 数据库 数据科学 数据结构 数据验证 文件处理 文件服务器 日语 正则表达式 死锁 深度学习 源码分析 爬虫 版本控制 监控 算法 线程安全 线程池 缓存 脚本 自动化 自然语言处理 虚拟线程 设计模式 语言学习 部署 锁机制 面试 项目实战 高可用
1357 words
7 minutes
深入浅出字符串匹配利器:Z 算法 (Z-Algorithm) 详解
什么是 Z 算法?
Z 算法(Z-Algorithm),也被称为“扩展 KMP 算法”,是一种用于处理字符串匹配问题的线性时间算法。它的核心功能是计算一个字符串 的所有后缀与 本身的最长公共前缀(Longest Common Prefix, 简称 LCP)的长度。
简单来说,Z 算法会生成一个数组(通常称为 数组),其中 表示从字符串第 个位置开始的后缀 与原字符串 的最长公共前缀长度。
举个例子: 假设字符串 ,长度为 10。 数组的计算结果如下:
| i | S[i] | S[i…] | 与 S 的 LCP | z[i] |
|---|---|---|---|---|
| 0 | a | aabcaabxaa | (自身) | 10 (通常记为 0 或 n) |
| 1 | a | abcaabxaa | ”a” vs “aab…” -> “a” | 1 |
| 2 | b | bcaabxaa | ”b” vs “aab…” -> "" | 0 |
| 3 | c | caabxaa | ”c” vs “aab…” -> "" | 0 |
| 4 | a | aabxaa | ”aab” vs “aab…” -> “aab” | 3 |
| 5 | a | abxaa | ”ab” vs “aab…” -> “a” | 1 |
| 6 | b | bxaa | ”b” vs “aab…” -> "" | 0 |
| 7 | x | xaa | ”x” vs “aab…” -> "" | 0 |
| 8 | a | aa | ”aa” vs “aab…” -> “aa” | 2 |
| 9 | a | a | ”a” vs “aab…” -> “a” | 1 |
核心概念:Z-Box (匹配区间)
Z 算法之所以能达到 的时间复杂度,关键在于它利用了已知的信息来加速后续的匹配。为了实现这一点,我们需要维护一个区间 ,称为 Z-box。
- 是当前已经匹配到的、最右端的、与 前缀相同的子串区间。
- 换句话说, 等于 。
- 我们在遍历计算 时,会根据 与 的关系来决定如何计算。
算法流程详解
我们从 遍历到 ( 显然为 ,通常无需计算)。在每一步中,我们需要计算 并更新区间 。
分两种情况讨论:
情况 1:当前位置 在 Z-box 之外 ()
这意味着我们没有任何历史信息可以利用。
- 操作:直接从 和 开始朴素比较(暴力匹配),直到字符不相等为止。
- 更新:计算出 后,如果 ,我们将 更新为 。此时 。
情况 2:当前位置 在 Z-box 之内 ()
这意味着 被包含在之前匹配过的段中。因为 与 相同,所以 对应的前缀位置是 。 我们可以利用已经计算过的 来加速。此时又细分为两种子情况:
- 子情况 2a:
- 含义:对应位置 的 LCP 长度没有超出当前 Z-box 的剩余长度。
- 操作:直接令 。因为已知 Z-box 边界 之后是不匹配的,或者内部结构限制了长度,所以不需要再往后看。
- 更新: 保持不变。
- 子情况 2b:
- 含义:对应位置 的匹配长度触碰到了或超过了 。
- 操作:我们至少知道从 到 这一段是匹配的。但是 之后的部分我们不知道。所以,我们需要从 位置开始,继续与前缀进行朴素比较,尝试扩展匹配。
- 更新:根据新扩展的匹配长度更新 ,并更新 和 。
代码实现 (Java)
import java.util.Arrays;
public class ZAlgorithm {
/**
* 计算字符串的 Z 数组
*
* @param s 输入字符串
* @return z 数组,z[i] 表示后缀 s[i...] 与 s 的最长公共前缀长度
*/
public static int[] zFunction(String s) {
int n = s.length();
int[] z = new int[n];
// l 和 r 维护当前的 Z-box,即区间 [l, r]
// 初始时 l = r = 0
for (int i = 1, l = 0, r = 0; i < n; ++i) {
// 情况 2:当前位置 i 在 Z-box 内 (i <= r)
// 我们可以利用已知的 z[i - l] 来初始化 z[i]
// min 的作用是防止访问越过当前的 Z-box (即不超过 r - i + 1)
if (i <= r) {
z[i] = Math.min(r - i + 1, z[i - l]);
}
// 尝试向后扩展匹配 (朴素比较)
// 无论是由情况 1 (i > r) 进入,还是情况 2 中初始化后继续尝试,都通过此循环处理
while (i + z[i] < n && s.charAt(z[i]) == s.charAt(i + z[i])) {
z[i]++;
}
// 如果匹配延伸超过了当前的 r,更新 Z-box 的边界
if (i + z[i] - 1 > r) {
l = i;
r = i + z[i] - 1;
}
}
return z;
}
public static void main(String[] args) {
String s = "aabcaabxaa";
int[] z = zFunction(s);
System.out.println("String: " + s);
System.out.println("Z-array: " + Arrays.toString(z));
// 简单测试模式匹配应用
// 查找 pattern "aab" 在 text "baabaa" 中的位置
// 构造 S = P + "$" + T -> "aab$baabaa"
String pattern = "aab";
String text = "baabaa";
String concat = pattern + "$" + text;
int[] zConcat = zFunction(concat);
System.out.println("\nPattern Matching Example:");
System.out.println("Combined String: " + concat);
for (int i = 0; i < zConcat.length; i++) {
// 如果某位置的 Z 值等于模式串长度,说明匹配成功
if (zConcat[i] == pattern.length()) {
// 计算在原文本中的索引
// i - (pattern.length() + 1)
System.out.println("Pattern found at index: " + (i - pattern.length() - 1));
}
}
}
}
Z 算法的应用
Z 算法最经典的应用是模式串匹配,完全可以替代 KMP 算法。
假设要在文本串 中查找模式串 :
- 构造新字符串 。其中 是一个既不在 也不在 中出现的特殊分隔符。
- 对 计算 Z 数组。
- 遍历 Z 数组中对应 的部分(即下标从 开始)。
- 如果某处的 等于 ,说明从该位置开始匹配到了完整的模式串 。
其他应用:
- 查找字符串的周期:利用 且 的性质。
- 前缀作为子串出现的次数。
- 最长回文子串(结合 Manacher 算法思想)。
总结
Z 算法是一种代码极其简短(核心逻辑仅约 10 行),但功能强大的字符串算法。它的 复杂度和直观的“匹配区间”逻辑,使其在处理前缀匹配、重复结构等问题时非常高效。理解了 Z-box 的维护过程,就掌握了 Z 算法的精髓。
深入浅出字符串匹配利器:Z 算法 (Z-Algorithm) 详解
https://mj3622.github.io/posts/编程实践/z算法/
