我眼中的KMP

0、关于KMP

KMP是用于字符匹配的一个常用算法。关于KMP概念、前缀、后缀概念参考文章中有详细介绍,这里就不做详细讨论,本文详细关注讨论KMP核心点,next数组的作用及求解思路,KMP算法的思路。

1、next数组是什么

next数组里面存放的是要查找的字符串前i个字符串的所有前缀、后缀相等的公共串中,最大的 长度值。比如需要查找的一个子串ababcd,next[0]表示子串中前1个字符串即a的前缀和后缀中相等字符串的最大长度,因为a的前缀和后缀没有,故next[0] = 0;对于next[2],即先求出子串aba的前缀和后缀出来,前缀为a,ab,后缀有ba,a,相等的公共串为a,长度为1,因此next[2] = 1;依次可以求出。

2、next数组的作用是什么

    在我们用暴力解决字符串匹配的过程中,当我们主串的下一个字符和子串的下一个字符不相等时,此时,主串指针需要回退到最近的一个字符和子串第一个字符相等的下一个位置继续匹配。当后面有一个字符和子串第一个字符相等时,再来一遍上述的匹配过程。整个过程中,主串指针会回退很多遍,导致我们暴力破解的复杂度达到了O(n*m)。
    我们再来考虑一下上面的情形,当我们主串的下一个字符和子串的下一个字符 不相等时,我们主串前面的一些字符是和子串经过了匹配的,而且匹配成功了,如下图:
我眼中的KMP_第1张图片

如果按照暴力方式,主串此时需要回退到第4个位置处的b处,继续进行扫描判断。为了 优化这个过程,我们现在来考察子串,对于子串前q-1个字符串s,即s = abcab:
(1)如果字符串s 存在着相等的前缀和后缀(即next[q-1]>0),这个时候我们不需要去回退主串指针,只要把我们的子串往后移动,使得我们字符串s中的前缀和后缀相等的那个前缀移动到后缀的位置处,因为 前缀=后缀=主串中的部分串,这样我们直接就将子串定位到了可以进行下一趟比较探测的位置,而没有回退过主串指针。我们 希望这个相等的前缀和后缀的长度越大越好(这也是为什么我们next数组中是求相等的前缀和后缀中最长的那个相等串的值),显然这样就可以匹配更多相同的元素。对于上面的例子来说就是,s = abcab,此时前缀和后缀相等的只有ab,也就是我们的next[q-1] = 2;这个时候,我们移动子串,使得我们的前缀ab移动到后缀ab处的位置,如下:
我眼中的KMP_第2张图片

这样,我们就可以在不会退主串指针的情况下,继续进行下一趟探测比较了。上面子串的 移动距离也很好计算,就是 前面已经匹配的子串的字符数量-next[q-1],即5-2=3。

(2)如果字符串s中 不存在相等的前缀和后缀(即next[q-1]=0),这个时候就更好办了,按照上面这个移动距离公式,可以计算出移动的距离是5-0 = 5,相当于直接移动到了i指针的下面。这里为什么可以这么移动,是因为这种情况下即使我们回退指针,在再次到i之前,我们也不能找到能够匹配到的字符串。

可以看到,我们 next数组的作用,就是在我们匹配失败的时候,确定我们子串需要往后移动的距离,而避免我们的主串指针进行回退。这样可以保证主串在只遍历一遍的情况下找到子串。

因此KMP算法的重点就是如何快速的求出这个next数组出来。

3、求出next数组

经过上面的分析,知道next数组是只与子串有关与主串无关的,它记录的是 子串到每个字符处那个公共前缀(或后缀)的最大长度。因此,我们现在主要就是要对子串来求出其next数组。
假设我们已经准备计算第i个位置字符的next值。我们可以利用前面已经计算出来的next值进行求解。假设已经求出的next[i-1] = k,即子串从开始到i-1处这段字符串中,最大的相等的前缀和后缀长度为k,如下图中的第二排所示:


