链接:28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)
KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
说到KMP,先说一下KMP这个名字是怎么来的,为什么叫做KMP呢。
因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP
KMP主要应用在字符串匹配上。
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。
Example: 要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时就要从头匹配了。
但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配,前缀表的作用就是在不匹配的时候,告诉你下一个应该跳去什么位置继续匹配。
next数组就是一个前缀表(prefix table)。
前缀表有什么作用呢?
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
前缀表是啥?
记录在模式串中下标i之前(包括i)的字符串中,每个位置的最长相等前后缀。
最长相等前后缀的概念是啥?
字符串的前缀:是指不包含最后一个字符的所有以第一个字符开头的连续子串;
后缀:是指不包含第一个字符的所有以最后一个字符结尾的连续子串;
前缀表要求的就是相同前后缀的长度,此时就要问了前缀表是如何记录的呢?
"aabaaf"
# 前缀包含 a;aa;aab;aaba,aabaa
# 后缀包含 f;af;aaf;baaf;abaaf;
# 所以它的最长相等前后缀长度为0
"a" #无前缀无后缀,最长相等前后缀长度为0
"aa" # 最长相等前后缀长度为1
"aab" # 最长相等前后缀长度为0
"aaba" # 最长相等前后缀长度为1
"aabaa" # 最长相等前后缀长度为2
人工从肉眼看,模式串“aabaaf”的前缀表是[ 0,1,0,1,2,0 ]
我们用肉眼观察写出了前缀表,现在思考,为什么有了前缀表之后,那为啥就能告诉我们 上次匹配的位置,并跳过去呢?
回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图:
然后就找到了下标2,指向b,继续匹配:如图:
以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要!也就是上面的图示说明的意思:
下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。
所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。
我们前面用肉眼观察得出了模式串的前缀表如下:
可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
那么在实际的代码中我们如何计算出前缀表呢? 这个过程实际上也用到了前缀表自身去生成前缀表。
总体的方法如下:
前缀表求出来了,再来看一下如何利用 前缀表找到 当字符不匹配的时候指针应该移动的位置。
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。所以要看前一位的 前缀表的数值。前一个字符的前缀表的数值是2, 所有把下标移动到下标2的位置继续比配。最后就在文本串中找到了和模式串匹配的子串了。
很多KMP算法的时间都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢? next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(或者右移一位,初始位置为-1)之后作为next数组。
为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。
其实这并不涉及到KMP的原理,而是具体实现,next数组即可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。
如果是右移一位,在查前缀表的时候,就不用查不匹配位置的前一位对应的next数值,而是直接查不匹配位置的next数值就可以了;
如果是统一减一,那还是查不匹配位置的前一位对应的next数值,要记得再加上1。
根据KMP的理论知识,首先需要算出前缀表next数组。
前缀表是:记录下标i之前,包括i,有多大长度相同前后缀,在某个字符不匹配的时候,前缀表会告诉你,下一步应该跳到什么位置。
定义指针i和j, i指向后缀的末尾,j指向前缀的末尾。
比如在模式串: a a a a b中,当j指向第4个a,i指向第一个b的时候,next已经有值=[ 0, 1,2,3]。此时的前缀是a,a,a ,a(j)后缀是a,a,a,b(i)。我们已知现有前后缀的前三位是可以完全对上的,于是找 j 前面一位的next数值,得到2。说明前缀a0,a1,a2(数字仅仅代表下标位置)中,a0 a1 =a1 a2, 而前缀a1 a2 = 后缀的a2 a3, 所以a0,a1 =a2 a3 ,j 调到a1后面的下标2处,继续比较。
总结: j 回退位置查找前一位元素的Next数组数值,回退到该处。如果回退后还是不相等,且没有回退到数组初始处,那么继续同理回退,直到相等为止。
如果回退到j=0处,不再回退,此时前后缀为0,仍然是赋值next[i] = j=0
这部分代码实现:
class Solution(object):
def strStr(self, haystack, needle):
"""
:type haystack: str
:type needle: str
:rtype: int
"""
def getNext(self,needle):
Next = [""for k in range(len(needle))]
j = 0
Next[0] = 0
for i in range(1,len(needle)):
while j > 0 and needle[i] != needle[j]:
j = Next[j-1]
if needle[i] == needle[j]:
j += 1
Next[i] = j
return Next
在得到next数组之后,我们利用next数组去做匹配。文本串为haystack,模式串为needle。首先特殊情况,如果模式串为空,返回值为0。
然后调用getNext函数,得到模式串的Next数组。
文本串的指针为 i ,初始化为0;模式串的指针为 j ,初始化为0。
用 i 指针遍历文本串haystack, 比较haystack[ i ] & needle [ j ]
这部分的代码实现
class Solution(object):
def strStr(self, haystack, needle):
"""
:type haystack: str
:type needle: str
:rtype: int
"""
if len(needle) == 0:
return 0
Next = self.getNext(needle)
j = 0
for i in range (len(haystack)):
while j >0 and haystack[i] != needle[j]:
j = Next[j-1]
if haystack [i] == needle[j]:
j += 1
if j == len(needle):
return (i-len(needle)+1)
return -1
class Solution(object):
def strStr(self, haystack, needle):
"""
:type haystack: str
:type needle: str
:rtype: int
"""
if len(needle) == 0:
return 0
Next = self.getNext(needle)
j = 0
for i in range (len(haystack)):
# 这里的三行指令顺序严格不可以变
while j >0 and haystack[i] != needle[j]:
j = Next[j-1]
if haystack [i] == needle[j]:
j += 1
if j == len(needle):
return (i-len(needle)+1)
return -1
def getNext(self,needle):
Next = [""for k in range(len(needle))]
j = 0
Next[0] = 0
for i in range(1,len(needle)):
# 这里的三行指令顺序严格不可以变
while j > 0 and needle[i] != needle[j]:
j = Next[j-1]
if needle[i] == needle[j]:
j += 1
Next[i] = j
return Next
时间复杂度:O(m+n)
其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大的提高的搜索的效率。
空间复杂度:O(m)
用于储存生成的Next数组,长度和模式串长度一致。
def getNext(self,needle):
Next = [""for k in range(len(needle))]
j = 0
Next[0] = 0
for i in range(1,len(needle)):
# 这里的三行指令顺序严格不可以变
while j > 0 and needle[i] != needle[j]:
j = Next[j-1]
if needle[i] == needle[j]:
j += 1
Next[i] = j
return Next
想想如果if放在while的前面,举个例子“ issip”经过if处理之后的j可以直接赋值了,但可能因为满足了while的条件又进入循环,回退到错误的值,比如这个例子就会得到错误的Next = [0,0,0,0,0]
同理,主函数的这个顺序也不可以改变。
代码随想录 (programmercarl.com)
本题学习时间:4小时。
ps: 这篇文章是根据我自己的理解和参考代码随想录的讲解写的,感觉比第一次做又理解深入了一点,对我来说最大的难点是求Next数组时j的回退的理解,本题花了4个小时。(求推荐!)