KMP字符串

KMP字符串

acwing-kmp

给定一个字符串 S S S,以及一个模式串 P P P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模式串 P P P 在字符串 S S S 中多次作为子串出现。

求出模式串 P P P 在字符串 S S S 中所有出现的位置的起始下标。

输入格式

第一行输入整数 N N N,表示字符串 P P P 的长度。

第二行输入字符串 P P P

第三行输入整数 M M M,表示字符串 S S S 的长度。

第四行输入字符串 S S S

输出格式

共一行,输出所有出现位置的起始下标(下标从 0 0 0 开始计数),整数之间用空格隔开。

数据范围

1 ≤ N ≤ 1 0 5 1 \le N \le 10^5 1N105
1 ≤ M ≤ 1 0 6 1 \le M \le 10^6 1M106

输入样例:
3
aba
5
ababa
输出样例:
0 2

y总AC代码

#include <iostream>

using namespace std;

const int N = 100010, M = 1000010;

int n, m;
int ne[N];
char s[M], p[N];

int main()
{
    cin >> n >> (p + 1) >> m >> (s + 1); // 输入长度和字符串

    // 构造 next 数组
    for (int i = 2, j = 0; i <= n; i++)
    {
        while (j && p[i] != p[j + 1]) j = ne[j];
        if (p[i] == p[j + 1]) j++;
        ne[i] = j;
    }

    // KMP 匹配过程
    for (int i = 1, j = 0; i <= m; i++)
    {
        while (j && s[i] != p[j + 1]) j = ne[j];
        if (s[i] == p[j + 1]) j++;
        if (j == n)
        {
            cout << i - n << " "; 
            j = ne[j];
        }
    }

    cout << endl; 
    return 0;
}

第一段:构建 next 数组
for (int i = 2, j = 0; i <= n; i++)
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j++;
    ne[i] = j;
}

i=2, j=0:

  • 我们从 i=2 开始计算,因为 ne[1]=0 已知(长度为1的字符串,前后缀必然为空集)。
  • j 表示“当前已匹配的前缀长度”。

while (j && p[i] != p[j+1])

  • 如果 p[i] ≠ p[j+1],就尝试缩短前缀长度:
    • 退回到上一个最长公共前后缀:j = ne[j]
  • 目的:找到一个次长公共前后缀,使得尝试继续匹配。

if (p[i] == p[j+1]) j++;

  • 如果字符相等,说明可以把当前最长公共前后缀长度 j 增加1。

ne[i] = j;

  • 保存长度为 i 的子串的最长公共前后缀长度。
第二段:主串匹配
for (int i = 1, j = 0; i <= m; i++)
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j++;
    if (j == n)
    {
       cout << i - n<<" ";
        j = ne[j];
    }
}

i=1, j=0:

  • i 遍历主串 s
  • j 表示当前匹配的模式串长度。

while (j && s[i] != p[j+1]) j = ne[j];

  • 如果 s[i] ≠ p[j+1]
    • 说明失配了。
    • 根据 ne[j] 快速回退模式串,找到次长公共前后缀。

if (s[i] == p[j+1]) j++;

  • 如果字符匹配成功,继续比较下一个字符。

if (j == n)

  • j==n,说明找到一个完整的匹配:
    • 输出匹配起始位置i-n
    • 重置 j = ne[j],准备寻找下一个匹配。

要点1:

为什么 p+1 / s+1,从下标 1 开始?

C++ 默认数组是 0-based,但 KMP 代码里用了 1-based,原因:
1.从 1 开始,可以让:

  • 模式串第一个字符是 p[1]
  • 前缀长度自然从 1 计数
  • 代码里 j+1 和数组位置一致。
2. 与 next 逻辑对齐
  • next 数组中 ne[i] 表示:
    • “p[1…i] 的最长公共前后缀长度”
  • 如果下标从0开始:
    • 就需要写成 p[0..i-1],会导致逻辑偏移,计算 j+1 时也不直观。
3. 避免 -1 越界问题
  • 例如匹配失败时,while (j && s[i] != p[j+1])
    • 如果下标从 0 开始,当 j=0 还要访问 p[j],可能出现越界。
  • 从 1 开始就不会有这个问题。
下标从1开始的好处原因
代码逻辑简化不需要频繁调整偏移量
容易处理边界情况避免-1越界问题
更符合算法思想next[j]p[j+1] 对齐

要点2:

for (int i = 2, j = 0; i <= n; i++)
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j++;
    ne[i] = j;
}

怎么保证 ne[i]=j的时候 p[1…j] == p[i-j+1…i]?

