一个看似简单实则很难的算法——字符串匹配问题:KMP算法

人的影响力是很大的,人产生的导向型也是很强的。
当我们有想法想拥有什么、想改变什么的时候。有可能一个人的舆论可能会把你导向另一边
当我们有心事的时候,可能有人把你导向好的一面;也有可能抓住你的心理,把你导向阴暗面。
讲道理,人心理是复杂度。所以不存在一种极端的评价去评论啥,更要考虑的是折中观念
说实话,还是让自己内心保持镇静,不被别人带跑。这才是一个看似不错的理性人。

本篇我想介绍一个有意思的算法,顺便以后找出来能够再次看到,因为这个算法还是对以后写代码、面试啥的很有用。这个算法代码虽然简单,但是理解真的很难。本篇介绍一个字符串匹配问题:KMP算法

1、蛮力法解字符串匹配问题

什么是字符串匹配问题

首先有一个原串:babababcbabababb;一个模式串:bababb
希望求出原串中是否包含模式串,并求出模式串在原串第一次出现的起始索引
例如,在上面的问题中,我们能找到一个第一次出现的起始索引:10,开始包含模式串

下面介绍一个蛮力法解决字符串匹配问题

蛮力法解决字符串匹配问题

蛮力法就是先从两个串起始下标开始对齐
(/localImg/ArtImage/2020/03/202003201945401584705202511.png ''图片title'')]

然后j下标以此向后走,和原串i之后的字符进行匹配。
(/localImg/ArtImage/2020/03/202003201947151584705297924.png ''图片title'')]

在这里插入图片描述

(/localImg/ArtImage/2020/03/202003201947411584705323536.png ''图片title'')]

如果碰见匹配不成功,就让i=i+1。把模式串放到i+1的位置上,j再从0开始遍历
(/localImg/ArtImage/2020/03/202003201948501584705392770.png ''图片title'')]
(/localImg/ArtImage/2020/03/202003201949271584705429371.png ''图片title'')]

(/localImg/ArtImage/2020/03/202003201949331584705435459.png ''图片title'')]

最终找到模式串在原串的位置
(/localImg/ArtImage/2020/03/202003201951411584705563504.png ''图片title'')]
。。。。。。。。。表示的是中间还有很多这样的模式串

我们看到蛮力法很麻烦每次都是从i+1位置寻找,j每次都是从模式串初始位置开始查找。效率低下

下面进行代码分析
我们可以用一个临时变量j记录i的位置,用k记录模式串下标,如果字符匹配成功就让j++,i++;否则就是i++,j记录此时i的位置,k=0重新遍历。如果k遍历完一遍了,说明找到了模式串在原串的位置,return i;否则没找到返回-1

public static void main(String[] args) {
		// TODO Auto-generated method stub
		String str1="babababcbabababb";
		String str2="bababb";
		
		int index = BruteForce(str1, str2);
		System.out.println("在"+index+"的位置上匹配该字符串");
	    }

        //蛮力法解字符串匹配问题
	public static int BruteForce(String str1,String str2) {
		if(str1.length()==0||str2.length()==0)
			return -1;
		if(str2.length()>str1.length())
			return -1;
		
		int i=0;
		int j=i;  //用j记录i下标
		int k=0;
		while(j<str1.length()) {
			if(str1.charAt(j)==str2.charAt(k)) {
				j++;
				k++;
				if(k==str2.length())
					return i;
			}
			else {
				i++;
				j=i;
				k=0;
			}
		}
		return -1;
	}

下面分析蛮力法求解字符串匹配问题

蛮力法求解字符串匹配问题分析

我们看代码,虽然只有一层循环。但是循环里面有变量再操控着产生了另一个循环。另一个循环就是遍历模式串。而且两个循环是内外层的关系,外层遍历原串,内层遍历模式串。所以时间复杂度就是O(m+n)(m,n分别代表原串或模式串的长度)

我们想寻找一个模式串不用回退到i+1位置上,而是就退到某种位置上而继续探寻的算法。解决这个问题的就是KMP算法

2、KMP算法

什么是KMP算法

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的。

