python KMP算法查找子字符串

最近在刷leecode,碰见字符串检索题。原题是这样的,有两个字符串,一长一短,就看看短的是不是在长的里面,如果在返回第一个字符的下标。

因为总忘记,所以记录下我的思路是咋捋顺的,其实看这一篇就够了,从头到尾彻底理解KMP

  • 最开始也是想,暴力执法,维护两个下标,一个划长字符串,一个来回遍历段字符串,直接上代码(python 里相对简单,功能比对都做了封装,但是原理一样的)。
class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        n = len(haystack)
        l = 0
        r = len(needle)
        # 维护一个短字符串长度,在长字符串滑动着比,就相当于把短字符串看作一个字符
        if r == 0:
            return 0
        while r <= n:
        	# 比较像不像
            if haystack[l: r] == needle:
                return l
            # 不像就下一个,一个一个往后移
            l += 1
            r += 1
        return -1
  • 但上面这样的做法,太暴力了,比对不成功的话,每次移动一个小单位,有点浪费,能不能智能地知道往后移动多少位,毕竟字符串都是已经见过的。比如:
     0123456789
a = "mississipp"
     01234
b = "issip"

a和b两个字符串,按照之前的方式肯定会挨个比下去,模拟下过程
 第一次: a[0] b[0]  "m""i" 不匹配 对下一个长字符串下标加一,短字符串下标归零
 "mississipp"
  "issip"
 第二次: a[1] b[0]  "i""i" 匹配,往后走 下标都加一
	  : a[2] b[1]  "s""s" 匹配,往后走 下标都加一
	  : a[3] b[2]  "s""s" 匹配,往后走 下标都加一
	  : a[4] b[3]  "i""i" 匹配,往后走 下标都加一
	  : a[5] b[4]  "s""p" 不匹配,比对下一个长字符串下标加一,短字符串下标归零
 "mississipp"
   "issip"
 第三次: a[2] b[0]  "s""i" 不匹配,比对下一个长字符串下标加一,短字符串下标归零
 	  ......

每次不匹配都是往后延一个,就像第二次的时候,我都已经看到a[1:5]是什么样子的了,还要我再从a[2]开始么?有点不公平奥,思考下,既然都看见了,那也应该知道我往后移动几位最合适了,或者说移动后的几位起码要和子字符串的开头要一样,这样我就对比后面的几位好了。

下面就是如何知道往后移动几位思考,我的想法比较幼稚,就是每移动一位看下匹配的字符梳理,比如在第二次的时候,这么做。

 "mississipp"
  "issip"
 维护一个下标index,这个小标记录着如果下一位不匹配,和子字符串做对比,该移动几位
 第二次: a[1] b[0]  "i""i" 匹配,往后走 下标都加一, 
  		"mississipp"
  		 "issip"
       假如下一位不匹配 index = 1 
	  : a[2] b[1]  "s""s" 匹配,往后走 下标都加一,
	   "mississipp"
  		 "issip"
       假如下一位不匹配 index = 2 
	  : a[3] b[2]  "s""s" 匹配,往后走 下标都加一,
	   "mississipp"
  		  "issip"
	    假如下一位不匹配 index = 3
	  : a[4] b[3]  "i""i" 匹配,往后走 下标都加一,
	   "mississipp"
  		   "issip"
	    假如下一位不匹配 这时又出现了"i"正好和子字符串的第一位开始记下这个位置,index = 4
	  : a[5] b[4]  "s""p" 不匹配,比对下一个长数组下标加一,短数组下标归零
       "mississipp"
           "issip"
        假如下一位不匹配 这时又出现了"is"正好和子字符串的前两位一样,下标不用变,index = 4
 第三次: a[1+(4-1)] b[0]  "i""i" 匹配,往后走 下标都加一,

这样做的麻烦就是每移动一位都要算下下一位不匹配,我该移动几位,计算量更大了,能不能再方便一点,还有既然已经知道移动几位了,那也知道移动几位后与子字符串的前面几位是相同的,就没必要去比对了,直接略过从不同的地方比较。

   0123456789		
a "mississipp"
       01234      
b     "issip"
我知道移动了3位,从a[1+(4-1)]也就是a[4]开始比,但我还知道index被截留了一次
那么我就应该知道,a[4]a[5]其实和b[0]b[1]是相同的,比对的时候直接比后面的就好了
   012345 6789		
a "missis sipp"
       01 234      
b     "is sip"

以上是我自己的想法,越想越复杂,还是直接看官方的说法吧也就是最长相等前后缀,会根据子串维护一个最大长度表。

这里是引用
失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值

代码说话

 -10001
 "issip"

class Solution:
	#  做一个最大长度表
    def getNext(self, s, next):
        j = -1
        next[0] = j
        for i in range(1, len(s)):
            while j >= 0 and s[i] != s[j+1]:
                j = next[j]
            if s[i] == s[j+1]:
                j += 1
            next[i] = j
        return

    def strStr(self, haystack: str, needle: str) -> int:
        if len(needle) == 0:
            return 0
        j = -1
        next = [0] * (len(needle))
        self.getNext(needle, next)
        for i in range(len(haystack)):
        	#  不匹配的时候我轻松知道回溯几位
            while j >= 0 and haystack[i] != needle[j+1]:
                j = next[j]
            if haystack[i] == needle[j+1]:
                j += 1
            if j == len(needle)-1:
                return i - len(needle) + 1
        return -1
posted @ 2022-03-25 10:04  赫凯  阅读(30)  评论(0)    收藏  举报