【字符串】字符串查找:kmp学习

933 阅读5分钟

题目:1910 || 28

给你两个字符串 s 和 part ,请你对 s 反复执行以下操作直到 所有 子字符串 part 都被删除: 找到 s 中 最左边 的子字符串 part ,并将它从 s 中删除。 请你返回从 s 中删除所有 part 子字符串以后得到的剩余字符串。 一个 子字符串 是一个字符串中连续的字符序列。

思路

这种字符串查找在java中的String类中有很多应用,用indexof查找到值,找到匹配的值在替换(也可以用截取等),不断递归就能得到。

代码实现

public static String removeOccurrences(String s, String part) {
    int pos = s.indexOf(part);
    while (pos != -1){
        s = s.replaceFirst(part,"");  //也可以用substring
        pos = s.indexOf(part);
    }
    return s;
}

思考

该方法时间复杂度,以及性能能否有待提升

indexof:
/**
source:原字符串
sourceOffset:原字符串查找起始值
sourceCount:原字符串长度
target:匹配的字符串
targetOffset:配置字符串的起始值
targetCount:配置字符串的长度
fromIndex:起始值
*/

image.png

配合题目我把方法替换了数值,indexof首先是找到和s的首字符相等的位置,然后在和part每个字符逐个匹配,有一个不匹配的直接返回。

static int indexOf(String s,String part) {
    char[] source = s.toCharArray();
    char[] target = part.toCharArray();
    if (0 == source.length) {
        return (target.length == 0 ? 0 : -1);
    }
    if (part.length() == 0) {
        return 0;
    }
    char first = target[0];
    int max = source.length - target.length;
    for (int i = 0; i <= max; i++) {
        /* Look for first character. */
        if (source[i] != first) {
            while (++i <= max && source[i] != first);
        }

        /* Found first character, now look at the rest of v2 */
        if (i <= max) {
            int j = i + 1;
            int end = j + target.length - 1;
            for (int k =  1; j < end && source[j]
                    == target[k]; j++, k++);

            if (j == end) {
                /* Found whole string. */
                return i;
            }
        }
    }
    return -1;
}

假设有字符串abcda去abcdeabsda匹配是否存在

第一次匹配: image.png

后面的匹配过程:

image.png

在查找的过程,第一次匹配index匹配到4的时候发现不匹配了,第二次匹配j又回溯到了index:1,这种匹配明显重复查找了,能否找到一种方法在已匹配的结果中得到j去跳转的地方,而不是回溯到原来就已经匹配过的位置。所以就有kmp算法,由 Knuth-Morris-Pratt 这三个人发明的算法。

例子:假设有个字符串s为abcdabeabcdabda,有个字符串part需要去s中查找是否存在。

第一次匹配

image.png KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率.所以kmp会记录一份需要移动数组用于part移动的位置。后面介绍怎么算出来该数组值。 image.png

第二次匹配

已知e与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

  移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。 image.png

第三次匹配

需要移动的位置为2-0 = 2 image.png

第四次匹配

数组j,k不相等,都向后移动一位

image.png

匹配k到最后一个字符,返回(j-k的值就是需要找到结果)。

代码实现

public static int kmp(String haystack, String needle) {
    char[] hay = haystack.toCharArray();
    char[] need = needle.toCharArray();
    int i = 0,j=0;
    int[] next = getNext(needle);
    while(i<hay.length && j<need.length){
        if(j == -1 || hay[i] == need[j]){
            i++;
            j++;
        }else{
            j = next[j];
        }
    }
    if(needle.length() == j){
        return i-j;
    }else{
        return -1;
    }
}

现在重点就是如何获取next数组了

获取next数组

构建next数组的时候,刚开始的时候还是有些小疑问的?

question-1:next数组还和原字符串s有关吗?

解答:无关的,只和part字符串有关,因为kmp的思想是s移动的位置j不会在回溯到之前的位置,只会不断的移动part字符串上k的位置。

question-2:part字符串需要移动的位置是通过公式:移动位数 = 已匹配的字符数 - 对应的部分匹配值。那这个对应的匹配值怎么获取。

解答:对应的部分匹配值就是next数组,要获取next数组,我们可以看我们要跳转的位置,还是以前面的例子为例: image.png s和part已经匹配的位置是6位(abcdab),需要移动的位置是4位(abcd),abcdab中公共最大前缀后缀的长度为2。

前缀:除了最后一个字符,从第一个字符往后拼接的字符串 后缀:除了第一个字符,从最后一个字符往前拼接的字符串

image.png

也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为:

image.png

我们通过上面的匹配的时候,发现k匹配最后一个字符d的时候,公共匹配数为2,也就是说计算的时候只是匹配(abcdab)的值,所以next的数组需要往后移动一位。

image.png

现在我们已经知道next数组通过前缀后缀的公共长度能获取出来了,但是代码怎么写呢?

字符串:abcdabd一开始p[j] != p[k],我们先看个简单点的字符:aaabac

image.png

当P[k] == P[j]时,

有next[j+1] == next[j] + 1=k+1。(next[j] == k)

image.png

当P[k] != P[j]时,

k应该通过回溯到坐标k之前的更短的子串来和j匹配,最笨的方法时用k之前的所有存在的子串来匹配,但考虑到next数组的含义,k对应的next[k]的表示k对应的字符之前的子串最大的相同前缀和后缀的长度,故直接将k左移到next[k]位置,继续匹配j

image.png

这个字符串看起来让人容易误解,假设s字符串为ababcd,part为abac.

image.png

下一次的匹配就变成了j和next[k]进行比较了 image.png

总结一下:


p[j] = p[k]时,next[j]=k,j,k向后移动
p[j] != p[k]时,k=next[k],然后接着匹配,一直回溯到最开始的位置

代码实现

public static int[] getNext(String need) {
    char[] p = need.toCharArray();
    int[] next = new int[p.length];
    next[0] = -1;
    int k = -1;
    int j = 0;
    while (j < p.length - 1) {
        //p[k]表示前缀,p[j]表示后缀
        if (k == -1 || p[k] == p[j]) {
            //即当p[k] == p[j]时,next[j+1] == next[j] + 1=k+1
            k++;
            j++;
            next[j] = k;
        } else {
            k = next[k];
        }
    }
    return next;
}

完成代码:

public static int kmp(String haystack, String needle) {
    char[] hay = haystack.toCharArray();
    char[] need = needle.toCharArray();
    int i = 0,j=0;
    int[] next = getNext(needle);
    while(i<hay.length && j<need.length){
        if(j == -1 || hay[i] == need[j]){
            i++;
            j++;
        }else{
            j = next[j];
        }
    }
    if(needle.length() == j){
        return i-j;
    }else{
        return -1;
    }
}

public static int[] getNext(String need) {
    char[] p = need.toCharArray();
    int[] next = new int[p.length];
    next[0] = -1;
    int k = -1;
    int j = 0;
    while (j < p.length - 1) {
        //p[k]表示前缀,p[j]表示后缀
        if (k == -1 || p[k] == p[j]) {
            //即当p[k] == p[j]时,next[j+1] == next[j] + 1=k+1
            k++;
            j++;
            next[j] = k;
        } else {
            k = next[k];
        }
    }
    return next;
}

git:gitee.com/zhwgit/leet…

链接: leetcode-cn.com/problems/re…

leetcode-cn.com/problems/im…

参考: 1. image.png 2. www.cnblogs.com/zhangboy/p/…

3.www.ruanyifeng.com/blog/2013/0…