leetcode 28 字符串匹配 O(N)复杂度解法

解法一:暴力
遍历长串,匹配到短串停止。此处提供一种比暴力稍好的策略,即不用每次都往前查找匹配,只有最后一个字母和中间字母都匹配再查找,这样能优化很多复杂度,最优复杂度为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
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 1024 设计师:上身试试 返回首页