主字符串中查找字符串与KMP算法

Cirno

判断字符串中是否包含有某子字符(串),是各种字符串相关算法题的基础,这里列举了博主学习时写的一些方法。如果包含某SubString,我们返回它在字符串的位置,如果不包含,返回-1。

1. 朴素算法

    public static int matchSubString(String str, String sub) {
        char[] strList = str.toCharArray();
        char[] subList = sub.toCharArray();    
        int subLen = sub.length();
        int strLen = str.length();
        for(int i = 0; i <= strLen - subLen; i++) {
            int temp = i;
            int j = 0;
            while(j < subLen && strList[temp] == subList[j]) {
                j++;
                temp++;
            }
            if (j == subLen) {
                return i;
            }
        }
        return -1;
    }

2. String.indexOf() 方法

    public static int matchSubString(String str, String sub) {
        return str.indexOf(sub);
    }

3. KMP算法

Donald Knuth 和 Vaughan Pratt 于1970年提出了线性复杂度的字符串匹配方法,James Morris在同年也独立发现了相同的方法,他们三人于1977年一起发布了相关论文,以他们三人姓氏命名的Knuth–Morris–Pratt(KMP)算法由此诞生。简单来说,与朴素算法最大的不同是,KMP算法不会去重复查找主字符串中已匹配过的字符,因此,KMP算法将朴素算法的O(m*n)优化成了O(m+n)

朴素算法的思路是,如果本次迭代未匹配到,下次迭代把pattern后移一位,然后再次尝试匹配。而KMP算法运用了部分匹配表(Partial Match Table)来优化这个过程,这也是KMP算法的核心。我们用一个例子来说明,我们有如下两个字符串,主字符串和待匹配字符串pattern:

主String:

abadabababac
Pattern:
ababacabcadc
想要求出Pattern的PMT,首先我们要明白前缀和后缀的概念,比如我们有一个字符串 ababa, 那么它的前缀有{a,ab,aba,abab},它的后缀有{a,ba,aba,baba},那么,字符串的最长公共前后缀就是aba,长度为3,这就是我们要在PMT里填入的值。Pattern可以被看做不同长度的字符串,比如我们说到的ababa,就是Pattern的前5位,那么在PMT的第五位就赋值为3,也就是PMT[4] = 3。同理,完整的PMT表就是:
ababac
012345
a0
ab0
aba1
abab2
ababa3
ababac0
换种写法:1
charababac
index012345
PMT001230
Next-100123
为了方便使用,我们将PMT的值,整体后移一位,然后将空出来的第一位补上-1,记作Next行。个人认为这么做纯粹是为了编程简洁,在后面代码中会进一步说明。 ### Iteration 1:
abadabababac
aaaaaaaaaaaa
ababac
第一次迭代,Pattern的第四位匹配失败,朴素算法在此时会将Pattern后移一位。而KMP算法,就要查询Next数组中对应的值了,此次迭代,Pattern的第四位,也就是Pattern[3]匹配失败,那么我们便查询Next[3]对应的值,对应的值是1,那么,就将Pattern[1]的位置对齐到当前匹配失败的位置。 ### Iteration 2:
abadabababac
aaaaaaaaaaaa
abababac
第二次迭代,我们不用管Pattern[1]之前的字符了,因为根据部分匹配表,之前的字符已经是相同的了。然后Pattern[1]匹配失败,这里Next[1]对应的值是0,于是将Pattern[0]对齐当前匹配失败的位置。 ### Iteration 3:
abadabababac
aaaaaaaaaaaa
abaababac
第三次迭代,Patttern[0]匹配失败,Next[0]对应的-1, 所以将Pattern后移一位。这时就可以看出,实际上在Next表中添加的-1仅仅是为了后移一位,你完全可以设它为-100,只要在代码中你能判断出,当前是要整体后移一位就可以,只不过,使用-1可以使你的代码更简洁。 ### Iteration 4:
abadabababac
aaaaaaaaaaaa
abaaababac
第四次迭代,匹配到弟6位失败,Next[5]对应的是3,于是将Pattern[3]与当前匹配失败的位置对齐。 ### Iteration 5:
abadabababac
aaaaaaaaaaaa
abaaabababac
第五次迭代,只需要看Pattern[3]、Pattern[4]和Pattern[5]是否匹配,结果是匹配,于是匹配完成。 ### 代码: 我们可以先得出PMT,然后后移一位得出Next表。 `Java public static int[] PMT(String str) { int[] pmt = new int[str.length()]; pmt[0] = 0; int i = 0; int j = 1; int count = 0; while(j < str.length()) { if(str.charAt(i) == str.charAt(j)) { count++; pmt[j] = count; i++; j++; } else { count = 0; pmt[j] = 0; i = 0; j++; } } return pmt; } public static int[] getNext(String str) { int[] pmt = PMT(str); int[] next = new int[str.length()]; next[0] = -1; for (int i=0;i < str.length()-1; i++) { next[i+1] = pmt[i]; } return next; } ` 当然我们也可以不写PMT表,我们直接得出Next表,这样会更加简洁: `java public static int[] getNext(String str) { int[] next = new int[str.length()]; next[0] = -1; int i = 0; int j = 1; int count = 0; while(j<str.length()-1) { if(str.charAt(i) == str.charAt(j)) { count++; next[j+1] = count; i++; j++; } else { count = 0; next[j+1] = count; i = 0; j++; } } return next; } ` 我们的主循环就是: `java public static int matchSubString(String str, String sub) { char[] strList = str.toCharArray(); char[] subList = sub.toCharArray(); int[] next = getNext(sub); int subLen = sub.length(); int strLen = str.length(); int i = 0,j = 0; while(i<strLen && j<subLen ) { // 这里就是设-1的好处,因为我们不用单独为了它写一种情况,比如: // if (j == -100) { // i++; // j = 0; // } if (j == -1 || strList[i] == subList[j]) { i++; j++; } else{ j = next[j]; } //匹配到字符串 if(j == subLen) { return i - subLen; } } //未匹配到 return -1; } ` ## 3. 结果 input: > str: Some Computing Science students are studying in the Computing Science Center. > sub: Computing output: > 5 很显然,这几种方法都是返回最先找到的SubString位置,后面若有相同的SubString会被忽略掉。
阅读 1.2k
1 声望
0 粉丝
0 条评论
1 声望
0 粉丝
文章目录
宣传栏