KMP算法究竟怎么回事儿~

如何理解KMP算法背后的思想?

首先,与暴力破解相比,KMP算法是一种更为高效的字符串匹配算法。那么,如何理解KMP算法背后的思想,为什么它可以比暴力破解更为高效呢?
  • leetcode第28题:给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
  • 暴力破解:时间复杂度为 O(haystackSize*needleSize),也就是 O(N * M)。
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
    public int strStr(String haystack, String needle) {
        int haystackSize = haystack.length();
        int needleSize = needle.length();
        // base case : "" or haystackSize < needleSize
        if(needleSize == 0){
            return 0;
        }
        if(haystackSize < needleSize){
            return -1;
        }

        int haystackIndex = 0, needleIndex = 0;
        for(int i = 0; i <= haystack.length() - needleSize; i++){
            for(int j = 0; j < needleSize; j++){
                haystackIndex = i + j;
                needleIndex = j;
                // 不等,则跳出当前 for 循环,相当于 needle 回退到初始位置 j = 0,重新开始比较。
                if(haystack.charAt(haystackIndex) != needle.charAt(needleIndex)){
                    break;
                }
                // 如果 needle 走到最后都没有出现不等的情况,说明是匹配的,则直接返回结果 i。
                if(needleIndex == needleSize - 1){
                    // System.out.println(haystack.substring(i, haystackIndex + 1));
                    return i;
                }
            }
        }
        return -1;
    }
}
  • 在上述暴力破解的过程中,每一次主串haystack和模式串needle都是在重新开始比对。
比如:
	主串 haystack = "abcdef", 模式串 needle = "abg";
	整个比对的过程如下:
		第一趟:haystack 从 'a' 开始,needle 从 'a' 开始。当发现 "abcdef"'c'"abg"'g' 不等,则进行第二趟对比。
		第二趟:haystack 从 'b' 开始,needle 还是从 'a' 开始。当发现 "bcdef"'b'"abg"'g' 不等,则进行第三趟对比。
		第三趟:haystack 从 'c' 开始,needle 还是从 'a' 开始。当发现 "cdef"'b'"abg"'g' 不等,则进行第四趟对比。
		第四趟:haystack 从 'd' 开始,needle 还是从 'a' 开始。当发现 ......
  • 上述暴力破解的过程,实际上就是穷举所有可能的过程,不过,我们如果进一步观察思考,可以发现,上述过程是可以优化的。首先,模式串 needle = “abg”; 有一个特点,就是每个字符都不同。 如果我们运用这一特点,其实第一趟比较完成,可以直接走到第三趟,也就是说,第二趟是可以被省略的,为什么呢?因为在第一趟的比较过程中,当发现 “abcdef” 中 ‘c’ 与 “abg” 的 ‘g’ 不等 的过程中,隐藏了一个信息,那就是:“abcdef” 中的 “ab” 与 “abg” 的 “ab” 相同,而 “abg” 每个字符都不同,进而可以直接推断出模式串 “abg” 中的字符 ‘a’ 与 主串 “abcdef” 中的前两个字符 ‘a’ 和 ‘b’ 都不同,进而可以直接进行第三趟比较就好。
  • 为了更好的理解,更进一步的,我们通过下面几种情况大体了解下,看看不同特征的模式串,如何加以利用,来优化减少每一趟的比较。
	优化:s即haystack,为主串,p即needle,为模式串。
				考虑几种情况:
					第一种,s = "abcdef",p = "gh",那么,kmp算法退化为暴力求解过程。
					第二种,s = "ghaghbghcghd",p = "ghi",那么,kmp算法优势不明显,但是与暴力破解还是有区别的,因为s的下标不回退,p的下标每次回退到初始位置0。这里我们思考下,为什么s的下标可以不回退,而p的下标每次都要回退到初始位置0呢?原因在于,我们利用next数组存储了p的一个关键信息,那就是,模式串p从始至终没有公共前后缀真子串(也就是各个字符都不相同),那么,当s与p进行比对的时候:
						s = "gha" + "ghbghcghd",p = "ghi"
           			s走到字符a,p走到字符i,此时发现,字符a!=字符i,如果是暴力破解方式,s的下标需要转移回退到h,p的下标需要转移回退到g,但是kmp显然不是这样,kmp中,s的下标保持不动(还是指向字符a),然后,p的下标回退到g,进一步直接和s的字符a进行比较。那么,问题又来了,为什么可以这样?
           			因为我们知道,p的各个字符都不相同,当s的字符a与p的字符i不等发生时,也意味着,在之前的对比过程中,s的gh与p的gh是相等的,所以呢,p中的i一定和p中的gh不等,那么也就一定和s中的gh不等,所以,s大佬,您下标别动,我小p直接从头开始就好。
           			综上所述,等等,我突然想到一句话,那就是,所有走过的路都不会白走,即便现在的解读你可能理解为这是弯路,但是未来决定过去,当有一天你会发现,原来所有的弯路,都会笔直的连接在一起,成就你独一无二的人生!抱歉,扯远了,但是你品,您细品,刚刚举的这个例子,是不是这么回事儿呢~
           			第三种,s = "aaacaabaacaaac",p = "aaaa",此时,p的next数组存储了一个关键信息,那就是p的所有字符都相同,都是字符a。那么,当s与p进行比对的时候:
           				s = "aaac" + "aaabaacaaac",p = "aaaa"
           			s走到字符c,p走到字符a(第4个字符a),发现字符c!=字符a,那么,此时我们保持s下标不动,那么p呢?此时p的最长公共真子串是aaa,但是此时p最应该移动到的位置是c的下一个字符(直接越过c),当模式串p比较特别的时候,比如,p = "aabaabaac",该如何最高效的计算next数组让匹配最高效呢?复旦大学朱洪教授对KMP串匹配算法进行了改进,其据说是针对此种情况的,同时,kmp也有一些特殊的扩展来响应一些特殊情况,但是万变不离其宗,本质还是借助next承载p的信息,进而借助走过的弯路,去开创更高效的未来,其实走来走去,走过的路都在p里~
           			
           			第四种,s = "aacaabaacaac",p = "aacaac",那么,此时的kmp优势就显现出来了~
  • 所以,KMP算法核心思想,其实是运用模式串的特征信息去简化暴力破解的各趟流程,其本质可以类比动态规划的思想,缓存中间步骤信息来减少执行次数。(比如abab是模式串,主串是abacabc,那么当模式串的第二次出现的b字符与主串的第一次出现的c字符相遇时,c与b不等,那么模式串直接回退到第一个a,因为对于模式串自身而言,ab与ab是相等的,而主串是无需回退的。)
