KMP算法:在字符串s中搜索匹配查找match字符串,如果能找到返回首个匹配位置i,否则返回-1

KMP算法:在字符串s中搜索匹配match字符串,如果能找到返回首个匹配位置i,否则返回-1

提示:字符串匹配算法KMP算法,速度极快

之前咱们学过KMP算法必须要预备的预设数组信息:next数组
(1)KMP算法预备知识:字符串match的每一个位置i之前的字符串,前缀与后缀匹配的最大长度是多少?
这个文章你必须学会了,学透了,
才能理解今天的KMP算法,在一个字符串s中快速搜索match字符串,并返回匹配的首个字符位置i


题目

KMP算法:
在字符串s中搜索匹配查找match字符串,如果能找到,返回首个匹配位置i,否则返回-1


一、审题

s=abcf abcabck st
match=abcabck
显然在s中首个与match能匹配的位置是i=4
故返回s

如果match是akc啥的
不在s中能匹配到,那就返回-1即可


KMP算法干嘛的?

KMP算法,是寻找字符串str中match串所在的首个位置i的最优算法。
比如
s=abc abck t
match=abck
那么s中首个能与match 匹配的子串(匹配就是相等的意思)的起始位置i是?
python函数中,正则化匹配模块的re.search(s,match)函数,就是干这件事的
在这里插入图片描述
自然,暴力解 ,从s的0–N-1都去寻找一遍,每个位置i,都去从match的j=0位置开始重头匹配
这样做复杂度极其高
o(n*m)
,n是s长度,m是match的长度

之后,如何加速这个匹配过程,那就要用到KMP算法
今天咱专门讲KMP算法,

之前说过的KMP算法是需要准备前奏的:next预设信息数组

众所周知,算法时间复杂度和空间复杂度从来都是一对冤家,
你想速度快,不好意思,那代码就复杂,空间复杂度就很高
你想省空间,代码简单,那不好意思,你运行速度就很慢。

暴力解匹配字符串,不消耗额外空间,但是很慢
KMP为了加速到o(n),自然就避免不了消耗额外空间!
KMP需要准备一个很重要的预设信息数组next
这个数组next是针对match串做的!只需要o(m)的速度,比o(n)小,可以忽略不计!

这个预设数组怎么定义的,代码怎么求?
看这个文章:
(1)KMP算法预备知识:字符串match的每一个位置i之前的字符串,前缀与后缀匹配的最大长度是多少?

你一定要把这个文章看透了!!!


KMP算法的预备知识:next预设信息数组

再说一遍next的含义:
next[i]代表什么,你多次反复理解,慢慢印象就很深刻了:
next数组啥意思呢?next[i]代表:match串的i位置前面,前缀与后缀相匹配的最大长度。
【1】即0–i-1范围上前后缀匹配的最大长度。
【2】next[i]还代表,0–i-1范围上第一个与i位置不匹配的位置。

这个含义理解了,咱们来设计代码快速根据0–i-1上的next信息,推导出next[i]来。

代码流程如下:
(1)咱们人为规定,next[0]=-1,next[1]=0
(2)来到i位置,0–i-1的next信息已经具备,咱只需要根据前面的状况推导i位置的next信息即可
(3)咱只需要确认y=next[i-1]位置的字符,与i-1位置的字符c是否相等?【next[i-1]>0的话】
如果相等,那next[i]=next[i-1]+1;
如果不相等,那就继续看y=next[y]位置的字符,与i-1位置的字符c是否相等?
如果next[y]<=0,说明,往前没有字符了,直接令next[i]=0;一个都没有匹配的
(4)i++,直到i越界

一定要把上面next的含义搞清楚,逻辑整明白了,咱们才能手撕代码,否则这个代码你都不知道在干啥

//KMP算法预备知识:字符串match的每一个位置i之前的字符串,前缀与后缀匹配的最大长度是多少?
    //next数组啥意思呢?**next[i]代表:match串的i位置前面,前缀与后缀相匹配的最大长度。**
    //【1】即0--i-1范围上前后缀匹配的最大长度。
    //【2】next[i]还代表,**0--i-1范围上第一个与i位置不匹配的位置。**
    public static int[] writeNextInfo(char[] m){
        int M = m.length;
        int[] next = new int[M];
        //(1)咱们人为规定,**next[0]=-1,next[1]=0**
        next[0] = -1;//next[1]=0默认
        int i = 2;//2开始
        int y = next[i - 1];//它是0--i-2已经匹配的长度,也是咱们直接对标跟i-1没有匹配上的那个位置
        while (i < M){
            //(2)来到i位置,0--i-1的next信息已经具备,咱只需要根据前面的状况推导i位置的next信息即可
            //(3)咱只需要确认y=next[i-1]位置的字符,与i-1位置的字符c是否相等?【next[i-1]>0的话】
            if (m[y] == m[i - 1]) {
                //**如果相等**,那next[i]=next[i-1]+1;
                next[i++] = y + 1;//它是0--i-2已经匹配的长度+1
                y++;//这个别忘了--这样的话,咱们可以接着往下比i+1位置
            }
            //**如果不相等**,那就继续看y=next[y]位置的字符,与i-1位置的字符c是否相等?
            else if (next[y] > 0) y = next[y];//往前整
            //如果next[y]<=0,说明,往前没有字符了,直接令next[i]=0;一个都没有匹配的
            else next[i++] = 0;//到头了都没有匹配上。
            //(4)i++,直到i越界
        }

        return next;
    }

