【每日力扣18】实现strStr()

一、题目[LeetCode-28]

实现 strStr() 函数。

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

说明:

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

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

示例 1:

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

输出:2

示例 2:

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

输出:-1

示例 3:

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

输出:0

提示:

  • 0 <= haystack.length, needle.length <= 5 * 10^4
  • haystack 和 needle 仅由小写英文字符组成

二、思路

双指针法

依题意,可以使用快慢指针fast和slow,先用fast遍历字符串haystack,当fast在haystack中找到needle的第一个字符needle[0]时,慢指针slow更新到此时fast的位置。然后再fast判断后续字符haystack[fast+1, fast+n-1]是否与needle[1, n-1]相同,如果相同则返回索引slow,否则退出if条件语句继续用fast往前遍历。若fast遍历完成,依旧没有找到,则返回-1。

class Solution {
public:
    int strStr(string haystack, string needle) {
        int slow = 0, fast = 0;
        int n = needle.size();
        if(n==0)
            return 0;//返回n==0的退化情况
        for(;fast < haystack.size();fast++)//快指针先进行遍历字符串haystack
        {
            if(haystack[fast]==needle[0])
            {
                if(haystack.size()-fast < n)
                    return -1;//先判断区间长度haystack[slow, haystack.size()-1]小于n的退化情况
                slow = fast;//慢指针更新
                bool find = true;//声明一个标志find,用于标记haystack[fast+1, fast+n-1]是否与needle[1, n-1]相同
                for(int i = 1; i<n&&slow+i<haystack.size(); i++)//判断这两个区间是否相等
                {
                    if(needle[i]!=haystack[slow+i])//如果不相等
                        find=false;//标志为否
                }
                if(find==true)
                    return slow;//如果相等,直接返回索引slow
            }
        }
        return -1;//如果没找到,返回-1
    }
};

三、官方解法(来源:力扣(LeetCode))

前言

本题是经典的字符串单模匹配的模型,因此可以使用字符串匹配算法解决,常见的字符串匹配算法包括暴力匹配、Knuth-Morris-Pratt 算法、Boyer-Moore 算法、Sunday 算法等,本文将讲解 Knuth-Morris-Pratt 算法。

因为哈希方法可能出现哈希值相等但是字符串不相等的情况,而 strStr 函数要求匹配结果必定正确,因此本文不介绍哈希方法,有兴趣的读者可以自行了解滚动哈希的实现(如 Rabin-Karp 算法)。

方法一:暴力匹配

我们可以让字符串 needle 与字符串 haystack 的所有长度为 m 的子串均匹配一次。

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

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;
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/implement-strstr/solution/shi-xian-strstr-by-leetcode-solution-ds6y/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

复杂度分析

  • 时间复杂度:O(n×m),其中 n 是字符串 haystack 的长度,m 是字符串 needle 的长度。最坏情况下我们需要将字符串 needle 与字符串 haystack 的所有长度为 m 的子串均匹配一次。
  • 空间复杂度:O(1)。我们只需要常数的空间保存若干变量。

方法二:Knuth-Morris-Pratt 算法

Knuth-Morris-Pratt 算法,简称 KMP 算法,由 Donald Knuth、James H. Morris 和 Vaughan Pratt 三人于 1977 年联合发表。

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

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

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

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

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

如何求解前缀函数

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

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

1.π(i)≤π(i−1)+1(当前值比上一值最多增加1)

  • 依据 π(i) 定义得:s[0:π(i)−1]=s[i−π(i)+1:i]
  • 将两区间的右端点同时左移,可得:s[0:π(i)−2]=s[i−π(i)+1:i−1]
  • 依据 π(i−1) 定义得:π(i−1)≥π(i)−1,即 上档次

2.如果 s[i]=s[π(i−1)],那么 π(i)=π(i−1)+1(当前值能比上一值增加1的条件)

  • 依据 π(i−1) 定义得:s[0:π(i−1)−1]=s[i−π(i−1):i−1]
  • 因为 s[π(i−1)]=s[i],可得 s[0:π(i−1)]=s[i−π(i−1):i]
  • 依据 π(i) 定义得:π(i)≥π(i−1)+1,结合第一个性质可得 π(i)=π(i−1)+1

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

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

  1. j 要求尽可能大,且满足 s[0:j−1]=s[i−j:i−1];
  2. j 要求满足 s[i]=s[j]。

由 π(i−1) 定义可知:s[0:π(i−1)−1]=s[i−π(i−1):i−1],那么 j=π(i−1) 符合第一个要求。

  • 如果 s[i]=s[π(i−1)],我们就可以确定 π(i)=j+1
  • 否则如果 s[i]≠s[π(i−1)],那么 π(i)≤π(i−1),因为 j=π(i)−1,所以 j < π(i−1),于是可以取 (1) 式两子串的长度为 j 的后缀,它们依然是相等的:s[π(i−1)−j:π(i−1)−1]=s[i−j:i−1]。

