秋招算法备战第9天 | 28. 实现 strStr()、459.重复的子字符串、字符串总结、双指针回顾

28. 找出字符串中第一个匹配项的下标 - 力扣(Leetcode)

可以调用find解决,代码如下

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

当然,除了使用内置函数,还有其他的方法可以找出字符串在另一个字符串中的第一个匹配项的下标。这些方法主要涉及到了搜索和匹配的算法。以下是几种可能的方法:

  1. 滑动窗口法:这是一个较为直观的方法,通过将 ‘needle’ 的长度作为滑动窗口,从 ‘haystack’ 的左边开始滑动,比较窗口内的字符串和 ‘needle’ 是否相等。
def strStr(haystack: str, needle: str) -> int:
    length = len(needle)
    for i in range(len(haystack) - length + 1):
        if haystack[i:i+length] == needle:
            return i
    return -1
  1. KMP 算法:这是一种经典的字符串匹配算法,其核心思想是利用已知信息,避免从头开始搜索。这种方法相对复杂一些,但是在 ‘needle’ 和 ‘haystack’ 都很长的情况下,效率会更高。
def compute_prefix_function(needle):
    length = len(needle)
    prefix = [0] * length
    j = 0
    for i in range(1, length):
        while j > 0 and needle[j] != needle[i]:
            j = prefix[j - 1]
        if needle[j] == needle[i]:
            j += 1
        prefix[i] = j
    return prefix

def strStr(haystack: str, needle: str) -> int:
    if not needle:
        return 0
    prefix = compute_prefix_function(needle)
    j = 0
    for i in range(len(haystack)):
        while j > 0 and haystack[i] != needle[j]:
            j = prefix[j - 1]
        if haystack[i] == needle[j]:
            j += 1
        if j == len(needle):
            return i - (j - 1)
    return -1

这两种方法在大多数情况下都可以很好地解决问题。对于较小的字符串,滑动窗口法就足够了。而对于较大的字符串,KMP算法可以提供更高的效率。

GPT4对KMP算法的解释

KMP(Knuth-Morris-Pratt)算法是一种用于字符串匹配的算法,主要目的是在一个字符串中找到一个子串的出现位置。其核心在于当子串与目标字符串不匹配时,利用已经获取的信息,跳过那些肯定不会匹配的位置。

假设我们正在用子串 needle 在字符串 haystack 中进行匹配,并且我们发现子串在索引 ij 之间的字符与 haystack 中相应位置的字符不匹配,通常的做法可能会在 haystack 中的 i+1 位置重新开始匹配。但是,KMP 算法将利用子串中已经比较过的信息,跳过那些肯定不会产生匹配的位置。

为了实现这个优化,KMP 算法在搜索开始前,会先生成一个前缀函数或部分匹配表。这个表将给出子串中每个前缀的最长的可能的后缀长度,但是这个后缀不能等于该前缀。该表将在字符串匹配过程中用于确定下一次比较应该在哪里开始。

举个例子,如果我们有一个子串 “ABCDABD”,对应的部分匹配表如下:

子串 A B C D A B D
部分匹配值 0 0 0 0 1 2 0

这个表表示,当子串与主串不匹配时,子串应该向右移动的距离。例如,如果最后一个 D 不匹配,我们知道前两个 AB 已经匹配,因此可以将子串向右移动两位,从第二个 A 开始比较。

这就是 KMP 算法的基本思路。具体的实现可能因语言和具体情况而异,但主要的优化思想都是减少不必要的比较。

459. 重复的子字符串 - 力扣(Leetcode)

可以通过暴力解法直接解,注意这里对于边界的判断。

