KMP字符串匹配算法思悟

KMP字符串匹配算法思悟

本文细致分析了KMP算法高效的原因以及KMP算法的实现原理;
源码github地址(这是我自己实现的相关算法(目前有排序算法和KMP算法),其中用到的数据结构也是自己实现的一套API,包括链表、栈、队列、树、图等内容,完善ing~)

1. KMP算法是什么?

KMP算法是一种高效(线性时间)的字符串匹配算法;

2. 为什么KMP算法高效?

KMP算法之所以高效是因为它充分利用了模式字符串本身的信息以及匹配问题的特点;(感觉任何一种高效算法都是通过充分利用已知信息(包括计算得到的消息和问题本身提供的信息),避免无用的计算,从而实现高效);

  1. 原因一:

    KMP算法充分利用模式字符串本身的信息体现在它会根据模式字符串计算出一个数组,这个数组是什么以及为什么要计算一个数组A以及如何计算?,且看下文一一道来(理解了KMP高效的真正原因,才能深入理解KMP算法代码,毕竟,代码背后是解决问题的思想~从思想入手看代码,就是从本质出发看现象,而从代码入手看思想,就有一点点难啦~废话有点多,入正题!);

  2. 原因二:

    KMP算法充分利用了匹配问题的特点,举个例子,比较好理解:
    《KMP字符串匹配算法思悟》
    在上图中,待匹配字符串中V所指向的字符与模式字符串中对应位置上的字符不匹配,接下来将向右移动模式字符串;
    以上例子表明,匹配问题的中无法避免的一个操作就是回溯,所谓回溯就是当发生不匹配时,我们需要推翻已经匹配了部分,开始新的匹配;但是问题来了,从哪里开始新的匹配呢?普通实现算法是将模式字符串右移一位,然后继续匹配; 我们按照普通思路来看这个匹配过程:下一步A将和B匹配(注意:A和B匹配表示模式字符串的A字符和待匹配字符串里的B字符匹配~):
    《KMP字符串匹配算法思悟》
    然后发现不匹配,之后再向右移动,A和A匹配;之后B和B匹配;A和A匹配;最终到达了下图位置:
    《KMP字符串匹配算法思悟》
    注意,在到达这个位置时,我们进行了5次字符间的比较;最后比较又停在了原来发生不匹配的地方;
    我们来观察这个过程的实质,其实就是从左到右的过程:
    《KMP字符串匹配算法思悟》
    而抛开最后一个字母不同(正是它们的不同导致匹配中断),前5个字母是相同的!为什么会相同呢?因为它们经过了匹配检测呀!(这是一句废话,但是这句废话会引出A处的答案~):也就是说,当经过一段匹配后发生了不匹配的情况,之后进行的匹配就相当于已经匹配的部分模式字符串自身和自身错开一位后进行匹配,我把它称为“自匹配”;显而易见的一个问题就是,匹配过程中会发生不少这样的由于不匹配而引发的模式字符串”自匹配”情况;这就是匹配问题的特点:在匹配过程中发生不匹配后所进行的匹配过程是已匹配部分字符串的“自匹配”~;

    既然“自匹配”是一种重复计算的过程,而该过程中使用的字符串都是模式字符串的一部分(更准确的说是模式字符串从0位开始的子串):那么我们就可以计算一遍而避免在匹配过程中频繁重复计算!这就是A处要计算一个数组的原因~;

    那么这个A数组到底是什么呢?我们希望当发现图1不匹配时,直接转到图3而跳过中间过程(这一过程其实就是自匹配的过程),可是怎样算跳过了呢?其实就是B和A匹配(当X和Y发生匹配,那么X之前的字符和Y之前的字符就一定是匹配的呀~)也就是当Pattern[5]与S[x]发生不匹配时,下一次直接将Pattern[3]与S[x]匹配;之所以这样做,是因为我们通过“自匹配”知道Pattern[0-2]一定与S[(x-3)-(x-1)]是匹配的;也就是说,A数组记录的信息将提示我们当发生不匹配后,下一次进行匹配检测的字符的索引~(至此,A是什么以及为什么都已解释,接下来,就是如何计算A了~)

3. 如何实现KMP算法?

首先引入前缀和后缀的概念:
对于字符串X=YZ;Y、Z也是字符串;Y称为X的前缀,Z称为X的后缀;当Y不为空时,我们成Z为真子后缀;

通过模式字符串P计算数组A的过程,就是寻找字符串P[0-x]的特殊真子后缀的过程,该后缀的特殊性体现在它同样也是模式字符串的一个前缀;

(非常重要!!!)例如:A[x]等于y,就表示A[0-x]的特殊后缀的长度为y(也就是该后缀是A[0-(y-1)]),该式也表示当模式字符串中下标为x+1的字符与对应位置上的字符不匹配时,下一次与待匹配字符串对应位置上的字符进行比较的模式字符串下标为y;

