线性时间字符串匹配算法 - KMP

先引出KMP的概念,以下引用自wikipedia:

在计算机科学中,Knuth-Morris-Pratt
字符串搜索算法(或KMP算法)通过采用以下观察方法来搜索W主“文本字符串”中“单词”的S出现:出现不匹配时,单词本身就包含了足够的信息。确定下一个匹配项从何处开始,从而绕过对先前匹配的字符的重新检查。

这是第一个用于字符串匹配的线性时间算法。

场景:找出字符串s1中,字符串s2的出现位置。
例题:28. Implement strStr()

Implement strStr().Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.Example 1:Input: haystack = “hello”, needle = “ll”
Output: 2
Example 2:Input: haystack = “aaaaa”, needle = “bba”
Output: -1
Clarification:What should we return when needle is an empty string? This is a great question to ask during an interview.For the purpose of this problem, we will return 0 when needle is an empty string. This is consistent to C’s strstr() and Java’s indexOf().

思考:
假设s1有n个字符,s2有m个字符:
1、可以用蛮力算法,设置两层循环,外层遍历s1的每个字符s1[i],内层比较从s1[i]开始的字符串和s2从s2[0]开始的每个字符。最差情况下(如s1= “aaaaaaaa”,s2 = “aab”),每次比较需要比较到s2的最后一个字符,复杂度为O(mn)。代码大致如下:

for (int i = 0; i < (s1.size() - s2.size()); i++) {
    int j = 0;
    for (; j < s2.size(); j++) {
        if (s1[i + j] != s2[j]) 
            return -1;
    }
    if (j == s2.size())
        return i;
}

2、使用KMP算法,用O(m)的建表时间和O(n)的比较时间。
分析:方法1中我们可以将上一次的比较结果用于下一次比较。举几个例子:
(1)s1 = “ababaaa”,s2 = “abc”,第一次循环时,(s1[2] = ‘a’) != (s2[2] = ‘c’),这个时候我们不仅得到了s1[2] != s2[2],显然还可以知道:我们第二次循环可以从i = 2开始比较。为什么知道可以从i =2开始比较呢?因为:s2[1] = s1[1] = ‘b’ != (‘a’ = s2[0]),s1[1]不是‘a’。
(2)S = “ABDAABDABD”,W=“ABDAB”,第一次循环时,有:
在这里插入图片描述
第二次循环,我们可以从i = 4,j = 1开始比较,为什么呢?因为:

  1. 在W中,已比较的字符串中的 后缀 与 前缀 最长的相同部分是’A’,长度为1;
  2. 而要找到完全相等的字符串,字符串的开头一定相等。
  3. 已经比较的字符串w1(‘ABDA’)中,与字符串开头(即前缀)相等的部分w-p,如果是出现在w1的末尾处w-s(即后缀),则下次比较时,可以直接从w-p的下一位(B)开始比较。如下图所示:
    在这里插入图片描述
    现在我们需要用一个表来存W的从0开始的子串的最长相等前缀后缀的长度,使得第3步容易进行,其代码如下:
    (这里的 len = lps[len - 1],可以用"aabaaac"来推导,在判断第五个"a"的时候就可以较好地理解为什么要这么写了)
vector kmpProcess(string w) {
    int n = w.size();
    vector lps(n, 0);
    for (int i = 1, len = 0; i < n; ) { //len : 最长相等前缀后缀的长度
        if (w[i] == w[len]) {
            len++;
            lps[i] = len; // 从下标0到下标i的子串的最长相等前缀后缀的长度为len
            i++;
        }
        else {
            if (len) { //len是前一个子串的最长相等前缀后缀的长度
                len = lps[len - 1]; //获得长度为len的前缀的 
                                    //最长相等前缀后缀的长度,
                                    //减短字符长度,放宽要求,看看能不能匹配到
            }
            else { //从i开始的子串的第一个字符就与w开头不一样,len = 0
                lps[i] = 0;
                i++;
            }
        }
    }
    return lps;
}

处理完数据后,对应的查找函数也应该修改为如下:

int stStr (string s, string k) {
    int m = s.size(), n = k.size();
    if (!n) {
        return 0;
    }
    if (m < n) {
        return -1;
    }
    vector lps = kmpProcess(k);
    for (int i = 0, j = 0; i < m;) {
       if (s[i] == k[j]) {
           i++; j++;
           if (j == n) {
                return (i - j);
           }
       }
       else {
           if (j) {
               j = lps[j - 1]; // 执行步骤三,利用已经比较的结果
           }
           else {
               i++; // j = 0,从一开始就不一样,从下一个i开始比较
           }
       }
   }
}

以上,作为KMP的思路的整理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值