KMP算法

情景导入

28. 找出字符串中第一个匹配项的下标

给你两个字符串 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 仅由小写英文字符组成

 很轻松的想到使用暴力的方式去解决问题,两个for循环从头开始遍历文本串与模式串,一匹配失败就从头开始遍历。

class Solution {
    public int strStr(String haystack, String needle) {
        int index=-1;
        if(needle.length()>haystack.length()) return -1;
        for(int i=0;i<haystack.length();i++){
            if(haystack.charAt(i)==needle.charAt(0)){
                boolean flag=true;
                for(int j=1;j<needle.length();j++){
                    if(i+j>=haystack.length()){
                        flag=false;
                        break;
                    }
                    if(needle.charAt(j)!=haystack.charAt(i+j)){
                        flag=false;
                        break;
                    }
                }
                if(flag) return i;
            }
        }
        return -1;
    }
}

明显这样子的时间复杂度O(n*m),那能不能降低时间复杂度去解决问题呢?

这时匹配失败,明显我们可以不从头开始匹配,可以从下图位置开始匹配。

但要怎么才能实现不从头开始匹配呢?

前缀表

前缀/后缀

前缀 是指从串首开始到某个位置  结束的一个特殊子串。

举例来说,字符串 abcabcd 的所有前缀为 {a, ab, abc, abca, abcab, abcabc, abcabcd}, 而它的真前缀为 {a, ab, abc, abca, abcab, abcabc}

后缀 是指从某个位置  开始到整个串末尾结束的一个特殊子串。

举例来说,字符串 abcabcd 的所有后缀为 {d, cd, bcd, abcd, cabcd, bcabcd, abcabcd},而它的真后缀为 {d, cd, bcd, abcd, cabcd, bcabcd}。 

 前缀函数

给定一个长度为n的字符串s,前缀表定义为pi[n]。

pi[0...n-1]表示s的从0开始到n-1的子串,最长相等真前缀与真后缀的长度。

现在举个例子方便理解:

s="aabaaf"

s1="a", 无真前缀与真后缀(不包括本身的) pi[0]=0;

s2="aa",真前缀={a} 真后缀={a} pi[1]=1;

s3="aab",真前缀={a,aa} 真后缀={b,ab} pi[2]=0;

s4="aaba",真前缀={a,aa,aab} 真后缀={a,ba,aba} pi[3]=1;

s5="aabaa",真前缀={a,aa,aab,aaba} 真后缀={a,,aa,baa,abaa} pi[4]=2;

s6="aabaaf",真前缀={a,aa,aab,aaba,aabaa} 真后缀={f,af,aaf,baaf,abaaf} pi[5]=0;

利用前缀表就可以知道该从哪重新开始匹配。 

求前缀表pi的函数称为前缀函数

朴素算法求前缀函数

这里j表示真前缀与真后缀最大长度,i表示前缀表的下标。这个函数简单来说外层的for就是i从1开始(不理解为什么i从1开始就重新看看真前缀和真后缀是什么),遍历到n;内层的for的作用是判断长度为[0,i]的子串的最大相等真前缀与真后缀是多少,j是这个子串的长度也是现在可能的最大真前缀与真后缀的值。理解不了可以代“aabaaf”去实验一下。

public int[] prefix_function(String s){
        int n=s.length();
        int[] pi = new int[n];
        for(int i=1;i<n;i++){
            for(int j=i;j>=0;j--){
                if(s.substring(0,j).equals(s.substring(i-j+1,i+1))){
                    pi[i]=j;
                    break;
                }
            }
        }
        return pi;
}

时间复杂度:O(n^3)

现在可以求到前缀表了,但是能不能再优化一下时间?

过程优化1

重要的优化1:相邻的前缀函数值至多增加1。

换种说法就是相邻的最大相等真前缀与真后缀最多相差1,转化到函数就是j不用从i开始,可以从前缀表pi[i-1]+1开始,因为最多就差1。

  

public int[] prefix_function(String s){
        int n=s.length();
        int[] pi = new int[n];
        for(int i=1;i<n;i++){
            for(int j=pi[i-1]+1;j>=0;j--){
                if(s.substring(0,j).equals(s.substring(i-j+1,i+1))){
                    pi[i]=j;
                    break;
                }
            }
        }
        return pi;
}

时间复杂度:O(n^2)

还能不能继续进行时间上的优化?

过程优化2

在优化1中我们知道我们匹配的时候可以从pi[i-1]+1开始遍历,因为邻近的最多增加1。匹配失败之后我们每次都要去str1.equals(str2),那我们能不能减少回溯产生的多余时间呢?

i 代表的是当前pi的下标也代表字符串 s 的下标为 i 的字符,j表示的是现在 s[0...i ]子串 时的可能最长相等真前缀的最后一个字符。如果s[i] == s[j]那么 j++ ,这个是没有问题的。当s[i] != s[j] 时,那我们就让 j=pi[j-1] ,pi[j-1]表示下标为j-1的最长相等真前缀的最后一个字符,还记得前缀表有什么用吗,前缀表可以知道跳转到什么位置(因为前缀表的值就表示该下标下的最长相等真前缀和后缀),如果这时 s[j]==s[i] 那么此时就可以继续匹配了,一直到 j=0为止。

public int[] prefix_function(String s){
        int n = s.length();
        int[] pi = new int[n];
        for (int i = 1; i < n; i++) {
            int j = pi[i-1];
            while (j>0&&s.charAt(i)!=s.charAt(j)){
                j=pi[j-1];
            }
            if(s.charAt(i)==s.charAt(j)) j++;
            pi[i]=j;
        }
        return pi;
}

时间复杂度O(n)

学会了前缀表的构建我觉得可以自己试着去匹配文本串了,两个原理都是一样的。

匹配子串

现在获得了前缀表,再重复一次前缀表作用是可以知道不匹配的时候应该跳转到哪里。两个指针,i指向haystack(文本串),j指向needle(模式串),当haystack[i] !=needle[j] 就跳转到 j=pi[j-1]上。相等就 j++,直到 j==n的时候证明指向末尾了就返回此时的 i-j+1表示模式串的起始。

public int strStr(String haystack, String needle) {
        int[] arr = new int[needle.length()];
        prefix_function(arr,needle);
        int m=haystack.length();
        int n=needle.length();
        int j=0;
        for(int i=0;i<m;i++){
            while(j>0&&haystack.charAt(i)!=needle.charAt(j)){
                j=arr[j-1];
            }
            if(haystack.charAt(i)==needle.charAt(j)){
                j++;
                if(j==n) return i-j+1;
            }
        }
        return -1;
}

参考网站:

前缀函数与 KMP 算法 - OI Wiki

代码随想录 (programmercarl.com)

  • 99
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值