KMP主体代码:
private int[] getNext(String p) {//模式串自己与自己比较
		int len = p.length(),step=0;
		int[] next=new int[len];
		next[0] = 0;
		for(int i=1;i<len;i++) {
			while(step>0&&p.charAt(step)!=p.charAt(i)) {//预判下一个字符是否相等
				step=next[step-1];//不相等则回溯
			}
			if(p.charAt(step)==p.charAt(i)) {
				step++;//相等则最长真前缀字符串长度加一,最长真前缀字符串是等于最长真后缀字符串的
			}
			next[i]=step;//记录到当前字符为止,最长真前缀字符串与最长真后缀字符串的长度值
		}
		return next;
	}
private int KMP_MATCHER(String T,String P) {
		int P_len=P.length();
		if(P_len==0)return 0;
		int T_len=T.length();
		if(T_len<P_len)return -1;
		if(P_len==1) {
			for(int i=0;i<T_len;i++) {
				if(T.charAt(i)==P.charAt(0))return i;
			}
			return -1;
		};
		int[] next=getNext(P);//得到模式串next数组
		int step=0;
		for(int i=-1;i<T_len-1;i++) {
			while(step>0&&P.charAt(step)!=T.charAt(i+1)) {//注意数组下标问题,数组下标是从0开始的,且不可以小于零
				step=next[step-1];//step是最长真前缀字符串的下一个字符,i+1是最长真后缀字符串的下一个字符
			}//一般伪码实现分析用1开始
			if(P.charAt(step)==T.charAt(i+1))step++;
			if(step==P_len) {
				System.out.println("Pattern occurs with shift"+(i-P_len+2));
				step=0;
				return i-P_len+2;//i+1-(P_len-1)
			}
		}
		return -1;
	}
