算法学习 Day8 字符串2

99 阅读9分钟

151. 反转字符串中的单词

文章讲解

视频讲解

题目:给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

注意: 输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

示例 1:

输入: s = "the sky is blue"
输出: "blue is sky the"

示例 2:

输入: s = "  hello world  "
输出: "world hello"
解释: 反转后的字符串中不能存在前导空格和尾随空格。

解题思路

  1. 去除空格:首先去掉字符串首尾的空格。
  2. 整体反转:将整个字符串反转。
  3. 逐个反转单词:使用双指针找到每个单词的起始和结束位置,逐个反转单词。
  4. 去除多余空格:最后使用 split() 和 join() 方法去掉多余的空格并返回结果。
  • 时间复杂度:O(n),其中 n 是字符串的长度。我们遍历字符串几次,因此时间复杂度为线性。
  • 空间复杂度:O(n),用于存储反转后的字符串。
class Solution:
    def reverseWords(self, s: str) -> str:
        s = s.strip()  # 去掉首尾空格
        size = len(s)
        # 整体反转
        text = self.reverse_range(s, 0, size - 1)
        
        # 双指针找到每个单词的边界,逐个单词反转
        slow = 0
        while slow < size:
            # 找到单词的起始位置
            while slow < size and text[slow] == " ":
                slow += 1
            
            # 找到单词的结束位置
            fast = slow
            while fast < size and text[fast] != " ":
                fast += 1
            
            # 反转当前单词
            text = self.reverse_range(text, slow, fast - 1)
            
            # 移动到下一个单词
            slow = fast
        
        # 去掉多余空格并返回结果
        return ' '.join(text.split())

    def reverse_range(self, text: str, a: int, b: int) -> str:
        ls = list(text)
        if a < 0 or b >= len(ls):
            return text
        left, right = a, b
        while left < right:
            ls[left], ls[right] = ls[right], ls[left]
            left += 1
            right -= 1
        return "".join(ls)

总结

总的来说,这个问题的关键在于利用双指针技巧来反转每个单词,并通过整体反转和去除多余空格来得到最终的结果。

55. 右旋字符串(第八期模拟笔试)

文章讲解

题目:字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。 

例如,对于输入字符串 "abcdefg" 和整数 2,函数应该将其转换为 "fgabcde"。

输入共包含两行,第一行为一个正整数 k,代表右旋转的位数。第二行为字符串 s,代表需要旋转的字符串。

输出共一行,为进行了右旋转操作后的字符串。

输入示例:

2
abcdefg

输出示例:

fgabcde

解题思路

为了实现字符串的右旋转,我们可以采用反转字符串的方式。具体步骤如下:

  1. 整体反转:首先反转整个字符串。
  2. 反转前 k 个字符:接着反转字符串的前 k 个字符。
  3. 反转剩余字符:最后反转从 k 到字符串末尾的字符。
  • 时间复杂度:O(n),其中 n 是字符串的长度,因为我们需要遍历字符串进行反转。
  • 空间复杂度:O(n), 构建了新的数组ls,占用了长度为n的大小
import sys

def reverse_range(text:str, a:int, b:int)->str:
    ls = list(text)
    if a<0 or b >= len(ls):
        return text
    left, right = a, b
    while left<right:
        ls[left], ls[right] = ls[right], ls[left]
        left += 1
        right -= 1
    return "".join(ls)
    
ipts = sys.stdin.readlines()
k = int(ipts[0].strip())
text = ipts[1].strip()
size = len(text)


text = reverse_range(text,0, size-1) # 整体反转 abcdefg->gfedcba
text = reverse_range(text,0, k-1)    # 反转前k个 gfedcba->fgedcba
text = reverse_range(text,k, size-1) # 反转k+1到size-1个 fgedcba->fgabcde
print(text)

总结

本题关键在于利用三次反转的方式来实现字符串的右旋转操作。第一次反转整个字符串,第二次反转前 k 个字符,第三次反转剩余的字符。这样就可以得到最终的结果。

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

文章讲解

视频讲解: 帮你把KMP算法学个通透!B站(理论篇)(opens new window)

帮你把KMP算法学个通透!(求next数组代码篇)(opens new window)

题目:给你两个字符串 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

暴力枚举,遍历原串的每个位置作为起始点,与匹配串进行匹配。如果匹配失败,原串取起始点后一个位置,匹配串回退到0的起点。

  • 时间复杂度:O(n*m) n 为原串的长度,m 为匹配串的长度。其中枚举的复杂度为 O(n−m),构造和比较字符串的复杂度为 O(m)。整体复杂度为 O((n−m)∗m)。
  • 空间复杂度:O(1)
class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        m, n = len(haystack), len(needle)
        for i in range(0, m-n+1):
            a, b = i, 0
            while b<n and haystack[a] == needle[b]:
                a += 1
                b += 1
            if b == n:
                return i
        return -1

解题思路2

使用KMP算法,构建一个前缀数组next,在匹配失败时,不是像方法1一样匹配串直接从j回退到0,而是找到以j-1为结尾的子串的最长相等前后缀的大小next[j-1]作为新的索引进行匹配。