像比如上面问题:
(/localImg/ArtImage/2020/03/202003202006461584706468387.png ''图片title'')]
我们可以让i,j一块走。就是,只要i和j指向的位置的字符一样时,就往前推进。当碰到字符不匹配的时候
在这里插入图片描述
我们怎么做啊,我们想让模式串从这里开始比较,i不变,模式串的位置不需要大改变,从这里继续比较字符是否一样,而不用让它像蛮力法那样。
因此我们可以把模式串挪到这个位置上
在这里插入图片描述
就这样再继续遍历,再次碰到字符不匹配的时候
(/localImg/ArtImage/2020/03/202003202008451584706587759.png ''图片title'')]
那现在怎么推模式串的位置呢
我们可以看到c前面的字符b和模式串的字符b已经校验过了,我们再看模式串第二个a前面有一个b,我们不妨拿那个b和刚才匹配过的b对应,让第二个a与c对应下
(/localImg/ArtImage/2020/03/202003202008591584706600996.png ''图片title'')]
结果发现字符a和c根本不匹配,那就得需要将模式串继续向前追踪,向上面那样我们发现模式串开头有个b可以拿来对应,所以我们用那个再试试
(/localImg/ArtImage/2020/03/202003202009161584706618336.png ''图片title'')]
发现还是不匹配,那么只能用模式串第一个元素进行比较了
(/localImg/ArtImage/2020/03/202003202010321584706694744.png ''图片title'')]

结果整个模式串都没有与这个字符c匹配的,那就说明这个字符c并不属于模式串里面的元素
那么我们就i++,j++,继续追踪。
(/localImg/ArtImage/2020/03/202003202010421584706704862.png ''图片title'')]
每次都是经过这样的操作,找到了模式串第一次出现在原串的位置上,return i-j。这个位置就是模式串第一次出现在原串的位置。

我们总结出了这么一个结论:当我们遇到字符不匹配的时候,我们要将模式串移动到哪呢,看**i前面k个字符要与模式串初始时以及往后的k个字符是一样的。**那么,我们就将模式串移动到那个位置上,匹配、对齐。你就会发现不管是原串还是模式串,前面的字符都是一样的。既然都是一样的,那就不必再花时间进行匹配了。

也就是说当在L处没法匹配的时候,j要进行回溯而i不需要回溯。当然j回溯,不一定要回溯到模式串0的位置上。那么接下来就要考虑j要回溯到哪里。这就是KMP里面的next方法

next()

next()方法里面考虑的是,一个模式串中j应该回溯到哪个位置上。这就是next()要考虑的东西。我们将模式串拿出来,用一个next数组记录回溯位置。
我们看到bababb,最后一处位置匹配成功,那么问题的解也就出来了。如果匹配不成功,j就要回溯。所以我们考虑的是babab的回溯位置点,为什么?因为模式串最后一个b匹配不成功,j要回溯。考虑回溯位置,所以就是考虑的是babab

当然我们建立next数组就是记录babab的回溯位置
如果j=0,即从b处就匹配不成功。我们定义在这种情况时,j=-1
(/localImg/ArtImage/2020/03/2020032020123901.png)]
如果j=1,即在一开始的b处匹配成功,在a处匹配不成功。那么我们考虑模式串ba,既然a不成功,那就看j-1,即b处位置。如果是相同字符串aa,那你也得要看j=0的位置。为什么?你想嘛,j=1匹配不成功,那回溯,肯定是1之前的位置啊。不然你为什么要回溯。所以后面也是一样的道理,**如果j处位置匹配不成功,那么回溯的位置一定是在j之前进行考虑。**还是那个问题,你考虑j的话,那你为什么要回溯。而且j=0是已经证明过了是匹配成功的位置,所以让j回溯到0是正确的做法。
我们定义在j=1匹配不成功时,j肯定是要回到0的位置上,那不妨就让j=0
(/localImg/ArtImage/2020/03/2020032020124902.png)]
在next()里面,初始化就是把next[0]=-1,next[1]=0
如果j=2,在j=0,1位置处匹配成功了,j=2处匹配失败。bab,我们根据上面所说的,要从j=2之前位置进行回溯。b之前的是ba。接下来就要分类讨论了,如果j=0,1是不一样的字符,就像ba这样的。因为b和a没有重复,如果匹配字串是这样的原串是b a c,模式串是b a b,在第二个b处发生了不匹配情况。c处之前的是b a而且你在模式串匹配成功且仅找到这么一对是符合原串的。也就是说,考虑回溯j=2之前的位置的时候,只找到了一次字串ba是满足原串的ba,如果你想回溯到1的位置上,那么b a c 和b a b,c对应的是a,但是原串的a可是对应的是模式串的b啊。所以不如让j回溯到0处。即next[2]=0
(/localImg/ArtImage/2020/03/2020032020130003.png)]
我们再想,如果模式串是a a b。假设原串变成了a a c。那在j=2处匹配不成功,那我们往前回溯,肯定先思考能不能让j=1呢。答案是可以的,原串的c对应着模式串的第二个a,那么原串的第二个a(令i=1),和模式串的第一个a是不是匹配关系啊。所以既然都匹配是不是就可以不用再重复考虑了。所以j=1是可以的。即next[2]=1
也就是说j=2之前的如果是不重复元素,那就只能从j=0回溯,如果是重复元素那就可以从j=1处回溯。

