1. KMP-base

利用之前的工作努力是十分重要的.

如何快速从一个主串中找出某一模式的子串?

其实这个问题这样描述就无形又增加了难度,换个说法:

如何将一个模式子串匹配主串,并返回其起始下标?

import java.util.Scanner;
 
//如何将一个模式字串匹配主串,并返回其起始下标
 
class KMP{
    //暴力直接:先逐个匹配,合适则继续,不合适让j回到0继续匹配.
    
    //当pat失配的时候,i应当回退到原本匹配开始时的后一个位置,但是事实上不必这么麻烦,我们让i作为开始匹配时的位置,在每次试配失败后才+1作为下一次的试配开始位置即可.那么怎么实现pat与txt逐个匹配呢?好办法便是i+j,这样使得txt是与pat中的元素同步比较的.
 
    public static int direct_right(String txt,String pat){
        int m = txt.length();int n = pat.length();
        Boolean flag = true;
        for(int i=0;i<m-n;++i){
            for(int j=0;j<n;++j){
                if(txt.charAt(i+j)!=pat.charAt(j)){
                    flag=false;
                    break;
                }
            }
            if(flag){
                return i;
            }
            flag=true;
        }
        return -1;
 
    }
 
    //如果上面是用j作为状态判断条件,是看到了j变化的实质是pat逐渐接近匹配完成的过程,更好!
    public static int direct_right_plus(String txt,String pat){
        int m = txt.length();int n = pat.length();
        for(int i=0;i<m-n;i++){
            int j;
            for(j=0;j<n;j++){
                if(txt.charAt(i+j)!=pat.charAt(j)){
                    break;
                }
            }
            if(j==n) return i;//这一步为名字里多的plus
        }
        return -1;
    }
    public static int KMP_autoState(String txt,String pat){
        int[][] dp;//帮助pat在匹配中移动的数组
        int m = txt.length();int n=pat.length();
        dp=new int[n][256];
        //代表着处于第一个字符匹配状态下的dp数组的状态(1=0+1,代表j的状态已经往后滑动进一位)
        //dp数组是只与pat有关的,是其特征表示之一
        //java把数组初始化为0,现在所有状态都是0,下面针对匹配的情况进行改写 
        dp[0][pat.charAt(0)]=1;
        //影子状态初始为0;引入X是为了通过X的状态转移图来获得重启位置,这样做可以减少匹配长度的回退,避免了暴力法那样一次回到原点"解放前"的情况,也正是KMP优于暴力法的核心所在
        int X=0;
        //j=0的状态从第一个字符能匹配开始已经结束,故j从1开始
        for(int j=1;j<n;j++){
            for(int c=0;c<256;c++){
                //不用管被匹配的txt如何,用256个字符模拟可能遇到的全部字符情况
                if(pat.charAt(j)==c)    
                    dp[j][c]=j+1;//如果匹配j的状态进一位
                else 
                    dp[j][c] = dp[X][c];
            }
            //初始的X状态从j=1,即pat中的第二个字母开始,这是因为至少当pat处于状态1才可能回退(到0,因为遇到了不匹配的字符).
            X = dp[X][pat.charAt(j)];
            //事实上,状态X设计开始便是为了和j具有相同的前缀(包括自身?),而在j需要回退的时候减少j回退的长度,因而X总是落后状态j一个状态,待命j遇到不匹配字符的时候回退
        }
        int j = 0;
        for(int i=0;i<m;i++) {
            //模拟逐个匹配的过程,调用dp数组里面的规则可直接判断失配或者匹配成功了j的状态,也就是说pat离完全匹配成功还有多远的状态
            j = dp[j][txt.charAt(i)];
            if(j==n) return i-n+1;
            //i是匹配完成后txt的下标,因为题目要求的是返回被匹配字串的中的起始索引;
            //注意j==m,因为从dp[][]求法总的第一个if可知,当遇到最后一个相匹配字符时,j仍会+1
        }
        //匹配失败,返回-1 
        return -1;
    }
    public static int KMP_traditon(String txt,String pat){
        int m=txt.length();int n = pat.length();
        //先计算Next数组--最长相等前后字串长度,同样为pat的特征表示,只与pat有关
        int[] next = new int[n];
        //不对第一位进行比较,直接置为0,从pat的第一位前缀与第一位后缀开始循环下面步骤
        next[0]=0;
        //TODO:设置一个数代表新增加的后缀,再设置一个数代表最长的前缀?-->大概是这个意思.
        int i,k;
        for(i=1,k=0;i<n;i++){
            //next[i-1]为i-1处的最长相等前缀后缀长度,将k首先置为它,是代表k从可能是最长前缀串的理想情况来考虑,这也保证了不出现前缀与后缀为重合的无意义情况
            //第一个最长前缀串其实也就是第一个字符,然后包括第二个字符,然后包括第三个字符...
            k = next[i-1];//i-1!!!
            while(pat.charAt(i)!=pat.charAt(k) && k>0)
                k  = next[k-1];//k-1~!!!
            //k从最长前缀串往回退,每一次从上一次的前缀尾部开始,从而探寻新增加的后缀尾部(pat.charAt(i))是否与前缀尾部(pat.charAt(k),在动态往回退中)相同--->
            //如果前后缀匹配则下一步next[i]在原有k已经代表前缀除了新增加的尾部已经匹配过的情况下(无论是否匹配)加1,代表最长相等前后缀又增加了一位,否则next[i]一路回退到0
            if(pat.charAt(i)==pat.charAt(k)) 
                next[i]=k+1;
            else 
                next[i]=0;
                //为什么不让next[i]=k?因为新增加的后缀如果失配前缀尾部,最长的前缀后缀字串只能是0,比如aaac-->0120
        }
        int j=0;
        //这里不能用i,避免与上面混淆
        for(int p=0;p<m;p++){
            while(j>0 && pat.charAt(j) != txt.charAt(p))
                j=next[j-1];
                //在不匹配的时候,j不停地回溯上一个状态以找到从j开始时会匹配的情况---利用上一个状态的努力,这种努力存放在next数组当中
            if(pat.charAt(j) == txt.charAt(p))
                j++;
            if(j==n) return p-n+1;//注意j=n!!,因为遇见最后一匹配相等只有,j还会++!从而从j所在的位置m-1加1变成了j的状态所在值m
        }
        return -1;
    }
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        String txt = input.nextLine();
        String pat = input.nextLine();
        System.out.println(direct_right(txt,pat));
        System.out.println(direct_right_plus(txt,pat));
        System.out.println(KMP_autoState(txt,pat));
        System.out.println(KMP_traditon(txt,pat));
 
