一篇帮助理解KMP算法的文章(附C++/Java源码)

先上需求

给定一个 str 字符串和一个 value 字符串,在 str 字符串中找出 value 字符串出现的第一个位置 (从0开始)。其实就是C语言的 strstr() 以及 Java的 indexOf() 实现原理。

输入案例:str:"hello" value=“ll”
输出案例:2

暴力解法

用一个长指针i跟短指针j分别指向主串跟模式串,从左往右,一个一个匹配,如果匹配,如果出现不匹配,那就把长指针移回第 i - j + 1位(假设下标从0开始),j移动到模式串的第0位,然后又重新开始这个步骤:




直到匹配成功,返回出去就好了
于是上代码

/**

 * 暴力破解法

 * @param str 主串

 * @param value 模式串

 * @return 如果找到,返回在主串中第一个字符出现的下标,否则为-1

 */

int match(string str, string value) {
    int i = 0; 
    int j = 0; 
    while (i < str.length() && j < value.length()) {
       if (str[i] == value[j]) { 
		   // 当两个字符相同,就比较下一个
           i++;
           j++;
       } else {
		   // i后退,j重头开始
           i = i - j + 1; 
           j = 0; 
       }
    }
    if (j == str.length()) {
       return i - j;
    } else {
       return -1;
    }
}

这样的方法也可以得到结果,但是时间复杂度来到了(n*m),n为主串的长度,m为模式串的长度,一般这种暴力匹配的方法就会被老板暴力地辞退。
这时候Donald Knuth、Vaughan Pratt、James H. Morris三大高叟就想到了能不能不让i指针不回退,只移动短指针j阿,于是就有了牛逼(头痛)的KMP算法

KMP算法

由于动画效果不好制作,贴上知乎的大佬讲解,里面有动画的呈现
知乎KMP
于是有了我自己对KMP算法的理解
其实KMP算法的基本操作流程如下:

  1. 假设现在文本串 S 匹配到 i 位置,模式串 P 匹配到 j 位置 如果 j = -1,或者当前字符匹配成功(即 S[i] ==
    P[j] ),都令 i++,j++,继续匹配下一个字符;
  2. 如果 j != -1,且当前字符匹配失败(即 S[i] != P[j] ),则令 i 不变,j =
    next[j]。此举意味着失配时,模式串 P相对于文本串 S 向右移动了 j - next [j] 位
  3. 换言之,将模式串 P 匹配位置的 next 数组的值对应的模式串 P 的索引位置移动到未重复出现位置

在这里插入图片描述

例如这样的小串,如果用暴力解法 主串就得从A后的BC重新开始,此时i指针指向主串的B,j指向主串的A
在这里插入图片描述

这种情况还不算特别慢,如果是在主串“SSSSSSSSSSSSSA”中查找“SSSSB”,比较到最后一个才知道不匹配,然后i回溯,这个的效率是显然是最低的。其实用人类的思维,其实只要让i不动j回到AB第一次重复后的下一个位置就可以
在这里插入图片描述此时i还是原来的指向,只是短指针j指向了C
所以,整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪?
据说这是有公式的,翻了很久资料(搜索引擎),终于找到了关于KMP的公式:
Value[0 ~ k-1] == Value[j-k ~ j-1]
公式的推导:
当str[i] != Value[j]时
有str[i-j ~ i-1] == Value[0 ~ j-1]
由Value[0 ~ k-1] == Value[j-k ~ j-1]
必然:str[i-k ~ i-1] == Value[0 ~ k-1]

这时候我们就列个表,看看这几个公式在作甚
模式串value子串对应的各个前缀后缀的公共元素的 最大长度表 下图。
在这里插入图片描述公共元素的最长重复元素就得到是2了
这时候我们只需要的到当前匹配不等的元素在模型串的位置,只需要回溯到上次不等的位置,并且将索引一起移动
将该步骤翻译成计算机步骤如下
1)找出前缀pre,设为pre[0~m];
2)找出后缀post,设为post[0~n];
3)从前缀pre里,先以最大长度的s[0~m]为子串,即设k初始值为m,跟post[n-m+1~n]进行比较:
  如果相同,则pre[0~m]则为最大重复子串,长度为m,则k=m;