现在我们考虑j=3的时候,即baba。如果在j=3处匹配不成功,那么依照前面所说的就要从这个不匹配位置的前面进行回溯。原串:b a b c,模式串是b a b a。我们看到啊j=0和j=2都是b。那么c之前也有一个b。我是不是可以把模式串中的第一个b对着原串的第二个b呢。此时就是c对应着a。为什么这么考虑?这是因为原串中c之前的b和模式串中两次出现的b是匹配关系。而且j=2是已经对应过了,所以我们要在模式串找到另一个b与c之前的b对应。方可匹配成功,不需要再重复匹配了。next[3]=1
(/localImg/ArtImage/2020/03/2020032020130904.png)]
那我们再想,如果模式串中变成了a a b a。原串是a a b c。我们考虑j=2的时候就已经介绍了,在这种情况下是可以回溯到j=1的位置上。再考虑j=3的时候不匹配的时候,综合多方面考虑,是不是要回溯到j=0的位置了。居然不是j=1?还真的不是,为什么?这是因为c之前的字符是b。然而整个模式串就出现了1个b。所以没法在模式串中可以找到第二个能够与b匹配的元素了。所以只能让模式串中初始下标对着c了。这个时候的是next[3]=0

那我们再看一种情况,如果模式串中变成了a a a b。原串是a a a c。我们考虑j=3的时候不匹配的情况。这个你去分析也能分析出来,c是要和模式串中的第三个a对应,即要回溯到j=2的位置。
我们考虑模式串,三个a,从前面数一个a,从后面数一个a(模式串b之前那个a)。则我们说模式串中存在一个重复匹配点;从前面数两个a(j=0,j=1),从后面数两个a(模式串b之前的两个a,是j=1,j=2),这就出现了两个重复匹配点。因此在a a a最多出现了两个重复匹配点。因此,原串中a a a c,模式串a a a b。当b与c不匹配,开始回溯。因为a a a最多是两个重复匹配点,所以我们说,c之前的两个a,是不是就和模式串最初的两个a是一样的呢。也就是说i前面k个字符要与模式串初始时以及往后的k个字符是一样的。那为什么不是三个呢?因为是三个的话,那不就是没有回溯。三个的话不就是原串中a a a c和模式串a a a b对应了吗。对吧前面三个不就是匹配好的了吗,所以我才说根本没有回溯啊。

所以思考点就变了。正确的思考点就是i前面k个字符要与模式串初始时以及往后的k个字符是一样的。那么怎么在模式串中找到k个字符呢?

具体思路就是找重复匹配点,是这样的,比如下一步要考虑babab,在j=4处出现不匹配。那么就要对j=4之前进行回溯。我们把要回溯的字串拿出来
baba
我们找重复匹配点,先从前面观察j=0,从后面观察j=3。这两个不匹配,就舍弃
再从前面观察j=0,j=1,从后面观察j=2,j=3,发现是匹配字串(“ba”==“ba”)。
再从前面观察j=0,j=1,j=2,从后面观察j=1,j=2,j=3。bab和aba。这不是一个匹配字串。
因为如果观察4个的话就不存在回溯,所以baba是一个含有两个的重复匹配点(记住重复匹配点是找最大的那一个)即:next[4]=2
(/localImg/ArtImage/2020/03/2020032020132005.png)]
我们再考虑aaaba在j=4处出现不匹配的时候,我们先将要回溯的字串取出来:aaab
先从前面观察j=0,从后面观察j=3。这两个不匹配,就舍弃
再从前面观察j=0,j=1,从后面观察j=2,j=3,这两个不匹配,就舍弃
再从前面观察j=0,j=1,j=2,从后面观察j=1,j=2,j=3。aaa和aab。这不是一个匹配字串。
因为如果观察4个的话就不存在回溯,所以aaab没有重复匹配点,直接回溯到j=0的位置上即:next[4]=0