这代码很简洁,关键在逻辑,需要你整清楚,捋清了,就好理解代码,也好写代码,第一次见不会没关系,多看看我的举例。
慢慢你就能理解了。【就连我学了多次了,都容易写错,这种题目,超级难,笔试不会出现,面试可能会,你准备了,可能面试就拿下了,哪怕说思想也行。】


KMP算法,舍弃思想,前缀对比过的那串就不要重复对比了

我们来讲KMP算法怎么加速寻找匹配字符串的
根据上面所述,咱们准备好m字符串的next数组信息(next[i]是i之前前后缀匹配的最大长度,next[i]也是0–i-1上第一个不匹配的位置)

咱们先说这个KMP算法的对比流程:
(0)对match串构建next信息
(1)咱们拿m的y=0位置开始与s的x=i位置一个一个往后面匹配
(2)如果发现x字符与y字符相等!则x++,y++;回到(2)继续比对
(3)如果发现x字符与y字符不相等!那直接宣告s中i–j-1这一段,谁开头都没法匹配处m串来!【舍弃i–j-1这一段开头的情况,不要对比了(图中粉色那段)】,下一次直接默认s从j开头从新匹配
(4)同时,还不是让y直接退回到m串的0位置从头匹配,而是让y=next[y](暂时叫它y2),直接判断x字符是否等于y2字符
若是相等,去(2)继续对比
若是不相等,去(3)继续对比
(5)如果不等的情况下发现next[y]=-1的话,说明m从y=0位置也跟x字符不相等,x++即可;去s的后面继续对比;
(6)如果x越界,或者y越界,发现y=M,则说明整个m串都匹配好了,返回结果x-y。因为x-m的长度M就是s中首个匹配上的位置i。
在这里插入图片描述

咱们再来解释,为啥KMP这么做就能加速呢?
(0)对match串构建next信息
(1)咱们拿m的y=0位置开始与s的x=i位置一个一个往后面匹配
(2)如果发现x字符与y字符相等!则x++,y++;回到(2)继续比对
解释:这没啥问题吧,你拿到字符串,自然从俩串的开头位置匹配,实际上上面的x也是从s的0位置开始对比的
在这里插入图片描述
(3)如果发现x字符与y字符不相等!那直接宣告s中i–j-1这一段,谁开头都没法匹配处m串来!【舍弃i–j-1这一段开头的情况,不要对比了(图中粉色那段)】,下一次直接默认s从j开头从新匹配
解释: 为啥呢?跟下面(4)的解释一块看!

(4)同时,还不是让y直接退回到m串的0位置从头匹配,而是让y=next[y](暂时叫它y2),直接判断x字符是否等于y2字符
若是相等,去(2)继续对比
若是不相等,去(3)继续对比
解释:
原因是next数组的含义:next[y]是y之前的前缀后缀匹配的最大长度,next[y]也是0–y-1上第一个不匹配的位置
图中y前面的L2那段后缀=L3那段前缀,范围是0–y2-1,自然前缀的长度=后缀的长度就是y2,也就是next[y],这个y2也是m的0–y-1上第一个与y字符不匹配的位置。
既然y不等于x,我们就宣告i开头的字符串是无法匹配出m来的。

在暴力解中,x就需要直接回退到i+1处,y需要回退到m的0位置,又重头对比……这真的有必要吗?

有了next数组,咱能就不需要回退那么多了!!!
有了next数组,咱能就不需要回退那么多了!!!
有了next数组,咱能就不需要回退那么多了!!!

其实,i–j-1那段谁开头都没法匹配出m串来,因为L2=L1=L4=L3,现在x不等于y字符,i+1之后谁开头,都起码要搞出L4字符串出来,与L3相对应,那自然是只有L1能作为下一个能匹配的L4了【你仔细品一下,是不是】【你看下图红色那段L4,未来要与现在黑色L1重合,这样才是未来能匹配m的真实状况】,因此,下一次最差情况也是从j为开头的字符串,才有课鞥与m匹配,这样也才能保证现在的j–x-1(L1)作为最新的能匹配的L4段,去对应m串中的L3段。