如果不相同,则k=k-1;缩小前缀的子串一个字符,在跟后缀的子串按照尾巴对齐,进行比较,是否相同。
如此下去,直到找到重复子串,或者k没找到。
这时候就引进了一个next数组来解决最大长度值的问题,并且要给初始0位置赋上-1(其实这一步是为了next数组记录下一个字串位置,并且确保匹配不成功,短指针能够下移,这一步得看到最后再回来理解)
在这里插入图片描述通过next数组我们就能够记录模型串的最大长度值
比如模式串的D 与文本串的 C 失配了,找出失配处模式串的 next数组 里面对应的值,这里为 0,然后将索引为 0 的位置移动到失配处。
贴个代码,继续讲

int* getNext(string value) {
	int *next = new int[value.length()];
	next[0] = -1;
	int j = 0;
	int k = -1;
	while (j < value.length() - 1) {
		if (k == -1 || value[j] == value[k]) {
			next[++j] = ++k;
		} else {
			k = next[k];
		}
	}
	return next;
}

k其实扮演了很重要的作用,通过k,我们回到重复串上一次结束重复的位置
根据别的博主博客,总结出这样的规律:
当value[k] == value[j]时,
有next[j+1] == next[j] + 1
其实这个是可以证明的:
因为在P[j]之前已经有value[0 ~ k-1] == value[j-k ~ j-1]。(next[j] == k)
这时候现有value[k] == value[j],我们是不是可以得到value[0 ~ k-1] + value[k] == value[j-k ~ j-1] + value[j]。

即:value[0 ~ k] == value[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。
其实说了这么多,next数组的主要构建思路就是,

  1. 用k去临时记录重复串的个数,如果value[j]与value[k]相等,此时让next[j]位置保存之前出现该值的位置,利于回溯
  2. 如果不等,将k值回到上一次不等的位置,继续1操作,最后把next里对应模型串的所有位置保存应该回溯的位置。

当理解了next数组其实剩下的就很容易了

int KMP(string str, string value) {
	int i = 0; 
	int j = 0; 
	int* next = getNext(value);
	while (i < str.length() && j < value.length()) {
		if (j == -1 || str[i] == value[j]) { 
			// 当j为-1时,要移动的是i,当然j也要归0
			i++;
			j++;
		} else {
			// j回到指定位置
			j = next[j]; 
		}
	}
	if (j == str.length()) {
		return i - j;

	} else {
		return -1;
	}

}

所以整个KMP的算法其实就是在next数组,确保模串中相同子串回溯的位置相等
该算法有小小瑕疵,这里有更优的算法
在这里插入图片描述在这里插入图片描述如果两个字串已经相等,其实回溯是没有意义的

int* getNext(string value) {
	int *next = new int[value.length()];
	next[0] = -1;
	int j = 0;
	int k = -1;
	while (j < value.length() - 1) {
		if (k == -1 || value[j] == value[k]) {
			if (value[++j] == value[++k]) { 
				// 当两个字符相等时要跳过
				next[j] = next[k];
			} else {
				next[j] = k;
			}
		} else {
			k = next[k];
		}
	}
	return next;
}

下面贴上java的代码,这是leecode上strStr()的用KMP解法
在这里插入图片描述

    public int strStr(String haystack, String needle) {
    	if(needle.length() == 0) {
    		return 0;
    	}
        if(needle.length() > haystack.length()){
            return -1;
        }
    	int i = 0;
    	int j = 0;
    	int[] next = getNext(needle);
    	char[] t = haystack.toCharArray();
    	char[] p = needle.toCharArray();
    	
    	while(i < t.length && j <p.length) {
    		//1、j回到起点的时候或者值相等都应该下移
    		if(j == -1 || t[i] == p[j]) {
    				j++;
        			i++;
    			
    		}else {
    			j = next[j];
    		}
    	}
    	if(j == p.length) {
    		return i-j;
    	}
    	
    	
    	return -1;
    }
    public int[] getNext(String needle) {
    	char[] p = needle.toCharArray();
    	int[] next = new int[p.length];
    	int j = 0;
    	int k = -1;
    	next[0] = -1;
    	while(j < p.length - 1) {
    		//两者相等或者k处于next的初始位置
    		if(k == -1 || p[j] == p[k]) {
    			j++;
    			k++;
    			if(p[j] == p[k]) {
    				next[j] = next[k];
    			}else {
    				next[j] = k;
    			}
    			
    		}else {
    			k = next[k];
    		}
    	}
    	return next;
    }

如果有不正确的地方,希望大家能够指出来,第一次看KMP算法,以前都是直接那indexOf套

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值