KMP题解:
//leetcode submit region begin(Prohibit modification and deletion)
/**
 * 1. next数组是什么?计算机是如何计算 next数组 的?
 *      一句话:记录到当前字符为止,最长“公共”真前缀字符串与最长真后缀字符串的长度值(最长公共前后缀长度值)。
 *      什么意思?这里面有几个关键词,分别是:“公共”、真前缀、真后缀。
 *          比如:
 *              1.1 p = "abab"; printArray : 0 0 1 2
 *                  a:最长公共前后缀长度值为 0,因为a是原字符串,而非真前缀、真后缀,真即原字符串的真子集。
 *                  ab:最长公共前后缀长度值为 0,此时,真前缀只能是 a,真后缀只能是 b。
 *                  aba:最长公共前后缀长度值为 1,此时,对应最长公共前缀为 a,最长公共后缀为 a。
 *                  abab:最长公共前后缀长度值为 2,此时,对应最长公共前缀为 ab,最长公共后缀为 ab。
 *
 *              1.2 p = "ababcbbac"; printArray : 0 0 1 2 0 0 0 1 0
 *                  a:0
 *                  ab:0
 *                  aba:1
 *                  abab:2
 *                  ababc:0
 *                  ababcb:0
 *                  ababcbb:0
 *                  ababcbba:1
 *                  ababcbbac:0
 *              所以,我们肉眼是可以很快发现规律,直接锁定找到对应的最长公共前后缀 字符串的。(人脑厉害吧~)
 *              但是,计算机如何查找锁定呢?每次都暴力破解去遍历成本太高~
 *              getNext的理解:
 *                  step变量作为最长公共前后缀 字符串长度 的存储。
 *                  每次最长前缀字符串从: step 开始
 *                  每次最长后缀字符串从: i 开始
 *                  初始第一个字符,因为非真,所以赋值 next[0] = 0
 *                  而后,
 *          for(int i=1;i<len;i++) {
 *             //预判下一个字符是否相等
 *             while(step>0&&p.charAt(step)!=p.charAt(i)) {
 *                 //不相等则回溯
 *                 step=next[step-1];
 *             }
 *             if(p.charAt(step)==p.charAt(i)) {
 *                 //相等则最长真前缀字符串长度加一,最长真前缀字符串是等于最长真后缀字符串的
 *                 step++;
 *             }
 *             //记录到当前字符为止,最长真前缀字符串与最长真后缀字符串的长度值
 *             next[i]=step;
 *         }
 *
 * 2. KMP核心思想是什么,巧妙在哪里?
 *      其实,联想动态规划的思想,也可以通过状态机的方式进行解读。
 *
 * 测试结果:
 * when:
 * String s = "abacabc", p = "abab";
 * printArray : 0 0 1 2
 * res : -1
 */
class Solution {
    public int strStr(String haystack, String needle) {
        return KMP_MATCHER(haystack, needle);
    }

    // 模式串自己与自己比较
    private int[] getNext(String p) {
            int len = p.length(), step = 0;
            int[] next = new int[len];
            next[0] = 0;
            for(int i = 1;i < len; i++) {

                // 预判下一个字符是否相等。
                while(step > 0 && p.charAt(step) != p.charAt(i)) {
                    // 不相等则回溯。p回溯,主字符串不动。
                    step = next[step - 1];
                }

                if(p.charAt(step) == p.charAt(i)) {
                    // 相等则最长真前缀字符串长度加一,最长真前缀字符串是等于最长真后缀字符串的
                    step++;
                }

                // 记录到当前字符为止,最长真前缀字符串与最长真后缀字符串的长度值
                next[i] = step;
            }
            return next;
        }

    private int KMP_MATCHER(String T,String P) {
            int P_len = P.length();
            if(P_len == 0){
                return 0;
            }

            int T_len = T.length();
            if(T_len<P_len){
                return -1;
            }

            if(P_len==1) {
                for(int i = 0; i < T_len; i++) {
                    if(T.charAt(i) == P.charAt(0)){
                        return i;
                    }
                }
                return -1;
            }

            // 得到模式串next数组
            int[] next = getNext(P);
            // step从0开始,此时有0个字符匹配。
            int step = 0;
            // 因为主字符串不会回退,所以其一直在往前移动~
            for(int i = -1; i < T_len - 1; i++) {
                // 注意数组下标问题,数组下标是从0开始的,且不可以小于零。
                // 子字符串需要根据走过的路的信息,自身聚焦调整。预判,所以是 i + 1
                while(step > 0 && P.charAt(step) != T.charAt(i + 1)) {
                    // step是最长真前缀字符串的下一个字符,i+1是最长真后缀字符串的下一个字符。
                    step = next[step-1];
                }

                // 一般伪码实现分析用1开始。
                if(P.charAt(step) == T.charAt(i + 1)){
                    step++;
                }

                // step从0开始,此时有0个字符匹配。step == P_len 则表示有 P_len 个字符匹配,即已经完全匹配。
                if(step == P_len) {
                    // System.out.println("Pattern occurs with shift"+(i-P_len+2));
                    step = 0;
                    // i + 1 - (P_len - 1),预判,所以是 i + 1,且 i + 1 - (P_len - 1) 为 startIndex。
                    return i - P_len + 2;
                }
            }
            return -1;
    }

    private void printArray(int[] array){
            for(int i = 0; i < array.length; i++){
                System.out.print(array[i] + " ");
            }
            System.out.println();
    }
}
//leetcode submit region end(Prohibit modification and deletion)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值