解法一:暴力
遍历长串,匹配到短串停止。此处提供一种比暴力稍好的策略,即不用每次都往前查找匹配,只有最后一个字母和中间字母都匹配再查找,这样能优化很多复杂度,最优复杂度为O(N),最坏仍为O(N(N-M))
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
if needle == '': return 0
def judge(pos):
if haystack[pos:pos+len(needle)] == needle:
return 1
return 0
for idx, c in enumerate(haystack):
if needle[0] == c:
p1, p2 = int(len(needle)/2), (len(needle)-1)
if idx + p2 < len(haystack) and needle[p2] == haystack[idx+p2] and needle[p1] == haystack[idx+p1]:
result = judge(idx)
if result: return idx
return -1
解法二:回溯
从开始处匹配,若不对则回溯。回溯到哪个位置是关键,若继续进行,会忽略之前可能匹配的串,如
“mississippi”
“issip”
是一个经典的例子,到s处发现不匹配时需要再向前回溯一些。合适的做法是回溯到开始字符的后一位,代码如下。
def strStr(self, haystack: str, needle: str) -> int:
if needle == '': return 0
p, pos = 0, 0
while p < len(haystack):
if haystack[p] == needle[pos]:
pos += 1
if pos == len(needle):
return p-pos+1
elif pos > 0:
p -= pos - 1
pos = 0
if haystack[p] == needle[pos]: pos += 1
p += 1
return -1
解法三:KMP算法(2维dp数组)
阅读前:我们称短串为pat,方便放入段落和代码中
上述两种如果你仔细观察,其实前面两种算法其实是差不多的,都是不匹配的时候从回溯到开始匹配时的下一个字符。而这个回溯就是造成时间复杂度高的重要原因。KMP算法就避免了这种回溯,通过计算pat的dp数组,可以用这个数组匹配任意的文本,写起来也非常优雅。所以如果有一个pat,多个文本,只需构建一次dp数组,可以匹配多个文本,非常高效,伪代码如下:
dp = KMP(pat)
match1 = search(txt1, pat, dp)
match2 = search(txt2, pat, dp)
此做法妙就妙在指向字符串的指针是不会回溯的,只需要经过O(N)时间就可完成匹配,并且每一次都要匹配,需要移动的只有指向pat的指针。
如何移动指向pat的指针?KMP采用的做法是构建转移状态图。我们可以通过一个二维dp数组来构建此转移状态图。
这里的核心思想是,当节点不匹配时,可以确定已经遍历过的元素是否匹配。而代码中的X会自动记录遍历过的痕迹以及是否匹配,较难理解。
import numpy as np
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
if needle == '': return 0
# 字符转换为索引
def c2i(c):
return ord(c)-ord('a')
def i2c(i):
return chr(i+ord('a'))
# dp矩阵:当前位置+输入元素 -> 下一个位置
dp = np.zeros((len(needle), 26), dtype=int)
# dp数组填充
dp[0][c2i(needle[0])] = 1
X = 0
for j in range(1, len(needle)):
for i in range(26):
if i2c(i) == needle[j]:
# 若等于该字符,转换到下一个
dp[j][i] = j+1
else:
# 若不等,寻求上一个相等元素帮助
dp[j][i] = dp[X][i]
X = dp[X][c2i(needle[j])]
# 搜索
j = 0
for i in range(len(haystack)):
j = dp[j][c2i(haystack[i])]
if j == len(needle):
return i-len(needle)+1
return -1
解法四:KMP算法(一维next数组)
这个算法是比上面的优雅许多,也好理解一些。当遇到不匹配时,我们应该把模式串往后移,并让他尽可能移动的少一些。移动后应该达到一个k位的匹配,此时即在j-1之前数组的前后缀是相等的,此时我们就可以让next[j] = k,代表模式串前k位是可以实现匹配的。
但是在计算next数组时,并不需要每次都找k,这样复杂度会高,我们可以用dp的思想来填充next数组,如下图所示:
已知next[j]=k,则j-1之前的字符串前k个和后k个是相同的,若第k个元素和第j个元素再相等,则j+1处的元素可直接用next[j]+1来算。如果不相等的话,只能找更靠前的相等子串,让新的k = next[j],直到第k个元素和第j个相等,用他的值,否则就等于1。
在实际应用中,我们让dp[0] = -1,这样可以判断有没有可以匹配的前缀和后缀,代码如下:
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
if needle == '': return 0
# 构建dp数组(next数组)
dp = [-1 for _ in range(len(needle))]
k, j = -1, 0 # k = next[j], j = 1
while j < len(dp):
# 每次迭代, next[j] = k
if k < 0 or needle[j] == needle[k]:
k += 1
j += 1
if j == len(dp):
break
dp[j] = k
else:
k = dp[k]
# 搜索
i, j = 0, 0
while i < len(haystack):
if j < 0 or needle[j] == haystack[i]:
i += 1
j += 1
if j == len(needle):
return i-j
else:
j = dp[j]
return -1