所以默认下次开头为j的话,就 轻松舍弃暴力解中i–j-1这段的重复对比

既然下一次能匹配的L4是现在的L1,L1=L3,那么y也不需要从m的0重头对比,所以y只需要去next[y]位置(y2那)继续对比s中的x字符【黑色x】,看看j开头的字符串后续能否与m匹配上?

于是,这样的话轻松舍弃暴力解中0–y2-1这一段重复的对比
在这里插入图片描述
这(3)(4)这段解释是就是KMP算法的核心关键之处!明白了这个舍弃思想,咱就能搞懂这个加速的本质了。
当然但凡遇到舍弃的思想,都是非常非常困难的,但是要捞清楚才行。

(5)如果不等的情况下发现next[y]=-1的话,说明m从y=0位置也跟x字符不相等,x++即可;去s的后面继续对比;
解释:next[y]=-1,说明现在y在m的0位置,,x和y都不相等,等价于第一个y就没法和x对比,x必须++。这本就是常规操作。
——这儿我都想了很久才想通,但是就是最开始第一个x和y对不上,只能动x啊……没啥问题

(6)如果x越界,或者y越界,发现y=M,则说明整个m串都匹配好了,返回结果x-y。因为x-m的长度M就是s中首个匹配上的位置i。
xy一个越界,没字符了,如果y已经对比完了呢?就说明没问题,否则还没匹配完,返回-1。
因为i–x=m的0–M-1,所以呢x-M就是开头匹配上m的开头位置i。返回去就行。


咱们举例说明KMP算法对比半天失败的例子

s=a a b a a c t a a b a a f…
m=a a b a a c t a a b a a k
i = 0 1 2 3 4 5 6 7 8 9 0 1 2

首先对m建立next数组信息:
在这里插入图片描述
s =a a b a a c t a a b a a f…
m=a a b a a c t a a b a a k
i = 0 1 2 3 4 5 6 7 8 9 0 1 2
(1)x=0,y=0开始对比,一直增加xy到x=12,y=12,发现x不等于y
(2)看y=next[y]=next[12]=5,咱们直接对比m的y=5位置与s的x字符相等吗?【舍弃0–4重复对比,因为0–4等于x-5–x-1这段了】看下图绿色的y,x与y并不相等
在这里插入图片描述
(3)看y=next[y]=next[5]=2,咱们直接对比m的y=2位置与s的x字符相等吗?【舍弃0–1重复对比,因为0–1等于x-2–x-1这段了】看下图粉色的y,x与y并不相等
在这里插入图片描述
(4)看y=next[y]=next[2]=1,咱们直接对比m的y=1位置与s的x字符相等吗?【舍弃0–0重复对比,因为0–0等于x-1–x-1这段了】看下图橘色的y,x与y并不相等
在这里插入图片描述
(5)看y=next[y]=next[1]=0,y回到m原点,咱们直接对比m的y=0位置与s的x字符相等吗?
仍然不相等,y已然在原点了,next[0]=-1,所以动x吧,x++滑动继续匹配新的可能性……
如果此时x越界,显然y!=M,说明s中就没有可能


咱们举例说明KMP算法对比半天成功的例子

s =a a b a a c t a a b a a f a a b a a c t a a b a a k
m=a a b a a c t a a b a a k
i = 0 1 2 3 4 5 6 7 8 9 0 1 2
比上面失败的例子呢,就s后面多了m呗,从13位置开始算就能匹配m出来

所以上面接着(5)对比
(6)x++,y++,不断,最后y越界,自然y=M,发现就OK了,哈哈哈哈!!!!!!

手撕KMP算法的代码

其实关键还是要回顾match串的next信息数组的建立代码:

//KMP算法预备知识:字符串match的每一个位置i之前的字符串,前缀与后缀匹配的最大长度是多少?
    //next数组啥意思呢?**next[i]代表:match串的i位置前面,前缀与后缀相匹配的最大长度。**
    //【1】即0--i-1范围上前后缀匹配的最大长度。
    //【2】next[i]还代表,**0--i-1范围上第一个与i位置不匹配的位置。**
    public static int[] writeNextInfo(char[] m){
        int M = m.length;
        int[] next = new int[M];
        //(1)咱们人为规定,**next[0]=-1,next[1]=0**
        next[0] = -1;//next[1]=0默认
        int i = 2;//2开始
        int y = next[i - 1];//它是0--i-2已经匹配的长度,也是咱们直接对标跟i-1没有匹配上的那个位置
        while (i < M){
            //(2)来到i位置,0--i-1的next信息已经具备,咱只需要根据前面的状况推导i位置的next信息即可
            //(3)咱只需要确认y=next[i-1]位置的字符,与i-1位置的字符c是否相等?【next[i-1]>0的话】
            if (m[y] == m[i - 1]) {
                //**如果相等**,那next[i]=next[i-1]+1;
                next[i++] = y + 1;//它是0--i-2已经匹配的长度+1
                y++;//这个别忘了--这样的话,咱们可以接着往下比i+1位置
            }
            //**如果不相等**,那就继续看y=next[y]位置的字符,与i-1位置的字符c是否相等?
            else if (next[y] > 0) y = next[y];//往前整
            //如果next[y]<=0,说明,往前没有字符了,直接令next[i]=0;一个都没有匹配的
            else next[i++] = 0;//到头了都没有匹配上。
            //(4)i++,直到i越界
        }

        return next;
    }

