题目描述

[!green]

牛客原题: ✅ 补充题 106. 字符串模式匹配计数

给你文本字符串 T 和非空模式字符串 S,返回 S 在 T 中出现的次数。

不同起始位置分别计数,允许匹配区间相互重叠。

示例 1:

输入: T = "aaaa", S = "aa"
输出: 3
解释: 匹配起点为 0、1、2,允许不同匹配区间重叠。

示例 2:

输入: T = "abababa", S = "aba"
输出: 3
解释: 匹配起点为 0、2、4。

提示:

  • 1 <= len(S) <= 500000
  • 1 <= len(T) <= 1000000

题意分析

每个合法起点都要统计,即使它与前一次命中的区间重叠。逐个起点重新比较可能反复扫描同一段文本,在当前长度上限下需要利用已经匹配的内容减少比较。

KMP 保存模式串自身的前后缀关系。失配后只调整已匹配长度,文本位置不用倒退;完整命中后也用同一关系保留可复用的后缀。

解法:KMP 完整命中后继续回退匹配

核心思路

[!blue]

pi[i] 表示模式前缀 pattern[0..i] 的最长相等真前缀、真后缀长度;“真”表示不能取整个区间。构造时,若下一个字符不能延长匹配,就令 j = pi[j - 1],尝试更短且仍可能成立的前后缀。

扫描文本时,j 表示当前文本后缀与模式前缀相等的长度。字符相等就把 j 加 1;失配则沿 pi 回退,直到能接上当前字符或 j 变成 0。被跳过的长度不满足前后缀关系,不可能构成有效匹配。

当 j == m 时,恰好发现一个以当前位置结尾的完整匹配,计数加 1,再令 j = pi[m - 1]。保留这段后缀才能继续发现重叠命中,同时每个结尾只计一次。回退后 j < m,下一次读取模式字符也不会越界。

解题步骤

  1. 构造模式串的前缀函数。
  2. 扫描文本,失配时沿前缀函数回退。
  3. 完整匹配后计数,再回退到最长真前后缀继续匹配。

代码实现

class Solution {
    public int countMatches(String text, String pattern) {
        int m = pattern.length();

        if (m == 0) {
            throw new IllegalArgumentException("pattern must not be empty");
        }

        int[] pi = new int[m];

        for (int i = 1, j = 0; i < m; i++) {
            while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
                j = pi[j - 1];
            }

            if (pattern.charAt(i) == pattern.charAt(j)) {
                j++;
            }

            pi[i] = j;
        }

        int answer = 0;
        int j = 0;

        for (int i = 0; i < text.length(); i++) {
            while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
                j = pi[j - 1];
            }

            if (text.charAt(i) == pattern.charAt(j)) {
                j++;
            }

            if (j == m) {
                answer++;
                j = pi[j - 1];
            }
        }

        return answer;
    }
}
func countMatches(text, pattern string) int {
    m := len(pattern)
    if m == 0 {
        panic("pattern must not be empty")
    }
    pi := make([]int, m)
    for i, j := 1, 0; i < m; i++ {
        for j > 0 && pattern[i] != pattern[j] {
            j = pi[j-1]
        }
        if pattern[i] == pattern[j] {
            j++
        }
        pi[i] = j
    }
    answer, j := 0, 0
    for i := 0; i < len(text); i++ {
        for j > 0 && text[i] != pattern[j] {
            j = pi[j-1]
        }
        if text[i] == pattern[j] {
            j++
        }
        if j == m {
            answer++
            j = pi[j-1]
        }
    }
    return answer
}

复杂度分析

  • 时间复杂度:$O(\lvert T\rvert+\lvert S\rvert)$。
  • 空间复杂度:额外空间 $O(\lvert S\rvert)$。

关键点总结

[!green]

  • pi 描述模式自身的结构,扫描状态 j 描述当前文本与模式的匹配长度。
  • 完整命中后的回退与失配回退共用前后缀关系,不能把 j 直接清零。
  • 每次前进最多让 j 增加 1,回退只使其减小,因此总比较次数为线性量级。

易错点总结

[!yellow]

完整匹配后不能直接返回,也不能把j清零;非空模式是题目约束。

相似题目

题目 难度 关联与区别
28. 找出字符串中第一个匹配项的下标 简单 原题首次命中即可返回,本题计数后必须继续扫描,并保留可重用的前后缀。
459. 重复的子字符串 简单 同样利用最长真前后缀信息,原题据此识别周期,本题用它支持重叠匹配。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/21469896
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!