编辑
2024-03-12
LeetCode
00
请注意,本文编写于 369 天前,最后修改于 369 天前,其中某些信息可能已经过时。

目录

找出字符串中第一个匹配项的下标
题解一:暴力匹配
题解二:Knuth-Morris-Pratt 算法

找出字符串中第一个匹配项的下标

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

示例 1:

输入:haystack = "sadbutsad", needle = "sad"

输出:0 解释:"sad" 在下标 06 处匹配。

第一个匹配项的下标是 0 ,所以返回 0

示例 2:

输入:haystack = "leetcode", needle = "leeto"

输出:-1

解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1

提示:

1 <= haystack.length, needle.length <= 104

haystackneedle 仅由小写英文字符组成

找出字符串中第一个匹配项的下标

c++
class Solution { public: int strStr(string haystack, string needle) { if(haystack.length()<needle.length()){ return -1; } for(int i=0;i<=haystack.length()-needle.length();i++){ for(int j=0;j<needle.length();){ if(needle[j]==haystack[i+j]){ j++; } else{ j=0; break; } if(j==needle.length()){ return i; } } } return -1; } };

空间复杂度:O(1)O(1)

使用了常数级别的额外空间,没有使用额外的数据结构来存储信息。

时间复杂度:O((nm+1)m)O((n-m+1)*m)

其中,nnhaystackhaystack的长度,mmneedleneedle的长度。算法的时间复杂度主要取决于两个嵌套的循环。外层循环最多执行nm+1n-m+1次,内层循环最多执行mm次。因此,总的时间复杂度为O((nm+1)m)O((n-m+1)*m)

题解一:暴力匹配

作者:力扣官方题解

链接:https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/solutions/732236/shi-xian-strstr-by-leetcode-solution-ds6y/

来源:力扣(LeetCode)

思路及算法

我们可以让字符串 needleneedle 与字符串 haystackhaystack 的所有长度为 mm 的子串均匹配一次。

为了减少不必要的匹配,我们每次匹配失败即立刻停止当前子串的匹配,对下一个子串继续匹配。如果当前子串匹配成功,我们返回当前子串的开始位置即可。如果所有子串都匹配失败,则返回 1−1

找出字符串中第一个匹配项的下标(题解一)

c++
class Solution { public: int strStr(string haystack, string needle) { int n = haystack.size(), m = needle.size(); for (int i = 0; i + m <= n; i++) { bool flag = true; for (int j = 0; j < m; j++) { if (haystack[i + j] != needle[j]) { flag = false; break; } } if (flag) { return i; } } return -1; } };

复杂度分析

时间复杂度:O(n×m)O(n×m),其中 nn 是字符串 haystackhaystack 的长度,mm 是字符串 needleneedle 的长度。最坏情况下我们需要将字符串 needleneedle 与字符串 haystackhaystack 的所有长度为 mm 的子串均匹配一次。

空间复杂度:O(1)O(1)。我们只需要常数的空间保存若干变量。

题解二:Knuth-Morris-Pratt 算法

思路及算法

KnuthMorrisPrattKnuth-Morris-Pratt 算法,简称 KMPKMP 算法,由 DonaldKnuthDonald KnuthJamesH.MorrisJames H. MorrisVaughanPrattVaughan Pratt 三人于 19771977 年联合发表。

KnuthMorrisPrattKnuth-Morris-Pratt 算法的核心为前缀函数,记作 π(i)\pi(i) ,其定义如下:

对于长度为 mm 的字符串 ss ,其前缀函数 π(i)(0i<m)\pi(i)(0 \leq i<m) 表示 ss 的子串 s[0:i]s[0: i] 的最长的相等的真前缀与真后缀的长度。特别地,如果不存在符合条件的前后缀,那么 π(i)=0\pi(i)=0 。其中真前缀与真后缀的定义为不等于自身的的前缀与后缀。

我们举个例子说明:字符串 aabaaaba a b a a a b 的前缀函数值依次为 0,1,0,1,2,2,30,1,0,1,2,2,3

  • π(0)=0\pi(0)=0 ,因为 aa 没有真前缀和真后缀,根据规定为 0 (可以发现对于任意字符串 π(0)=0\pi(0)=0必定成立);
  • π(1)=1\pi(1)=1 ,因为 aaa a 最长的一对相等的真前后缀为 aa ,长度为 1 ;
  • π(2)=0\pi(2)=0 ,因为 aaba a b 没有对应真前缀和真后缀,根据规定为 0 ;
  • π(3)=1\pi(3)=1 ,因为 aabaa a b a 最长的一对相等的真前后缀为 aa ,长度为 1 ;
  • π(4)=2\pi(4)=2 ,因为 aabaaa a b a a 最长的一对相等的真前后缀为 aaa a ,长度为 2 ;
  • π(5)=2\pi(5)=2 ,因为 aabaaaa a b a a a 最长的一对相等的真前后缀为 aaa a ,长度为 2 ;
  • π(6)=3\pi(6)=3 ,因为 aabaaaba a b a a a b 最长的一对相等的真前后缀为 aaba a b ,长度为 3 。

