代码随想录训练营 Day9打卡 字符串part02
一、 力扣151 . 翻转字符串里的单词
给你一个字符串 s ,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
**注意:**输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
示例 1:
输入:s = “the sky is blue”
输出:“blue is sky the”
示例 2:
输入:s = " hello world "
输出:“world hello”
解释:反转后的字符串中不能存在前导空格和尾随空格。
我们可以把思路整理成以下几个步骤:
- 移除多余的空格: 这一步是为了确保字符串中不含多余的空格,尤其是开头和结尾的空格。这也有助于简化之后的反转操作,因为我们不需要关心空格的错误位置。
- 反转整个字符串: 通过这一步我们将整个字符串的字符顺序倒置,从而也将单词的顺序置于逆序。但此时,单词内部的字符也是逆序的。
- 反转每个单词: 通过遍历反转后的字符串中的每个单词并将其内部字符顺序再次倒置,我们可以恢复每个单词原本的顺序,但保持单词整体的逆序排列。
通过上面的三个步骤,我们能有效地实现题目的要求,即翻转字符串中的单词顺序,同时保持单词内字符的原始顺序。这种方法的空间复杂度主要由于使用了额外的列表来进行单词的存储和反转操作,通常为 O(n),其中 𝑛 是字符串的长度。
版本一 : 删除空白、整体反转、单词反转
这个方法首先移除字符串的前后空白,然后反转整个字符串,最后反转字符串中的每个单词。由于Python中的字符串是不可变类型,我们需要用列表来处理字符串的反转操作。
实现思路:
- 使用.strip()方法去除字符串的前后空白。
- 使用切片[::-1]反转整个字符串。
- 使用.split()方法将反转后的字符串分割成单词列表。
- 对列表中的每个单词再次进行反转,然后使用.join()方法将它们以空格连接成一个新的字符串。
class Solution:
def reverseWords(self, s: str) -> str:
# Step 1: 删除字符串s的前后空白
s = s.strip()
# Step 2: 反转整个字符串s
s = s[::-1]
# Step 3: 拆分字符串为单词列表,并对每个单词进行反转
# 这里使用生成器表达式在遍历的同时进行反转,最后用' '.join()合并成一个字符串
s = ' '.join(word[::-1] for word in s.split())
return s
版本二 : 使用双指针法
这个方法先将字符串拆分成单词列表,然后使用双指针技术在列表中前后交换单词,实现单词的逆序。
实现思路:
- 使用.split()方法将字符串拆分成单词列表。
- 初始化两个指针,一个指向列表的开始,另一个指向列表的末尾。
- 通过循环交换指针指向的单词,直到两指针相遇。
- 使用.join()方法将列表中的单词合并为一个字符串。
class Solution:
def reverseWords(self, s: str) -> str:
# 将字符串拆分为单词列表
words = s.split()
# 使用双指针反转单词列表
left, right = 0, len(words) - 1
while left < right:
words[left], words[right] = words[right], words[left]
left += 1
right -= 1
# 将反转后的单词列表转换回字符串
return " ".join(words)
版本三 : 拆分字符串 + 反转列表
这个方法是版本二的简化版,直接使用列表的切片操作进行反转。
实现思路:
- 使用.split()方法将字符串拆分为单词列表。
- 利用切片[::-1]来反转整个单词列表。
- 使用.join()方法将反转后的单词列表合并成一个新的字符串。
class Solution:
def reverseWords(self, s):
# 拆分字符串为单词列表
words = s.split()
# 使用切片快速反转整个列表
words = words[::-1]
# 将反转后的单词列表转换回字符串
return ' '.join(words)
二、 卡码网 . 55.右旋转字符串
题目描述:
字符串的 右旋转操作 是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。
例如,对于输入字符串 “abcdefg” 和整数 2,函数应该将其转换为 “fgabcde”。
输入描述:
输入共包含两行,第一行为一个正整数 k,代表右旋转的位数。第二行为字符串 s,代表需要旋转的字符串。
输出描述:
输出共一行,为进行了右旋转操作后的字符串。
输入示例:
2
abcdefg
输出示例:
fgabcde
提示信息:
数据范围:
1 <= k < 10000,
1 <= s.length < 10000;
代码实现:
通过将一个字符串分成两部分,并交换这两部分的位置。这种操作通常称为字符串的循环移位。
实现思路:
- 读取输入:首先读取整数 k 和字符串 s。整数 k 表示需要将字符串末尾的 k 个字符移至字符串的开头。
- 字符串切片:使用 Python 的切片功能来分割和重组字符串。s[-k:] 获取字符串 s 的最后 k 个字符,s[:-k] 获取除最后
k 个字符外的所有字符。 - 组合字符串:通过拼接 s[-k:] 和 s[:-k] 来实现字符串的循环移位。
- 输出结果:输出经过移位处理的字符串。
# 获取输入的数字k和字符串s
k = int(input())
s = input()
# 切片操作用于反转字符串的两部分
# 利用Python的切片语法,先取出字符串的最后k个字符和前面的部分
# 字符串不可变性意味着我们实际上是在创建一个新的字符串
s = s[len(s)-k:] + s[:len(s)-k]
print(s)
# 读取第二组输入,目的是为了演示同样的操作
k = int(input())
s = input()
# 通过简化的切片操作实现相同的字符串移位效果
print(s[-k:] + s[:-k])
三、 力扣28 . 实现 strStr()
给你两个字符串 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 。
KMP算法简介
KMP(Knuth-Morris-Pratt)算法是一种改进的字符串匹配算法,它通过避免重复检查已匹配的部分来提高匹配效率。
起源和命名
KMP算法的名称来自于它的三位发明者:Donald Knuth, Vaughan Pratt, 和 James H. Morris。这种算法于1977年被发明,目的是解决字符串匹配问题的效率问题。
KMP算法的用途
KMP算法主要用于字符串搜索,即在一个文本(主串)中搜索一个特定的模式(子串或模式串)。与传统的暴力匹配方法相比,KMP算法减少了回溯的次数,从而提高了效率。
核心思想
KMP算法的核心在于它的前缀表(也称为部分匹配表或next数组)。这个表利用已知的信息减少了搜索的工作量:
- 当在文本串中进行匹配时,如果遇到不匹配的情况,KMP算法使用前缀表确定下一步的匹配位置,而不是简单地从文本串的下一个位置重新开始。
- 前缀表记录了模式串中每个位置前的字符串段中“最长公共前后缀”的长度。这使得在发生不匹配时,算法可以跳过那些已经知道不可能匹配的部分。
举例说明
假设我们在文本串 “aabaabaafa” 中查找模式串 “aabaaf”:
- 初始化匹配: 从文本串的开始和模式串的开始进行匹配。
- 发现不匹配: 当文本串和模式串在某一位置上不匹配时(例如,文本串的 ‘b’ 和模式串的
‘f’),通过查阅前缀表可以知道下一次尝试匹配的最优位置。 - 根据前缀表调整: 前缀表指出,在模式串中,已匹配的部分(“aabaa”)有一个长度为2的公共前后缀
“aa”。因此,下一次匹配可以直接从模式串的第三个字符开始匹配,而不是从头开始。
前缀表的定义
- 前缀: 指的是一个字符串的开始部分直到任意位置的子串,不包括字符串的最后一个字符。
- 后缀: 指的是从字符串的任意位置开始到字符串结束的子串,不包括字符串的第一个字符。
- 前缀表(Next数组): 每个位置i的值表示在模式串中,截止到该位置的子串中,最长公共前后缀的长度。
通过这种方式,KMP算法显著提高了字符串匹配的效率,特别是在模式串和文本串很长或模式重复较多的情况下。这也解释了为什么在面试和实际应用中,KMP算法被广泛提及和使用。
如何计算前缀表
对于子串 “aabaa”,我们需要找到其最长的相同前缀和后缀。我们来详细分析:
前缀:指不包括子串最后一个字符的所有可能的起始部分。对于 “aabaa”,可能的前缀包括:“a”, “aa”, “aab”, “aaba”。
后缀:指不包括子串第一个字符的所有可能的结束部分。对于 “aabaa”,可能的后缀包括:“a”, “aa”, “baa”, “abaa”。
在这些前缀和后缀中,最长的相同前后缀是 “aa”。因此,子串 “aabaa” 的最长相同前后缀的长度为 2。
以此类推,那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:
可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:
版本一:前缀表(减一)
这个版本使用了 KMP 算法的前缀表,前缀表减一的版本。
class Solution:
def getNext(self, next, s):
j = -1
next[0] = j # 初始化前缀表的第一个位置为-1
for i in range(1, len(s)):
while j >= 0 and s[i] != s[j+1]: # 当前后缀不匹配时,向前回溯
j = next[j] # 前缀表回退
if s[i] == s[j+1]: # 当前后缀匹配
j += 1
next[i] = j # 更新前缀表
def strStr(self, haystack: str, needle: str) -> int:
if not needle:
return 0 # 特殊情况,空needle直接返回0
next = [0] * len(needle) # 初始化前缀表
self.getNext(next, needle) # 填充前缀表
j = -1
for i in range(len(haystack)):
while j >= 0 and haystack[i] != needle[j+1]: # 不匹配时,使用前缀表回退
j = next[j]
if haystack[i] == needle[j+1]: # 匹配时,指针移动
j += 1
if j == len(needle) - 1: # 完全匹配,返回起始索引
return i - len(needle) + 1
return -1 # 未找到
版本二:前缀表(不减一)
这个版本使用了常见的 KMP 算法实现,前缀表不减一的版本。
class Solution:
def getNext(self, next: List[int], s: str) -> None:
j = 0
next[0] = 0 # 初始化前缀表的第一个元素为0
for i in range(1, len(s)):
while j > 0 and s[i] != s[j]: # 前后缀不匹配时
j = next[j - 1] # 前缀表回退
if s[i] == s[j]: # 前后缀匹配时
j += 1
next[i] = j # 更新前缀表
def strStr(self, haystack: str, needle: str) -> int:
if len(needle) == 0:
return 0 # 特殊情况,空needle返回0
next = [0] * len(needle) # 初始化前缀表
self.getNext(next, needle) # 填充前缀表
j = 0
for i in range(len(haystack)):
while j > 0 and haystack[i] != needle[j]: # 不匹配时
j = next[j - 1]
if haystack[i] == needle[j]: # 匹配时
j += 1
if j == len(needle): # 完全匹配,返回起始索引
return i - len(needle) + 1
return -1 # 未找到
版本三:暴力法
这个版本直接使用暴力法进行匹配,时间复杂度较高。
class Solution(object):
def strStr(self, haystack, needle):
m, n = len(haystack), len(needle)
for i in range(m - n + 1): # 遍历haystack
if haystack[i:i+n] == needle: # 检查子串是否与needle相等
return i
return -1 # 未找到
版本四:使用 index
利用 Python 内置的 index() 方法,简洁且效率较高,但当 needle 不在 haystack 中时会抛出 ValueError。
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
try:
return haystack.index(needle) # 直接返回索引
except ValueError:
return -1 # 未找到时返回-1
版本五:使用 find
这个版本使用 Python 的 find() 方法,当 needle 不在 haystack 中时返回 -1,代码更加安全。
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
return haystack.find(needle) # 使用find方法查找needle,自动处理未找到的情况
力扣题目链接
题目文章讲解
KMP理论篇视频讲解
KMP代码篇视频讲解
四、 力扣459 . 重复的子字符串
给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
示例 1:
输入: s = “abab”
输出: true
解释: 可由子串 “ab” 重复两次构成。
示例 2:
输入: s = “aba”
输出: false
移动匹配
当一个字符串s:abcabc,内部由重复的子串组成,那么这个字符串的结构一定是这样的 :前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个s,如图:
这种方法的逻辑基础是,如果一个字符串可以由其自身的一个子串重复多次构成,那么通过将字符串与自身拼接(称为 s+s),并在新字符串中查找原始字符串 s 时,如果 𝑠 在除了起始位置以外的任何位置出现,这表明 s 是周期性重复的。
这个方法的步骤可以这样描述:
- 构造新字符串:首先,将字符串 s 与自身拼接,形成新的字符串 s+s。
- 搜索操作:在 s+s 中搜索字符串 s,但是需要忽略掉新字符串的第一个字符和最后一个字符。这是为了避免直接匹配到原始的
𝑠,而是要寻找是否存在除了原始 s 外的其他位置的 s。 - 判断条件:如果 s 在 s+s 中的新位置被找到(即 s 出现在除了第一位置之外的任何位置),则 s 由重复的子串组成。
这个技巧的关键在于它利用了字符串的周期性结构。通过 s+s并刨除首尾字符的方法,确保了只有当 s 真正由一个或多个相同的子串构成时,才会在 s+s 中的中间部分找到 𝑠。
KMP
在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里拿字符串s:abababab 来举例,ab就是最小重复单位,如图所示:
如何找到最小重复子串
步骤一: 因为 这是相等的前缀和后缀,t[0] 与 k[0]相同, t[1] 与 k[1]相同,所以 s[0] 一定和 s[2]相同,s[1] 一定和 s[3]相同,即:s[0]s[1]与s[2]s[3]相同 。
步骤二: 因为在同一个字符串位置,所以 t[2] 与 k[0]相同,t[3] 与 k[1]相同。
步骤三: 因为 这是相等的前缀和后缀,t[2] 与 k[2]相同 ,t[3]与k[3] 相同,所以,s[2]一定和s[4]相同,s[3]一定和s[5]相同,即:s[2]s[3] 与 s[4]s[5]相同。
步骤四: 循环往复。
所以字符串s,s[0]s[1]与s[2]s[3]相同, s[2]s[3] 与 s[4]s[5]相同,s[4]s[5] 与 s[6]s[7] 相同。
正是因为 最长相等前后缀的规则,当一个字符串由重复子串组成的,最长相等前后缀不包含的子串就是最小重复子串。
数组长度 减去 最长相同前后缀的长度 相当于是 第一个周期的长度,也就是 一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
版本一:移动匹配
class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
n = len(s)
if n <= 1: # 如果字符串长度小于等于1,不可能由重复子串组成
return False
ss = s[1:] + s[:-1] # 创建新字符串,移除第一个和最后一个字符并拼接
return ss.find(s) != -1 # 检查原始字符串s是否为新字符串ss的子串
版本二:使用前缀表(减一版本)
实现思路:
使用 KMP 算法的前缀表来找出最长的公共前后缀。
判断整个字符串的长度是否能被 (len(s) - (nxt[-1] + 1)) 整除,这个值表示模式的潜在重复长度。
class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
if len(s) == 0:
return False # 如果字符串为空,则无法形成重复的子串
nxt = [0] * len(s) # 初始化前缀表
self.getNext(nxt, s) # 构建前缀表
# nxt[-1]表示整个字符串的最长相同前后缀长度,如果此长度不为-1,并且
# 字符串长度减去最长相同前后缀的长度后的结果可以整除字符串长度,说明存在重复的子串模式
if nxt[-1] != -1 and len(s) % (len(s) - (nxt[-1] + 1)) == 0:
return True
return False
def getNext(self, nxt, s):
j = -1 # 前缀末尾位置
nxt[0] = -1 # 初始化前缀表的第一个值为-1
for i in range(1, len(s)): # 从第二个字符开始构建前缀表
while j >= 0 and s[i] != s[j+1]: # 当前缀后缀不匹配时
j = nxt[j] # 回退到前一个匹配的位置
if s[i] == s[j+1]: # 当前缀后缀匹配时
j += 1 # 前缀后缀匹配位置增加
nxt[i] = j # 记录当前位置的最长匹配前缀末尾位置
return nxt
版本三:使用前缀表(不减一版本)
实现思路:
类似版本二,但使用不减一的常规前缀表。
检查模式串长度是否能被 (len(s) - nxt[-1]) 整除,这个值代表重复模式的长度。
class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
if len(s) == 0:
return False # 如果字符串为空,则无法形成重复的子串
nxt = [0] * len(s) # 初始化前缀表
self.getNext(nxt, s) # 构建前缀表
# nxt[-1]为最长相同前后缀的长度,如果最长相同前后缀的长度不为0,并且
# 字符串长度减去最长相同前后缀的长度后的结果可以整除字符串长度,说明存在重复的子串模式
if nxt[-1] != 0 and len(s) % (len(s) - nxt[-1]) == 0:
return True
return False
def getNext(self, nxt, s):
j = 0 # 前缀末尾位置
nxt[0] = 0 # 初始化前缀表的第一个值为0
for i in range(1, len(s)): # 从第二个字符开始构建前缀表
while j > 0 and s[i] != s[j]: # 当前缀后缀不匹配时
j = nxt[j - 1] # 回退到前一个匹配的位置
if s[i] == s[j]: # 当前缀后缀匹配时
j += 1 # 前缀后缀匹配位置增加
nxt[i] = j # 记录当前位置的最长匹配前缀长度
return nxt
版本四:暴力法
实现思路:
遍历所有可能的子串长度 i(从1到字符串长度的一半),检查是否为字符串长度的因子。
如果是,通过重复该子串检查是否能重构原字符串。
class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
n = len(s)
if n <= 1: # 如果字符串长度为1或更少,不可能有重复的子串
return False
for i in range(1, n//2 + 1):
if n % i == 0: # 如果i是n的因子
substr = s[:i]
if substr * (n//i) == s: # 检查重复i长度的子串是否构成整个字符串
return True
return False