您的当前位置:首页正文

AcWing 算法基础 KMP字符串

2024-11-07 来源:个人技术集锦

KMP

KMP解决了一个什么问题

假设有两个字符串,一个长字符串,一个短字符串,我们想要知道在这个长字符串中是否包含这个短字符串,就需要在长字符串中搜索短字符串。那么我们最容易想到的方法是什么呢,就是暴力法:

public void main(){
    //a为长字符串
    String a = "ababababaab";
    //b为短字符串
    String b = "babaa";
    for(int i = 0; i < a.length(); i++){
        for(int j = 0; j < b.length(); j++){
            if(a.charAt(i+j) != b.charAt(j)) break;
            if(j == b.length()-1) System.out.println("匹配字符串的初始下标为:" + i);
        }
    }
}

上述方法为解决该问题的暴力法时间复杂度为O(m*n),mn为两个字符串的长度。该时间复杂度是比较高的,所以有很多人苦思冥想,最终想出了KMP法来解决这个问题

KMP思想讲解

KMP利用了短字符串的一些性质,避免了大部分错误的枚举。如下图所示,在第一次搜索时搜索到了绿色的位置,发现不匹配,这个时候,如果按照暴力法,那么会变成图2中显示的情况。再开始匹配,但是我们知道因为绿色之前的字符串是已经匹配了的,而绿色之后的字符串是不匹配的,这个时候短字符串还想匹配到图1中绿色的位置,假设短字符串现在匹配到了长字符串的第4位和短字符串的第4位,在第五位的时候不匹配了,那么短字符串向后移动一位之后,如果短字符串能与长字符串匹配到绿色的的位置也就是,长字符串的2,3,4位匹配短字符串的1,2,3位,那么这次枚举才是有意义的,不然这次枚举就是没有必要的,肯定会失败。
那么,我们通过短字符串自身的性质就可以判断出该次枚举是否有必要,我们知道第一次循环长字符串的1,2,3,4位和短字符串的1,2,3,4位匹配,在第5位不匹配,我们发现长字符串的2,3,4位就是对应的短字符串的1,2,3位,也就是说只有短字符串的1,2,3位对应短字符串的2,3,4位这次枚举就是有效的,否则就是无效的,我们可以来判断是否满足这个性质,如果满足这个性质,那么就进行这次枚举,如果不满足这个性质,就不进行这次枚举,那么我们接着来看下一次枚举,假如说字符串又向右移动了一位,这次长字符串的3,4位对应的短字符串的1,2位,而长字符串的3,4位有和短字符串的3,4位在图1中匹配,所以我们转换得到如果短字符串的1,2位和短字符串的3,4位是相同的,那么该次枚举就是有效的,否则就是无效了,也就是说,当我们知道在字符串的前i位构成的字串中,我们事先知道每一个字串中的前缀与后缀的的字符相等的最大长度,那么就可以避免很多不必要的枚举。现在假设我们已经知道了上述前缀与后缀相等的最大长度,我们看一下我们的代码会发生什么变化。

这里我们定义next数组代表字符串前i位所组成的字串的前缀与后缀相等的最大长度。

public void main(){
    //a为长字符串
    String a = "ababababaab";
    //b为短字符串
    String b = "babaa";
    //这里短字符串的第j位如果不匹配,那么我们首先要访问第j-1位的上述最大长度。
    //得到了最大长度之后我们就可以将让i直接向前移动j-1-next[j-1]。
    //j变为next[j-1],这样我们就避免了不必要的枚举
    //通过考虑边界值之后,我们发现我们的代码变成了这样
    for(int i=0, j=0 ; i < a.length(); i++){
        //这里把j移到外面是因为我们通过next数组来设置j,而next数组的第0位就是0,如果我们短字符串第一位就匹配不上,那么我们需要将i++,然后重新匹配。
        for(;; j++){
            if(i+j >= a.length()) break;
            if(a.charAt(i+j) != b.charAt(j)) {
                if( j != 0){
                    i = i + j-1-next[j-1];
                    j = next[j-1];
                }else break;
            }
            if(j == b.length()-1) {
                System.out.println("匹配字符串的初始下标为:" + i);
                i = i + j-1-next[j-1];
                j = next[j];
                break;
            }
        }
    }
}

