给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串。
示例:输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"
说明:如果 S 中不存这样的子串,则返回空字符串 ""。
如果 S 中存在这样的子串,我们保证它是唯一的答案。来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-window-substring
方法1:该方法主要使用的是滑动窗口思想,大概含义:
用left,right表示滑动窗口的左边界和右边界,通过改变i,j来扩展和收缩滑动窗口,可以想象成一个窗口在字符串上游走,当这个窗口包含的元素满足条件,即包含字符串T的所有元素,记录下这个滑动窗口的长度right-left +1,这些长度中的最小值就是要求的结果
具体实现过程如下:
(1)计算目标值t中每个char的个数,作为char的需求清单need,即需要need中k*v个char;
用处:当检索到滑动窗口中存在一个char时,就抵消掉need中对应的char值,即need[char] -1 ;当检索到窗口函数中不存在一个char时,就增加need中对应的char值,即need[char] +1 ;
(2)记录目标值t的长度,needCnt,即总共需要的字符串数,needCnt等于need中value的总和。
用处:取代遍历need,通过总数来判断滑动窗口是否包含了所有的t中char
(3)不断增加指针right
使滑动窗口增大,直到窗口包含了T的所有元素:当窗口包含了所有的need字符(即needCnt=0),意味着可以剔除窗口括进来的不需要字符,这时可以缩小窗口了
(4)不断增加left,使滑动窗口缩小。因为是要求最小字串,所以将不必要的元素排除在外,使长度减小,直到碰到一个必须包含的元素(其实是清单need中必须包含的子串不能小于0,当=0时,意味着需求刚好满足),这个时候不能再扔了。再扔就不满足条件了,记录此时滑动窗口的left和right的值,right-left +1,即是最小子串的长度。并保存最小值。
(5)当right再增加一个位置,滑动窗口就不满足条件的时候,这是就要移动left,是滑动窗口增大,直至窗口包含了所有的need字符(即needCnt=0)。这时从步骤(3)从新来,寻找新的满足条件的滑动窗口,如此反复,直到left超出了字符串S范围。
class Solution:
def minWindow(self, s: str, t: str) -> str:
# 目标字符记录
need=collections.defaultdict(int)
for c in t:
need[c]+=1
# 目标字符长度
needCnt=len(t)
left=0
res=(0,float('inf'))
for r,c in enumerate(s):
if need[c]>0:
# 如果need[c]>0,意味着窗口搜索到目标值
needCnt-=1
# 目标值被搜索到,则need相应value-1,即用以记录搜索到need中字符的个数;
# =0代表搜索到的值和need值相同;<0代表need中同一个字符被多次搜索到;
# >0,代表need中该字符没有全部被遍历出来
need[c]-=1
if needCnt==0: # 滑动窗口包含了所有T元素
# 开始窗口函数内部的搜索
while True: # 移动滑动窗口右边界i,排除多余元素
c=s[left]
if need[c]==0:
# need[c]==0,break 情况,代表着这轮的滑动窗口不符合要求
break
# 窗口中字符和need相互抵消
need[c]+=1
r+=1
if r-left <res[1]-res[0]: #记录结果
res=(left,r)
# 窗口函数内部计算完后,向下一步迭代
# s[left]是目标集中t的字符,这时从窗口函数清除,
# 则意味着窗口函数需要新的字符填充,即need[s[left]]和needCnt需要加一
need[s[left]]+=1 # left增加一个位置,寻找新的满足条件滑动窗口
needCnt+=1
# 新一轮的右边界
r+=1
return '' if res[1]>len(s) else s[res[0]:res[1]+1] #如果res始终没被更新过,代表无满足条件的结果
总结:该类题目主要就是利用left和right俩套指针循环,形成一个可滑动的窗口,将目标值t与滑动窗口进行比较。主要的难点也在如何进行滑动窗口和目标值的比较。我们利用记账簿的形式,将需要的字符记录为字符整数集need,每满足一个目标值就减1.在这个过程中,我们还记录了滑动窗口中的每个字符,这些不需要的字符都记为负值。当他们被移除滑动窗口时,这些字符就记为0.那些从滑动窗口添加进need的char,其中不是目标值的char的value永远都不会为正。
当need中所有的字符value都小于等于0时,即需求被满足了。而维护一个额外的变量needCnt的作用是,来记录所需元素的总数量,当我们碰到一个所需元素c,不仅need[c]的数量减少1,同时needCnt也要减少1,这样我们通过needCnt就可以知道是否满足条件,这样就无需遍历need字典了。
前面也提到过,need记录了遍历到的所有元素,不是目标值的char的value永远都不会为正,而只有need[c]>0时,代表c就是所需元素,当need中所有char的value都是0时,则代表所有的目标值都被满足,且滑动窗口中没有多余的字符。
链接:https://leetcode-cn.com/problems/minimum-window-substring/solution/tong-su-qie-xiang-xi-de-miao-shu-hua-dong-chuang-k/
另参考:
class Solution:
def minWindow(self, s: str, t: str) -> str:
need = collections.defaultdict(int)
for c in t:
need[c] += 1
needCnt = len(t)
i = 0 #记录起始位置
res = (0, float('inf')) #用两个元素,方便之后记录起终点
#三步骤:
#1. 增加右边界使滑窗包含t
for j,c in enumerate(s):
if need[c] >0:
needCnt -= 1
need[c] -= 1 #这行放在外面不可以,看19行 need[c] == 0
#2. 收缩左边界直到无法再去掉元素 !注意,处理的是i
if needCnt == 0:
while True:
c = s[i]
if need[c] == 0: #表示再去掉就不行了(need>0)
break
else:
need[c] += 1
i += 1
if j-i < res[1] - res[0]: #这里是否减一都可以,只要每次都是这样算的就行,反正最后也是输出子串而非长度
res = (i,j)
#3. i多增加一个位置,准备开始下一次循环(注意这步是在 needCnt == 0里面进行的 )
need[s[i]] += 1
needCnt += 1 #由于 移动前i这个位置 一定是所需的字母,因此NeedCnt才需要+1
i += 1
return "" if res[1]>len(s) else s[res[0]: res[1]+1]