参考《算法通关手册》字符串篇
字符串有2类共4种表示方法:
字符串是字符的有序序列,可以对其中的字符进行索引。
特殊字符:使用转义符\
,\b
表示回退;\n
表换行(光标移动到下行首;\r
表示回车(光标移动到本行首),\t
表示制表符(4格)
format官方文档、re文档
操作符及查找 | 描述 |
---|---|
x+y | 连接两个字符串x和y |
nx或xn | 复制n次字符串x |
x in s | 如果x是s的子串,返回True,否则返回False |
str.index(‘’) | 子串第一次出现的位置,不存在报错ValueError |
str.rindex(‘’) | 子串最后一次出现的位置,不存在报错ValueError |
str.find(‘’) | 子串第一次出现的位置,不存在返回-1 |
str.rfind(‘’) | 子串最后一次出现的位置,不存在返回-1 |
函数及使用 | 描述 |
---|---|
len(x) | 返回字符串的长度,非空字符串都有长度(换行符、空格和符号长度1) |
str(x) | 将x转换为string类型 |
hex(x)或oct(x) | 将整数x转换成16进制或8进制字符串 |
chr(x) | 将一个(0-255)10或16进制整数(Unicode编码),转换成对应ASCII字符 |
ord(x) | x为字符,返回其对应的Unicode编码(原始值0-65535) |
大小写转换 | 描述 |
---|---|
str.lower()或str.upper() | 全部字符小写或大写,产生新字符串 |
str.swapcase() | 字符串大写改小写,小写改大写,产生新字符串 |
str.capitalize() | 首字符大写,其余小写,产生新字符串 |
str.title() | 每个字符首字符大写,其余小写,产生新字符串 |
字符串对齐 | 描述 |
---|---|
str.center(width,’stp’) | 居中对齐,width指定宽度,stp指定分隔符,默认空格分割。 |
str.ljust(width,’stp’) | 左对齐,width指定宽度,stp指定分隔符,默认空格分割。 |
str.rjust(width,’stp’) | 右对齐,width指定宽度,stp指定分隔符,默认空格分割。 |
str.zfill(width) | 右对齐,左侧用0填充,width指定宽度。 |
以上对齐方式,指定宽度小于实际宽度时返回原字符串
函数及使用 | 描述 |
---|---|
str.split(‘sep’,maxsplit) | 从str左侧开始分割,分隔符为sep,默认空格。第二个参数指定最大分割次数。最大分割后剩余字符串成一个元素 |
str.rsplit(‘sep’,maxsplit) | 同上,从右侧开始分割。直接写’/’或者sep=’/’都可以。最大分割后的元素不同。没有指定最大分割时是一样的 |
str.count(sub) | 返回子串sub在str中的出现的次数 |
str.replace(old,new,num) | 返回字符串str副本,所有old子串被替换为new,num为最大替换次数 |
str.center(width[,fillchar]) | 字符串str根据宽度width居中,fillchar可选填 |
str.strip(chars) | 从str中去掉在其左侧和右侧chars中列出的字符 |
str.join(iter) | 在iter变量除最后元素外每个元素后增加一个str |
str.isidentifier() | 判断字符串是否是合法标识符(汉字也算) |
str.isspace() | 判断字符串是否都是由空白字符组成(回车、换行、水平制表符) |
str.isalpha() | 判断字符串是否都是由字母组成(汉字也算) |
str.isdecimal() | 判断字符串是否都是由十进制数字组成 |
str.isnumeric() | 判断字符串是否都是由数字组成(包括罗马数字) |
str.isalnum() | 判断字符串是否都是由数字和字母组成 |
str.encode(encoding=’gbk’)
(gbk大小写都可以,开头b表二进制)byte.decode((encoding=’gbk’)
(byte为二进制编码),解码格式应该与编码格式一致字符串格式化有两种方式:
整数类型输出格式:
浮点数类型输出格式:
②格式控制
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0125 | 验证回文串 | Python | 字符串、双指针 | 简单 |
0005 | 最长回文子串 | Python | 字符串、动态规划 | 中等 |
0003 | 无重复字符的最长子串 | Python | 字符串、哈希表、双指针、字符串、滑动窗口 | 中等 |
0344 | 反转字符串 | Python | 字符串 | 简单 |
0557 | 反转字符串中的单词 III | Python | 字符串 | 简单 |
0049 | 字母异位词分组 | Python | 字符串、哈希表 | 中等 |
0415 | 字符串相加 | Python | 字符串、大数加法 | 简单 |
0151 | 颠倒字符串中的单词 | Python | 双指针、字符串 | 中等 |
0043 | 字符串相乘 | Python | 数学、字符串、模拟 | 中等 |
0014 | 最长公共前缀 | Python | 字符串 | 简单 |
如果将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 。
class Solution:
def isPalindrome(self, s: str) -> bool:
s = "".join(ch.lower() for ch in s if ch.isalnum())
return s == s[::-1]
作者:LeetCode-Solution
链接:https://leetcode.cn/problems/valid-palindrome/solution/yan-zheng-hui-wen-chuan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
给你一个字符串 s
,找到 s
中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
思路 1:动态规划
初始化一个 n * n
大小的布尔类型数组 dp[][]
,dp[i][j]
表示字符串 s
上 从位置 i
到 j
的子串 s[i...j]
是否是一个回文串,下面进行判断:
当子串只有 1
位或 2
位的时候,如果 s[i] == s[j]
,该子串为回文子串, dp[i][j] = (s[i] == s[j])
。
如果子串大于 2
位,则如果 s[i + 1...j - 1]
是回文串,且 s[i] == s[j]
,则 s[i...j]
也是回文串,dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
。
当判断完 s[i: j]
是否为回文串时,判断并更新最长回文子串的起始位置和最大长度。
class Solution(object):
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
n=len(s)
if n<2:
return s
# 定义dp[i][j]表示从i到j的子串是否是回文串,初始设为False
dp=[[False]*n for _ in range(n)]
max_len=1 # 回文串最大长度
start=0 # 最长回文串起始位置
for j in range(1,n):
for i in range(j):
if s[i]==s[j]:
# 子串长度只有1或2时,一定是回文串
if j-i<3:
dp[i][j]=True
else:
dp[i][j]=dp[i+1][j-1]
if dp[i][j] and j-i+1>max_len:
max_len=j-i+1
start=i
return s[start:start+max_len]
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
class Solution(object):
def lengthOfLongestSubstring(self, s):
"""
:type s: str
:rtype: int
"""
left,right=0,0
se=set()
ans=0
while right<len(s):
if s[right] not in se:
se.add(s[right])
right+=1
ans=max(ans,right-left)
else:
se.remove(s[left])
left+=1
return ans
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
l,r=0,0
ans=0
se=set()
for r in range(len(s)):
while s[r] in se:
se.remove(s[l])
l+=1
se.add(s[r])
ans=max(ans,r-l+1)
return ans
给定两个字符串形式的非负整数 num1
和num2
,计算它们的和并同样以字符串形式返回。
你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。
解题思路:本题不能将字符串直接转为整数之后,在整数之间相加计算。只能对两个大整数模拟「竖式加法」的过程,如下所示:
算法流程:
i
,j
两指针分别指向 num1
,num2
尾部,模拟人工加法;add= tmp // 10
,代表当前位相加是否产生进位;tmp = n1 + n2 + add
,即当前位的结果。但是这其中要除去进位,所以当前位实际结果是 tmp % 10
,将其添加至 ans
头部;(比如计算9+4=13,当前位计算结果是3,但是进位add=1
,累积到高一位的计算中)i
或j
走过数字首部后,给 n1
,n2
赋值为 0
,相当于给 num1
,num2
中长度较短的数字前面填 0,以便后续计算。num1
,num2
后跳出循环,并根据add
值决定是否在头部添加进位 1,最终返回 res 即可。这其中的关键点,就是用add来表示前一位是否对当前位产生了进位,并将这个进位状态不断累加到后续每一位的计算中。
class Solution:
def addStrings(self, num1: str, num2: str) -> str:
ans=""
i,j,add=len(num1)-1,len(num2)-1,0
while i>=0 or j>=0:# 只要有一个数还可以计算
#位数减为负则补0
n1=int(num1[i]) if i>=0 else 0
n2=int(num2[j]) if j>=0 else 0
temp=n1+n2+add # 当前位计算结果
add=temp//10 #判断是否要进一位
ans=str(temp%10)+ans # 当前位写入的结果
i-=1
j-=1
return "1"+ans if add==1 else ans
给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。
注意:不能使用任何内置的 BigInteger 库或直接将输入转换为整数。
输入: num1 = "123", num2 = "456"
输出: "56088"
思路一:先算乘数的每一位相乘结果再相加
num1
),较短的作为乘数(num2
)。num2
的每一位j
,将num2[j]
乘以num1
的每一位,得到结果ans
ans
存在列表ls
中,最后再将ls
的所有中间结果依次相加(借用上一题的相加函数)比如996*32=1992+29980=31872
。
class Solution(object):
def multiply(self, num1, num2):
"""
:type num1: str
:type num2: str
:rtype: str
"""
if num1=='0' or num2=='0':
return '0' # 如果没有这一步,会算出'00000'之类的
# 将nums1设为更长的数
if len(num1)-1<len(num2)-1:
num1,num2=num2,num1
ls=[] # 记录nums2每一位和nums1相乘的结果
count=0 # 记录nums2的位数,每进一位中间结果要乘以10
for j in range(len(num2)-1,-1,-1):
n2=int(num2[j]) # 用nums2的每一位乘以nums1的所有位
add=0
ans=''
for i in range(len(num1)-1,-1,-1):
n1=int(num1[i])
#print(n1,n2)
temp=n1*n2+add
add=temp//10
ans=str(temp%10)+ans
ans=str(add)+ans if add!=0 else ans # 因为是相乘,进位可以是1到8(9*9=81)
ls.append(ans+'0'*count)
count+=1
#print(ls)
res='0'
for i in range(len(ls)):
res=self.addStrings(res,ls[i])
return res
def addStrings(self, num1, num2): # 加法函数
ans=""
i,j,add=len(num1)-1,len(num2)-1,0
while i>=0 or j>=0:# 只要有一个数还可以计算
#位数减为负则补0
n1=int(num1[i]) if i>=0 else 0
n2=int(num2[j]) if j>=0 else 0
temp=n1+n2+add # 当前位计算结果
add=temp//10 #判断是否要进一位
ans=str(temp%10)+ans # 当前位写入的结果
i-=1
j-=1
return "1"+ans if add==1 else ans
思路二:各位相乘后,再相加
长度为 len(num1)
的整数 num1
与长度为 len(num2)
的整数 num2
相乘的结果长度为 len(num1) + len(num2) - 1
或 len(num1) + len(num2)
。所以我们可以使用长度为 len(num1) + len(num2)
的整数数组 nums
来存储两个整数相乘之后的结果。(相当于首位可能补了0,如果最终结果没有进位的话)
整个计算流程的步骤如下:
num1
,取得每一位数字 digit1
。从个位数字由低位到高位开始遍历 num2
,取得每一位数字 digit2
。digit1 * digit2
的结果累积存储到 nums
对应位置 i + j + 1
上。(比如1996*23
时,nums=[0, 2, 21, 45, 39, 18]
)len(num1) + len(num2) - 1
的位置由低位到高位遍历数组 nums
。将每个数位上大于等于 10
的数字进行进位操作,然后对该位置上的数字进行取余操作。0
,则从第 1
个位置开始将答案数组拼接成字符串。如果首位不为 0
,则从第 0
个位置开始将答案数组拼接成字符串。并返回答案字符串。class Solution:
def multiply(self, num1: str, num2: str) -> str:
if num1 == "0" or num2 == "0":
return "0"
len1, len2 = len(num1), len(num2)
nums = [0 for _ in range(len1 + len2)] # 首位补0
for i in range(len1 - 1, -1, -1):
digit1 = int(num1[i])
for j in range(len2 - 1, -1, -1):
digit2 = int(num2[j])
# 比如num1的十位*num2的个位,和num2的十位*num1的个位,结果都在十位上,要累加起来
nums[i + j + 1] += digit1 * digit2
# nums是每一位乘法计算之后的结果,比如[0, 2, 21, 45, 39, 18]
for i in range(len1 + len2 - 1, 0, -1): # 从个位遍历到最高位的前一位
nums[i - 1] += nums[i] // 10 # 先计算进位,并累加到下一位
nums[i] %= 10 # 再将当前位结果取余数
if nums[0] == 0:
ans = "".join(str(digit) for digit in nums[1:])
else:
ans = "".join(str(digit) for digit in nums[:])
return ans
编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""
。
思路一:纵向比较
从前往后遍历所有字符串的每一列,比较相同列上的字符是否相同,如果相同则继续对下一列进行比较,如果不相同则当前列不再属于公共前缀
class Solution(object):
def longestCommonPrefix(self, strs):
"""
:type strs: List[str]
:rtype: str
"""
if not strs:
return ""
le=[len(x) for x in strs]
min_le=min(le) # 最短的长度
i=0
while i <min_le:
se=set(strs[0][i]) # 从第一个字符串的第一个字符开始比较
for ch in strs[1:]:
if ch[i] not in se:
return strs[0][:i]
i+=1
return strs[0][:i]
或者是:
class Solution:
def longestCommonPrefix(self, strs: List[str]) -> str:
s = ""
for i in zip(*strs): # 直接取出所有字符串的第i列
if len(set(i)) == 1:
s += i[0]
else:
break
return s
参考《算法通关手册》单模式串匹配篇
Brute Force 算法:简称为 BF 算法。中文意思是暴力匹配算法,也可以叫做朴素匹配算法。
BF 算法思想:对于给定文本串 T
与模式串 p
,从文本串的第一个字符开始与模式串 p
的第一个字符进行比较,如果相等,则继续逐个比较后续字符,否则从文本串 T
的第二个字符起重新和模式串 p
进行比较。依次类推,直到模式串 p
中每个字符依次与文本串 T
的一个连续子串相等,则模式匹配成功。否则模式匹配失败。
Brute Force 算法步骤
T
与模式串 p
,求出文本串 T
的长度为 n
,模式串 p
的长度为 m
。T
和模式串 p
,先将 T[0]
与 p[0]
进行比较。
T[1]
和 p[1]
。以此类推,一直到模式串 p
的末尾 p[m - 1]
为止。T
移动到上次匹配开始位置的下一个字符位置,模式串 p
则回退到开始位置,再依次进行比较。T
或者模式串 p
的时候停止搜索。def bruteForce(T: str, p: str) -> int:
n, m = len(T), len(p)
i, j = 0, 0 # i 表示文本串 T 的当前位置,j 表示模式串 p 的当前位置
while i < n and j < m: # i 或 j 其中一个到达尾部时停止搜索
if T[i] == p[j]: # 如果相等,则继续进行下一个字符匹配
i += 1 # i和j同步右移
j += 1
else:
i = i - (j - 1) # 如果匹配失败则将 i 移动到上次匹配开始位置的下一个位置,
j = 0 # 匹配失败 j 回退到模式串开始位置
if j == m:
return i - j # 匹配成功,返回匹配的开始位置
else:
return -1 # 匹配失败,返回 -1
BF 算法非常简单,容易理解,但其效率很低。主要是因为在匹配过程中可能会出现回溯:当遇到一对字符不同时,模式串 p
直接回到开始位置,文本串也回到匹配开始位置的下一个位置,再重新开始比较。
最坏时间复杂度为 O ( m × n ) O(m \times n) O(m×n)。在回溯之后,文本串和模式串中一些部分的比较是没有必要的。由于这种操作策略,导致 BF 算法的效率很低。最坏情况是每一趟比较都在模式串的最后遇到了字符不匹配的情况,每轮比较需要进行 m
次字符对比,总共需要进行 n - m + 1
轮比较,总的比较次数为 m * (n - m + 1)
。
最佳时间复杂度是 O ( m ) O(m) O(m)。最理想的情况下(第一次匹配直接匹配成功)。
平均时间复杂度为 O ( n + m ) O(n + m) O(n+m)。在一般情况下,根据等概率原则,平均搜索次数为 ( n + m ) 2 \frac{(n + m)}{2} 2(n+m)。
参考:
- 《算法通关手册:KMP 算法》
- 《如何更好地理解和掌握 KMP 算法?》
- 《【宫水三叶】简单题学 KMP 算法》
KMP 算法:全称叫做 「Knuth Morris Pratt 算法」,是由它的三位发明者 Donald Knuth、James H. Morris、 Vaughan Pratt 的名字来命名的。KMP 算法是他们三人在 1977 年联合发表的。
KMP 算法思想:对于给定文本串 T
与模式串 p
,当发现文本串 T
的某个字符与模式串 p
不匹配的时候,可以利用匹配失败后的信息,尽量减少模式串与文本串的匹配次数,避免文本串位置的回退,以达到快速匹配的目的。
在朴素匹配算法的匹配过程中,我们分别用指针 i
和指针 j
指示文本串 T
和模式串 p
中当前正在对比的字符。当发现文本串 T
的某个字符与模式串 p
不匹配的时候,j
回退到开始位置,i
回退到之前匹配开始位置的下一个位置上(下图的B),继续匹配,直到能够与匹配串对上位置(下图第二个A),如下图所示。
那么有没有哪种算法,可以让 i
不发生回退,一直向右移动呢?
如果我们可以通过每一次的失配而得到一些「信息」,并且这些「信息」可以帮助我们跳过那些不可能匹配成功的位置,那么我们就能大大减少模式串与文本串的匹配次数,从而达到快速匹配的目的。
比如文本串
T[i: i + m]
与模式串p
的失配是下标位置j
上发生的,那么文本串T
从下标位置i
开始连续的j - 1
个字符,一定与模式串p
的前j - 1
个字符一模一样,即:T[i: i + j] == p[0: j]
。例如上图中,失配是在下标
i+5
这个位置发生的,那么失配位置的前5
个字符,一定与模式串p
的前5
个字符一模一样,即:"ABCAB" == "ABCAB"
。
5
个字符中,前 2
位前缀和后 2
位后缀又是相同的,即 "AB" == "AB"
。 所以根据上面的信息,我们可以推出:文本串子串的后 2
位后缀和模式串子串的前 2
位是相同的,即 T[i + 3: i + 5] == p[0: 2]
,而这部分(即下图中的蓝色部分)是之前已经比较过的,不需要再比较了,可以直接跳过。
那么我们就可以将文本串中的 T[i + 5]
对准模式串中的 p[2]
,继续进行对比。这样 i
就不再需要回退了,可以一直向右移动匹配下去。在这个过程中,我们只需要将模式串 j
进行回退操作即可。
实际上,我们会创建一个next数组作为「部分匹配表」,
next[j]
表示的含义是:**记录下标 j 之前(包括 j)的模式串p
中,最长相等前后缀的长度。**下一节会详细说明。
由于模式串数组中,next[4] == 2
,所以不用回退i
,而是将j
移动到下标为2
的位置,让T[i + 5]
直接对准p[2]
,然后继续进行比对。
下图参考《【宫水三叶】简单题学 KMP 算法》
也就是说,匹配失败时,匹配串会检查之前已经匹配成功的部分中里是否存在相同的「前缀」和「后缀」。如果存在,则跳转到「前缀」的下一个位置继续往下匹配:
跳转到下一匹配位置后,尝试匹配,发现两个指针的字符对不上,并且此时匹配串指针前面不存在相同的「前缀」和「后缀」,这时候只能回到匹配串的起始位置重新开始:
KMP 算法就是使用了这样的思路,对模式串 p
进行了预处理,计算出一个 「部分匹配表」(也叫PMT:Partial Match Table)**,用一个数组 next
来记录。然后在每次失配发生时,不回退文本串的指针 i
,而是根据「部分匹配表」中模式串失配位置 j
的前一个位置的值,即 next[j - 1]
的值来决定模式串可以向右移动的位数。
上文提到的「部分匹配表PMT」,也叫做「前缀表」,在 KMP 算法中使用 next
数组存储。next[j]
表示的含义是:记录下标 j 之前(包括 j)的模式串 p
中,最长相等前后缀的长度。 也可以理解为,PMT中的值是 字符串的前缀集合与后缀集合的交集中最长元素的长度。
- 前缀:
- 如果字符串A和B,存在
A=BS
,其中S是任意的非空字符串,那就称B
为A
的前缀。- 例如,”
Harry
”的前缀包括{”H”, ”Ha”, ”Har”, ”Harr”
},我们把所有前缀组成的集合,称为字符串的前缀集合。- 后缀:
- 若有
A=SB
, 其中S是任意的非空字符串,那就称B
为A
的后缀- 例如,”
Potter
”的后缀包括{”otter”, ”tter”, ”ter”, ”er”, ”r”
},然后把所有后缀组成的集合,称为字符串的后缀集合。- 对于字符串”
ababa
”,它的前缀集合为{”a”, ”ab”, ”aba”, ”abab”
},它的后缀集合为{”baba”, ”aba”, ”ba”, ”a”
}, 两个集合的交集为{”a”, ”aba”
},其中最长的元素为”aba
”,长度为3
。- 要注意的是,字符串本身并不是自己的前缀或者后缀。
举个例子来说明一下,以 p = "ABCABCD"
为例。
next[0] = 0
,因为 "A"
中无有相同前缀后缀,最大长度为 0
。next[1] = 0
,因为 "AB"
中无相同前缀后缀,最大长度为 0
。next[2] = 0
,因为 "ABC"
中无相同前缀后缀,最大长度为 0
。next[3] = 1
,因为 "ABCA"
中有相同的前缀后缀 "a"
,最大长度为 1
。next[4] = 2
,因为 "ABCAB"
中有相同的前缀后缀 "AB"
,最大长度为 2
。next[5] = 3
,因为 "ABCABC"
中有相同的前缀后缀 "ABC"
,最大长度为 3
。next[6] = 0
,因为 "ABCABCD"
中无相同前缀后缀,最大长度为 0
。 同理也可以计算出 "ABCABDEF"
的前缀表为 [0, 0, 0, 1, 2, 0, 0, 0]
。"AABAAAB"
的前缀表为 [0, 1, 0, 1, 2, 2, 3]
。"ABCDABD"
的前缀表为 [0, 0, 0, 0, 1, 2, 0]
。
在之前的例子中,当 p[5]
和 T[i + 5]
匹配失败后,根据模式串失配位置 j
的前一个位置的值,即 next[4] = 2
,我们直接让 T[i + 5]
直接对准了 p[2]
,然后继续进行比对,如下图所示。
但是这样移动的原理是什么?
如果文本串 T[i: i + m]
与模式串 p
的失配是在第 j
个下标位置发生的,那么:
文本串 T
从下标位置 i
开始连续的 j
个字符,一定与模式串 p
的前 j
个字符一模一样,即:T[i: i + j] == p[0: j]
(上图中的"ABCAB" == "ABCAB"
)。
而如果模式串 p
的前 j
个字符中,前 k
位前缀和后 k
位后缀相同,("ABCAB"
中有相同的前后缀"AB"
,即k=2
)那么可以断言:文本串中i
指针失配位置之前的 k
位(“AB
”)一定与模式字符串的第0
位至第 k
位是相同的(“AB
”),即长度为 k
的后缀与前缀相同。
这样一来,我们就可以将这些字符段的比较省略掉。具体的做法是,保持i
指针不动,然后将j
指针指向模式字符串的next[j −1]
位即可(表示模式串中,前j-1
个子符里,最长相同前后缀的长度k
)。
其实相当于因为模式串存在相同的前后缀,所以失配后,模式串不用退回起始位置,退到相同前缀的下一位置就行。
其实,求next
数组的过程完全可以看成字符串匹配的过程,即以模式字符串为文本串串,以模式字符串的前缀为目标字符串,一旦字符串匹配成功,那么当前的next
值就是匹配成功的字符串的长度。
具体来说,就是从模式字符串的第一位(注意,不包括第0位)开始对自身进行匹配运算。 在任一位置,能匹配的最长长度就是当前位置的next
值。如下图所示。
数组下标从0开始,所以图中应该是
next[5]=4
,以此类推。下图模式串有next=[0, 0, 1, 2, 3, 4, 0, 1]
这样我们就可以使用KMP本身的匹配原理来计算next数组。
i
和j
来遍历。因为文本串第一个位置即使匹配上 ,也有next[0]=0,而不等于1,所以初始时令j = 0
,i = 1
。p[i] != p[j]
,说明文本串在此位置失配,同上面所讲, i
不动,模式串指针 j
不断回退到 next[j - 1]
位置。
p[i] == p[j]
,说明匹配上了一个字符,令j
右移,此时此时 j
既是前缀下一次进行比较的下标位置,又是当前最长前后缀的长度,所以next[i]=j
。最后移动指针i遍历下一个位置;j=0
,表示文本串在i
位置匹配不到任何一个字符,next[i]=0=j
,i+=1
;p[i] == p[j]
,同样先将 j += 1
,next[i]=j
,i+=1
;next
数组的构造步骤生成「前缀表」next
。i
、j
,其中 i
指向文本串中当前匹配的位置,j
指向模式串中当前匹配的位置。初始时,i = 0
,j = 0
。j = next[j - 1]
,直到 j == 0
时或前缀匹配成功时停止回退。1
位,即 j += 1
。p
在文本串 T
中的开始位置,即 i - j + 1
。1
位,即 i += 1
,然后继续匹配。-1
。# 生成 next 数组
# next[j] 表示下标 j 之前的模式串 p 中,最长相等前后缀的长度
def generateNext(p: str):
m = len(p)
next = [0 for _ in range(m)] # 初始化数组元素全部为 0
left = 0 # left 表示前缀串开始所在的下标位置
for right in range(1, m): # right 表示后缀串开始所在的下标位置
while left > 0 and p[left] != p[right]: # 匹配不成功, left 进行回退, left == 0 时停止回退
left = next[left - 1] # left 进行回退操作
if p[left] == p[right]: # 匹配成功,找到相同的前后缀,先让 left += 1,此时 left 为前缀长度
left += 1
next[right] = left # 记录前缀长度,更新 next[right], 结束本次循环, right += 1
return next
# KMP 匹配算法,T 为文本串,p 为模式串
def kmp(T: str, p: str) -> int:
n, m = len(T), len(p)
next = generateNext(p) # 生成 next 数组
j = 0 # j 为模式串中当前匹配的位置
for i in range(n): # i 为文本串中当前匹配的位置
while j > 0 and T[i] != p[j]: # 如果模式串前缀匹配不成功, 将模式串进行回退, j == 0 时停止回退
j = next[j - 1]
if T[i] == p[j]: # 当前模式串前缀匹配成功,令 j += 1,继续匹配
j += 1
if j == m: # 当前模式串完全匹配成功,返回匹配开始位置
return i - j + 1
return -1 # 匹配失败,返回 -1
p
的长度。i
并没有进行回退,可以看出匹配阶段的时间复杂度是 O ( n ) O(n) O(n),其中 n n n 是文本串 T
的长度。参考资料
- 【博文】从头到尾彻底理解 KMP - 结构之法 算法之道 - CSDN博客
- 【博文】字符串匹配的 KMP 算法 - 阮一峰的网络日志
- 【题解】多图预警详解 KMP 算法 - 实现 strStr() - 力扣
- 【题解】「代码随想录」KMP算法详解 - 实现 strStr() - 力扣
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0028 | 找出字符串中第一个匹配项的下标 | Python | 字符串、双指针 | 简单 |
0459 | 重复的子字符串 | Python | 字符串、字符串匹配 | 简单 |
0686 | 重复叠加字符串匹配 | Python | 字符串、字符串匹配 | 中等 |
1668 | 最大重复子字符串 | |||
0796 | 旋转字符串 | Python | 字符串、字符串匹配 | 简单 |
1408 | 数组中的字符串匹配 | Python | 字符串、字符串匹配 | 简单 |
2156 | 查找给定哈希值的子串 | Python | 字符串、滑动窗口、哈希函数、滚动哈希 | 中等 |
class Solution(object):
def strStr(self, haystack, needle):
"""
:type haystack: str
:type needle: str
:rtype: int
"""
# KMP 匹配算法,haystack 为文本串,needle 为模式串
m,n = len(haystack), len(needle)
next = self.next(needle) # 生成 next 数组
j = 0 # j 为模式串中当前匹配的位置
for i in range(m): # i 为文本串中当前匹配的位置
while j > 0 and haystack[i] != needle[j]: # 如果模式串前缀匹配不成功, 将模式串进行回退, j == 0 时停止回退
j = next[j - 1]
if haystack[i] == needle[j]: # 当前模式串前缀匹配成功,令 j += 1,继续匹配
j += 1
if j == n: # 当前模式串完全匹配成功,返回匹配开始位置
return i - j + 1
return -1
def next(self,p):
# 等同于模式串自己和自己匹配,只不过文本串从1开始(第一位匹配到结果也应该是0不是1),模式串从0开始
next=[0 for _ in range(len(p))]
j=0 # 模式串匹配起始位置,也是匹配到的长度
for i in range(1,len(p)):
#次序反过来,left右移,下一步判定就不对了
#if p[left]==p[right]:
#left+=1
# 先判定不相等就回退,直到相等再右移模式串指针,进行下一步比较
while j>0 and p[j]!=p[i]:
j=next[j-1]
if p[i]==p[j]:
j+=1 # 匹配上了,公共前后缀长度+1
next[i]=j
return next
给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
输入: s = "abcabcabcabc"
输出: true
解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)
class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
return (s+s).find(s,1)!=len(s)
思路二:KMP(参考《算法通关手册》)
我们知道 next[j]
表示的含义是:记录下标 j
之前(包括 j)的模式串 p
中,最长相等前后缀的长度。
而如果整个模式串 p 的最长相等前后缀长度不为 0,即 next[len(p) - 1] != 0
,则说明整个模式串 p 中有最长相同的前后缀。假设 next[len(p) - 1] == k
,则说明 p[0: k] == p[m - k: m]
。比如字符串 “abcabcabc”,最长相同前后缀为 “abcabc” = “abcabc”。
len(p) % (len(p) - next[size - 1]) == 0
是否成立,如果成立,则字符串 s 可由 s[0: len(p) - next[size - 1]]
构成的子串重复构成,返回 True
。否则返回 False
。0
的空串,则剩余的部分其实就是去除后缀部分的剩余部分,上述结论依旧成立。class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
# 创建next数组
le=len(s)
if le==1:
return False
j=0
next=[0 for _ in range(le)]
for i in range(1,le):
while j>0 and s[i]!=s[j]:
j=next[j-1]
if s[i]==s[j]:
j+=1
next[i]=j
# next数组最后一位的值,表示模式串的最长相等前后缀长度。
# 将其除去前后缀重叠部分和剩余的后缀部分,就是剩余前缀部分。
# 这部分就是最小周期长度,可以被模式串长度整除
if next[le-1]!=0 and le%(le-next[le-1])==0:
return True
return False
给定两个字符串 a 和 b,寻找重复叠加字符串 a 的最小次数,使得字符串 b 成为叠加后的字符串 a 的子串,如果不存在则返回 -1。
输入:a = "abcd", b = "cdabcdab"
输出:3
解释:a 重复叠加三遍后为 "abcdabcdabcd", 此时 b 是其子串。
首先,可以分析复制次数的「下界」和「上界」为何值:
class Solution:
def repeatedStringMatch(self, a: str, b: str) -> int:
# 将b看做模式串,a最低重复次数是len([a]*count)=len(b),即至少的是一样长
# 最大重复次数是count+1。
# 将a复制(n+1)次后匹配b,如果匹配下标超过n,则表示无法重复,否则返回count
m,n=len(a),len(b)
count=(n//m)+1 if n%m!=0 else n//m
a=a*(count+1)
idx=a.find(b)
if idx ==-1:
return -1
else:
idx=idx+n-1 # 匹配串的结束位置
return count if idx<m*count else count +1
如果用KMP算法代替find函数写,就是:
class Solution:
def repeatedStringMatch(self, a: str, b: str) -> int:
# 将b看做模式串,a最低重复次数是len([a]*count)=len(b),即至少的是一样长
# 最大重复次数是count+1。
# 将a复制(n+1)次后匹配b,如果匹配下标超过n,则表示无法重复,否则返回count
m,n=len(a),len(b)
count=(n//m)+1 if n%m!=0 else n//m
a=a*(count+1)
idx=self.KMP(a,b)
print(idx)
if idx ==-1:
return -1
else:
return count if idx<m*count else count +1
def KMP(self, t, p):
"""
:type haystack: str
:type needle: str
:rtype: int
"""
# KMP 匹配算法,haystack 为文本串,needle 为模式串
m,n = len(t), len(p)
next = self.next(p) # 生成 next 数组
j = 0 # j 为模式串中当前匹配的位置
for i in range(m): # i 为文本串中当前匹配的位置
while j > 0 and t[i] != p[j]: # 如果模式串前缀匹配不成功, 将模式串进行回退, j == 0 时停止回退
j = next[j - 1]
if t[i] == p[j]: # 当前模式串前缀匹配成功,令 j += 1,继续匹配
j += 1
if j == n: # 当前模式串完全匹配成功,返回匹配结束位置
return i
return -1
def next(self,p):
# 等同于模式串自己和自己匹配看前缀,只不过文本串从1开始(第一位匹配到结果也应该是0不是1),模式串从0开始
next=[0 for _ in range(len(p))]
j=0 # 共同前缀下标
for i in range(1,len(p)):
#次序反过来,left右移,下一步判定就不对了
#if p[left]==p[right]:
#left+=1
# 先判定不相等就回退,直到相等再右移模式串指针,进行下一步比较
while j>0 and p[j]!=p[i]:
j=next[j-1]
if p[i]==p[j]:
j+=1 # 匹配时,公共前后缀长度+1
next[i]=j
#print(next)
return next
给你一个字符串 sequence ,如果字符串 word 连续重复 k 次形成的字符串是 sequence 的一个子字符串,那么单词 word 的 重复值为 k 。单词 word 的 最大重复值 是单词 word 在 sequence 中最大的重复值。如果 word 不是 sequence 的子串,那么重复值 k 为 0 。
给你一个字符串 sequence 和 word ,请你返回 最大重复值 k 。
输入:sequence = "ababc", word = "ba"
输出:1
解释:"ba" 是 "ababc" 的子字符串,但 "baba" 不是 "ababc" 的子字符串。
class Solution(object):
def maxRepeating(self, sequence, word):
"""
:type sequence: str
:type word: str
:rtype: int
"""
ans=0
while (ans*word) in sequence:
ans+=1
return ans -1
class Solution(object):
def maxRepeating(self, sequence, word):
"""
:type sequence: str
:type word: str
:rtype: int
"""
m,n=len(sequence),len(word)
max_count=m//n
ans,count=0,1
while count<=max_count:
temp=word*count
if sequence.find(temp)!=-1:
ans=max(ans,count)
count+=1
else:
break
return ans
输入: s = "abcde", goal = "cdeab"
输出: true
class Solution(object):
def rotateString(self, s, goal):
"""
:type s: str
:type goal: str
:rtype: bool
"""
return goal in s+s if len(s)==len(goal) else False