【字符串】KMP 算法

KMP 算法是一种高效的字符串匹配算法,用于在一个主文本串(text)中查找一个模式串(pattern)是否出现,如果出现,返回其首次出现的起始位置。


1. 问题背景

假设我们要在字符串 text = "ABABDABACDABABCABC" 中查找 pattern = "ABABCAB"

暴力匹配(Brute Force)的缺点

  • 每次匹配失败后,主串指针回退,模式串从头开始。
  • 时间复杂度最坏为 O(n * m),其中 n 是主串长度,m 是模式串长度。

KMP 算法的核心思想是:当匹配失败时,利用已匹配的信息,避免主串指针回退,从而将时间复杂度优化到 O(n + m)。


2. KMP 核心思想:最长相等前后缀(LPS 数组)

KMP 的关键在于构建一个辅助数组:LPS(Longest Proper Prefix which is also Suffix)

什么是 LPS 数组?

对于模式串 pattern[0..m-1]lps[i] 表示子串 pattern[0..i]最长相等真前缀和真后缀的长度

  • 真前缀(Proper Prefix):不等于整个字符串的前缀。
  • 真后缀(Proper Suffix):不等于整个字符串的后缀。
举例说明

模式串:"ABABCAB"

ipattern[0…i]真前缀真后缀最长相等前后缀lps[i]
0A--(空)0
1ABAB0
2ABAA, ABA, BAA1
3ABABA,AB,ABAB,AB,BABAB2
4ABABC0
5ABABCAA1
6ABABCABAB2

所以 LPS 数组为:[0, 0, 1, 2, 0, 1, 2]


3. LPS 数组的构造(预处理)

我们使用双指针技巧来构造 LPS 数组:

vector<int> buildLPS(const string& pattern) {
    int m = pattern.length();
    vector<int> lps(m, 0);
    int len = 0;  // 当前最长相等前后缀的长度
    int i = 1;

    while (i < m) {
        if (pattern[i] == pattern[len]) {
            len++;
            lps[i] = len;
            i++;
        } else {
            if (len != 0) {
                len = lps[len - 1];  // 回退到更短的相等前后缀
            } else {
                lps[i] = 0;
                i++;
            }
        }
    }
    return lps;
}

解释

  • len 表示当前 pattern[0..i-1] 的最长相等前后缀长度。
  • 如果 pattern[i] == pattern[len],说明可以扩展,len++
  • 如果不相等,且 len > 0,则 len = lps[len-1],这是关键!它利用了已计算的信息,避免重新比较。

4. KMP 匹配过程

有了 LPS 数组后,我们就可以进行匹配:

int kmpSearch(const string& text, const string& pattern) {
    int n = text.length();
    int m = pattern.length();

    if (m == 0) return 0;

    vector<int> lps = buildLPS(pattern);

    int i = 0;  // text 的索引
    int j = 0;  // pattern 的索引

    while (i < n) {
        if (text[i] == pattern[j]) {
            i++;
            j++;
        }

        if (j == m) {
            return i - j;  // 找到匹配,返回起始位置
        } else if (i < n && text[i] != pattern[j]) {
            if (j != 0) {
                j = lps[j - 1];  // 利用 LPS 跳过不必要的比较
            } else {
                i++;  // j 已经是 0,无法再跳,只能移动 i
            }
        }
    }

    return -1;  // 未找到匹配
}

关键点

  • text[i] != pattern[j]j > 0 时,j = lps[j-1],表示模式串向右滑动,但 i 不回退。
  • j == 0 时,说明模式串已经无法再利用前缀信息,只能移动主串指针 i

5. 完整 C++ 实现

#include <iostream>
#include <vector>
#include <string>
using namespace std;

// 构建 LPS 数组
vector<int> buildLPS(const string& pattern) {
    int m = pattern.length();
    vector<int> lps(m, 0);
    int len = 0;
    int i = 1;

    while (i < m) {
        if (pattern[i] == pattern[len]) {
            len++;
            lps[i] = len;
            i++;
        } else {
            if (len != 0) {
                len = lps[len - 1];
            } else {
                lps[i] = 0;
                i++;
            }
        }
    }
    return lps;
}

// KMP 字符串匹配
int kmpSearch(const string& text, const string& pattern) {
    int n = text.length();
    int m = pattern.length();

    if (m == 0) return 0;  // 空模式串

    vector<int> lps = buildLPS(pattern);

    int i = 0;  // text 索引
    int j = 0;  // pattern 索引

    while (i < n) {
        if (text[i] == pattern[j]) {
            i++;
            j++;
        }

        if (j == m) {
            return i - j;  // 找到匹配
        } else if (i < n && text[i] != pattern[j]) {
            if (j != 0) {
                j = lps[j - 1];
            } else {
                i++;
            }
        }
    }

    return -1;  // 未找到
}

// 测试函数
int main() {
    string text = "ABABDABACDABABCABC";
    string pattern = "ABABCAB";

    int pos = kmpSearch(text, pattern);
    if (pos != -1) {
        cout << "模式串在位置 " << pos << " 处找到。" << endl;
    } else {
        cout << "未找到模式串。" << endl;
    }

    return 0;
}

6. 时间复杂度分析

  • 预处理(构建 LPS):O(m)
  • 匹配过程:O(n)
  • 总时间复杂度O(n + m)

空间复杂度:O(m)(用于存储 LPS 数组)


7. 总结

  • KMP 算法通过预处理模式串,构建 LPS 数组,避免了主串指针的回退。
  • 核心是理解 lps[j-1] 的含义:它告诉我们当 pattern[j] 匹配失败时,模式串应该从哪个位置继续匹配。
  • KMP 是字符串匹配的经典算法,是理解自动机、AC 自动机等更高级算法的基础。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值