那么如果编写代码的话怎么编写,一个一个的取出来查找吗?不没有这么复杂

讲解next()编写

首先初始化,next[0]=-1,next[1]=0。考虑next[2],是不是就是考虑j=2的不匹配问题后怎么回溯,考虑j=2不匹配问题后怎么回溯,那就得考虑从j=2之前的j=0,j=1有没有最大的重复匹配点。在这种情况下就是比较str[i](str是模式串数组,i=1)与str[0](j=0)如果相等那么就要让str[2]=1;如果不相等就是str[2]=0。

对bababb进行考虑
现在考虑j=3的情况就是baba,要回溯点在bab上。刚才我们经过思考得出了next[2]=0,就是说已经比较过b[0],b[1]是不匹配的。在bab上str[0]是与str[2]可以匹配。所以说next[3]=1
这里令i=3,j=1。为什么这么做?

babab中在j=4处没有匹配成功,回溯点在baba上。刚才得出next[3]=1,且i=3,j=1。那我们是不是可以直接判断str[i]与str[j]是否匹配,可以。因为在上一步两个b我们已经匹配完了,找到了一个最大匹配点,那我们要再找一个重复匹配点,是不是直接在i+1,j+1后面找啊。大家想一想是不是啊。就像这个例子一样,找到了,再加1,next[4]=2。找不到,比如说这样的:babb,我们看到这个模式串next[3]=1,而str[i]!=str[j](i=3,j=1)不相等就要进行回溯,j=next[j],j=0,而i=3。然后next[++i]=++j,即next[4]=1。回溯的头就是j=-1。j=-1也是next[++i]=++j

解答疑问:为什么是next[++i]=++j。我们刚才的思考都是先考虑j=3处时匹配不成功找回溯点,然而是不是也可以正向思考下,由next[2]推一下next[3]。你思考3处的回溯点,不也是在02的位置找吗,而我直接先将02的可以回溯的位置找出来给你就行了。
如果没找到匹配点要么回溯要么j=-1。回溯就是j=next[j]。比如我们再看一个例子:对于这个模式串babaab,计算next[5]。而前面next[0]=-1,next[1]=0,next[2]=0,next[3]=1,next[4]=2,此时i=4,j=2。下面判断str[4]和str[2]是不相等的。那么回溯:j=next[2]=0,str[4]和str[0]进行比较,发现不匹配。再追溯,j==-1。则直接next[++i]=++j使得next[5]=0。

你也许又发提问为什么回溯是j=next[j]。这么一问我们回到了起点,为什么创建next数组。是不是就是找到模式串中j应该回溯到哪个位置上。这就是next()要考虑的东西。我们将模式串拿出来,用一个next数组记录回溯位置。next数组里面是位置。我们看一个例子:bbbba。求next[5]
我们能推出来next[0]=-1,next[1]=0,next[2]=1,next[3]=2,next[4]=3(此时i=4,j=3)
然后考虑str[4]与str[3]是否匹配,不匹配j=next[j],回溯之后j就是2。我们是不是把a当成了原串,把前4个b当成了模式串。最后一个b不行,那就前1个b。而j=0,j=1的b是彼此对应的。什么意思,就是和原问题那种对应方式一样,我们在自己模式串找对应关系也是依靠这样的关系来找。所以原串和模式串、模式串内部的回溯都是j=next[j]

最后,再看一个bababb,即求j=5的时候的回溯点:babab也就是next数组最后一个位置。我们用一般思路就是j=0,j=1,j=2和j=2,j=3,j=4对应上了。所以最大重复匹配点是3
如果是next数组推算,i=4,j=2推算next[5]。str[4]与str[2]相等。next[++i]=++j。故next[5]=3
我们也可以通过babab更好的观察next[++i]=++j
next[2]=0(i=2,j=0)str[2]==str[0],next[3]=1
next[3]=1(i=3,j=1)str[3]==str[1],next[4]=2
next[4]=2(i=4,j=2)str[4]==str[2],next[5]=3
(/localImg/ArtImage/2020/03/2020032020133406.png)]

