【算法】剑指 Offer 专项突击版 Day6字符串部分
题目地址:https://leetcode-cn.com/study-plan/lcof/?progress=wgzvtig
目标:要点总结,分享思路
文章目录
【困难】017. 含有所有字符的最短字符串
难度:思考较难,但换成词频统计+滑窗双指针角度就简单了
要点:
- 思路:暴力思考就是在s中的所有子串中寻找包含t的最短子串,而这里包含可以优化为字符串词频统计,如t=abac则t的词频就是a2b1c1,当子串si=aabaxxac时词频为a4b1c1,满足词频要求即包含,对于非t中字符无需统计词频
- 因此,不断在s中移动右指针,使得当子串si的每个词的词频均≥t的词频时表示si已经包含了t
- 此时,我们就要在子串si中寻找包含的最小子串了,即移动左指针,减小si的有效词频,并满足词频≥t的要求
- 如果我没解释清楚,请直接参照下面这个示例即可:
– s = “ADOBECODEBANC”,t = “ABC”
– 初始时左右指针l,r=0,0代表子串si=“A”不满足词频要求
– 移动右指针直到r=5代表子串si=“ADOBEC”满足词频要求了
– 此时移动左指针缩小子串继续寻找,l=1代表子串si=“DOBEC”不满足词频要求接着移动右指针r,直到r=10代表子串si=“DOBECODEBA”满足词频要求
– 移动左指针l=3代表子串si=“BECODEBA”满足词频,可以继续移动左指针l=5代表子串si=“CODEBA”满足词频,继续移动l=6代表子串si=“ODEBA”不满足词频
– 移动右指针到r=12代表子串si=“ODEBANC”满足词频,移动左指针缩小
– 最终得到BANC最短包含子串 - 总结:笔者承认解释的还是不够清楚,但只要记住双指针,移动右指针代表扩大窗口直到满足词频包含要求,求出解,此时改为移动左指针代表缩小窗口,求出更优解
代码如下:
from collections import defaultdict
class Solution:
def minWindow(self, s: str, t: str) -> str:
ans = ""
if len(s)<len(t):return ans
wordDic = defaultdict(int)
for ct in t:wordDic[ct]+=1
l,r = 0,0
valid_alpha = len(wordDic)
while r<len(s):
c = s[r]
if c in wordDic:
wordDic[c]-=1
if wordDic[c]==0:valid_alpha-=1
# 当valid_alpha=0时代表此时s已经包含了t,记录答案并移动左指针缩小
while valid_alpha==0 and l<=r:
if len(ans)>(r-l+1) or not ans:
ans = s[l:r+1]
cl = s[l]
if cl in wordDic:
wordDic[cl]+=1
if wordDic[cl]>0:valid_alpha+=1
l+=1
r+=1
return ans
【简单】018. 有效的回文
难度:简单,回文字符串的判断方法有很多种,这里提供思考和实现简单的方法:双端队列法
要点:
- 思路:将字符串s装换成双端队列queue,若队头队尾的字符相同则分别出队,若不同则不是回文字符串
- 这里只要注意题目细节即可,仅考虑数字和字母,且不区分大小写,故需要一定的字符串清理
- 当然,也有偷鸡方法,回文判断可以通过颠倒字符串判断即s==s[::-1]
代码如下:
class Solution:
def clean(self,s:str):
s = s.lower()
cleaned_s = []
for i in s:
if i.isdigit() or i.isalpha():
cleaned_s.append(i)
return "".join(cleaned_s)
def isPalindrome(self, s: str) -> bool:
s = self.clean(s)
# # 小技巧:字符串颠倒后仍相同则回文,这样一行代码就解决了
# return s==s[::-1]
# 双端队列
queue = list(s)
while len(queue)>1:
if queue[0]!=queue[-1]:
return False
del queue[0]
del queue[-1]
return True
【简单】019. 最多删除一个字符得到回文
难度:不算简单,但好理解,与上一题思考一样。
要点:
- 思路:删除字符得回文,即当判断字符串不回文时,去头或去尾再多两次判断就好,此时自然想到递归
- 其实这里还是可以用双端队列写的,但会超时。因而改为同理的双指针法实现,本质一样。
- 注意改成双指针写法时,边界要考虑两边,很可能仅注意左边界忽略了右边界
代码如下:
class Solution:
def __init__(self):
self.flag = 1
"""
def validPalindrome(self, s: str) -> bool:
# 双端队列写法,超时
queue = list(s)
while len(queue)>1:
if queue[0]!=queue[-1] and self.flag:
self.flag = 0
return self.validPalindrome(queue[1:]) or self.validPalindrome(queue[:-1])
elif queue[0]==queue[-1]:
del queue[0]
del queue[-1]
else:
return False
return True
"""
def validPalindrome(self, s: str) -> bool:
# 双指针写法
left,right = 0,len(s)-1
while left<right:
if s[left]==s[right]:
left+=1
right-=1
elif s[left]!=s[right] and self.flag:
self.flag = 0
# 这里要注意递归传入字符串s的左右指针
# 很可能仅注意左边界忽略了右边界
return self.validPalindrome(s[left+1:right+1]) or self.validPalindrome(s[left:right])
else:
return False
return True
【中等】020. 回文子字符串的个数
难度:个人感受较难,自己做的时候能想到回文中心延伸和动态规划,但写不出来。
要点:
-
思路:
-
(法1)直观想法暴力遍历s的所有子串,并判断统计子串是否回文,遍历需要 O ( n 2 ) O(n^2) O(n2),判断回文需要 O ( n ) O(n) O(n),总体时间复杂度 O ( n 3 ) O(n^3) O(n3)
优化:遍历子串必不可少,判断回文可以用字符串颠倒判断优化为 O ( 1 ) O(1) O(1),时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( n ) O(n) O(n) -
(法2)回文中心拓展法,确定一个字符为回文中心,同时向左向右扩展
比如奇数长度s=bab,当回文中心为b时无法向左扩展,当回文中心为a时可以向左右扩展一次得到bab,当回文中心为b无法向右扩展
比如偶数长度s=baab,当回文中心为b时无法向左扩展,当回文中心为a时可以向左右扩展一次得到baa,当回文中心为a时可以向左右扩展一次得到aab,当回文中心为b时无法向右扩展
但这样会错过aa子串,因而增加可以确定一组回文中心即,start=i,end=i+1这样aa子串也包含进去了 -
Manacher马拉车算法巧妙但需要理解,请自行学习:链接在此
代码如下:
class Solution:
def countSubstrings(self, s: str) -> int:
# 暴力优化法
count = 0
n = len(s)
for i in range(n):
s1 = ''
s2 = ''
# 回文串判断就是:字符串颠倒后不变
# 因此优化回文判断O(n)->O(1)
for j in range(i,n):
s1=s1+s[j]
s2=s[j]+s2
if s1==s2:count+=1
return count
def countSubstrings(self, s: str) -> int:
# 中心扩展法
if len(s)<2:return len(s)
def MoveCount(s,start,end):
count = 0
while start>=0 and end<len(s):
if s[start]!=s[end]:
break
count+=1
start-=1
end +=1
return count
ans = 0
for i in range(len(s)):
#
ans += MoveCount(s,start=i,end=i)
ans += MoveCount(s,start=i,end=i+1)
return ans