这篇讲解不包含kmp最基本的讲解,也不包含朴素方法对比和详细解释,主要是针对next数组构建和字符串匹配时候的指针跳转思想细节,为什么匹配失败时需要赋值j = next[j-1],防止绕不明白。
字符串匹配失败跳转方式
首先需要了解next数组的值的用法和跳转理由:
举个🌰,如下图,上面的是原串haystack,下面的是需要匹配的子串needle,匹配到第5个字符时,haystack[i]≠needle[j],此时需要查看子串needle的next数组。
⚠️需要查看的是j之前的串部分,是否存在前缀后缀相同的情况,例如上图存在一个前缀后缀相同(为a),所以最长相同前后缀为1,为什么我们要知道最长相同前后缀?记住这个1,之后会解释。
我们继续看上面的例子,KMP算法在于遇到不同的字符,不用再从原串haystack的下一个字符(这个例子里为i=1时的b)起从头开始匹配needle,而是利用相同前后缀,直接让匹配子串needle梭到下图所示的地方:
如图,i指向的位置不变,j指向1,所表示的意思就是图片这样,像是让needle直接往后梭了三格,直接让自己的前缀和haystack之前匹配完成部分的后缀对齐,这样就不用重新查看他们是否匹配了。
之前发生不匹配的地方(haystack中 i=4,needle中 j=4 的地方) 之前的部分是匹配相同的(haystack中 i<4 和 needle中 j<4处)。所以只需要计算needle的最长相同前后缀(next数组)就能知道需要梭多少格,也就是j应该重新指向哪里。
上面例子我们说过,当时的最长相同前后缀是1,为什么我们要知道最长相同前后缀?看看j重新指向的地方,就是1,这是因为在匹配失败时,我们查看失败处前一个位置的next数组对应的值,这个值记录的是最长相同前后缀的长度,参照上面的例子,重新梭到的地方(j的新值)就是最长相同前后缀中前缀后面一个位置,因此 j 的新值和最长相同前后缀的长度相等。
创建next数组
初始化: next = [0]*len(pattern), next[0]=0, j=0, i=1
步骤1:i=1, j=0, 此时pattern[i]≠pattern[j],直接赋值next[i]=j=0,所以next[1]=0:
步骤2: i=2, j=0, 此时pattern[i]≠pattern[j],直接赋值next[i]=j=0,所以next[1]依然等于0:
步骤3: i=3, j=0, 此时pattern[i]=pattern[j],先让j = j+1 = 1,然后赋值next[i]=j=1,更新的 j 用红色标明:
步骤4: i=4, j=1, 此时pattern[i]=pattern[j],先让j = j+1 = 2,然后赋值next[i]=j=2,更新的 j 用红色标明:
步骤5: i=5, j=2, 此时pattern[i]=pattern[j],先让j = j+1 = 3,然后赋值next[i]=j=3,更新的 j 用红色标明:
步骤6: i=6, j=3, 此时pattern[i]=pattern[j],先让j = j+1 = 4,然后赋值next[i]=j=4,更新的 j 用红色标明:
步骤7: i=7, j=4, 此时pattern[i]≠pattern[j],此时j>0,j需要首先跳转到next[j-1]的值,此时next[j-1] = next[3]=1,所以赋值 j=4,更新的 j 用红色标明:
📌 这里的跳转和之前的haystack、needle的匹配失败跳转是一个意思,也是为了减少匹配的次数,同样是跳转到next[j-1],也就是 j 跳转到前方子串的最长前缀后缀长度所指向的位置,也就是这里的:next[j-1]=next[3]=1。
所以,最长前缀后缀的思想其实在”字符匹配题“中利用了两次,一次在next数组的构建中,pattern串自己和自己匹配,一次在原串和匹配子串的过程中。
此时仍然pattern[i]≠pattern[j],且j>0,所以j需要继续跳转到next[j-1]的值,此时next[j-1] = next[0] = 0,所以赋值 j=0,更新的 j 用红色标明:
此时仍然pattern[i]≠pattern[j],但是j不大于0,所以赋值next[i] = j = 0;
步骤8: i=8, j=0, 此时pattern[i]≠pattern[j],且j不大于0,直接赋值next[i]=j=0,结束:
next数组构建的代码:
def next_arr(self, pattern: str):
n = len(pattern)
next_array = [0]*n # next_array存放next数组
j = 0
for i in range(1,n):
while j>0 and pattern[i]!=pattern[j]: # 循环查看前缀
j = next_array[j-1]
if pattern[i]==pattern[j]:
j += 1
next_array[i] = j
return next_array
字符串匹配代码:
def strStr(self, haystack: str, needle: str) -> int:
m = len(haystack)
n = len(needle)
next = self.next_arr(needle)
j = 0
for i in range(m):
while j>0 and haystack[i]!=needle[j]:
j = next[j-1]
if haystack[i]==needle[j]:
j += 1
if j == n: # 子串数到头了
return i-j+1
return -1