当 s[i]≠s[π(i−1)] 时,我们可以修改我们的方案为:找到最大的 j,满足 s[0:j−1]=s[π(i−1)−j:π(i−1)−1],且 s[i]=s[π(i−1)](这样就有s[0:j]=s[π(i−1)−j:π(i−1)],即 π(i)=π(i−1)+1)。

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

  1. j 要求尽可能大,且满足s[0:j−1]=s[π(i−1)−j:π(i−1)−1];
  2. j 要求满足 s[i]=s[j]。

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

此时,我们可以发现 j 的取值总是被描述为 π(π(π(…)−1)−1) 的结构(初始为π(i−1))。于是我们可以描述我们的算法:设定 π(i)=j+1,j 的初始值为 π(i−1)(s[0, i-1]的公有前后缀中前缀的最末元素的长度)。我们只需要不断迭代 j(令 j变为 π(j−1))直到 s[i]=s[j] 或 j=0 即可,如果最终匹配成功(找到了 j 使得 s[i]=s[j]),那么 π(i)=j+1,否则 π(i)=0。

复杂度证明

  • 时间复杂度部分,注意到 π(i)≤π(i−1)+1,即每次当前位的前缀函数至多比前一位增加一,每当我们迭代一次,当前位的前缀函数的最大值都会减少。可以发现前缀函数的总减少次数不会超过总增加次数,而总增加次数不会超过 m 次,因此总减少次数也不会超过 m 次,即总迭代次数不会超过 m 次。
  • 空间复杂度部分,我们只用到了长度为 m 的数组保存前缀函数,以及使用了常数的空间保存了若干变量。

如何解决本题

记字符串 haystack 的长度为 n,字符串 needle 的长度为 m。

我们记字符串 str=needle+#+haystack,即将字符串 needle 和 haystack 进行拼接,并用不存在于两串中的特殊字符 # 将两串隔开,然后我们对字符串 str 求前缀函数。

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

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

我们无需显式地创建字符串 str。

为了节约空间,我们只需要顺次遍历字符needle、特殊字符 # 和字符串 haystack 即可。

也无需显式地保存所有前缀函数的结果,而只需要保存字符串 needle 部分的前缀函数即可。

特殊字符 # 的前缀函数必定为 0,且易知 π(i)≤m(真前缀不可能包含特殊字符 #)。

这样我们计算 π(i) 时,j=π(π(π(…)−1)−1) 的所有的取值中仅有 π(i−1) 的下标可能大于等于 m。我们只需要保存前一个位置的前缀函数,其它的 j 的取值将全部为字符串 needle 部分的前缀函数。

我们也无需特别处理特殊字符 #,只需要注意处理字符串 haystack 的第一个位置对应的前缀函数时,直接设定 j 的初值为 0 即可。

这样我们可以将代码实现分为两部分:

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

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

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;
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/implement-strstr/solution/shi-xian-strstr-by-leetcode-solution-ds6y/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

复杂度分析

  • 时间复杂度:O(n+m),其中 n 是字符串 haystack 的长度,m 是字符串 needle 的长度。我们至多需要遍历两字符串一次。
  • 空间复杂度:O(m),其中 m 是字符串 needle 的长度。我们只需要保存字符串 needle 的前缀函数。

四、学习心得

KMP算法

①对于一个长为m的模式串s,其共有前后缀数组π[i](0≤i<m)的意义及创建方法

π[i]意义:对s的子串s[0, i],其共有前后缀的长度。s[0, i]共有前缀:s[0, π[i]-1],共有后缀:s[i-π[i]+1, i]

数组π[]创建方法:双指针i和j实现

(在视频【帮你把KMP算法学个通透!(求next数组代码篇)-哔哩哔哩】 帮你把KMP算法学个通透!(求next数组代码篇)_哔哩哔哩_bilibili 有详细介绍)

//来自该视频作者公众号:代码随想录,上文所提π数组即为这里的next数组
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;//初始化
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) {//处理s[j]与s[i]不相同的情况,直至它们相同
j = next[j - 1];
}
if (s[i] == s[j]) {//处理s[j]与s[i]相同的情况
j++;
}
next[i] = j;//更新next[i]
}
}

i:s子串s[0, i]的后缀的末尾索引(指向最后一个元素,未越界)

j:s子串s[0, i]的前缀的末尾索引(恰好越界),同时也是前缀(也即后缀)的长度

冲突(即迭代i时若s[i]!=s[j]):对于发生冲突位置i的前一位i-1的前缀最后一位j-1取π[j-1] 得到s[0, j-1]的前缀的末尾索引(恰好越界),让j成为它。再次循环。直到能够不冲突(s[i]==s[j]为止)。

②前后缀数组π的应用

本题即为实例——字符串匹配

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值