KMP与前后缀绝对通俗易懂的讲解

        KMP想必是每个初学算法的人敲来的一记重棒,代码写出来十分的简洁,但是其中的思想不能说高深莫测,只能说是晦涩难懂(bushi)。啃了两天的KMP,终于是对其有了一个相对浅显易懂的理解:

        1.暴力解法与KMP的作用

        KMP是用于高效匹配一个字符串中的目标串的算法,如果按照按照常规暴力匹配的话

//haystack为原串,needle为要查找的串,n,m分别为其长度
for (int i = 0; i <= n - m; i++) {
        int j = 0;
        // 检查从 haystack[i] 开始的 m 个字符是否和 needle 匹配
        while (j < m && haystack[i + j] == needle[j]) 
            j++;
        // 如果完全匹配,返回起始索引 i
        if (j == m) 
            return i; 
    }
    // 如果没有找到匹配,返回 -1
    return -1;

那必然是O(N*M)的复杂度对吧,基本就是超时的命

但KMP能做到O(N+M)

        所以就有了KMP这一个算法来实现这一需求,STL里面的find()就是基于KMP实现的,只不过是KMP PRO++ MAX,效率更高。虽然我们完全可以使用find(),但建议大家还是先能熟练写基础KMP再使用,一是锻炼自己,二是可以自己创造出特殊需求的KMP

        2.最长公共前后缀

        前缀(后缀):

                假如有串ABCD,就有

前缀AABABC
后缀DCDBCD

前缀,后缀不包括本身!顺序一致!(都是从左到右数)

        显而易见的,ABCD没有公共的前后缀,所以其最长公共前后缀为0

我们换一个:ABAB

前缀AABABA
后缀AABBAB

        又显而易见的发现,其最长公共前后缀为2(AB)

我们一般用next来存储一个字符串的最长公共前后缀,以ABAB为例子,比如next[2]代表ABA的最长公共前后缀(为1,最长公共前后缀为A)

接下来我们看为什么最长公共前后缀能实现高效搜索

        3.KMP的原理

        一切的算法都是为了优化,KMP就是优化了无效的搜索

                比如我们要在AABAABAAC中搜索AABAAC

     从头开始匹配,发现 AABAABAAC 和 AABAAC 中B和C不对了

            按照暴力来的话,会重新从第二位开始匹配,KMP优化的就是这里.

你看,已经匹配成功的AABAA next是不是为2?也就是AA 和 AA是相同的!!!        

                那为什么不能从AA开始搜索呢?这不就省去很多无用搜索吗!!!

                       

(大致意思如图)

        有人可能会问,这样的Blink是不是太夸张?AAAA之间不可能会有答案吗?直接跳到了AA?                                                                                        

        还真不可能!                                                                                        

如果通过字母分析会过于抽象,我画了个图:

        块2,块3你可以又无限分,最后平移的结果仍是最长前缀平移到后缀处才有可能匹配(建议自己画一下)

        4.利用next搜索

        这里先假设我们求得了next数组(next较为难求)

那只需要按图索骥即可,每次搜索出错后便退回此时的next的值重新匹配

for(int i = 0,j = 0;i<n;i++){ //枚举
            while(j != 0 && needle[j] != haystack[i])
                j = next[j-1];     //发现不对,尝试回退
            if(needle[j] == haystack[i])
                j++;        //记录匹配了几个
            if(j == m)
                return i-m+1;         //完全匹配,返回答案
        }
        return -1;    //全部起点都枚举完,没找到
    }

注意是next[j-1]假如是AABAAC,我们要找的是AABAA的最长前后缀,所以是next[j-1]

j = next[j-1]就相当于回退(原本匹配到AABAA现在重新到AA匹配)

        5.next的计算

               先贴上代码:

for(int i =0,j = 1;j<m;j++){ //i代表左端,j代表右端
            while(i >0 && needle[i] != needle[j])
                i= next[i-1];
            if(needle[j] == needle[i])
                i++;
            next[j] = i;
        }
 if(needle[j] == needle[i])
                i++;
            next[j] = i;

        这一段很好理解,i代表左端,j代表右端,如果新增的(needle[j])仍然匹配,就左边右移一位,总长度+1

        但如果不匹配呢?

while(i >0 && needle[i] != needle[j])
                i= next[i-1];

我们会把i回退! 也就是左端回退!

但是,有人会想,应该是枚举位数重新匹配啊,如果3位对不上就换2位看看对的上不啊

这样来计算next是没问题,但这样不是很慢吗?又要重新匹配一遍?

                                                于是乎,公共前后缀的奇妙之处来了

新加的一位没匹配上,大概就是这个情况:

块2和块3不匹配,怎么办?回退!

别忘了,前端和后端(绿线部分)也有最大前后缀的!

那么我们为什么不这么匹配呢?                                                        (参照之前Blink)

相当于转到了i= next[i-1];

道理就是前端的(最长)前缀等于后端的(最长)后缀!毕竟你前端一定等于后端嘛,再没加新字母的情况下!

那出现不符字母时只需看前端的最长前缀后一位和新的字母一不一样!!

如果不一样,再回退!

所以就有如下代码

for(int i =0,j = 1;j<m;j++){ //i代表左端,j代表右端
            while(i >0 && needle[i] != needle[j])
                i= next[i-1];
            if(needle[j] == needle[i])
                i++;
            next[j] = i;
        }

总代码为

class Solution {
public:
    int strStr(string haystack, string needle) {
        int n = haystack.size();
        int m = needle.size();
        int next[m];
        next[0] = 0;
        for(int i =0,j = 1;j<m;j++){ //i代表左端,j代表右端
            while(i >0 && needle[i] != needle[j])
                i= next[i-1];
            if(needle[j] == needle[i])
                i++;
            next[j] = i;
        }
        for(int i = 0,j = 0;i<n;i++){
            while(j != 0 &&needle[j] != haystack[i])
                j = next[j-1];
            if(needle[j] == haystack[i])
                j++;
            if(j == m)
            return i-m+1;
        }
        return -1;
    }
};

附上练习题:28. 找出字符串中第一个匹配项的下标

希望大家能有帮助!有错误或者模糊的地方还望指正!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值