首先来看一道题。
题目:给定两个字符串haystack(大海捞针),needle(针),写一个函数去证明needle是否为haystack的字串,如果是就返回字串在字符串出现的第一个位置,反之返回-1.规定needle为空字符串则返回0.(注:下标是从0开始的)
比如:
haystack="hello",needle="ll",返回值为2,因为在字符串haystack下标为2的位置找到了字串
haystack="helleo",needle="lee",返回值为-1
或许大家会发现这怎么跟strstr函数作用有点像呢,其实题目的要求就是写代码去解释strstr函数的原理。
大家先思考动手写代码。
答案:
图解:
时间复杂度O(m*n),m为haystack的长度,n为needle的长度
如果haystack为"aaaaaaaac",needle为"aaab",如果再使用上面的方法效率就非常低了。
那应该怎么去优化呢,这就引入了今天主题KMP算法,也就是三个大佬名字的首个字母组成的。
我学这个算法的时候有些懵逼,一整天的时间都花在上面了。今天我就来讲解一下我的理解。
首先先看一下KMP算法的原理:
如果主串S="bbabbdbbabbcd",需要匹配的T="bbabbc"
如果按照朴素模式匹配的算法,流程是上图的②->⑦
这样的算法是比较让人能够理解的,但是会发现效率比较低,重复比较多的元素比较多。大家仔细的观察一下,对于匹配的字符串来说,"bbabbc",其中前面有bb,后面也有重复的bb,如果直到字符c才不相等,c之前的bbabb都是和主串相等的,bb是重复的出现了,下一次的比较直接从字符a开始即可,不需要在重复的去比较bb。所以直接可以跳过流程②③,将④的流程改为:
这是KMP算法的关键。这里我换一个更简单的栗子给大家进一步说明。
需要匹配的字符串为"abcx",其中第一个字符‘a’与后面的字符“bcx”都不相等,流程①中,前三位字符都是相等的,这就意味这字符‘a’不会不可能与主串的下标为1,2字符‘b’‘c’相等的,所以流程②③是多余的。
那么KMP算法具体是怎么判断下标j的位置的,也就是怎么判断匹配字符串的下标位置的。
在这里我再举个栗子让大家找找规律:
根据上面已有的知识点,需要匹配的串首字母‘a’与第二个字符‘b’和第三个字符‘c’是不想等的,流程②和③是多余的。
因为首字符的'a'和第四位字符'a',第二位字符'b'和第五位字符'b'相等,在流程①的时候就已经知道第四位字符‘a’和第五位字符‘b’是和主串相等的,那么意味着,第一位字符‘a’和第二位字符‘b’就不再需要和主串的第四位字符‘a’和第五位字符‘b’进行比较了,因为肯定也是相等的,因此流程④也是多余的。
再这个栗子中,如果是朴素算法的话,主串的下标在流程①是5,经过②③④之后,主串下标又重新回到5。大家都知道朴素算法都是不断的回溯主串的下标和需要匹配串的下标来完成的。对于KMP算法来说主串的下标不需要回溯,因为那些回溯是没有必要的,这也是KMP算法优势之处。
"abcabe",其中前缀“ab”和“e”之前串的后缀“ab”相等的,j由5变成2。对于"abcx",当中是没有重复的字符,j由3变成0.因此在这里我们可以得出结论了,j的值取决于当前字符之前的前后缀相等的程度。
大家或许就发现了,不相等的字符在不同的下标j的值会不一样,比如上图,如果在c就不相等,c之前是不存在前后缀相等的情况,因为‘c’之前的字符分别为‘a’和‘b’,所以j由2变成0,即又回到字符‘a’开始比较。那么j的值每次可能都不一样,需要怎么去找呢。
这就是所有的关键了,也就是j位置的变化可以定义一个数组next来存储,next长度等于需要匹配字符串的长度。函数定义是这样的:
next数组的推导:
对于推导的话,我建议大家去看青岛大学王卓老师讲的kmp算法那章节内容视频,时长很短的,花二十分钟估计就可以搞明白next数组的推导。如果给大家用文字写出来估计大家得看上半个多小时。
视频链接:
https://b23.tv/ststufP?share_medium=android&share_source=qq&bbid=XY53CAC5574B6F55731B3FE9D81C2271AAFB1&ts=1649491165765
KMP算法的实现:
其中needle是模式串,即需要匹配的字符串。
解释:next[i]=j的含义,在第i个位置出现不相等的时候,模式串需要返回到下标为j的位置。
①如果j=0,跳出while循环,判断needle[i]是否等于needle[j],不相等直接为next数组赋值,即next[i]=j,但明显是相等的,即存在前后缀相等的情况,j++,再赋值
next[1]=1含义就是在下标为1的字符‘b’与主串不相等时,j的下标变成1.
②j=1,i=2时,明显不相等,j回溯,j=next[j-1]=next[0]=0,发现还是不相等,即前缀(‘b’,‘bb’)和后缀(‘a’,‘ba’)不相等,意味着在该字符串‘b’之前没有相等的前后缀。
③j=0,i=3,相等,意味着第五个字符‘b’之前存在相等的前后缀,即时第一个字符‘b’和第四个字符‘b’。因此在该字符出现不祥的时候,j回溯到第二个字符‘b’。即next[3]=1.
.........
KMP算法整体代码:
而我觉得还是不是很明白,我就改写为下面的代码:
为什么我要用到-1呢,也就是说,没有元素可以和它再相等了,意味着不存在前后缀相等的情况了。
KMP算法查找子串代码:
j=next[j];这行代码的意义就是如果和主串存在不相等的情况,j直接回溯。
KMP算法整体代码: