【经典算法】LeetCode28 找出字符串中第一个匹配项的下标(Java/C/Python3实现含注释说明,Easy)

简介: 【经典算法】LeetCode28 找出字符串中第一个匹配项的下标(Java/C/Python3实现含注释说明,Easy)

题目:【经典算法】LeetCode 20:有效的括号(Java/C/Python3实现含注释说明,Easy)

  • 作者简介:阿里非典型程序员一枚 ,记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法(公众号同名
  • ❤️觉得文章还不错的话欢迎大家点赞👍➕收藏⭐️➕评论,💬支持博主,记得点个大大的关注,持续更新🤞
    ————————————————-

题目描述

给你两个字符串 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 仅由小写英文字符组成

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

思路及实现

方式一:暴力匹配

思路

暴力匹配是一种直观的解决方案。我们从字符串s的第一个字符开始,逐个字符与子串t进行比较。如果找到一个匹配,就返回当前的索引。如果整个s都被遍历完了还没有找到匹配,则返回-1。

代码实现

Java版本
public class Solution {
    public int strStr(String s, String t) {
        int m = s.length();
        int n = t.length();
        for (int i = 0; i <= m - n; i++) {
            int j;
            for (j = 0; j < n; j++) {
                if (s.charAt(i + j) != t.charAt(j)) {
                    break;
                }
            }
            if (j == n) {
                return i;
            }
        }
        return -1;
    }
}

说明:

  • 外部循环遍历字符串s,内部循环遍历子串t
  • 如果在内部循环中发现字符不匹配,则跳出内部循环。
  • 如果内部循环正常结束(即j == n),说明找到了一个匹配,返回当前的索引i
C语言版本
#include <string.h>
int strStr(char *s, char *t) {
    int m = strlen(s);
    int n = strlen(t);
    for (int i = 0; i <= m - n; i++) {
        int j;
        for (j = 0; j < n; j++) {
            if (s[i + j] != t[j]) {
                break;
            }
        }
        if (j == n) {
            return i;
        }
    }
    return -1;
}

说明:

  • C语言版本与Java版本逻辑相同,只是语法上有所不同。
Python3版本
class Solution:
    def strStr(self, s: str, t: str) -> int:
        m = len(s)
        n = len(t)
        for i in range(m - n + 1):
            if s[i:i+n] == t:
                return i
        return -1

说明:

  • Python使用切片操作s[i:i+n]来比较子串,更加简洁。
Golang版本
func strStr(s string, t string) int {
    m := len(s)
    n := len(t)
    for i := 0; i <= m-n; i++ {
        if s[i:i+n] == t {
            return i
        }
    }
    return -1
}

说明:

  • Golang的字符串切片操作和Python类似,也很简洁。

复杂度分析

  • 时间复杂度:O((m-n+1)n),其中m是字符串s的长度,n是子串t的长度。最坏情况下,s的每个子串都需要与t进行比较。
  • 空间复杂度:O(1),只使用了常量级别的额外空间。

方式二:KMP算法

思路

KMP算法(Knuth-Morris-Pratt算法)是一种高效的字符串匹配算法。它能在O(m+n)的时间复杂度内找出子串在字符串中的位置。KMP算法的关键在于利用已经部分匹配的信息,避免不必要的比较。

代码实现

Java版本
public class Solution {
    public int strStr(String s, String t) {
        if (t.isEmpty()) return 0;
        int[] lps = computeLPSArray(t);
        int i = 0; // index for s[]
        int j = 0; // index for t[]
        int m = s.length();
        int n = t.length();
        while (i < m) {
            if (s.charAt(i) == t.charAt(j)) {
                j++;
                i++;
            }
            if (j == n) {
                return i - j;
            } else if (i < m && s.charAt(i) != t.charAt(j)) {
                if (j != 0)
                    j = lps[j - 1];
                else
                    i = i + 1;
            }
        }
        return -1;
    }
    private int[] computeLPSArray(String pat) {
        int M = pat.length();
        int lps[] = new int[M];
        int len = 0; // length of the previous longest prefix suffix
        int i = 1;
        lps[0] = 0; // lps[0] is always 0
        // the loop calculates lps[i] for i = 1 to M-1
        while (i < M) {
            if (pat.charAt(i) == pat.charAt(len)) {
                len++;
                lps[i] = len;
                i++;
            } else {
                if (len != 0) {
                    len = lps[len - 1];
                } else {
                    lps[i] = 0;
                    i++;
                }
            }
        }
        return lps;
    }
}

说明:

  • computeLPSArray方法用于计算最长前缀后缀数组(LPS),这是KMP算法的关键部分。
  • 在主方法中,我们使用两个指针ij分别遍历字符串s和子串t
  • 当发现字符不匹配时,利用LPS数组回溯j指针,而不是简单地将ij都重置为0。
C语言版本
#include <string.h>
void computeLPSArray(char *pat, int M, int *lps) {
    int len = 0; // length of the previous longest prefix suffix
    int i = 1;
    lps[0] = 0; // lps[0] is always 0
    while (i < M) {
        if (pat[i] == pat[len]) {
            len++;
            lps[i] = len;
            i++;
        } else {
            if (len != 0) {
                len = lps[len - 1];
            } else {
                lps[i] = 0;
                i++;
            }
        }
    }
}
int KMPSearch(char *pat, char *txt) {
    int M = strlen(pat);
    int N = strlen(txt);
    int lps[M];
    computeLPSArray(pat, M, lps);
    int i = 0; // index for txt[]
    int j = 0; // index for pat[]
    while (i < N) {
        if (pat[j] == txt[i]) {
            j++;
            i++;
        }
        if (j == M) {
            return i - j;
            j = lps[j - 1];
        } else if (i < N && pat[j] != txt[i]) {
            if (j != 0)
                j = lps[j - 1];
            else
                i = i + 1;
        }
    }
    return -1;
}
int strStr(char *s, char *t) {
    return KMPSearch(t, s);
}

说明:

  • C语言版本的KMP算法实现与Java版本逻辑相似,但语法和内存管理有所不同。
Python3版本

由于Python的字符串是不可变的,并且Python的标准库已经提供了高效的字符串匹配功能,通常不需要手动实现KMP算法。Python可以使用内置的find()方法或in运算符来查找子串。

class Solution:
    def strStr(self, s: str, t: str) -> int:
        return s.find(t)

说明:

  • find()方法返回子串在字符串中首次出现的索引,如果未找到则返回-1。
Golang版本

下面是Golang版本的KMP算法实现:

package main
import (
  "fmt"
)
func computeLPSArray(pat string, M int) []int {
  lps := make([]int, M)
  len := 0 // length of the previous longest prefix suffix
  i := 1
  lps[0] = 0 // lps[0] is always 0
  for i < M {
    if pat[i] == pat[len] {
      len++
      lps[i] = len
      i++
    } else {
      if len != 0 {
        len = lps[len-1]
      } else {
        lps[i] = 0
        i++
      }
    }
  }
  return lps
}
func KMPSearch(pat string, txt string) int {
  M := len(pat)
  N := len(txt)
  lps := computeLPSArray(pat, M)
  i := 0 // index for txt
  j := 0 // index for pat
  for i < N {
    if pat[j] == txt[i] {
      j++
      i++
    }
    if j == M {
      return i - j
    } else if i < N && pat[j] != txt[i] {
      if j != 0 {
        j = lps[j-1]
      } else {
        i++
      }
    }
  }
  return -1
}
func strStr(s string, t string) int {
  return KMPSearch(t, s)
}
func main() {
  s := "hello world"
  t := "world"
  index := strStr(s, t)
  if index != -1 {
    fmt.Printf("Found substring '%s' at index: %d\n", t, index)
  } else {
    fmt.Printf("Substring '%s' not found\n", t)
  }
}

说明:

  • Golang版本的KMP算法实现与Java和C语言版本类似,主要区别在于语法和类型声明。
  • computeLPSArray函数计算最长前缀后缀数组。
  • KMPSearch函数是KMP算法的核心实现,用于在文本字符串txt中查找模式字符串pat
  • strStr函数是对外暴露的接口,它调用了KMPSearch函数,并返回子串在文本中的索引。
  • main函数是程序的入口点,它演示了如何使用strStr函数来查找子串并打印结果。

复杂度分析

对于朴素字符串匹配算法(Naive Search),时间复杂度为O((N-M+1)*M),其中N是母串的长度,M是模式串的长度。空间复杂度为O(1),因为它只使用了有限的变量来追踪匹配位置。

对于KMP算法,时间复杂度在最坏情况下是O(N+M),其中N是母串的长度,M是模式串的长度。空间复杂度是O(M),用于存储最长公共前后缀数组(LPS数组)。

对于标准库函数(如Go中的strings.Index),其时间复杂度和空间复杂度通常是由实现决定的,并且经过优化。在大多数情况下,这些函数使用高效算法,并且性能优于手动实现的简单算法。

总结

下面是朴素字符串匹配算法和KMP算法的特点总结表格:

方式 优点 缺点 时间复杂度 空间复杂度
朴素字符串匹配 实现简单 效率低,当模式串与母串不匹配时,需要多次比较 O((N-M+1)*M) O(1)
KMP算法 效率较高,通过预处理减少不必要的比较 实现相对复杂 O(N+M) O(M)
标准库函数(如strings.Index 简洁易用,性能通常经过优化 依赖外部库,可能不是最灵活的选择 取决于实现 取决于实现

相似题目

以下是一些与字符串匹配算法相关的相似题目:

相似题目 难度 链接
LeetCode 28. 实现 strStr() 简单 LeetCode链接
LeetCode 76. 最小覆盖子串 困难 LeetCode链接
LeetCode 438. 找到字符串中所有字母异位词 中等 LeetCode链接
LeetCode 472. 连接词 困难 LeetCode链接
LeetCode 567. 字符串的排列 中等 LeetCode链接

这些题目涉及字符串匹配、子串查找、异位词检测以及字符串排列等概念,可以帮助你进一步巩固和扩展对字符串处理算法的理解。

欢迎一键三连(关注+点赞+收藏),技术的路上一起加油!!!代码改变世界

  • 关于我:阿里非典型程序员一枚 ,记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法(公众号同名),回复暗号,更能获取学习秘籍和书籍等

相关文章
|
3天前
|
存储 算法 Java
leetcode算法题-有效的括号(简单)
【11月更文挑战第5天】本文介绍了 LeetCode 上“有效的括号”这道题的解法。题目要求判断一个只包含括号字符的字符串是否有效。有效字符串需满足左括号必须用相同类型的右括号闭合,并且左括号必须以正确的顺序闭合。解题思路是使用栈数据结构,遍历字符串时将左括号压入栈中,遇到右括号时检查栈顶元素是否匹配。最后根据栈是否为空来判断字符串中的括号是否有效。示例代码包括 Python 和 Java 版本。
|
3天前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
18 2
|
30天前
|
存储 机器学习/深度学习 算法
蓝桥杯练习题(三):Python组之算法训练提高综合五十题
蓝桥杯Python编程练习题的集合,涵盖了从基础到提高的多个算法题目及其解答。
51 3
蓝桥杯练习题(三):Python组之算法训练提高综合五十题
|
15天前
|
机器学习/深度学习 人工智能 算法
【车辆车型识别】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+算法模型
车辆车型识别,使用Python作为主要编程语言,通过收集多种车辆车型图像数据集,然后基于TensorFlow搭建卷积网络算法模型,并对数据集进行训练,最后得到一个识别精度较高的模型文件。再基于Django搭建web网页端操作界面,实现用户上传一张车辆图片识别其类型。
55 0
【车辆车型识别】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+算法模型
|
18天前
|
JavaScript
力扣3333.找到初始输入字符串Ⅱ
【10月更文挑战第9天】力扣3333.找到初始输入字符串Ⅱ
31 1
|
19天前
|
机器学习/深度学习 算法 Java
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
|
25天前
|
算法
每日一道算法题(Leetcode 20)
每日一道算法题(Leetcode 20)
21 2
|
1月前
|
C++
Leetcode第43题(字符串相乘)
本篇介绍了一种用C++实现的字符串表示的非负整数相乘的方法,通过逆向编号字符串,将乘法运算转化为二维数组的累加过程,最后处理进位并转换为字符串结果,解决了两个大数相乘的问题。
24 9
|
27天前
|
机器学习/深度学习 人工智能 算法
【玉米病害识别】Python+卷积神经网络算法+人工智能+深度学习+计算机课设项目+TensorFlow+模型训练
玉米病害识别系统,本系统使用Python作为主要开发语言,通过收集了8种常见的玉米叶部病害图片数据集('矮花叶病', '健康', '灰斑病一般', '灰斑病严重', '锈病一般', '锈病严重', '叶斑病一般', '叶斑病严重'),然后基于TensorFlow搭建卷积神经网络算法模型,通过对数据集进行多轮迭代训练,最后得到一个识别精度较高的模型文件。再使用Django搭建Web网页操作平台,实现用户上传一张玉米病害图片识别其名称。
50 0
【玉米病害识别】Python+卷积神经网络算法+人工智能+深度学习+计算机课设项目+TensorFlow+模型训练
|
2月前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
下一篇
无影云桌面