private static int[] getMoveInfo(char[] pattern){
        int m=pattern.length;//模式字符串的长度
        int[] info=new int[m];//记录真子后缀长度信息的数组
        /** 表示已经匹配的字符数(也是模式字符串前缀以及真子后缀的长度)、也等价于 下一次检查的字符的坐标;这个变量含义众多,不同的地方有不同的意义~ 我觉得这是KMP最精彩的部分 */
        int matchedNum=0;
        /** 注意从1开始,因为当第一个字符(下标为0)不匹配时,只能继续前移了, 没有操作空间;另一种解释是一个字符的话,他也没有真子后缀呀; */
        for(int i=1;i<m;i++){
            /** 已经有字符匹配,但是在模式字符串下标为matchedNum处发生了不匹配的情况; */
            while(matchedNum>0&&pattern[matchedNum]!=pattern[i]){
            /* 这里,我们将matchedNum往回退一次;info[matchedNum]表示模式字符串 A[matchedNum]的特殊真子后缀的长度,由于该后缀同时也是模式字符串的一个前缀, 所以它们一定是相同的,即相匹配的,那么接下来只需要检查pattern[i]和该前缀后面 的一个字符即可,而长度为y的字符串后面一个字符的下标也是y,所以matchedNum被赋 值为info[matchedNum]; 2018年4月21日更新: 注意!!!以上分析过程是合理的,但是结论是错误的:matchedNum应 该被赋值为info[matchedNum-1];首先,根据上面“非常重要”一段,在matchedNum 处发生不匹配,那么x+1=matchedNum;x自然等于matchedNum-1,然后下一次比 较的字符(模式字符串中的字符)下标为info[x]=info[matchedNum-1]; 然后,求解info数组的过程,说白了也是一次匹配问题,而这里和后文匹配 函数里对matchedNum回退的处理不一致,这本应该引起注意,但是坦白的说, 由于测试用例没有出现错误,加上总结时是站在理解、解释代码的角度,所以并没 有质疑,以后绝对引以为戒!!! 为什么会出现这种错误?一方面是因为对info数组的含义以及如何应用理解的不到位, 另一方面也是因为测试用例不够全面,没能及时反映错误; 末尾; */
                matchedNum=info[matchedNum];//这是错误的代码
                matchedNum=info[matchedNum-1];//这才是符合的代码;
            }
            /*退出时,要么matchedNum=0,即没有发生匹配,也就是当前子串 pattern[i]没有这样的特殊子后缀;要么是存在这样的后缀*/
            if(pattern[matchedNum]==pattern[i]){
            /*如果比较的字符是匹配的,也就是说这样的子后缀是存在的, 那么更新matchedNum,即更新后缀的长度;*/
                matchedNum++;
            }
            info[i]=matchedNum;//记录模式字符串P[0-i]的满足要求的后缀长度
        }
        return info;
    }

在明白了A数组是什么、为什么以及怎样计算后,接下来就是KMP算法的第二部分:匹配字符串~;

public static void match(String pattern, String source){
        int patternLength=pattern.length();//模式字符串长度
        int sourceLength=source.length();//待匹配字符串长度
        char[] p=pattern.toCharArray();//获取模式字符串数组
        char[] s=source.toCharArray();//获取待匹配字符串数组
        int[] moveInfo=getMoveInfo(p);//获得移动信息数组——计算数组A
        int matchedNum=0;//这里matchedNum表示待匹配字符串同模式字符串已经匹配的字符数
        int patternBorder=patternLength-1;//该变量仅为输出信息时使用
        for(int i=0;i<sourceLength;i++){//从头到尾遍历一遍待匹配字符串
            while(matchedNum>0&&p[matchedNum]!=s[i]){//已经有字符匹配,但是在模式字符串下标为matchedNum处发生了不匹配的情况;
            /** 注意这里和getMoveInfo函数里的区别;在这里matchedNum处的字符同s[i]字符不匹 配,说明模式字符串中0-(matchedNum-1)的字符是同待匹配字符串相应子串匹配 的;info[matchedNum-1]表示模式字符串A[matchedNum-1]的特殊真子后缀的长 度,那么接下来只需要检查s[i]和该后缀后面的一个字符即可,而长度为y的字符串后面 一个字符的下标也是y,所以matchedNum被赋值为info[matchedNum-1]; */
            /* 2018年4月21日更新 事实证明,这里和getMoveInfo()函数不应该有区别;错误原因见上文更新处~ */
                matchedNum=moveInfo[matchedNum-1];
            }
            if(p[matchedNum]==s[i])//如果对应位置上的字符是匹配的,那么更新matchedNum;
                matchedNum++;
            if(matchedNum==patternLength){//如果matchedNum等于模式字符串的长度,那么说明完成了一次匹配;
                System.out.println(pattern+" occurs with "+(i-patternBorder)+" in "+source);
                /** 我们可以认为模式字符串后边添加了一个特殊标记,该标记与任何可能出现在字符串 中的字符都不相同,也就是模式字符串在下标为patternLength的字符(这是一个 假想的标记)处发生了不匹配,所以下一次匹配检查的字符下标为 moveInfo[patternLength-1]=moveInfo[matchedNum-1]; */
                matchedNum=moveInfo[matchedNum-1];
            }
        }
    }
    原文作者:KMP算法
    原文地址: https://blog.csdn.net/slx3320612540/article/details/79831839
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