KMP算法

1.典型例题

28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)

题干:给你两个字符串 haystack 和 needle ,请你在 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
  • haystack 和 needle 仅由小写英文字符组成

1.1暴力求解 

昨天我们也时间c语言中关于strstr()函数的模拟实现,用暴力求解当然不在话下.

以下附上暴力求解的代码(c语言)

int strStr(char * haystack, char * needle){
    if(*needle == '\0')
    {
        return 0;
    }
    char* cp;
    char* s1;
    char* s2;
    int count = 0;
    cp = haystack;
    while(*cp)
    {
        s1 = cp;
        s2 = needle;
        while(*s1 &&*s2 &&*s1 == *s2)
        {
            s1++;
            s2++;
        }
        if(*s2 == '\0')
        {
            return count;
        }
       
        count++;
        cp++;
        

    }
    return -1;

}

 2.KMP算法解决这类查找子串的问题

我将从如下顺序来依次讲解KMP算法的操作以及实现过程(最后会附上代码实现)

1.什么是KMP算法

2.KMP算法有啥用

3.前缀表的定义

4.为什么是前缀表

5.如何计算前缀表

6.前缀表和Next数组之间的关系

7.使用Next数组来匹配

8.时间复杂度的分析

9.构造Next数组

10.使用Next数组来匹配

11.代码实现

2.1 什么是KMP算法

我们先介绍一下KMP算法的由来吧,它是由Knuth,Morris和Pratt三位学者发明的,所以选取了三位学者的首字母命名.

 

2.2 KMP算法有啥用

KMP算法主要用于字符串匹配问题的解决.

主要思想: 当出现字符串不匹配时,可以知道一部分之前已经匹配的字符串内容,从而不需要从头开始重新匹配,减少了时间复杂度.

KMP算法的核心就是这个Next数组了,它也就是保证不从头开始匹配的关键,

这里我们要把Next数组的运行机制搞明白,也要把它的原理搞明白,下面我们就开始看Next数组.

 

2.3 前缀表的定义

实际上所谓的Next数组,也就是一个前缀表(Prefix),那么前缀表有啥用呢?

前缀表用于回退,当主串和子串开始不匹配时,前缀表就记录了子串应该从哪里开始重新匹配

举例:在主串: aabaabaafa 中查找子串 aabaaf

我们发现子串在第6个字符f发生了不匹配,如果是暴力求解的方式,这里我们就要从头开始匹配了,但是使用前缀表我们就会从子串第三个字符开始匹配.

那么前缀表是如何记录的呢?

首先我们要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,这样意味着在某个字符失配时,前缀表会告诉你在下一步匹配中,子串该跳到哪个位置.

前缀表:记录下标i之前(包含i)的字符串中,有多大长度的相同前后缀.

前缀:不包含最后一个字符的所有以第一个字符为开头的连续子串,例如上文子串中的a,aab..

后缀:同上,不包含首字母,例如fa,aafa...

2.4 为什么是前缀表

刚刚我们子串的跳转过程是

下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。

 

2.5 如何计算前缀表

我们以aabaaf举例

前缀表的元素就是其子串的最长相等前后缀的元素

a                  0

aa                1 

aab              0

aaba            1

aabaa           2

aabaaf           0

我们这时候找到不匹配的字符f,找到它前一个字符所对应的前缀表的数值,数值是2,所以直接跳转到下标为2的字符b开始重新匹配,最后就找到了和主串匹配的子串了.

 

2.6 前缀表和Next数组之间的关系

关于网上对Next数组的定义,有的直接用前缀表,有的用前缀表右移一位,第一个元素赋值为-1,有的则使用前缀表所有的元素-1,最后再加回来,这里方式多样,不做过多赘述,这里不涉及KMP算法的核心,有多种实现方式都可行.

 

2.7 时间复杂度的分析

这里假设m是主串长度,n是子串长度,由于在匹配中,前缀表不断的调整位置,匹配的过程是O(n),但是还要单独实现Next数组,复杂度是O(m),所以整个KMP算法的时间复杂度是O(m+n)

暴力求解遍历两个字符串显而易见是O(m*n),所以KMP算法在字符串匹配的过程中极大的提高了搜索的效率.

 

2.8 构造Next数组

我们定义一个函数getNext来构建next数组,函数参数为指向next数组的指针,和一个字符串。代码如下:

void getNext(int* next, const string& s)

构造Next数组其实本质上就是计算子串的前缀表的过程,我们分为以下三步 :

1.初始化

2.处理前后缀相同情况

3.处理前后缀不相同情况

 

2.8.1 初始化  

定义两个指针i,j, j用来指向前缀起始位置,i用来指向后缀起始位置

下面对next数组赋初值

int j = -1;
next[0] = j;

j还有一个含义就是最长相等前后缀的大小,所以next[0] = j

2.8.2 处理前后缀不等情况 

因为j初始化为-1,所以i就从1开始,进行s[i] 和 s[j+1]比较

for(int i = 1; i < s.size(); i++) {

如果他们不相等就要进行回退操作

while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
    j = next[j]; // 向前回退
}

 

2.8.2 处理前后缀相等情况

if (s[i] == s[j + 1]) { // 找到相同的前后缀
    j++;
}
next[i] = j;

 

getNext最终代码
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]
    }
}

2.9 使用Next数组来匹配

在主串s里 找是否出现过子串t。

定义两个下标j指向子串起始位置,i指向主串起始位置

j依然为-1,因为next数组的起始位置为-1

这时候i从0开始遍历主串

for (int i = 0; i < s.size(); i++)

如果s[i] 与 t[j + 1] 不相同,就要到next数组中寻找下一个匹配的位置

while(j >= 0 && s[i] != t[j + 1]) {
    j = next[j];
}

相同的话i和j同时向后移动

if (s[i] == t[j + 1]) {
    j++; // i的增加在for循环里
}

那么如何判断主串完全包含子串呢?

当j指向子串的最后一个字母时,就说明完全包含

if (j == (t.size() - 1) ) {
    return (i - t.size() + 1);
}
int j = -1; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.size(); i++) { // 注意i就从0开始
    while(j >= 0 && s[i] != t[j + 1]) { // 不匹配
        j = next[j]; // j 寻找之前匹配的位置
    }
    if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动
        j++; // i的增加在for循环里
    }
    if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t
        return (i - t.size() + 1);
    }
}

2.10 代码实现

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;
        }
        int next[needle.size()];
        getNext(next, 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;
    }
};

java版本

class Solution {
    public void getNext(int[] next, String s){
        int j = -1;
        next[0] = j;
        for (int i = 1; i<s.length(); i++){
            while(j>=0 && s.charAt(i) != s.charAt(j+1)){
                j=next[j];
            }

            if(s.charAt(i)==s.charAt(j+1)){
                j++;
            }
            next[i] = j;
        }
    }
    public int strStr(String haystack, String needle) {
        if(needle.length()==0){
            return 0;
        }

        int[] next = new int[needle.length()];
        getNext(next, needle);
        int j = -1;
        for(int i = 0; i<haystack.length();i++){
            while(j>=0 && haystack.charAt(i) != needle.charAt(j+1)){
                j = next[j];
            }
            if(haystack.charAt(i)==needle.charAt(j+1)){
                j++;
            }
            if(j==needle.length()-1){
                return (i-needle.length()+1);
            }
        }

        return -1;
    }
}

注:参考代码随想录的解法,附上B站视频

帮你把KMP算法学个通透!(理论篇)_哔哩哔哩_bilibili

  • 15
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值