然后根据KMP的法流程,很简单手撕出来了
咱们先说这个KMP算法的对比流程:
(0)对match串构建next信息
(1)咱们拿m的y=0位置开始与s的x=i位置一个一个往后面匹配
(2)如果发现x字符与y字符相等!则x++,y++;回到(2)继续比对
(3)如果发现x字符与y字符不相等!那直接宣告s中i–j-1这一段,谁开头都没法匹配处m串来!【舍弃i–j-1这一段开头的情况,不要对比了(图中粉色那段)】,下一次直接默认s从j开头从新匹配
(4)同时,还不是让y直接退回到m串的0位置从头匹配,而是让y=next[y](暂时叫它y2),直接判断x字符是否等于y2字符
若是相等,去(2)继续对比
若是不相等,去(3)继续对比
(5)如果不等的情况下发现next[y]=-1的话,说明m从y=0位置也跟x字符不相等,x++即可;去s的后面继续对比;
(6)如果x越界,或者y越界,发现y=M,则说明整个m串都匹配好了,返回结果x-y。因为x-m的长度M就是s中首个匹配上的位置i。

//KMP算法的对比流程:
    public static int KMPSearchMatchInStr(String s, String match){
        if (s == null || s.equals("") || s.length() < match.length()) return -1;

        int N = s.length();
        int M = match.length();

        char[] str = s.toCharArray();
        char[] m = match.toCharArray();

        //对match串构建next信息
        int[] next = writeNextInfo(m);

        //(1)咱们拿m的y=0位置开始与s的x=i位置一个一个往后面匹配
        int x = 0;
        int y = 0;
        while (x < N && y < M){
            //(2)如果发现**x字符与y字符相等**!则x++,y++;回到(2)继续比对
            if (str[x] == m[y]){
                x++;
                y++;//往后滑动即可
            }
            //(3)如果发现**x字符与y字符不相等**!那直接宣告s中i--j-1这一段,谁开头都没法匹配处m来!
            // 【省掉i--j-1这一段开头的情况,不要对比了】,下一次直接默认s从j开头从新匹配
            //(4)同时,还不是让y直接退回到m的0位置从头匹配,而是让**y=next[y]**(暂时叫它y2),
            // 直接判断x字符是否等于y2字符
            //若是相等,去(2)继续对比
            //若是不相等,去(3)继续对比
            //(5)如果不等的情况下发现next[y]=-1的话,说明m从y=0位置也跟x字符不相等,x++即可;去s的后面继续对比
            else if (y == 0) x++;
            else y = next[y];//如果y>0的话,继续回去对比
        }
        //(6)如果x越界,或者y越界,发现y=M,则说明整个m都匹配好了,返回结果x-y。
        // 因为x-m的长度M就是s中首个匹配上的位置i。
        return y == M ? x - y : -1;
    }

其实KMP算法的代码是非常非常简单的,关键就是搞定match的next数组信息,才能轻松搞定KMP算法。
KMP对比过程中,x总是在++的进程中,哪怕y往回退也退得少,整体x走一遍o(n)就能匹配完,故算法复杂度整体o(n),比暴力解好多了吧。

然后就可以测试了,

public static void test(){
        String s = "aabaactaabaafaabaaaabaakx";
        String match = "aabaaaabaak";
        char[] m = match.toCharArray();
        int[] next = writeNextInfo(m);
        for(Integer i : next) System.out.print(i +" ");

        int ans = KMPSearchMatchInStr(s, match);
        System.out.println();
        System.out.println(ans);
    }

    public static void main(String[] args) {
        test();
    }

结果
next和咱们匹配到的位置

-1 0 1 0 2 2 2 2 3 4 5 
13

总结

提示:重要经验:

1)KMP算法的关键是match串的预设数组信息next数组,next数组啥意思呢?next[i]代表:match串的i位置前面,前缀与后缀相匹配的最大长度。next[i]还代表0–i-1范围上第一个与i位置不匹配的位置。
2)然后KMP算法的加速在于舍弃前缀匹配过那段重复对比,是复杂度加速到了o(n)
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值
>