        //下面主要考虑到txt串中可能有多个子串的情况
        int p = KMP_autoState(txt,pat);
         //p+pat.length()是本身某位字符的对应的txt中的下标,只有当它小于m-n的时候,后面才可能再出现字串
         while(p>0 && (p+pat.length())<txt.length()-pat.length()){
            //如果是空的0或者失配的-1,所以p>0作为不继续查询的终止条件
            p=KMP_autoState(txt,pat);
            if(p!=-1){//因为进入循环已经说明没有失配,现在只是检测后面还有没有字串
                System.out.printf("%-3d ",p);
            }
        }
        System.out.print("\n");
        input.close();
    }                
}
 
 

1.1. 理解KMP_tradiion的几问:

1.1.1. 怎么求next[]? next[]究竟是什么?

一顿诡异的操作之后求得next[],而令人迷惑的无疑是next[]多重身份的一致性: next[]先后扮演了三个角色: - 最长前缀串长度未知 - 最长相等前缀后缀长度寻找过程决定的 - pat失配后应该去的位置(next[j-1]指示的位置)技巧

1.1.2. 最长相等前缀后缀部分是怎么起作用的?

pat失配后,肯定要向右滑动,暴力法是每次只滑动一位,并把j=0,然后再逐个匹配. 对于KMP,它知道前面已经匹配过的最长相等前缀后缀是必然匹配的,因此j的状态不必从0开始,直接从最长相等前缀后缀部分下一位开始即可—这样成功利用了上一次(j-1)已经匹配的”努力”,省去了再把上一次已经匹配过的最长相等前缀后缀(如图中的abab)再匹配一次的步骤.而这个下一位的下标正是next数组所找的j-1状态下的最长相等前缀后缀部分的长度(因为正好下标是从0开始的,这个长度的下一位的下标就是这个长度,否则这个长度对应的数值还得+1) ![@KMP笔记|left|300*0](https://i.loli.net/2019/10/31/8k1wATYCZ2NrdbX.jpg =500x)

1.1.3. 为什么pat在j处失配后,却需要j-1处最长相等前缀后缀部分的长度,即得到next[j-1]来作为j下一位置的状态?

我们刚刚解释了这个部分是怎么起作用的,但是这个作用为什么会是这样不知道.我认为这是因为next[j-1]正好就是下一位置应该在的状态,技巧而已.至于数学上对next[]的递归定义,以及更为广泛的字符串特征向量的本质---非我所能关心了.