这个时候,我们需要计算到第i个字符处next值,这个时候就有两种情况了(假设待查子串的字符串以str变量表示):
(1)如果 str[i] == str[k],这种情况下,前缀往后再加上一个字符之后依然会和后缀往后加上一个字符相等,因为此时前缀和后缀加上的是同一个字符。因此,此时 next[i] = next[i-1] + 1,即next[i] = k+1
对于上图来说,就是前缀a...b加上一个字符d后,变为a..bd,后缀加上一个字符c后,变为a..bc(这里是不等的,只是为了说明一下)。
(2)如果 str[i] != str[k],说明前、后缀分别加上一个字符扩展之后是不相同了,这个时候, a...b这一段是不能再用了,也就是next[i-1]的值没有考察意义了,也即k此时需要调整。那就只能缩小范围,前缀要往前收缩,后缀要往后收缩。因为此时,前缀和后缀是相同的字符串(即如上图中的前面k个字符串a...b,和后面k个字符串a...b是相同的,因此只要在前缀字符串中找出新的前缀和后缀,这个 新的前缀=新的后缀=原来的后缀的一小部分)。如下图所示:
我眼中的KMP_第3张图片

注意: 上面的k有两层含义,一个指的是next[index]中的值,表示到第index处字符串相等前缀和后缀的最大长度;另一个是,由前一层含义可知最大相等的前缀长度为k,也就可以用这个k作为下标索引值,即前缀是从str[0]...str[k-1]处

如上图,继续分析,如果str[i] != str[k],此时需要将k个字符串前缀a...b进行划分,先考察k-1处的next值,令k' = next[k-1],即a...b划分成了如下所示的样子:


这时,继续判断str[i] == str[k '],轮回到上面两种情况的判断,如果相等,即可以直接确定next[i]的值,不等又继续对str[0]...str[k '-1]处进行划分,依次下去,直到i遍历完子串。

因此,根据上述思路,我们可以编写出以下代码来实现:    
 private int[] getNextArray(char[] chs){
        int i;//字符数组的下标指示器
        int k;//前一个字符处的最大公共(相等)前、后缀子串的长度
        int[] next = new int[chs.length];
        for(i = 1,k = 0; i < chs.length; i++){
            while(k > 0 && chs[i] != chs[k])    //此处的k可以作为上面讲到的第一层含义理解
                k = next[k - 1];    
            if(chs[i] == chs[k]){
                k++;
            }
            next[i] = k;
        }
        return next;
    }

4、KMP实现

上面我们把一个重要问题next数组的求解问题解决了。下面就可以开始KMP的实现了,KMP的步骤或原理其实在第2点中 “ next数组的作用是什么”已经体现出来了。我们对比第2点和第3点中next数组的求解,发现 其实进行next数组求解的过程,类似于主串和子串进行匹配的过程,只不过是在next数组求解过程中,是子串和子串自己进行比较而已。因此整个KMP算法的代码过程如下:     
public boolean kmp(String str1,String str2){
        char[] strA = str1.toCharArray();
        char[] strB = str2.toCharArray();
        int[] next = getNextArray(strB);    //获取需要匹配子串的next数组
        int i,k;
        for(i = 0,k = 0; i < strA.length; i++){
            while(k > 0 && strA[i] != strB[k])
                k = next[k-1];
            if(strA[i] == strB[k]){
                k++;
            }
            /*
             * 注意,这里和求next数组有一点区别,因为kmp里面是主串和子串进行比较,当子串最后一个元素都相等的时候,k就相当于是子串和主串相同的公共部分长度,
             * 而对于求next数组中的方法来说,相当于是自身和自身进行比较
             */
            if(k == strB.length){
                return true;
            }
        }
        return false;
    }

    //获取next数组
    private int[] getNextArray(char[] chs){
        int i;//字符数组的下标指示器
        int k;//前一个字符处的最大公共(相等)前、后缀子串的长度
        int[] next = new int[chs.length];
        for(i = 1,k = 0; i < chs.length; i++){
            while(k > 0 && chs[i] != chs[k])    //此处的k可以作为上面讲到的第一层含义理解
                k = next[k - 1];    
            if(chs[i] == chs[k]){
                k++;
            }
            next[i] = k;
        }
        return next;
    }
经过测试,符合要求。

我们比较上面两个方法的代码,正如我们前面所说的那样,KMP过程和next数组求解过程非常类似,因此这两个方法非常类似,可以说, next数组求解过程就是自身匹配(或自身KMP)的过程

参考文章:
【经典算法】——KMP,深入讲解next数组的求解

KMP算法详解

一幅图让你彻底理解KMP算法




你可能感兴趣的:(算法,KMP,算法面试题)