【算法日记】KMP算法

KMP算法

一个人能能走的多远不在于他在顺境时能走的多快,而在于他在逆境时多久能找到曾经的自己。 ————KMP

基础理论

当我们使用模式串对字符串进行匹配时,最简单易想的思路就是两个for循环暴力遍历。但这种匹配模式会当两个字符串不匹配时很傻的把子串回滚到最开始的位置。那么既然我们已经遍历完字串了,知道那些位置匹配那些位置不匹配了,那么有没有一种方法使得模式串智能的回滚到不匹配的位置呢?

KMP(Knuth-Morris-Pratt)算法是一种高效的字符串匹配算法,它可以在O(n+m)的时间复杂度内完成对主字符串S长度为n和模式字符串P长度为m的匹配。KMP算法通过避免字符串的重复比较,提高了匹配的效率。

KMP算法的核心思想是利用已经比较过的信息,即通过构建一个能够反映模式字符串P之前部分与主字符串S匹配情况的部分匹配表(也称作“next”表或“部分匹配表”),来确定模式字符串P中下一个字符的比较位置。

以下是KMP算法的具体步骤:

  1. 构建部分匹配表
    • 初始化一个数组next,长度为模式字符串P的长度+1,next[0]设为-1。
    • 设置两个指针i和j,分别指向模式字符串P的头部。
    • 遍历模式字符串P,当i小于m时,比较S[i]和P[j],如果相等,则i和j同时后移一位,并更新next[i+1]为next[j]。如果不相等,则查询next[j-1]的值,如果next[j-1]不为-1,则将j移动到next[j-1]的位置,如果不为-1,则将i移动到i+1的位置。
  2. 字符串匹配
    • 设置两个指针i和j,分别指向主字符串S和模式字符串P的头部。
    • 遍历主字符串S,当i小于n且j小于m时,比较S[i]和P[j],如果相等,则i和j同时后移一位。如果不相等,则查询next[j-1]的值,如果next[j-1]不为-1,则将j移动到next[j-1]的位置,如果不为-1,则将i移动到i+1的位置。
    • 如果j等于m,说明找到了匹配,返回i-j。如果i等于n,说明没有找到匹配,返回-1。

下面是一个简单的例子来说明KMP算法的过程:

主字符串S:abcabcabc
模式字符串P:abc

部分匹配表next:[-1, 0, 1]

匹配过程:
   i     j
abcabcabc
   abc

在例子中,部分匹配表next为[-1, 0, 1],表示当模式字符串P的前两个字符匹配失败时,P应该从第三个字符开始重新比较;当P的前三个字符匹配失败时,P应该从第一个字符开始重新比较。

通过使用部分匹配表,KMP算法避免了字符串的重复比较,大大提高了字符串匹配的效率。在实际应用中,KMP算法被广泛应用于各种文本处理和搜索场景中。

代码实现

求NEXT数组

class KMP {
public:
    vector<int> buildNext(string pattern) {
        vector<int> next;
        next.push_back(0);
        for (int i = 1, j = 0; i < pattern.length(); i++) {
            while (j > 0 && pattern[j] != pattern[i]) { 
                j = next[j - 1];
            }
            if (pattern[i] == pattern[j]) {
                j++; 
            }
            next.push_back(j);
        }
        return next;
    }
};

程序实现

int main() {
    string modeStr = "ABA";
    KMP kmp;
    vector<int> mode = kmp.buildNext(modeStr);
    string mainStr = "AABAABA";
    for (int i : mode) cout << i << ' ';
    cout << endl;
    // i指向主串,j指向模式串
    for (int i = 0, j = 0; i < mainStr.size(); i++) {
        // 不断调整j的位置
        while (j > 0 && mainStr[i] != modeStr[j]) {
            j = mode[j - 1];
        }
        if (mainStr[i] == modeStr[j]) {
            j++;
        }
        if (j == modeStr.size()) {
            cout << j - i + 1 << ' ';
            break;
        }
    }
    return 0;
}

实战 力扣28

题目描述

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

示例 1:

输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。

示例 2:

输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。

提示:

  • 1 <= haystack.length, needle.length <= 104
  • haystackneedle 仅由小写英文字符组成

源码

class Solution {
public:
    void getNext(int* next, const string& s) {
        int j = -1;
        next[0] = j;
        for(int i = 1; i < s.size(); i++) { // 注意i从1开始
            while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
                j = next[j]; // 向前回退
            }
            if (s[i] == s[j + 1]) { // 找到相同的前后缀
                j++;
            }
            next[i] = j; // 将j(前缀的长度)赋给next[i]
        }
    }
    int strStr(string haystack, string needle) {
        if (needle.size() == 0) {
            return 0;
        }
		vector<int> next(needle.size());
		getNext(&next[0], needle);
        int j = -1; // // 因为next数组里记录的起始位置为-1
        for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始
            while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配
                j = next[j]; // j 寻找之前匹配的位置
            }
            if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动
                j++; // i的增加在for循环里
            }
            if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t
                return (i - needle.size() + 1);
            }
        }
        return -1;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值