我们总结一下next():
1、next()方法里面考虑的是,一个模式串中j应该回溯到哪个位置上。这就是next()要考虑的东西。我们将模式串拿出来,用一个next数组记录回溯位置。
2、正确理解思路:i前面k个字符要与模式串初始时以及往后的k个字符是一样的
3、如果j处位置匹配不成功,那么回溯的位置一定是在j之前进行考虑
4、模式串也要回溯进行匹配
5、模式串匹配和原串——模式串匹配适用于同一个回溯:j=next[j]
6、对next进行初始化:next[0]=-1,next[1]=0
7、对哪个位置求回溯点,是直接通过前一个变量来推,即next[++i]=++j
8、j==-1||str[i]==str[j]直接next[++i]=++j

关于理解KMP算法最难的next()讲完了

下面我们思考怎么用next数组

next数组的使用

我们可以通过上面的例子,明白了如何回溯。就是说如果j==-1||str[i]==str[j]。要么原串中有字符没有出现在模式串中,要么字符匹配成功
i++,j++
否则进行回溯
j=next[j]

代码很好写,这里就不再分析了。
下面直接上代码

public static void main(String[] args) {
		// TODO Auto-generated method stub
		String str1="babababcbabababb";
		String str2="bababb";
	    int[] next = next(str2);
		
		int index = TracingofKMP(str1, str2, next);
		System.out.println("在"+index+"的位置上匹配该字符串");
	}

	//定义next方法
	public static int[] next(String str) {
		int next[]=new int[str.length()];
		char[] charArray = str.toCharArray();
		
		next[0]=-1;
		if(str.length()==1)
			return next;
		
		next[1]=0;
		int i=1,j;  //j是next[i]里面的最大匹配数
		j=next[i];
		while(i<str.length()-1) {
			//如果i到了0的位置上(即j=-1)或者在字符数组charArray[i]和charArray[j]相等
			//也就是说字符数组在以i和j为下标的位置上相等,当然它们前面的元素分别相等。
			if(j<0||charArray[i]==charArray[j])
				next[++i]=++j;
			//若都不满足,则回溯循环体重新判断,直到满足上面的条件
			else
				j=next[j];
		}
		
		return next;
	}

	//KMP算法
	public static int TracingofKMP(String str1,String str2,int next[]) {
		if(str1.length()==0||str2.length()==0)
			return -1;
		if(str2.length()>str1.length())
			return -1;
		
		int i=0;
		int j=0;
		
		while(i<str1.length()) {
			//如果j取到-1,或者字段匹配成功。则i++,j++
			//否则就要回溯循环体继续判断
			if(j==-1||str1.charAt(i)==str2.charAt(j)) {
				i++;
				j++;
			}
			else
				j=next[j];
			
			if(j==str2.length())
				return (i-j);
		}
		return -1;
	}

代码很简单,但是理解真的不好理解。所以说KMP算法是一个看似简单实则很难的算法一点也不为过。本篇介绍可能还会让你很懵逼的话,下方评论说出你的疑问。

下面分析KMP算法

KMP算法分析

我们一开始是要先填写next数组。而且是一直往前写,不会回退。如果你说回溯了啊,即使回溯。假设m为模式串的长度,n为待匹配的字符串的长度。
O(m+n)=O( [m,2m]+ [n,2n] ) = 计算next数组的时间复杂度+遍历比较的复杂度。
也就是:
计算next数组时的比较次数介于[m,2m]。
遍历比较的比较次数介于[n,2n],最坏情形形如T=“aaaabaaaab”,P=“aaaaa”。
所以算法时间复杂度时O(m+n).

什么意思呢,看两个极端情况。例如在next数组中。aaaaas。next数组在s前面都不会回溯。从s开始要回溯,回溯长度几乎是m。所以是2m;同样的如果是这样的abcdef。那么next数组回溯都是回到next[0]位置上。所以是累计了几乎m次。同样的,在原串中也是一样的。大家可以自己想想。所以时间复杂度最好是O(m+n),最坏是O(2m+2n)还是O(m+n)

综上KMP算法的时间复杂度就是O(m+n)
######                石可破也,而不可夺坚;丹可磨也,而不可夺赤。《吕氏春秋·诚廉》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值