计算指针字符串长度_LeetCode笔记:字符串匹配——KMP算法

LeetCode第28题:

实现 strStr() 函数。

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

示例 1:

输入: haystack = "hello", needle = "ll"

输出: 2

示例 2:

输入: haystack = "aaaaa", needle = "bba"

输出: -1

说明:

当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。

字符串匹配是计算机的基本任务之一,有许多种算法,其中Knuth-Morris-Pratt算法(简称KMP)是常用的之一,思路比较复杂,以其三个发明者命名


算法思路

以文本串"BBC ABCDAB ABCDABCDABDE"和模式串"ABCDABD" 为例

首先,文本串与模式串的第一个字符比较

2ef2434d42ee1675ab593ac6bd03d00d.png

不匹配,所以文本串指针后移一位

2cf7f75ab84055a4807de7d395f77a41.png

不匹配,文本串指针继续后移

……

f72744124228d3636c740002d4d90afc.png

直到模式串的第一个字符匹配成功,双指针同时后移一位

0228f1803e01cb7da206227ba8177a85.png

继续匹配成功,双指针继续后移

……

2b1a0d2f77d772b53e7b710ef01a2a74.png

至此匹配失败

此时如果将文本串指针回退到下一个起始位置,并将模式串指针归零,那么就是暴力搜索算法,时间复杂度为O(n * m)(设文本串长度为n,模式串长度为m),较高

2240c0af9dc526d13e78750ba6e62f8a.png

KMP算法的思路是

2b1a0d2f77d772b53e7b710ef01a2a74.png

当匹配失败时,模式串指针前面的"AB"与模式串开头的"AB"是匹配的,利用这个信息,可以将模式串的指针从6回退到2,这里的从6到2的映射,需要事先计算出来,也就是“模式串的部分匹配值”,这个我们后面再讲

6d05c441a2726088c6c17c48046dbddb.png

不匹配,根据“模式串的部分匹配值”,模式串指针从2回退到0

13550f4d4159fd07cebaee28592f03ff.png

不匹配,由于模式串的指针在开头,因此文本串指针右移一位

……

bd19cdec4df113cfad35595ccdbb213d.png

不匹配,根据“模式串的部分匹配值”,模式串指针从6回退到2

……

cf659a89bf06f0b288a58da7e2eeae37.png

直至匹配成功,算法结束

以上KMP算法的总体思路是:

当匹配成功时,双指针同时后移一位

当匹配失败时,如果模式串指针为0,则文本串指针后移一位

否则模式串指针根据“模式串的部分匹配值”回退

下面讲一下“模式串的部分匹配值”的计算,这里我们定义字符串的:

前缀:字符串的全部头部子串(不包含自身)

后缀:字符串的全部尾部子串(不包含自身)

abaac67570cd2c9263aa767f78df41ab.png

模式串的部分匹配值,定义为一个i->j的映射,j为模式串前i位子串的前缀和后缀的最大相同长度

以"ABCDABD"为例

"A"的前缀和后缀都为空集,共有元素的长度为0;

"AB"的前缀为["A"],后缀为["B"],共有元素的长度为0;

"ABC"的前缀为["A", "AB"],后缀为["BC", "C"],共有元素的长度0;

"ABCD"的前缀为["A", "AB", "ABC"],后缀为["BCD", "CD", "D"],共有元素的长度为0;

"ABCDA"的前缀为["A", "AB", "ABC", "ABCD"],后缀为["BCDA", "CDA", "DA", "A"],共有元素为"A",长度为1;

"ABCDAB"的前缀为["A", "AB", "ABC", "ABCD", "ABCDA"],后缀为["BCDAB", "CDAB", "DAB", "AB", "B"],共有元素为"AB",长度为2;

"ABCDABD"的前缀为["A", "AB", "ABC", "ABCD", "ABCDA", "ABCDAB"],后缀为["BCDABD", "CDABD", "DABD", "ABD", "BD", "D"],共有元素的长度为0。

581dee1122c8bf11b1a8c2bcea00b1d1.png

“部分匹配”的实质(其实也就是KMP算法的本质)是,部分模式串的头部和尾部有时会重复,当模式串与文本串匹配失败时,模式串当前指针(例子中的"ABCDABD")的前面一部分("ABCDABD")与模式串的头部("ABCDABD")相同,利用这个信息,可以跳过一些文本串和模式串的指针,从而降低复杂度

但是,按照上文描述的计算逻辑,计算“模式串的部分匹配值”这个过程,时间复杂度是很高的O(m ^ 3),所以需要通过一些算法来降低复杂度,比如使用动态规划

