力扣28. 找出字符串中第一个匹配项的下标(暴力手撕+KMP算法)

找出字符串中第一个匹配项的下标:Java解法详解

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

题目

给你两个字符串 haystack 和 needle ,请你在 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 。

提示:

  • 1 <= haystack.length, needle.length <= 104
  • haystack 和 needle 仅由小写英文字符组成

 详解

一、暴力手撕法(双指针法)

思路

采用双指针法进行匹配:

  • 初始化指针p(主串)和q(子串),从头开始逐个字符比对
  • 若当前字符不匹配,则p回溯到主串下一位置,q重置到子串起始位置
  • 重复上述过程,直至q遍历完整个子串
  • 此时p-q的差值即为子串首次出现的位置索引

 完整代码

class Solution {
    public int strStr(String haystack, String needle) {
        if (needle.isEmpty()) {
            return 0;
        }
        int p = 0; // 索引 p 用于 haystack
        int q = 0; // 索引 q 用于 needle

        while (p < haystack.length() && q < needle.length()) {
            if (haystack.charAt(p) == needle.charAt(q)) {
                p++;
                q++;
            } else {
                p = p - q + 1; // 重置 p 和 q
                q = 0;
            }

            // 如果 needle 全部匹配,返回起始位置
            if (q == needle.length()) {
                return p - q;
            }
        }
        return -1; // 未找到匹配
    }
}

缺点

重复匹配失败会导致从头开始重新尝试,不仅效率低下且浪费资源。

二、KMP算法(最通俗易懂版)

当 haystack = "abcabefg" 且 needle = "abefg" 时,采用双指针匹配方法。在匹配过程中,当遇到主串中的字符'c'与模式串不匹配时,算法会触发回溯机制。

abcabefg
abefg

匹配失败后重新从起始位置开始尝试:

haystackabcabefg
needleabefg

为什么不直接从匹配失效后面再接着重新找呢?这样不就快一些吗?

haystackabcabefg
needleabefg

原因:直接从匹配失效的后面接着找可能会导致遗漏某些潜在的匹配位置。因为模式串内部可能存在重复的子串,回溯到部分匹配的位置可以确保我们不会错过这些子串可能带来的匹配机会。

好,接下来再看一个例子

haystack = "abababcaa" 且 needle = "ababc"

haystackabababcaa
needleababc

当匹配最后一个元素不成功时,若直接从匹配失效后再接着找那就出错了,因为实质上要从一下地方再进行匹配

haystackabababcaa
needleababc

如何确定匹配的起始位置?当匹配失败时,如何精准定位需要回溯的位置?

当两个字符串在匹配过程中出现错误时,若它们之前存在相同的前缀(如"ab"),我们可以通过定义一个数组来记录每个位置匹配失败时,其前缀是否与其他元素相匹配。

代码解析

    private int[] computeLPSArray(String needle) {
        int length = 0; // 当前相同前缀后缀的长度
        int i = 1; // 从第二个字符开始计算 LPS
        int[] lps = new int[needle.length()]; // 初始化 LPS 数组
        lps[0] = 0; // 第一个字符的 LPS 值为 0

        while (i < needle.length()) { // 遍历 needle
            if (needle.charAt(i) == needle.charAt(length)) { // 如果字符匹配
                length++; // 增加相同前缀后缀的长度
                lps[i] = length; // 记录 LPS 值
                i++; // 移动 i
            } else { // 如果字符不匹配
                if (length != 0) {
                    length = lps[length - 1]; // 回退 length
                } else {
                    lps[i] = 0; // 如果 length 为 0,记录 LPS 值
                    i++; // 移动 i
                }
            }
        }

        return lps; // 返回 LPS 数组
    }

这个方法就可以得到一个数组,数组元素表示当前元素存在的最大前缀

例如,对于模式串 needle = "ababc",部分匹配表如下:

字符ababc
前缀函数值00120
  • 对于第一个字符 a,没有前缀和后缀,所以值为0。
  • 对于第二个字符 b,没有相同前后缀,所以值为0。
  • 对于第三个字符 a,与第一个字符相同,所以值为1。
  • 对于第四个字符 b,与第二个字符相同,所以值为2。
  • 对于第五个字符 c,没有相同前后缀,所以值为0。
  1. 匹配过程
    在匹配过程中,当发生不匹配时,我们根据部分匹配表中的值来决定回溯的位置。

    例如,对于 haystack = "abababcaa" 和 needle = "ababc"

    haystackabababcaa
    needleababc
    • 当匹配到第四个字符 b 时,发生不匹配。
    • 根据部分匹配表,我们知道在位置3(从0开始计数)的值为2,这意味着在模式串的前缀 abab 中,最长相同前后缀的长度为2。
    • 所以,我们可以将模式串回溯到位置2(即第二个 a 的位置)重新开始匹配。
  2. 回溯位置的确定

    • 当匹配失败时,我们查看部分匹配表中当前位置的值,这个值就是我们需要回溯到的位置。
    • 在上述例子中,匹配失败时,我们根据部分匹配表的值2,将模式串回溯到第二个 a 的位置。

借助该数组,我们能够快速确定匹配失效时需要回溯的位置。完整代码及具体解析如下:

class Solution {
    public int strStr(String haystack, String needle) {
        if (needle.isEmpty()) {
            return 0; // 如果 needle 为空,直接返回 0
        }

        int[] lps = computeLPSArray(needle); // 计算 needle 的 LPS 数组
        int i = 0; // 索引 i 用于遍历 haystack
        int j = 0; // 索引 j 用于遍历 needle

        while (i < haystack.length()) { // 遍历 haystack
            if (haystack.charAt(i) == needle.charAt(j)) { // 如果字符匹配
                i++; // 移动 i
                j++; // 移动 j
            }

            if (j == needle.length()) { // 如果 needle 全部匹配
                return i - j; // 返回匹配的起始位置
            } else if (i < haystack.length() && haystack.charAt(i) != needle.charAt(j)) { // 如果字符不匹配
                if (j != 0) {
                    j = lps[j - 1]; // 根据 LPS 数组回退 j
                } else {
                    i++; // 如果 j 为 0,直接移动 i
                }
            }
        }

        return -1; // 未找到匹配
    }

    private int[] computeLPSArray(String needle) {
        int length = 0; // 当前相同前缀后缀的长度
        int i = 1; // 从第二个字符开始计算 LPS
        int[] lps = new int[needle.length()]; // 初始化 LPS 数组
        lps[0] = 0; // 第一个字符的 LPS 值为 0

        while (i < needle.length()) { // 遍历 needle
            if (needle.charAt(i) == needle.charAt(length)) { // 如果字符匹配
                length++; // 增加相同前缀后缀的长度
                lps[i] = length; // 记录 LPS 值
                i++; // 移动 i
            } else { // 如果字符不匹配
                if (length != 0) {
                    length = lps[length - 1]; // 回退 length
                } else {
                    lps[i] = 0; // 如果 length 为 0,记录 LPS 值
                    i++; // 移动 i
                }
            }
        }

        return lps; // 返回 LPS 数组
    }
}

 既然你看到这里了,那么恭喜你,KMP算法你已经会写了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值