有了前缀函数,我们就可以快速地计算出模式串在主串中的每一次出现。

如何求解前缀函数

长度为 mm 的字符串 ss 的所有前缀函数的求解算法的总时间复杂度是严格 O(m)O(m) 的,且该求解算法是增量算法,即我们可以一边读入字符串,一边求解当前读入位的前缀函数。

为了叙述方便,我们接下来将说明几个前缀函数的性质:

  1. π(i)π(i1)+1\pi(i) \leq \pi(i-1)+1
  • 依据 π(i)\pi(i) 定义得: s[0:π(i)1]=s[iπ(i)+1:i]s[0: \pi(i)-1]=s[i-\pi(i)+1: i]
  • 将两区间的右端点同时左移,可得: s[0:π(i)2]=s[iπ(i)+1:i1]s[0: \pi(i)-2]=s[i-\pi(i)+1: i-1]
  • 依据 π(i1)\pi(i-1) 定义得: π(i1)π(i)1\pi(i-1) \geq \pi(i)-1 ,即 π(i)π(i1)+1\pi(i) \leq \pi(i-1)+1
  1. 如果 s[i]=s[π(i1)]s[i]=s[\pi(i-1)] ,那么 π(i)=π(i1)+1\pi(i)=\pi(i-1)+1
  • 依据 π(i1)\pi(i-1) 定义得: s[0:π(i1)1]=s[iπ(i1):i1]s[0: \pi(i-1)-1]=s[i-\pi(i-1): i-1]
  • 因为 s[π(i1)]=s[i]s[\pi(i-1)]=s[i] ,可得 s[0:π(i1)]=s[iπ(i1):i]s[0: \pi(i-1)]=s[i-\pi(i-1): i]
  • 依据 π(i)\pi(i) 定义得: π(i)π(i1)+1\pi(i) \geq \pi(i-1)+1 ,结合第一个性质可得 π(i)=π(i1)+1\pi(i)=\pi(i-1)+1

这样我们可以依据这两个性质提出求解 π(i)\pi(i) 的方案:找到最大的 jj ,满足 s[0:j1]=s[ijs[0: j-1]=s[i-j : i1]i-1], 且 s[i]=s[j]s[i]=s[j] (这样就有 s[0:j]=s[ij:i]s[0: j]=s[i-j: i] ,即 π(i)=j+1\pi(i)=j+1 )。

注意这里提出了两个要求:

  1. jj 要求尽可能大,且满足 s[0:j1]=s[ij:i1]s[0: j-1]=s[i-j: i-1]
  2. jj 要求满足 s[i]=s[j]s[i]=s[j]

π(i1)\pi(i-1) 定义可知:

s[0:π(i1)1]=s[iπ(i1):i1]s[0: \pi(i-1)-1]=s[i-\pi(i-1): i-1]

那么 j=π(i1)j=\pi(i-1) 符合第一个要求。如果 s[i]=s[π(i1)]s[i]=s[\pi(i-1)] ,我们就可以确定 π(i)\pi(i) 。 否则如果 s[i]s[π(i1)]s[i] \neq s[\pi(i-1)] ,那么 π(i)π(i1)\pi(i) \leq \pi(i-1) ,因为 j=π(i)1j=\pi(i)-1 ,所以 j<π(i1)j<\pi(i-1) ,于是可以取 (1) 式两子串的长度为 jj 的后缀,它们依然是相等的: s[π(i1)j:π(i1)1]=s[is[\pi(i-1)-j: \pi(i-1)-1]=s[i- j:i1]j: i-1]

s[i]s[π(i1)]s[i] \neq s[\pi(i-1)] 时,我们可以修改我们的方案为: 找到最大的 jj ,满足 s[0:j1]=s[π(is[0: j-1]=s[\pi(i- 1) j:π(i1)1]-j: \pi(i-1)-1], 且 s[i]=s[j]s[i]=s[j] (这样就有 s[0:j]=s[π(i1)j:π(i1)]s[0: j]=s[\pi(i-1)-j: \pi(i-1)], 即 π(i)=\pi(i)= π(i1)+1)\pi(i-1)+1)

注意这里提出了两个要求:

  1. jj 要求尽可能大,且满足 s[0:j1]=s[π(i1)j:π(i1)1]s[0: j-1]=s[\pi(i-1)-j: \pi(i-1)-1]
  2. jj 要求满足 s[i]=s[j]s[i]=s[j]

π(π(i1)1)\pi(\pi(i-1)-1) 定义可知 j=π(π(i1)1)j=\pi(\pi(i-1)-1) 符合第一个要求。如果 s[i]=s[π(π(i1)1)]s[i]=s[\pi(\pi(i-1)-1)],我们就可以确定 π(i)\pi(i)

