KMP字符串匹配算法

例题:leetcode28. 找出字符串中第一个匹配项的下标

给你两个字符串 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 。

使用暴力匹配求解:


public int strStr(String haystack, String needle) {
    //转char[]
    char[] chHay = haystack.toCharArray();
    char[] chNeed = needle.toCharArray();
    int m = haystack.length();
    int n = needle.length();
    for(int i = 0; i <= m - n; i++){
        //注意,这里不能<,要加=
        int j,k = i;
        for(j = 0; j < n; j++){
            if(chHay[k] != chNeed[j]){
                //如果不匹配,结束循环
                break; 
            }else{
                k++;
            }
        }
        if(j == n){
            //j == n,说明匹配成功
            return i;
        }
    }
    return -1;
}

通过暴力求解我们可以发现,时间复杂度为O(m * n),因为在每次匹配失败后,主串都会回到上一次的下一个位置,与模式串重新一个一个的匹配,这样就造成了很多次重复的比较(模式串的前面某部分我们可能已经在上一次的循环中比较过了)

KMP算法的时间复杂度为O(m + n),就是巧妙地利用了我们上一次循环的结果,避免出现很多次与上一次循环中重复的比较,并且匹配失败后不会对主串进行回退。

下面我们来看KMP算法的具体实现过程:

最长相等前后缀:前缀等于后缀的集合中最长的前后缀

我们对“abaacabaa”来求解它的最长相等前后缀

前缀

后缀

a

a

相等

ab

aa

不相等

aba

baa

不相等

abaa

abaa

相等(最长)

abaac

cabaa

不相等

abaaca

acabaa

不相等

.......

......

......

abaacaba

baacabaa

不相等

前缀(不包含最后一个字符),后缀(不包括第一个字符)

通过上表我们可以知道abaa就是“abaacabaa”的最长相等前后缀。

KMP的执行流程:

以主串"abaacababcac "和模式串"ababc"为例:

KMP在最大程度利用上一次已经匹配成功的结果,避免了主串的回退以及避免了每次匹配失败后都要对模式串从头开始匹配。通过上图可见KMP对上一次匹配结果的利用是借助了模式串已匹配成功的字串的最长相等前后缀来实现的,通过匹配成功的子串的最长相等前后缀来确定下一次匹配时模式串的指针位置,所以,KMP的核心也就是求出模式串的每个子串的最长相等前后缀。通常,我们用next数组来存储最长相等前后缀。

如何求解next数组?

用P数组来表示模式串

求解next数组是针对与模式串,next[i]表示的是P[0]~P[i]的最长相等前后缀,记len为next[i],那么P[0]~P[len - 1](前缀)与P[P.length - 1 - len]~P[P.length - 1](后缀)相等。

在求解next数组过程中,我们知道了next[0] ~ next[i - 1]的值后,如何求解next[i]?

我们设next[i - 1]的值为k,next数组就是求解最长相等前后缀,现在我们知道了next[i - 1]也就是P[0] ~ P[i - i]的最长相等前后缀,只要P[i] == P[k](这里不用k + 1,因为k长度,本身比索引大1),我们就可以将next[i]赋值为k + 1,可以理解为,在本就相等的前后缀后面分别添加了一个相同的字符。

其实,求next数组就是在比较模式串的前缀与后缀,我们在通过next[i - 1]找到P[i]之前的子串(P[0] ~P[i - 1])的最长相等前后缀长度k后,将后缀的右边第一个元素P[i]与前缀右边的第一个元素P[k]进行比较,若是相等,我们就可以直接对next[i - 1]进行加1赋值给next[i]。那么,当不相等时怎么处理?

我们知道,求next数组就是借助当前子串P[0] ~ P[i]的最长前缀子串(P[0] ~ P[i - 1])的next值(最长相等前后缀)来确定的,P[i] != P[k]]时,说明目前的前后缀不是P[0] ~ P[i]的最长相等前后缀这时,我们就需要回退next数组来缩短前后缀,重新判断缩短后的前后缀是否相等。我们如何来确定next数组回退的长度(如何确定缩短的前后缀的长度)呢。我们肯定希望缩短的前后缀不要太短,在保持“P[0]~P[i-1]的前缀仍然等于后缀”的前提下,让这个新的前后缀尽可能大一点。

借助大佬的例子看一下:

在这个例子中就是要找“子串A的前缀等于子串B的后缀“,并且要找到其中最大的,怎么找呢,仔细观察我们发现,子串A和子串B是完全相等的,所以就等价于要找“子串A的前缀等于子串A的后缀”,而这一值不就是next[k]嘛,这样我们就解决了P[i] != P[k]]的情况。

KMP算法Java代码实现:


public int[] getNext(String str2){
    if(str2 == null || "".equals(str2)){
        return null;
    }
    char[] ms = str.toCharArray();
    int[] next = new int[str2.length()];
    //next数组前两个位置初始化为 -1 , 0
    next[0] = -1;
    next[1] = 0;
    int i = 2;//next 数组的位置
    //next[i] 的值就是ms[0] ~ ms[i - 1]结尾的子串的最长相等前后缀,也是最长相等前后缀的后一个索引位置,用 cn 表示
    int cn = 0; //初始化为 0(因为next数组的起始位置是从2开始的,且next[2 - 1] == 0,所以cn初始化为0) 
    while(i < next.length){
        if(ms[i - 1] == ms[cn]){
            next[i++] == ++cn;
        }else if(cn > 0){
            //往回跳
            cn = next[cn];
        }else{
            next[i++] = 0
        }
    }
}

public int kmp(String str1,String str2){
    if(str1 == null || str2 == null || str1.length() < 1 || str1.length() < str2.length()){
        return -1;
    }
    char[] s1 = str1.toCharArray();
    char[] s2 = str2.toCharArray();
    int i1 = 0;
    int i2 = 0;
    int[] next = getNext(str2);
    while(i1 < s1.length && i2 < s2.length){
        if(s1[i1] == s2[i2]){
            //相等同时后移
            i1++;            
            i2++;
        }else if(next[i2] == -1){
            //next[]回退到0位置还没有匹配成功,s1后移,让下一个值与s2头开始匹配
            i1++;
        }else{
            //s1[i1] != s2[i2] ,next数组回退,缩小最长相等前后缀继续匹配
            i2 = next[i2];
        }
    }
    return i2 == s2.length ? i1 - i2 : -1;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值