Z算法(线性时间模式搜索算法)

Z算法(线性时间模式搜索算法)

在许多涉及字符串的编程问题中,我们通常需要在 文本中寻找某种模式的出现。一种经典方法如朴素字符串匹配算法在文本的每个索引处检查图案,导致时间复杂度为O(n·m),其中n是文本长度,m是模式长度。对于大量输入来说,这种方式效率较低。

为此,存在几种高效的字符串匹配算法,其中之一是Z算法,它允许我们在线性时间内进行模式匹配。这意味着它能在 O(n + m) 时间内找到所有匹配。

目录

什么是Z阵列?

Z阵列的计算

Z阵列在模式匹配中的作用

Z算法的优势

现实应用

相关问题

什么是Z阵列?

Z算法围绕计算一种叫做Z阵列的东西展开。

假设我们有一串长度为n的字符串s。

Z-数组 Z[0...n-1] 对于每个索引 i,存储从 i 起且也是字符串 s 前缀的最长子串长度。

简单来说:

Z[i] 告诉我们从位置 i 开始有多少字符与字符串开头匹配。

在进入Z阵列构造之前,先举一个小例子来更好地理解这一点。

示例:

s = "aabxaab"。

Z[0] = 0 // by definition; we don't compare the full string with itself

现在,我们计算索引1到6的剩余值。

Z[1] = 1 → 只有第一个字符"a"与前缀匹配。

Z[2] = 0 → 'b' 与前缀 'a' 的第一个字符不匹配。

Z[3] = 0 → 'x' 与前缀 'a' 的第一个字符不匹配。

Z[4] = 3 → 子串"aab"与前缀"aab"匹配。

Z[5] = 1 → 只有"a"与前缀的第一个字符匹配。

Z[6] = 0 → 'b' 与前缀 'a' 的第一个字符不匹配。

最终Z阵列

z[] = [0, 1, 0, 0, 3, 1, 0]

Z阵列的计算

计算Z数组检查的简单方法是,对于每个索引i,s[i...]中的字符数与s[0]开头的前缀相符。这在最坏情况下可能导致O(n²)时间。

然而,使用Z算法,我们可以在O(n)时间内计算所有Z[i]值。

在计算Z数组时,我们维护一个窗口[l, r],称为Z盒,表示 与字符串前缀匹配的最右侧子串。

l 是当前 Z 盒的起始索引(前缀匹配的起点)。

r 是结尾索引(与前缀最远的位置)。

具体来说,s[l...r] 匹配 s[0...(r - l)]。

该窗口帮助我们重用之前的计算以优化Z阵列的构建。

为什么Z-box有用?

处理索引i时,有两种可能:

如果我>r(Z框外):

=>开始比较前缀和以i开头的子串。

=> 统计匹配字符的数量,并将此长度存储在Z[i]中。

=> 更新窗口 [L, R] 以表示这个新的匹配段。

如果 i ≤ r:

=> 设 k 是前缀中对应于 i 的位置 (k = i - L)。

=> 使用值 Z[k] 作为参考。

-> 如果 Z[k] 严格小于 [L, R] 剩余长度,则赋值 Z[i] = Z[k]。

-> 否则,开始比较当前窗口以外的角色以延长匹配。

=> 扩展后,如果找到更长的匹配,则更新窗口 [L, R]。

复制代码
import java.util.ArrayList;
import java.util.Arrays;

public class GfG {
    public static ArrayList<Integer> zFunction(String s) {
        int n = s.length();
        ArrayList<Integer> z = new ArrayList<>(n);
        for (int i = 0; i < n; i++) {
            z.add(0);
        }
        int l = 0, r = 0;

        for (int i = 1; i < n; i++) {
            if (i <= r) {
                int k = i - l;
                
                // Case 2: reuse the previously computed value
                z.set(i, Math.min(r - i + 1, z.get(k)));
            }

            // Try to extend the Z-box beyond r
            while (i + z.get(i) < n &&
                s.charAt(z.get(i)) == s.charAt(i + z.get(i))) {
                z.set(i, z.get(i) + 1);
            }

            // Update the [l, r] window if extended
            if (i + z.get(i) - 1 > r) {
                l = i;
                r = i + z.get(i) - 1;
            }
        }
        return z;
    }

    public static void main(String[] args) {
        String s = "aabxaab";
        
        ArrayList<Integer> z = zFunction(s);
        
        for (int x : z) {
            System.out.print(x + " ");
        }
    }
}

输出

0 1 0 0 3 1 0

时间复杂度:O(n)

辅助空间:O(n)

为什么这在线性时间内有效

线性时间复杂度的关键在于,每次进行字符比较(手动匹配)时,我们都会扩展Z盒的右端。

由于r只向前移动,从不向后移动,此类比较的总数最多为n。

Z阵列在模式匹配中的作用