此时,我们可以发现 jj 的取值总是被描述为 π(π(π()1)1)\pi(\pi(\pi(\ldots)-1)-1) 的结构(初始为 π(i1)\pi(i-1) )。于是我们可以描述我们的算法: 设定 π(i)=j+1j\pi(i)=j+1 , j 的初始值为 π(i1)\pi(i-1) 。我们只需要不断迭代 jj (令 jj 变为 π(j1)\pi(j-1) ) 直到 s[i]=s[j]s[i]=s[j]j=0j=0 即可,如果最终匹配成功(找到了 jj 使得 s[i]=s[i]= s[j]s[j] ),那么 π(i)=j+1\pi(i)=j+1 ,否则 π(i)=0\pi(i)=0

复杂度证明 时间复杂度部分,注意到 π(i)π(i1)+1\pi(i) \leq \pi(i-1)+1 ,即每次当前位的前缀函数至多比前一位增加一,每当我们迭代一次,当前位的前缀函数的最大值都会减少。可以发现前缀函数的总减少次数不会超过总增加次数,而总增加次数不会超过 mm 次,因此总减少次数也不会超过 mm 次,即总迭代次数不会超过 mm 次。

空间复杂度部分,我们只用到了长度为 mm 的数组保存前缀函数,以及使用了常数的空间保存了若干变量。

如何解决本题

记字符串 haystack 的长度为 nn ,字符串 needle 的长度为 mm 。 我们记字符串 str=needle+#+s t r=n e e d l e+\#+ haystack,即将字符串 needle 和 haystack 进行拼接,并用不存在于两串中的特殊字符 # 将两串隔开,然后我们对字符串 strs t r 求前缀函数。

因为特殊字符 #\# 的存在,字符串 str 中 haystack 部分的前缀函数所对应的真前缀必定落在字符串 needle 部分,真后缀必定落在字符串 haystack 部分。当 haystack 部分的前缀函数值为 mm 时,我们就找到了一次字符串 needle 在字符串 haystack 中的出现(因为此时真前缀恰为字符串 needle)。

实现时,我们可以进行一定的优化,包括:

  1. 我们无需显式地创建字符串 strs t r
  • 为了节约空间,我们只需要顺次遍历字符串 needle、特殊字符# 和字符串 haystack 即可。
  1. 也无需显式地保存所有前缀函数的结果,而只需要保存字符串 needle 部分的前缀函数即可。
  • 特殊字符 #\# 的前缀函数必定为 0 ,且易知 π(i)m\pi(i) \leq m (真前缀不可能包含特殊字符 #)。
  • 这样我们计算 π(i)\pi(i) 时, j=π(π(π()1)1)j=\pi(\pi(\pi(\ldots)-1)-1) 的所有的取值中仅有 π(i1)\pi(i-1) 的下标可能大于等于 mm 。我们只需要保存前一个位置的前缀函数,其它的 jj 的取值将全部为字符串 needle 部分的前缀函数。
  1. 我们也无需特别处理特殊字符 #,只需要注意处理字符串 haystack 的第一个位置对应的前缀函数时,直接设定 jj 的初值为 0 即可。

  2. 第一部分是求 needle 部分的前缀函数,我们需要保留这部分的前缀函数值。

  3. 第二部分是求 haystack 部分的前缀函数,我们无需保留这部分的前缀函数值,只需要用一个变量记录上一个位置的前缀函数值即可。当某个位置的前缀函数值等于 mm 时,说明我们就找到了一次字符串 needle 在字符串 haystack 中的出现(因为此时真前缀恰为字符串 needle,真后缀为以当前位置为结束位置的字符串 haystack 的子串),我们计算出起始位置,将其返回即可。

找出字符串中第一个匹配项的下标(题解二)

c++
class Solution { public: int strStr(string haystack, string needle) { int n = haystack.size(), m = needle.size(); if (m == 0) { return 0; } vector<int> pi(m); for (int i = 1, j = 0; i < m; i++) { while (j > 0 && needle[i] != needle[j]) { j = pi[j - 1]; } if (needle[i] == needle[j]) { j++; } pi[i] = j; } for (int i = 0, j = 0; i < n; i++) { while (j > 0 && haystack[i] != needle[j]) { j = pi[j - 1]; } if (haystack[i] == needle[j]) { j++; } if (j == m) { return i - m + 1; } } return -1; } };

复杂度分析

时间复杂度:O(n+m)O(n+m),其中 nn 是字符串 haystackhaystack 的长度,mmm 是字符串 needleneedle 的长度。我们至多需要遍历两字符串一次。

空间复杂度:O(m)O(m),其中 mm 是字符串 needleneedle 的长度。我们只需要保存字符串 needleneedle 的前缀函数。

本文作者:古月流新

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!