例如:原串:aabaabaaf,匹配串aabaaf,在原串匹配到aabaab时候,末尾的b和匹配串末尾的f匹配失败。

  • 方法1会从原串的a[a]baabaaf和匹配串的[a]abaaf开始重新匹配;
  • 使用KMP方法会查看匹配串的失败位置f前面的子串[aabaa]f,找到最长相等前后缀为aa长度为2;然后从索引为2的aa[b]aaf开始继续与原串的aabaa[b]aaf进行匹配。如图:

  • 时间复杂度:O(n+m)
  • 空间复杂度:O(m) next数组占了匹配串长度m的大小
class Solution:
    def strStr(self, haystack: str, needle: str) -> int: 
        if len(needle) == 0:
            return 0
        j = 0 # 模式串needle的指针
        next_table = self.getNext(needle)
        for i in range(len(haystack)):
            # 处理原串字符和模式串不相同的情况
            while j >0 and haystack[i] != needle[j]:
                j = next_table[j-1]
            # 处理相同情况
            if haystack[i] == needle[j]:
                j += 1
            
            # 处理匹配完的情况
            if j == len(needle):
                return i-len(needle)+1

        return -1

    
    def getNext(self,pattern_str:str)->list:
        # 构建前缀数组next_table[i]表示以i为结尾的子串的最长相等前后缀的长度。例如aabaaf next[4]=2
        # 1 初始化
        size = len(pattern_str)
        j = 0 # j是前缀的末尾 i是后缀的末尾
        next_table = [0] * len(pattern_str)
        for i in range(1,size):
            # 2 处理前后缀末尾不相同情况
            while j>0 and pattern_str[j] != pattern_str[i]:
                j = next_table[j-1]
            # 3 处理相同情况
            if pattern_str[j] == pattern_str[i]:
                j += 1
            # 更新前缀标
            next_table[i] = j

总结

使用KMP方法能高效的进行子串的匹配。要记住next[j]表示以j为结尾的子串的最长相等前后缀长度,匹配失败后j跳到前一个子串对应的next[j-1]位置继续匹配。构建next数组时候有四个步骤:1 初始化next数组,j=0表示前缀子串的末尾,i=1表示后缀子串的末尾 2 处理前后缀末尾不相同的情况 3 处理前后缀末尾相同的情况 4 更新next数组。 这道题建议看视频弄清楚KMP算法,再看代码,否则理解比较难。

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

文章讲解

视频讲解

题目:给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

示例 1:

输入: s = "abab"
输出: true
解释: 可由子串 "ab" 重复两次构成。

示例 2:

输入: s = "aba"
输出: false

示例 3:

输入: s = "abcabcabcabc"
输出: true
解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)

解题思路

使用 KMP 算法。构建 s+s 的 next 数组,然后检查 (s+s)[1:-1] 是否包含 s

设原串s长度为n,由子串长度为n'的子串s'重复n/n'组成。该串有下列性质:

  • 1 n 一定是 n′ 的倍数
  • 2 s′ 一定是 s 的前缀,同样也后缀
  • 3 对于任意的 i∈[n′,n),有s[i]=s[i−n′]

如果原串s有多个重复子串构成,那么拼接成s+s后,去掉首末尾,第一个s的末尾和第二个s的开头还是能拼成一个完整的s。例如s=abcabc;s+s=a[bcabcabcab]c,去掉首和末尾元素的中间部分刚好能找到原串bc[abcabc]ab。

  • 时间复杂度:O(n) KMP复杂度O(m+n),原串长度为2n,匹配串为n,复杂度为3n,即O(n)
  • 空间复杂度:O(n) 空间复杂度为next数组大小,即匹配串长度n
class Solution:
    def repeatedSubstringPattern(self, s: str) -> bool:
        return (s + s)[1:-1].find(s) != -1


class Solution:
    def repeatedSubstringPattern(self, s: str) -> bool:
        result = self.kmp((s+s)[1:-1], s)
        return result != -1 

    def getNext(self,pattern_str):
        # 构建前缀表
        j = 0  # j记录前缀的末尾 
        next_table = [0] * len(pattern_str)
        for i in range(1, len(pattern_str)):
            # 不相同
            while j>0 and pattern_str[j] != pattern_str[i]:
                j = next_table[j-1]

            # 相同
            if pattern_str[i] == pattern_str[j]:
                j += 1

            # 更新next
            next_table[i] = j
        return next_table


    def kmp(self,s,p):
        # s是原串,p是模式串,找到p在s中的位置
        # 初始化i迭代s p迭代p
        if len(p) == 0: return 0
        j = 0
        next_table = self.getNext(p)
        for i in range(len(s)):
            # 不相同
            while (j>0) and (s[i] != p[j]):
                j = next_table[j-1]

            # 相同
            if s[i] == p[j]:
                j += 1

            # 更新答案
            if j == len(p):
                return i - len(p) + 1
        
        return -1

总结

本题考察字符串的重复性质。可以通过将字符串 s 拼接成 s + s,去掉首尾字符后,使用KMP方法检查中间部分是否包含原字符串 s 来判断是否为重复串。