设模式串为s,定义“模式串的部分匹配值”为a[],给定初始值a[0] = 0

在计算a[i]时,令j = a[i - 1]

根据a[i - 1]的含义,我们知道s[: i]中最长的相同前缀和后缀是

s[: j]与s[i - j: i]

比较s[i]和s[j],如果相同,那么这两个字符就可以分别加到上面的后缀和前缀中,成为s[: i + 1]中最长的相同前缀和后缀,长度为j + 1,即

j = a[i - 1]if s[i] == s[j]:    a[i] = j + 1

如果s[i] != s[j],那么就需要寻找s[: i + 1]中短一点的相同前缀和后缀

就是要寻找s[i - j: i]去掉头部一些元素,再加上s[i],能否与s的头部对上,这里需要观察到,由于s[: j] = s[i - j: i],所以上述问题等价于,s[: j]去掉头部一些元素,再加上s[i],能否与s的头部对上,s[: j]中最长的相同前缀和后缀,是

s[: a[j - 1]]和s[j - a[j - 1]: j]

因此只需要对比s[i]和s[a[j - 1]],这里就是动态规划递归产生的地方

if s[i] != s[j]:    j = a[j - 1]

对j循环迭代,直至s[i]与s[j]匹配成功或者j回退到0退出


复杂度分析

设文本串长度n,模式串长度m

时间复杂度

  • 计算“模式串部分匹配值”的过程

计算第i位需要O(m),即指针回退的最大步数,总共计算m位

时间复杂度为O(m ^ 2)

  • 匹配过程

设文本串指针为i,模式串指针为j

初始值i = 0,j = 0

算法结束的最差条件为i > n

双指针共有三种移动类型:

a:匹配成功,i ++,j ++

b:匹配失败且j = 0,i ++

c:匹配失败且j != 0,j回退

设每种移动类型的次数为f()

a和b中i都是单向移动,所以

f(a) + f(b) <= n

这里我们构建一个虚拟的移动类型:

d:匹配失败且j != 0,j --

c中j回退到0的速度是大于等于d中j --到0的速度的,所以

f(c) <= f(d)

这里我们可以注意到,由于d的前置条件是j != 0,所以

f(d) <= f(a)

通俗理解就是,初始位置在0的j,右移的次数必然大于等于左移的次数

综上

f(a) + f(b) + f(c) <= f(a) + f(b) + f(d) <= f(a) + f(b) + f(a) <= 2(f(a) + f(b)) <= 2n

时间复杂度为O(n)

  • KMP算法总的时间复杂度为O(n + m ^ 2)

空间复杂度

“模式串部分匹配值”的结果需要维护一个长度为m的数组

匹配过程需维护双指针+已匹配的字符数,共3个变量

总的空间复杂度为O(m)


代码(python3)

class Solution:    def strStr(self, haystack: str, needle: str) -> int:        # 特殊情况                if len(needle) == 0:            return 0        if len(haystack) == 0:            return -1        return self.kmpMatch(haystack, needle)    def kmpMatch(self, haystack, needle):        '''        KMP算法        '''        result = -1        needle_partial_match = self.partialMatch(needle)        # 双指针        i = 0        j = 0        # 已匹配的字符数        matched_count = 0        while i < len(haystack) and j < len(needle):            # 双指针匹配,双指针同时右移一位            if haystack[i] == needle[j]:                if j == len(needle) - 1:                    result = i - j                    return result                i += 1                j += 1                matched_count += 1                continue            # 双指针不匹配,且needle的指针是第0位,则haystask指针右移一位,已匹配的字符数更新为0            if j == 0:                i += 1                matched_count = 0                continue            # 双指针不匹配,且needle的指针不是第0位,则needle指针左移至(已匹配的字符数对应的部分匹配值)位置,已匹配的字符数更新为needle指针前面的位数            j = needle_partial_match[j - 1]            matched_count = j        return result    def partialMatch(self, s):        '''        对于模式串,求出每一位的部分匹配值,即当匹配失败时,needle需要移动的位数        部分匹配值定义为:部分字符串的前缀和后缀相同的最大长度        使用动态规划降低复杂度        '''        result = [None] * len(s)        # 边界条件        result[0] = 0        if len(s) == 1:            return result        for i in range(1, len(s)):            result[i] = 0            j = result[i - 1]            while j >= 0:                if s[i] == s[j]:                    result[i] = j + 1                    break                if j == 0:                    break                j = result[j - 1]        return result

参考文献

阮一峰的网络日志:字符串匹配的KMP算法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值