1、指针相关
344. 反转字符串.
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:[“h”,“e”,“l”,“l”,“o”]
输出:[“o”,“l”,“l”,“e”,“h”]
双指针即可
class Solution:
def reverseString(self, s: List[str]) -> None:
"""
Do not return anything, modify s in-place instead.
"""
len_s = len(s)
j = len_s - 1
for i in range(len_s // 2):
s[i], s[j] = s[j], s[i]
j -= 1
541. 反转字符串 II.
给定一个字符串 s 和一个整数 k,你需要对从字符串开头算起的每隔 2k 个字符的前 k 个字符进行反转。
- 如果剩余字符少于 k 个,则将剩余字符全部反转。
- 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
示例:
输入: s = “abcdefg”, k = 2
输出: “bacdfeg”\
题目要求每隔2k个字符就将前k个字符进行反转,我们可以直接遍历字符串,让i每次移动2k,反转每2k区间的前k个字符
class Solution:
def reverseStr(self, s: str, k: int) -> str:
s = list(s)
def reverse(s):
left, right = 0, len(s) - 1
while left < right:
s[left], s[right] = s[right], s[left]
left += 1
right -= 1
return s
for i in range(0, len(s), 2*k):
s[i:i+k] = reverse(s[i:i+k])
return "".join(s)
这里要注意不要因为要统计2k和前k个字符串,而把代码搞得很复杂;这道题虽然只是按照固定的规律去处理每一段字符串,但也需要考虑如何在for循环里下心思
剑指 Offer 05. 替换空格.
实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例 1:
输入:s = “We are happy.”
输出:“We%20are%20happy.”
在python/java中,字符串是不可变对象,无法原地修改(java可以使用StringBuilder类进行修改),需要新建字符串空间来解决
我们初始化一个列表res用来保存结果,遍历字符串中的每个字符,分两种情况:
- 当c为空格时,在res后添加字符串“%20”
- 当c不为空格时,向res后添加该字符
最后需要将结果列表转化为字符串,这样时间复杂度和空间复杂度都为O(N)
class Solution:
def replaceSpace(self, s: str) -> str:
res = []
for c in s:
if c == ' ':
res.append("%20")
else:
res.append(c)
return "".join(res)
151. 翻转字符串里的单词.
给一个字符串 s ,逐个翻转字符串中的所有 单词 。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回一个翻转 s 中单词顺序并用单个空格相连的字符串。
说明:
- 输入字符串 s 可以在前面、后面或者单词间包含多余的空格。
- 翻转后单词间应当仅用一个空格分隔。 翻
- 转后的字符串中不应包含额外的空格。
示例 1:
输入:s = “the sky is blue”
输出:“blue is sky the”
如果使用语言自带的库函数,如split(拆分)先将字符串按空格分割成字符串数组、reverse(翻转)将字符串数组进行翻转和join(连接)将字符串数组拼接成一个字符串等方法,简单地调用内置API就能完成操作:
class Solution:
def reverseWords(self, s: str) -> str:
return " ".join(reversed(s.split()))
如果不想使用语言自带的API,也可以自己编写对应的函数,对于java/python这种字符串不可变的语言,需要先将字符串转化为其他可变的数据结构,在转化的过程中去掉空格(对于字符串可变的语言,不需要额外开辟空间,可以原地同时完成反转字符串和去除空格的操作)。
这里主要介绍双端队列的做法,双端队列支持从队列头部插入,因此可以沿着字符串处理每个单词,用一个数组来维护每一个单词,将单词压入队列头部,再将队列转成字符串
class Solution:
def reverseWords(self, s: str) -> str:
left, right = 0, len(s) - 1
#去掉字符串开头结尾的空白字符
while left <= right and s[left] == ' ':
left += 1
while left <= right and s[right] == ' ':
right -= 1
d, word = collections.deque(), []
#将单词压入队列头部
while left <= right:
#每遇到一个单独的单词字符串,压入队列头部
if s[left] == ' ' and word:
d.appendleft(''.join(word))
word = []
#维护单词字符串数组
elif s[left] != []:
word.append(s[left])
left += 1
d.appendleft(''.join(word))
return ' '.join(d)
2.KMP算法
实现 strStr().
给两个字符串 haystack 和 needle ,在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
示例 1:
输入:haystack = “hello”, needle = “ll”
输出:2
做这道题之前首先要了解一下KMP算法。
首先概述KMP算法的思想就是:当字符串不匹配时,先记录一部分之前已经匹配的文本内容,利用这些内容避免再去从头开始匹配。
下面对KMP进行讲解:
2.1为什么叫KMP
三个人发明的,取这三个人名字的首字母。
2.2KMP作用
主要应用在字符串匹配。
主要思想上面已经说过,这里面也凸显了KMP的重点,就是如何记录已匹配的文本内容,也就是算法里的next数组。
2.3前缀表
上文提到的next数组就是一个前缀表,前缀表的作用:用于回退,记录了模式串与主串(文本串)不匹配时,模式串应该从何处开始重新匹配。
举个栗子:
在文本串:aabaabaafa 中查找是否出现过模式串:aabaaf
在这里,文本串中的第六个字符’b’和模式串中的第六个字符’f’不匹配,如果暴力匹配,此时就要从头匹配了
但如果使用前缀表,就会从上次已经匹配的内容开始匹配,找到模式中第三个字符’b’继续开始匹配
这里有个问题就是:前缀表如何记录的?
首先要知道前缀表的作用是:当前位置匹配失败时,找到之前已经匹配的位置,再重新匹配。也就是在某个字符不匹配时,告知模式串下一步匹配应该跳到哪个位置
所以前缀表就是:记录下标i及之前的字符串中,有多大长度的前缀后缀。
2.4最长公共前后缀
前缀:不包含最后一个字符的所有以第一个字符开头的连续子串
后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串
“最长公共前后缀”这个词是在网上看到的,但是也不清楚在KMP里具体什么意思,这里理解前缀、后缀的概念就好。
然后需要理解:
字符串a的最长相等前后缀长度为0
字符串aa的最长相等前后缀长度为1
字符串aaa的最长相等前后缀长度为2
…
2.5为什么一定要用前缀表
为什么前缀表可以告知上次匹配的位置,并跳到该位置?
拿2.3中例子,文本匹配的时候在下标5的地方不匹配,模式串指向’f’:
然后找到下标2,指向b,继续匹配:
下标5之前的的字符串(aabaa)的最长相等的前缀和后缀字符串是子字符串aa,由于找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串之后,因此找到与其相同的前缀之后从新匹配即可。
因此前缀表可以告知我们当前位置匹配失败,并跳到之前已经匹配过的地方
2.6如何计算前缀表
对于模式串:aabaaf
模式串a:最长相同前后缀长度为0
模式串aa:最长相同前后缀长度为1
模式串aab:最长相同前后缀长度为0
模式串aaba:最长相同前后缀长度为1
模式串aabaa:最长相同前后缀长度为2
模式串aabaaf:最长相同前后缀长度为0
求得的最长相同前后缀长度就是对应前缀表的元素:
因此,模式串与前缀表对应位置的数字表示:下标i及之前的字符串中,有多大长度的相同前后缀
如何利用前缀表找到字符不匹配时,指针应该移动的位置:
找到不匹配的位置,看它前一个字符的前缀表数值是多少(因为要找前面字符串的最长相同的前后缀),比如前一个字符的前缀表数值是2,把指针移动到下标为2的位置继续匹配,具体可以看动画:gif
2.7 前缀表与next数组
next数组与前缀表的关系:next数组可以是前缀表,也可以把前缀表统一右移一位之后作为next数组
2.7.1 前缀表减一后作为next数组
设n:文本串长度,m:模式串长度,在匹配的过程中,根据前缀表不断调整匹配的位置,匹配的过程是O(n);在这之前还要单独生成next数组,该时间复杂度为O(m);因此KMP算法的时间复杂度为O(n+m)。
如果使用暴力解法,时间复杂度为O(n*m),因此KMP能够极大地提高搜索效率。
以下将haystack作为文本串,needle作为模式串。构造next数组:
定义函数getNext用于构造next数组,函数参数分别为:指向next数组的指针,以及一个字符串:
def getNext(next, s)
构造next数组就是计算模式串s与前缀表的过程:
1.初始化
2.处理前后缀不同的情况
3.处理前后缀相同的情况
以下是详细过程:
1.初始化:
定义两个指针分别指向前缀起始位置、后缀起始位置,同时对next数组继续初始化赋值:
j = -1; #前缀起始位置
next[0] = j;
j初始化为-1是因为这里是将前缀表减一作为next数组的实现方式。
next[i]表示i之前(包括i)最长相等的前后缀,相当于j,因此初始化next[0]=j。
2.处理前后缀不同的情况
j初始化为-1,因此i从1开始,进行s[i]与s[j+1]的比较:
for i in range(1, len(s)):
当s[i]与s[j+1]不相同时,即前后缀末尾不相同,需要向前回退:next[j]记录j之前(包括j)的子串的相同前后缀的长度,因此当s[i]与s[j+1]不相同时,需要找j+1的前一个元素在next数组里的值,即next[j]:
while j >= 0 and s[i] != s[j + 1] : // 前后缀不相同了
j = next[j] // 向前回退
}
3.处理前后缀相同的情况
当s[i]与s[j+1]相同,同时向后移动i和j,将j(前缀长度)赋给next[i],因为next[i]需要记录相同前后缀长度:
if s[i] == s[j + 1]: // 找到相同的前后缀
j++
next[i] = j
整体构造next数组的函数代码如下:
def getNext(next, s):
j = -1
next[0] = j
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 # 将前缀的长度赋给next[i]
构造next数组的动画展示:link.
2.7.2使用next数组进行匹配
在文本串s里查找是否出现过模式串t。
定义下标j指向模式串起始位置,下标i指向文本串起始位置,j初始值为-1,因为next数组里记录的初始位置为-1;i则从0开始遍历文本串:
for i in range(len(s)):
然后是s[i]与t[j+1]进行比较,二者不同时,j从next数组里寻找下一个匹配的位置:
while j >= 0 and s[i] != t[j+1]:
j = next[j]
如果二者相同,则i与j同时向后移动:
if s[i] == t[j+1]:
j += 1
如果j指向了模式串的末尾,说明模式串t完全匹配文本串s中的某个子串。
题目要求在文本串中找出模式串出现的第一个位置,所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,即为文本串中首次出现模式串的位置:
if j == len(t) - 1:
return i - len(t) + 1
使用next数组,用模式串匹配文本串的代码如下:
j = -1
for i in range(len(s)):
while j >= 0 and s[i] != t[j+1]: #不匹配
j = next[j] #找上次匹配的位置
if s[i] == t[j+1]: #匹配
j += 1 #i与j后移
if j == len(t) - 1: #文本串s中出现了模式串t
return i - len(t) - 1
本题最终实现代码:
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
a=len(needle)
b=len(haystack)
if a==0:
return 0
next=self.getnext(a,needle)
p=-1
for j in range(b):
while p>=0 and needle[p+1]!=haystack[j]:
p=next[p]
if needle[p+1]==haystack[j]:
p+=1
if p==a-1:
return j-a+1
return -1
def getnext(self,a,needle):
next=['' for i in range(a)]
k=-1
next[0]=k
for i in range(1,len(needle)):
while (k>-1 and needle[k+1]!=needle[i]):
k=next[k]
if needle[k+1]==needle[i]:
k+=1
next[i]=k
return next
总结:
介绍了KMP算法,分析了next数组即为前缀表(减1),用next数组求出文本串s是否出现过模式串,并实现了具体代码。
459. 重复的子字符串.
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
输入: “abab”
输出: True
解释: 可由子字符串 “ab” 重复两次构成。
解题思路:
从上述内容我们知道,KMP中的next数组记录的就是最长相同前后缀,如果(数组长度 - 最长相等前后缀的长度)可以被数组长度整除,说明该字符串有重复的字符串。
原因:(数组长度 - 最长相等前后缀的长度)的结果相当于一个周期的长度,如果这个周期可以被整除,说明整个数组就是这个周期的循环。
数组长度:len
最长相等前后缀的长度:next[len - 1] + 1
可以打印出next数组,看看数组规律:
图中next[len - 1] = 7, next[len - 1] + 1 = 8即为此时字符串的最长相同前后缀的长度。
(len - (next[len - 1] + 1))= (12 - 8)= 4,4可被12整除说明有重复的字符串(asdf)。
class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
n = len(s)
next = [0] * n
self.getNet(next, s)
if next[n - 1] != -1 and n % (n - (next[-1] + 1)) == 0:
return True
return False
def getNet(self, next, s):
j = -1
next[0] = j
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 # 将前缀的长度赋给next[i]
return next