力扣刷题day08|28找出字符串中第一个匹配项的下标(KMP算法)

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

力扣题目链接

实现 strStr() 函数。

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

示例1:

输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。

示例2:

输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。

思路

KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。

前缀:前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串

后缀:后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串

难点1:为什么能够回退重新匹配

为啥能通过获得next数组中存放的"aabaa"的"最长相等前后缀",移动到b的原因:你已经知道了f前面的文本串与模式串字符都匹配上了那么

  1. 文本串不会从第二个a开始,而是可以直接从当前与f不匹配的的b开始:因为有"aabaa"的"最长相等前后缀"的存在,知道模式串前面一定是aa,文本串"aabaa"的相同后缀可以直接默认与模式串的相同前缀匹配过了,直接从相同前后缀的下一位开始匹配
  2. 模式串也不需要从第一个a开始,而是从b开始:因为有"aabaa"的"最长相等前后缀"的存在,知道文本串与f不配的b前面的一定是aa

j指向前缀末尾位置,i指向后缀末尾位置

难点2:构建next数组

构建next数组就是求模式串上的每个位置的相同前后缀

  1. 初始化:
int j = 0;
next[0] = j;

next[i] 表示 i(包括i)之前最长相等的前后缀长度,这里的j表示相等前缀的末尾位置

所以初始化next[0] = j

for (int i = 1; i < s.size(); i++) 

i初始化为1,因为i是后缀末尾,不能包括首字符,所以从1开始

  1. 处理前后缀不相同的情况

如果 s[i] 与 s[j]不相等,也就是遇到前后缀末尾不相同的情况,就要向前回退,回退到next数组的前一位位置,这是个嵌套,第一个是回退到写好的next数组,而这个是用前后缀的next数组求next数组,现在我们也是匹配只不过匹配的时前缀(相当于模式串)和后缀(相当于文本串)

现在又可以分为两种情况加深了解,并用例子来加以解释:

image-20220929210902976

上面是一个子串的next数组的值,可以看到这个子串的对称程度很高,所以next值都比较大

  • s[i] != s[j]的前一位s[i-1]匹配时也是不相等的

此时next[i-1]为0,当s[i-1]进行匹配时j已经直接跳到开头第一个字符(j为0),不满足两个判断条件直接给next[i]赋值j(即0)

如图所示,当i=3时,j因为上一轮的比较肯定在位置0处,不满足两个判断的要求,直接给next赋值为j

image-20220929212806310

  • s[i] != s[j]的前一位s[i-1]匹配时是相等的

此时next[i-1]不为0,j直接会赋值为next[j-1],s[i-1]进行匹配时j++跳到的位置j正好是相同前后缀的下一位,对应的next肯定为0,接着跳到j为0的位置,然后进行s[i] == s[j]的判断

while (j > 0 && s[i] != s[j]) { // 前后缀不相同了
    j = next[j-1]; // 向前回退
}

如图所示,当i=7时,j因为上一轮s[i-1]的比较肯定在相同前后缀的下一位(位置next[7-1]=3),s[7] != s[3]

image-20220929213338941

j就跳到位置next[j-1]=next[3-1]=0

image-20220929213545225

然后进行s[i] == s[j]的判断即s[7]与s[0]的判断,判断相等i++,next[i] = j = 1

image-20220929213741138

求next数组,如果当前字符s[i] != s[j],不断回退的时候,j每次回退所在的位置一定也是和当前的s[i]不匹配的,最终j一定会回到0的位置,然后进行s[i] == s[j]的判断

  1. 处理前后缀相同的情况

因为next数组存的是长度,此时j和i指向相同前后缀的最后一个字符,j还要+1表示相同前后缀的长度,然后将j赋值给next[i]

if (s[i] == s[j]) { // 找到相同的前后缀
    j++;
}
next[i] = j;

现在又可以分为两种情况加深了解,并用例子来加以解释:

image-20220929210902976

  • s[i] == s[j]的前一位s[i-1]匹配时也是相等的

只用看上一轮s[i-1]匹配时,相同前后缀的下一位和当前的s[i]是否相等,相等的话到时候的next的值肯定比上一轮大1

如图所示,i=6时,j因为上一轮s[i-1]的比较肯定在相同前后缀的下一位(位置next[6-1]=2),s[2] == s[6],j++向后移得到长度,next[6]=j

image-20220929214911499

  • s[i] == s[j]的前一位s[i-1]匹配时是不相等的,此时j肯定为0,因此只用比较了s[i] == s[j],next的值仅能为1

如图所示,i=4时,j因为上一轮s[i-1]的比较肯定在0处,只用比较了s[0] == s[4],next的值仅能为1

image-20220929220057520

难点3:使用next数组来做匹配

在文本串s里找是否出现过模式串t。

定义两个下标

  • j 指向模式串起始位置
  • i指向文本串起始位置

i就从0开始,遍历文本串

for (int i = 0; i < haystack.length(); i++) {

接下来就是 s[i] 与 t[j] 进行比较。

如果 s[i] 与 t[j] 不相同,j就要从next数组里寻找下一个匹配的位置。

while (j > 0 && haystack.charAt(i) != needle.charAt(j)){// 不匹配
    j = next[j - 1]; // j 寻找之前匹配的位置
}

如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动, 代码如下:

// 如果文本串有与模式串相等的字符
if (haystack.charAt(i) == needle.charAt(j)) {
    j++; // j指向模式串,i在for里面++
}

如何判断在文本串s里出现了模式串t呢:如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。

本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。

// 当j到模式串的最后+1的时候,说明有了字串
if (j == needle.length()) {
    return i - needle.length() + 1;
}

整体代码:

public class StrStr {
    public int strStr(String haystack, String needle) {
        // 前缀表的大小
        int numNext = needle.length();
        // 定义前缀表
        int[] next = new int[numNext];
        // 得到前缀表
        getNext(next, needle);

        // 使用next数组来做匹配
        // 定义j指向模式串起始位置,i指向文本串起始位置
        int j = 0;
        for (int i = 0; i < haystack.length(); i++) {

            while (j > 0 && haystack.charAt(i) != needle.charAt(j)){// 不匹配
                j = next[j - 1]; // j 寻找之前匹配的位置
            }
            // 如果文本串有与模式串相等的字符
            if (haystack.charAt(i) == needle.charAt(j)) {
                j++; // j指向模式串,i在for里面++
            }
            // 当j到模式串的最后+1的时候,说明有了字串
            if (j == needle.length()) {
                return i - needle.length() + 1;
            }
        }

        // 没有匹配的
        return -1;

    }

    // 定义获得前缀表不减一的next数组的方法
    public void getNext(int[] next, String s) {
        // 初始化
        int j = 0; // 相同前缀的末尾
        next[0] = j;
        // i是相同后缀的末尾
        for (int i = 1; i < s.length(); i++) {
            // 处理不相等的情况
            while (j > 0 && s.charAt(i) != s.charAt(j)) {
                j = next[j - 1];
            }
            // 处理相等的情况
            if (s.charAt(i) == s.charAt(j)) {
                j++; // 因为next数组存的是长度,此时j和i指向前后缀的最后一个字符,j还要+1表长度
            }
            next[i] = j;
        }
    }
}
  • 时间复杂度:O(n+m)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值