如果 p[i] == p[j+1]

  • 当前前缀长度 j 可以扩展 1。

    • 说明:p[1..j] == p[i-j..i-1]
      并且 p[j+1] == p[i]

    所以:p[1..j+1] == p[i-j..i]

如果 p[i] != p[j+1]

  • 不断回退 j = ne[j]
  • 直到找到一个较短的前后缀,使得 p[1..j] == p[i-j+1..i-1]
  • 再尝试 p[i] == p[j+1]

要点3

char s[M], p[N];  
cin >> n >> p + 1 >> m >> s + 1;

p+1 /s+1表示什么?

p+1 的意思是 指针偏移
p+1 == &p[1]
它指向数组的第二个元素(即 p[1]
效果:让用户输入的字符串存放在 p[1] 开始的位置。

要点4

  • <iterator> 头文件中,C++11 引入了一个叫做 std::next 的模板函数:

    template <class ForwardIterator>
    ForwardIterator next(ForwardIterator it, typename iterator_traits<ForwardIterator>::difference_type n = 1);
    
  • 它的作用是:返回一个迭代器,表示在 it 基础上前进 n 步后的位置。

所以 next 在全局作用域中被占用了

如果直接写:

int next[N];

编译器可能会报错或者和 <iterator> 里的 std::next 冲突。

所以我们用ne命名next数组就不会冲突了
### KMP字符串匹配算法的原理 KMP(Knuth-Morris-Pratt)算法是一种高效的字符串匹配算法,它通过构建部分匹配表(通常称为`next数组`),避免了传统暴力匹配中的重复比较操作。以下是KMP算法的核心原理: 1. **前缀和后缀的概念** 在模式串中,定义某个子串的最大公共前后缀长度。例如,在模式串 `aabaa` 中,子串 `aab` 的最大公共前后缀为 `a`,因此它的长度为 1。 2. **部分匹配表(Next 数组)** 部分匹配表记录了模式串中每个字符之前的部分所能达到的最大相同前后缀长度。这个表可以用来指导当发生失配时应该跳转到哪个位置继续匹配[^1]。 3. **减少回溯次数** 当发现当前字符不匹配时,利用已经计算好的 Next 数组来决定下一步从哪里重新开始匹配,而不是像朴素算法那样简单地移动一位并重试整个过程[^4]。 --- ### Python 实现 KMP 算法 下面是基于上述理论的一个完整的 Python 实现示例: #### 构建 Next 数组 ```python def build_next(pattern): next_array = [-1] * len(pattern) if len(pattern) > 0: next_array[0] = -1 k = -1 for q in range(1, len(pattern)): while k >= 0 and pattern[k + 1] != pattern[q]: k = next_array[k] if pattern[k + 1] == pattern[q]: k += 1 next_array[q] = k return next_array ``` #### 主函数实现 ```python def kmp_search(text, pattern): m = len(pattern) n = len(text) # 如果模式为空,则认为找到的位置是0 if m == 0: return 0 next_array = build_next(pattern) q = -1 for i in range(n): while q >= 0 and pattern[q + 1] != text[i]: q = next_array[q] if pattern[q + 1] == text[i]: q += 1 if q == m - 1: return i - m + 1 # 返回匹配起始索引 return -1 # 表示未找到 ``` --- ### Java 实现 KMP 算法 对于 Java 用户来说,也可以采用类似的逻辑实现 KMP 算法。下面是一个简单的例子: #### 构造 Next 数组 ```java public static int[] computeLPSArray(String pat) { int M = pat.length(); int lps[] = new int[M]; int length = 0; lps[0] = 0; int i = 1; while (i < M) { if (pat.charAt(i) == pat.charAt(length)) { length++; lps[i] = length; i++; } else { if (length != 0) { length = lps[length - 1]; } else { lps[i] = 0; i++; } } } return lps; } ``` #### 主函数实现 ```java public static int KMPSearch(String txt, String pat) { int N = txt.length(); int M = pat.length(); int lps[] = computeLPSArray(pat); int i = 0; // index for txt[] int j = 0; // index for pat[] while ((N - i) >= (M - j)) { if (pat.charAt(j) == txt.charAt(i)) { j++; i++; } if (j == M) { return i - j; // Match found at position i-j. } else if (i < N && pat.charAt(j) != txt.charAt(i)) { if (j != 0) { j = lps[j - 1]; } else { i = i + 1; } } } return -1; // No match found. } ``` --- ### 性能分析 相比于传统的暴力匹配方法 O((m-n+1)*n)[^2],KMP 算法的时间复杂度仅为 O(m+n),其中 m 和 n 分别表示文本串和模式串的长度。这是因为 KMP 利用了已有的匹配信息减少了不必要的回溯操作[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值