• 算法补天系列之——KMP算法,即字符串匹配算法


    什么是KMP呢?

    我们有一个string1,又有一个string2,这个时候我们想知道string2是不是string1的子串。这就是KMP
    首先我们说经典算法,其实时间复杂度是比较大的,一般就是O(n*m)
    其实KMP的主要思路和经典暴力方法还是一样的,但是KMP通过了一定的策略实现了加速。

    首先我们要知道前缀和和后缀和

    这个字符前面字符串中前后缀的最大匹配长度。记得不要取到整体

    我们要这个干什么呢?是要给string2求的,string2的每一个字符都需要这个信息。我们把这个数据存储在next arr数组中。这里怎么求我们先不说,我们知道了这个简单求法先用着,看KMP过程有了这个数组之后怎么加速

    开始执行KMP算法

    两个string比较,我们到了string1的第n个位置和string2的i位置,我们发现没有对上,我们是不是就要从string的n+1位置和string2的0位置继续比,两个string双双回跳。

    而现在我们知道了i之前的匹配情况,我们把string2的比较位置回跳到后缀和的位置。
    这里特别抽象。我们细说。

    我们之前是string1的0位置(比如说)开始比的,而此时到了n发现了匹配不上,但是我们此时不用叫string2从头去比,因为前后缀有一样的部分,这个时候我们将后缀当做前缀,我们从前缀后面开始去比,因为后缀和前缀一样,并且string1已经匹配好前缀了,后缀不用比也知道一样。
    在这里插入图片描述

    以此为例,经典过程我们最后发现没对上怎么办,是不是要从string1的b开始,再去和string2的a比。

    而此时我们知道string2前缀后缀有一定的一样的位置abbstk,那么此时我们是要从string1的第二个abbstk之后和string2前缀之后的位置开始比。而我们知道这个时候string1之前是已经指定配不出来了,所以我们可以大胆把string2挪过去。

    但是为什么呢?为啥一定配不出来了呢?

    我们假设能配出来,是不是在错误之前的部分,至少得和string2的前半截得能配上,然后我们发现假设不成立,因为这样就意味着string2要找到一个更长的后缀。和我们之前next存在冲突。

    核心思路

    KMP的核心思路就可以说是string1死活不回跳。String2只要能往前跳就往前,不能跳了,说明前后缀直接用完也没匹配上,也可以理解为string2在不断的右移,此时下一轮比较节省的越来越少。string1就往下走走一个我们重新一轮再比。

    KMP看来真的是挺麻烦的对吧。。不过代码确实是异常的简单。

    我们就是要找i2越界,只有一路对比一路加走完越界才说明找到了。
    一般来讲时间不超过2n次,所以也可以说就是常数级别的。

    最后我们来看next数组怎么求就好了。

    首先我们知道0,1位置分别是-1,0.
    其次我们知道n号位置是需要前面n-1个next数组共同计算的就可以。就是当a[next[n-1]+1]=a[n-1]的时候,next[n]=next[n-1]+1,如果没有对上就要往前看了,这里往前看下一次比的就是a[next[next[n-1]]],这样一次次知道不能再往前了。那就0,往后走。

    实现代码:

    #include 
    #include 
    
    using namespace std;
    
    
    vector<int> getnextArray(string s)
    {
        vector<int> nextarray;
        if(s.length()==1){
            nextarray.push_back(-1);
            return nextarray;
        }
        nextarray.push_back(-1);
        nextarray.push_back(0);
    /*
    我们知道n号位置是需要前面n-1个next数组共同计算的就可以。
    就是当a[next[n-1]+1]=a[n-1]的时候,next[n]=next[n-1]+1,
    如果没有对上就要往前看了,这里往前看下一次比的就是a[next[next[n-1]]],
    这样一次次知道不能再往前了。那就0,往后走。
    */
        //这个变量的意义就是遍历到的字符要去和谁去比,顺便可以记录之前的前后缀重合程度
        int biao=0;
        for(int i=2;i<s.length();){
            //这里要说明的一点就是我们要求的next数组是这个字符之前的部分的前后缀
            //所以是i-1
            if(s[i-1]==s[biao]){
                //这里就是成功借助了之前的积累
                nextarray.push_back(++biao);
                i++;
            }
            //这个地方就是因为我们是第biao个没有匹配上,但是因为biao之前的部分也有自己的前后缀
            //所以这一次我们放低标准,缩短了前后缀匹配长度,再去比
            else if(biao!=0){
                biao=nextarray[biao];
            }
            //这个意思就是biao=0了,就是两个比一下都没对上,那就没救了,下一个了。
            else{
                nextarray.push_back(0);
                i++;
            }
        }
        return nextarray;
    
    }
    
    
    int getindexof(string a,string b)
    {
        //这里的意思就是说必不能找到子串的几种可能。
        if(a.length()==0||b.length()==0||a.length()<b.length()){
            return -1;
        }
        //也算是双指针吧
        int biao1=0;
        int biao2=0;
        vector<int> nextay=getnextArray(a);
        //这里其实如果只是为了遍历的话,前面的条件就够了,不过这里附加了一个找到跳出的条件
        //这里也是KMP算法最精髓的地方
        while(biao1<a.length()&&biao2<b.length()){
            //能匹配上,自然是皆大欢喜,我们继续往下走
            if(a[biao1]==b[biao2]){
                biao1++;
                biao2++;
            }
            //接下来看一下如果没有匹配上,按照我们之前的老思路,就是biao1++,biao2=0了
            //但是这样是浪费了不少时间的
            //所以这里我们利用之前得到的前缀和进行了一下调整。
            /*
            KMP的核心思路就可以说是string1死活不回跳。
            String2只要能往前跳就往前,不能跳了,说明前后缀直接用完也没匹配上,
            也可以理解为string2在不断的右移,此时下一轮比较节省的越来越少。
            string1就往下走走一个我们重新一轮再比。
            */
            //这里就是说明已经没救了,这个点位是绝对匹配不上了,换下一个点
            else if(nextay[biao2]==-1){
                biao1++;
            }
            //这里就是虽然我们之前匹配了半截之后断了,但是我们还是有一小截能匹配上的
            //所以就还是可以少匹配一些
            else{
                biao2=nextay[biao2];
            }
        }
        //这里就是看一下从循环是怎么出来的,如果是找到了出来的,返回找到的起始点,反之-1
        return biao2==b.length()?biao1-biao2:-1;
    }
    
    int main()
    {
        string s;
        cin>>s;
        string t;
        cin>>t;
    
        /*
        vector nextay=getnextArray(s);
        for(int i=0;i
        cout<<getindexof(s,t)<<endl;
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
  • 相关阅读:
    Docker上下载安装Nginx——解决多端口访问问题
    并发数的计算
    【MySQL】表的基础增删改查
    【leetcode】【剑指offer Ⅱ】062. 实现前缀树
    因斯布鲁克大学团队量子计算硬件突破了二进制
    EasyExcel 模板导出excel、合并单元格及单元格样式设置。 Freemarker导出word 合并单元格
    FFmpeg开发笔记(十九)FFmpeg开启两个线程分别解码音视频
    密码暴力破解漏洞(kali crunch)
    html5自定义属性--------Dataset
    HarmonyOS开发:动态共享包的依赖问题
  • 原文地址:https://blog.csdn.net/weixin_51529433/article/details/126004751