给定两个字符串:文本(文本)和模式(模式),由小写英文字母表组成,找到所有以0为基础的起始索引,其中模式作为文本中的子字符串出现。

示例:

输入:文本 = "aabxaabxaa",模式 = "aab"

输出:[0, 4]

解释:

KMP模式搜索算法

关键思想是预处理一个由模式和文本结合形成的新字符串,该字符串之间用一个特殊的分隔符(例如, )分隔,该分隔符在任何字符串中都不存在。这样可以避免意外的重叠。我们构造一个新字符串如下: s = p a t t e r n + ′ )分隔,该分隔符在任何字符串中都不存在。这样可以避免意外的重叠。 我们构造一个新字符串如下: s = pattern + ' )分隔,该分隔符在任何字符串中都不存在。这样可以避免意外的重叠。我们构造一个新字符串如下:s=pattern+′' + text

然后我们计算该组合字符串的Z-数组。

任意位置 i 的 Z 数组告诉我们,与该位置起始文本子串匹配的最长前缀长度(根据图案和分隔符调整偏移量)。

因此,每当我们找到一个位置i使得:

Z[i] == length of pattern

这意味着整个图案在某一位置与文本相匹配:

match position = i - (pattern length + 1)

插图:













复制代码
import java.util.ArrayList;
import java.util.Arrays;

public class GfG {
    
    // Z-function to compute Z-array
    static ArrayList<Integer> zFunction(String s) {
        int n = s.length();
        ArrayList<Integer> z = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            z.add(0);
        }
        int l = 0, r = 0;
        
        for (int i = 1; i < n; i++) {
            if (i <= r) {
                int k = i - l;
                
                // Case 2: reuse the previously computed value
                z.set(i, Math.min(r - i + 1, z.get(k)));
            }
            
            // Try to extend the Z-box beyond r
            while (i + z.get(i) < n && 
                    s.charAt(z.get(i)) == s.charAt(i + z.get(i))) {
                z.set(i, z.get(i) + 1);
            }
            
            // Update the [l, r] window if extended
            if (i + z.get(i) - 1 > r) {
                l = i;
                r = i + z.get(i) - 1;
            }
        }
        
        return z;
    }
    
    // Function to find all occurrences of pattern in text
    static ArrayList<Integer> search(String text, String pattern) {
        String s = pattern + '$' + text;
        ArrayList<Integer> z = zFunction(s);
        ArrayList<Integer> pos = new ArrayList<>();
        int m = pattern.length();
        
        for (int i = m + 1; i < z.size(); i++) {
            if (z.get(i) == m){
                
                // pattern match starts here in text
                pos.add(i - m - 1); 
            }
        }
        return pos;
    }

    public static void main(String[] args) {
        String text = "aabxaabxaa";
        String pattern = "aab";
        
        ArrayList<Integer> matches = search(text, pattern);
        
        for (int pos : matches)
            System.out.print(pos + " ");
    }
}

输出

0 4

时间复杂度:O(n + m),其中n是文本长度,m是图案长度,因为字符串和Z数组的组合是线性处理的。

辅助空间:O(n + m),用于存储组合字符串和Z数组,实现高效的模式匹配。

Z算法的优势

模式匹配的线性时间复杂度。

使用前缀比较,避免对匹配字符进行重新评估。

比KMP更容易编写;直接与前缀匹配工作。

对于多字符串问题的预处理非常有用,除了模式匹配之外。

现实应用

文本编辑器中的搜索工具(例如 VsCode、Sublime)

抄袭检测系统(检测重复阻挡)

生物信息学(寻找精确的DNA/RNA模式匹配)

入侵检测系统(匹配已知威胁特征)

编译器(识别重复序列或关键词)

信息检索(文档或关键词扫描)

相关问题

搜索模式

查找数组中所有子数组的出现

查找最长的前缀,也是一个后缀

回文开头添加的最小字符

弦之间的旋转

复制代码
编程资源
https://pan.quark.cn/s/7f7c83756948
更多资源
https://pan.quark.cn/s/bda57957c548
相关推荐
NAGNIP4 小时前
轻松搞懂全连接神经网络结构!
人工智能·算法·面试
勇哥java实战分享4 小时前
程序员的明天:AI 时代下的行业观察与个人思考
后端
NAGNIP4 小时前
一文搞懂激活函数!
算法·面试
董董灿是个攻城狮4 小时前
AI 视觉连载7:传统 CV 之高斯滤波实战
算法
掘金码甲哥5 小时前
超性感的轻量级openclaw平替,我来给你打call
后端
用户8356290780518 小时前
无需 Office:Python 批量转换 PPT 为图片
后端·python
啊哈灵机一动8 小时前
使用golang搭建一个nes 模拟器
后端
日月云棠9 小时前
各版本JDK对比:JDK 25 特性详解
java
间彧9 小时前
SpringBoot + ShardingSphere 读写分离实战指南
后端