class Solution:
    def repeatedSubstringPattern(self, s: str) -> bool:
        n = len(s)
        for i in range(1, n//2+1):
            if n % i == 0:
                if s[:i] * (n//i) == s:
                    return True
        return False

另一个比较巧妙的解决方案是使用字符串的性质。这个解决方案的基本思路是,对于一个字符串,如果它可以由它的子串重复多次构成,那么在去掉头尾字符的字符串中,这个字符串应该会再次出现。

例如,字符串 “abab” 可以由子串 “ab” 重复两次构成。如果我们创建一个新的字符串,它是原始字符串的两倍,然后去掉第一个和最后一个字符,那么原始的字符串应该在这个新字符串中出现不止一次。例如,对于 “abab”,新字符串是 “abababab”,去掉第一个和最后一个字符后得到 “bababa”,原始的字符串 “abab” 在这个新字符串中出现了两次。

所以,我们可以使用这个性质来解决这个问题。下面是 Python 代码:

def repeated_substring_pattern(s: str) -> bool:
    return s in (s + s)[1:-1]

这个函数的工作原理是首先创建一个新字符串 s + s,然后通过 [1:-1] 去掉第一个和最后一个字符,然后检查原始的字符串 s 是否在这个新字符串中。如果在,则返回 True,否则返回 False

这个解决方案的时间复杂度是 O(n),其中 n 是字符串的长度。因为字符串的连接和子字符串的查找都可以在 O(n) 时间内完成。这个解决方案比前一个解决方案更加高效。

下面是GPT4对此解法原理的解释

我们假设存在一个字符串 s,它可以由它的一个子串 sub 重复多次构成。换句话说,字符串 s 就等于子串 sub 重复 r 次的结果,我们用
s = sub * r 来表示。

现在我们构造一个新的字符串 ss = s + s,它是原始字符串 s 两次重复的结果。那么,ss 实际上就等于子串 sub 重复 2r
次的结果,我们用 ss = sub * 2r 来表示。

然后我们创建一个新的字符串 ss_cut,它是去掉 ss 的头尾字符后的结果。这个新字符串 ss_cut 中间的部分,实际上还是 sub
重复很多次的结果。因为我们只是去掉了头尾的两个字符,对于重复的部分并没有影响。

现在我们来考虑原始的字符串 s 在 ss_cut 中出现的次数。因为 s = sub * r,所以 s 实际上就是子串 sub 重复 r
次的结果。而 ss_cut 中间的部分,是 sub 重复很多次的结果。因此,原始的字符串 s(即子串 sub 重复 r 次的结果)必定会在
ss_cut(即子串 sub 重复很多次的结果)中出现不止一次。这就是我们要检查的条件。

下面是一个例子,帮助你理解这个原理:

假设我们有一个字符串 s = “abcabc”,它可以由子串 “abc” 重复两次构成。我们构造一个新的字符串 ss =
“abcabcabcabc”,它是原始字符串 s 两次重复的结果。然后我们创建一个新的字符串 ss_cut =
“bcabcabcab”,它是去掉 ss 的头尾字符后的结果。在这个新字符串 ss_cut 中,我们可以找到原始字符串 “abcabc”
出现了两次。这就表明原始字符串 “abcabc” 可以由它的子串 “abc” 重复多次构成。

所以,这个原理的基本思想就是:如果一个字符串 s 可以由它的一个子串 sub 重复多次构成,那么在去掉头尾字符的重复串 ss 中,这个字符串
s 应该会再次出现。这就是为什么我们可以通过这个方法来检查一个字符串是否可以由它的一个子串重复多次构成。

字符串:总结篇

Summary
本文总结了字符串的相关知识,包括字符串的定义和存储方式、C/C++中字符串的处理方式、使用库函数的建议、双指针法在字符串操作中的应用,以及字符串的反转和KMP算法的介绍。

Facts
字符串是若干字符组成的有限序列,可以理解为字符数组。
C语言中,在存储字符串时会使用结束符 ‘\0’ 判断字符串是否结束。
C++中提供了string类,可以通过size接口判断字符串是否结束,不需要使用’\0’。
string类和vector在基本操作上没有区别,但是string提供了更多的字符串处理接口。
建议在基础学习阶段不过度依赖库函数,除非对库函数的实现原理和时间复杂度非常清楚。
双指针法在数组、链表和字符串操作中常用,可以高效解决反转、替换和删除等问题。
反转字符串可以通过固定步长的双指针来实现,提高代码的效率。
字符串操作题可以综合运用多种操作,例如先整体反转再局部反转来实现翻转单词。
KMP算法利用已匹配的文本内容避免从头匹配,提高字符串匹配的效率。

双指针总结

Summary
这篇文章总结了双指针法在不同数据结构中的应用。通过使用双指针法,可以在数组、字符串和链表等数据结构中实现高效的操作。

Facts
数组篇: 双指针法可以在O(n)时间复杂度内原地移除数组中的元素,避免使用erase操作的O(n^2)时间复杂度。
字符串篇: 双指针法可用于反转字符串、替换空格以及删除冗余空格等操作,时间复杂度为O(n)。
链表篇: 双指针法可以翻转链表、判断链表是否有环以及找到环的入口,这些操作对于面试来说是常见的问题。
N数之和篇: 双指针法在解决两数之和的问题上也有应用,可以通过双指针法或哈希表来实现不同形式的问题求解。

总结

  1. KMP的两题也有其他比较优美的解法
  2. 字符串和双指针的典型例题需要掌握

附录

代码随想录算法训练营第九天 | 字符串总结、双指针回顾_小蛙先森的博客-CSDN博客

你可能感兴趣的:(算法,python,开发语言)