上述代码中i表示长字符串起始位置,j表示短字符串未匹配将要匹配的下标,我们现在改变定义,另i表示长字符串的未匹配但是将要匹配的下标,j表示短字符串未匹配将要匹配的下标,我们看看代码会发生什么变化:

public void main(){
    //a为长字符串
    String a = "ababababaab";
    //b为短字符串
    String b = "babaa";
    for(int i=0, j=0 ; i < a.length(); i++){
        for(;;){
            if(a.charAt(i) != b.charAt(j)) {
                if( j != 0){
                    //这里如果i表示的未匹配但是将要匹配的值,那么我们有图1可以发现,在短字符串移动后,我们还是将长字符串的第5个字符和短字符串进行匹配,所以i在这里不变
                    j = next[j-1];
                }else break;
            }
            if(a.charAt(i) == b.charAt(j)){
                if(j == b.length()-1) {
                    System.out.println("匹配字符串的初始下标为:" + i-j);
                    j = next[j];
                }else {
                    j++;
                }
                break;
            }
        }
    }
}

这里我们发现除了if(a.charAt(i) != b.charAt(j) && j != 0)这种情况,其他情况都会跳出死循环。于是我们只需要将循环添加到该判断语句就可以。整理代码可得

public void main(){
    //a为长字符串
    String a = "ababababaab";
    //b为短字符串
    String b = "babaa";
    for(int i=0, j=0 ; i < a.length(); i++){
        while(j!=0 && a.charAt(i) != b.charAt(j)) {
            j = next[j-1];
        }
        if(a.charAt(i) == b.charAt(j)){
            if(j == b.length()-1) {
                System.out.println("匹配字符串的初始下标为:" + i-j);
                j = next[j];
            }else {
                j++;
            }
        }
    }
}

现在这样就是比较常见的KMP算法的代码了。

next数组的求解

在上面的算法中,我们在以知next数组的情况下,了解到KMP算法的步骤,下一步我们将来寻找一个求解next数组的方法。
我们发现求解next数组可以采用动态规划与KMP的思想。
我们把字串的后缀当作上述长字符串,子串的前缀当作上述短字符串,于是,我们设定初始条件next[0] = 0;

next[0] = 0;
//i代表后缀的最后一位,j代表前缀(不是最后一位)。
for(int i = 1,j = 0;i < b.length(); i++){
    if(b.charAt(i) == b.charAt(j)){
        next[i] = next[i-1]+1;
        j++;
    }else{
        while(j != 0 && b.charAt(i) != b.charAt(j)) j = next[j-1];
        if(j == 0 &&b.charAt(i) != b.charAt(j)) next[i] = 0;
        else if(j == 0) next[i] = 1;
        else next[i] = next[j-1]+1;
    }
}

下面是KMP比较简单的写法,方便记忆,背会就对了

import java.util.*;
class Main{
    public static void main(String[] args){
        int ne[] = new int[10010];
        ne[0] = -1;
        Scanner Reader = new Scanner(System.in);
        int n = Reader.nextInt();
        String s = Reader.next();
        int m = Reader.nextInt();
        String c = Reader.next();
        //求next数组
        for(int i = 1,j = -1; i < n; i++){
            while(j >=0 && s.charAt(i) != s.charAt(j+1)) j = ne[j];
            if(s.charAt(i) == s.charAt(j+1)) j++;
            ne[i] = j;
        }
        for(int i = 0,j = -1; i < m; i++){
            while(j >=0 && c.charAt(i) != s.charAt(j+1)) j = ne[j];
            if(c.charAt(i) == s.charAt(j+1)) j++;
            if(j == n-1) {
                System.out.print(i-j+" ");
                j = ne[j];
            }
        }
    }
}
Top