1. 程式人生 > 其它 >演算法:滑動視窗

演算法:滑動視窗

滑動視窗

一、介紹

演算法中的滑動視窗,它類似於網路資料傳輸中用於流量控制的滑動視窗協議以及深度學習的卷積操作中的滑窗。實際上這兩種的滑動視窗在某一個時刻就是固定視窗大小的滑動視窗,隨著一些因素改變視窗大小也會隨著改變。
滑動視窗是一種解決問題的思路和方法,通常用來解決一些連續問題。一般情況下,可以通過題目中關鍵詞判定是否是滑動視窗型別的題目,如:“(連續)子串”、“(連續)子陣列”等等。

二、型別

滑動視窗的型別主要分為兩大類:固定視窗大小可變視窗大小

1.固定視窗

對於固定視窗,根據視窗大小,確定好左右邊界,在固定區間內進行判斷操作。
演算法過程:

  1. l 初始化為 0
  2. 初始化 r,使得 r - l + 1 等於視窗大小
  3. 同時移動 l 和 r
  4. 判斷視窗內的連續元素是否滿足題目限定的條件
    4.1 如果滿足,再判斷是否需要更新最優解,如果需要則更新最優解
    4.2 如果不滿足,則繼續。

但在實際應用中,固定視窗大小k是已知的,因此可以不用設定r,只用設定左邊界l即可,從而進行簡化。
演算法過程:

  1. l 初始化為 0,要進行操作的序列長度為 n,視窗大小為k
  2. 移動 l,從 0 移動到 n-k
  3. 判斷視窗內的連續元素(l->l+k-1)是否滿足題目限定的條件
    3.1 如果滿足,再判斷是否需要更新最優解,如果需要則更新最優解
    3.2 如果不滿足,則繼續。

虛擬碼:

初始化 ans
for 左邊界 in 移動區間
  判定條件計算
  if 滿足條件
    更新答案
返回 ans

2.可變視窗

對於可變視窗,沒有固定的視窗大小,但有需要滿足的限定條件,滿足條件的視窗可以有很多且視窗大小不定,但通常要求滿足需求的最佳視窗。一般的題目中都是求解最大/小的滿足條件的視窗。

因此需要設定左右邊界,右邊界移動來判定視窗中元素是否符合條件,當有滿足的情況產生後,判斷是否為最優視窗從而決定是否移動左邊界。簡言之:右邊界指標不停向右移動,左邊界指標僅僅在視窗滿足條件之後才會移動,起到視窗收縮的效果。

演算法過程:

  1. l 和 r 都初始化為 0
  2. r 指標移動一步
  3. 判斷視窗內的連續元素是否滿足題目限定的條件
    3.1 如果滿足,再判斷是否需要更新最優解,如果需要則更新最優解。並嘗試通過移動 l 指標縮小視窗大小。迴圈執行 3.1
    3.2 如果不滿足,則繼續。

虛擬碼:

左邊界指標 = 0
初始化 ans
for 右邊界指標 in 可移動區間
   更新視窗內資訊
   while 視窗內不符合題意
      視窗資訊變化
      左邊界指標移動
   更新答案
返回 ans

三、例項

固定視窗

1. 題目連結:子陣列最大平均數 I

[題目分析]:由於規定了子陣列的長度為 k,最大平均數也可用子陣列最大和代替。為了找到子陣列的最大元素和,需要對陣列中的每個長度為 k 的子陣列分別計算元素和。對於長度為 n 的陣列,一共有 n-k+1 個長度為 k 的子陣列(k<=n),暴力求解則計算每一個子陣列的和,但通過固定大小的滑動視窗可以很便捷的解決此問題。

[演算法]:左邊界從0移動到n-k,每次移動過程中,左邊界的元素退出視窗,並在視窗右方加入新的元素,從而更新結果。

點選檢視程式碼
class Solution:
    def findMaxAverage(self, nums: List[int], k: int) -> float:
        n=len(nums)
        res=s=sum(nums[0:k])
        
        for i in range(1,n-k+1):
            #當前視窗為 [i...i+k-1]
            #i-1位置的元素退出視窗,i+k-1位置的元素進入視窗
            s=s-nums[i-1]+nums[i+k-1]
            #更新結果
            res=max(res,s)
        return res/k
 

[複雜度分析]

  • 時間複雜度:T(n)=O(n),n 為陣列nums的長度
  • 空間複雜度:S(n)=O(1)
     

2. 題目連結:學生分數的最小差值

[題目分析]:暴力求解思路簡單,即求出 n 個分數中任意 k 個分數中的最大最小值做差,最終選擇最小的。很明顯時間按複雜度很高,我們可以選擇排序+滑動視窗的方法解決問題。問題一:為什麼要排序?因為題中所求是 k 個元素中最大最小值差值的最小值,那麼這 k 個元素必然是連續的,因為連續的值中才會出現最小的差值。問題二:滑動視窗如何使用?將資料排序後,即對每連續的 k 個元素進行判斷,然後掃描所有大小為 k 的視窗,直接找到答案。

[演算法]:先排序,左邊界從0移動到n-k,每次移動過程中,左邊界的元素退出視窗,並在視窗右方加入新的元素,然後更新結果。

點選檢視程式碼
class Solution:
    def minimumDifference(self, nums: List[int], k: int) -> int:
        nums.sort()
        res,n=100000,len(nums)
        for i in range(n-k+1):
            res=min(res,nums[i+k-1]-nums[i])
        return res
 

[複雜度分析]

  • 時間複雜度:T(n)=O(nlogn),n 為陣列nums的長度
    排序的複雜度 O(nlogn),遍歷尋找答案的時間複雜度 O(n),整體為 O(nlogn)。
  • 空間複雜度:S(n)=O(logn),即排序所需的棧空間
     

3. 題目連結:存在重複元素 II

方法1:排序
[演算法]:將元素與其位置構造成二元組元素,再將元組序列按照元組的第一個元素排完序後,相等的元素位於相鄰位置,判斷相等元素的位置是否滿足條件。

點選檢視程式碼
class Solution:
    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
        #構造(元素,位置)的二元組序列
        lis=[(num,idx) for idx,num in enumerate(nums)]
        #以元素大小進行排序
        lis2=sorted(lis,key=lambda x:x[0])
        n=len(lis2)
        for i in range(n):
            #判斷相等的相鄰元素是否滿足位置需求
            if i<n-1 and lis2[i][0]==lis2[i+1][0]:
                if lis2[i+1][1]-lis2[i][1]<=k:
                    return True
        return False
 

[複雜度分析]

  • 時間複雜度:T(n)=O(nlogn),n 為陣列nums的長度
    排序的複雜度 O(nlogn),遍歷尋找答案的時間複雜度 O(n),整體為 O(nlogn)。
  • 空間複雜度:S(n)=O(logn),即排序所需的棧空間
     

方法二:雜湊
[演算法]:從前到後遍歷陣列,記錄距離當前元素最近的相等元素的位置,判斷是否滿足位置需求

點選檢視程式碼
class Solution:
    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
        dic={}
        for idx,num in enumerate(nums):
            if num in dic and idx-dic[num]<=k:
                return True
            #記錄最新的元素位置,在此之前的相等元素位置沒有意義
            dic[num]=idx
        return False
 

[複雜度分析]

  • 時間複雜度:T(n)=O(n),n 為陣列nums的長度。需要遍歷陣列一次,對於每個元素,雜湊表的操作時間都是O(1)。
  • 空間複雜度:S(n)=O(n),需要使用雜湊表記錄每個元素的最大下標,雜湊表中的元素個數不會超過 n。
     

方法三:滑動視窗
[演算法]:維護一個大小為 k 的滑動視窗,掃描每一個視窗。從前向後遍歷每一個元素,當視窗中元素個數小於k時,判斷當前元素是否在視窗中,不存在則加入視窗中;當視窗中元素個數大於k時,彈出視窗最左側的元素(當前元素位置i,彈出i-k-1位置的元素)後再進行相同的判斷。

點選檢視程式碼
class Solution:
    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
        #使用集合記錄視窗中存在的元素 
        s=set()
        for idx,num in enumerate(nums):
            #視窗中元素大於視窗大小k時,彈出視窗最左側元素
            if idx>k:
                s.remove(nums[idx-k-1])
            #判斷當前元素是否已在視窗中
            if num in s:
                return True
            #不存在則加入視窗
            s.add(num)
        return False
 

[複雜度分析]

  • 時間複雜度:T(n)=O(n),n 為陣列nums的長度。需要遍歷陣列一次。
  • 空間複雜度:S(n)=O(k),需要使用集合記錄當前視窗中存在的元素,視窗中元素個數不會超過視窗大小 k。
     

4. 題目連結:可獲得的最大點數

[題目分析]:根據體重拿牌的規則可總結出可拿牌的方式:① 卡牌前 k 張;② 卡牌後 k 張;③ 從牌尾第 n-k 張開始到牌尾最後一張作為起始,依次取牌共取 k 張,不夠的從牌頭依次補齊。更形象理解,將已有牌複製一份,加於牌尾,分別在[0...k]、[n-k+1...n+k]的範圍內連續取 k 張牌。

[演算法]:在實際演算法實現中,不用進行復制加長,可直接使用 mod 實現取牌頭補齊的操作,類似於迴圈連結串列。其餘過程都和固定視窗大小的基本過程一致。

點選檢視程式碼
class Solution:
    def maxScore(self, cardPoints: List[int], k: int) -> int:
        n=len(cardPoints)
        res=total=sum(cardPoints[n-k:])

        for i in range(n-k,n):
            #計算判定條件即點數和
            #滑動視窗每向右移動一格,增加從右側進入視窗的元素值,並減少從左側離開視窗的元素值
            total=total-cardPoints[i%n]+cardPoints[(i+k)%n]
            #更新結果
            res=max(res,total)
        return res
 

[複雜度分析]

  • 時間複雜度:T(n)=O(n),n 為陣列cardPoints的長度。
  • 空間複雜度:S(n)=O(1)。
     

5. 題目連結:重複的DNA序列

[題目分析]:所求子字串長度固定,只用判定子串是否出現過。因此使用雜湊+滑動視窗即可解決問題。

[演算法]:視窗大小為10,掃描所有視窗(此處視窗即為長度為10的子串)。從前向後遍歷,判斷已當前位置元素為起點,長度為10的子串是否出現過。在實現過程中,整體框架仍為固定視窗大小演算法框架。為避免重複問題,在子串出現次數遞增的前提下,只有當出現次數為1時更新結果。

點選檢視程式碼
class Solution:
    def findRepeatedDnaSequences(self, s: str) -> List[str]:
        n=len(s)
        res=[]
        dic=defaultdict(int)
        for i in range(n-9):
            st=s[i:i+10]
            #更新結果
            if dic[st]==1:
                res.append(st)
            dic[st]+=1
        return res
 

[複雜度分析]

  • 時間複雜度:T(n)=O(n*k),n 為字串 s 的長度,k 為子串的長度10。
  • 空間複雜度:S(n)=O(n),長度固定的子串數量不會超過 n 個。
     

[優化]:
很明顯看出,上面的滑動視窗實則就是字串 s 的一個子串,我們是對每個長為 10 的子串都單獨計算的,滑動視窗的意義體現不大。滑動視窗通常在滑動過程中,保留中間部分的資訊,只對視窗最左側以及新進入視窗的元素處理,從而更新判斷條件。

因此,可進行如下優化,使用二進位制數表示序列元素即DNA序列,每個元素使用兩位二進位制數表示。如 00——A,01——C,10——G,11——T。這樣,一個長為 10 的字串就可以用 20 個位元表示,而一個 int 整數有 32 個位元位,足夠容納該字串,因此我們可以將 s 的每個長為 10 的子串(即一個滑動視窗)用一個 int 整數表示(只用低 20 位),從而構成雜湊以便條件判斷。

而之後問題的關鍵就在於,滑動視窗向右移動一位,此時會有一個新的字元進入視窗,以及視窗最左邊的字元離開視窗,這些操作可使用位運算進行操作,從而簡化時間複雜度。對應的位運算,按計算順序表示如下,假設當前滑動視窗對應的整數為x:

  • 滑動視窗右移一位:x = x << 2。因為每一個元素用兩位二進位制數表示,因此視窗表示的數要左移兩位。
  • 一個新字元 c 進入視窗:x = x | rep[c]。rep[c]為字元 c 代表的二進位制數,等號右邊的 x 為右移後的整數。
  • 視窗最左邊字元離開視窗:x = x & ((1 << 2 * L) - 1)。因為視窗只用 int 整數的低 20 位表示,等號右邊的 x 已經是移動後加入新字元的整數了,有效位佔 20 位,只需將其餘位都置為 0 即可,L = 10。
點選檢視程式碼
class Solution:
    def findRepeatedDnaSequences(self, s: str) -> List[str]:
        L = 10
        rep = {'A': 0, 'C': 1, 'G': 2, 'T': 3}

        n = len(s)
        if n <= L:
            return []
        ans = []
        x = 0
        #求出整個序列的整數表示
        for ch in s[:L - 1]:
            x = (x << 2) | rep[ch]
        dic = defaultdict(int)
        for i in range(n - L + 1):
            #視窗移動
            x = ((x << 2) | rep[s[i + L - 1]]) & ((1 << (L * 2)) - 1)
            #更新雜湊(判斷條件)
            dic[x] += 1
            #條件判斷
            if dic[x] == 2:
                ans.append(s[i : i + L])
        return ans
 

[複雜度分析]

  • 時間複雜度:T(n)=O(n),n 為字串 s 的長度。
  • 空間複雜度:S(n)=O(n),雜湊長度不會超過 n 個。
     

6. 題目連結:找到字串中所有字母異位詞

[題目分析]:異位詞的長度與字串 p 長度相同,且為原字串的連續子串。因此,可以維護一個大小與 p 長度相等的滑動視窗,在滑動過程中維護視窗中字母的數量。

[演算法]:使用固定視窗的演算法框架,用雜湊表維護視窗中字母數量。滑動過程中的判定條件即視窗中每種字母的數量與字串 p 中每種字母的數量是否相等,相等則為異位詞,不等則繼續滑動視窗尋找。在實現過程中,使用兩個雜湊表,一個雜湊表存放字串 p 的數量,另一個雜湊表存放實時視窗中字母的數量。

點選檢視程式碼
class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        #dic為字串p的字母數量雜湊,dic1為視窗實時字母數量雜湊
        dic=collections.Counter(p)
        dic1=dict.fromkeys(dic,0)
        res=[]

        #若字串p的長度大於s,則s中一定不包含異位詞
        #因為異位詞的長度和p相等
        n,m=len(s),len(p)
        if n<m:
            return []
        
        #初始化視窗字母數量的雜湊表
        for i in range(m):
            if dic.get(s[i]):
                dic1[s[i]]+=1
 
        for i in range(n-m+1):
            #判斷視窗中每種字母的數量與字串 p 中每種字母的數量是否相等
            if dic1==dic:
                res.append(i)
            #刪除視窗最左邊的字母,更新視窗字母雜湊
            if dic1.get(s[i])!=None:
                dic1[s[i]]-=1
            #新增視窗右端的字母,更新視窗字母雜湊
            if i+m<n and dic1.get(s[i+m])!=None:
                dic1[s[i+m]]+=1
        return res
 

[複雜度分析]

  • 時間複雜度:T(n)=O(m+(n-m)*c),其中 n 為字串 s 的長度,m 為字串 p 的長度,c 為所有可能的字元數。
    統計字串p字母數量與初始化視窗字母數量的雜湊均為O(m),視窗滑動遍歷為O(n-m),遍歷過程中判斷雜湊相等為O(c)。
  • 空間複雜度:S(n)=O(c)。
     

[優化]:

[演算法]:上面方法在掃描每個視窗過程中,判斷視窗中每種字母的數量與字串 p 中每種字母的數量是否相等依靠判斷兩個雜湊中對應元素個數是否相等,這使得時間複雜度倍數增加。因此,可使用一個變量表示對應字母數量差值總和,在滑動過程中維護它。而判斷條件即當該差值為0時,表示二者字母數量相等。具體維護差值 dif 過程:

  1. 初始化:
  • 統計兩個字串中字母數量,遍歷長度為字串 p 長度 m,字串 s 中的字母次數加一,字串 p 中的字母次數減一。雜湊中最終值為正數、零以及負數三種。正數表示該字母數量多餘,負數表示該字母數量缺少,零表示該字母數量與 p 中數量相等。
  • 統計完後,只要字母雜湊值不為零則 dif 加一。
  1. 滑動過程:
  • 視窗最左邊字母退出視窗,其雜湊值需要減一,但是在減之前,若該字母雜湊值為 1,移動後就變為 0,即該字母由數量多餘變成數量滿足需求,則 dif 減一;若該字母雜湊值為 0,移動後就變為 -1,即該字母由數量滿足需求變成數量缺少,則 dif 加一;其餘數值情況對 dif 沒有影響。
  • 視窗右端字母進入視窗,其雜湊值需要加一,在加之前,若該字母雜湊值為 0,移動後就變為 1,即該字母由數量滿足需求變成數量多餘,則 dif 加一;若該字母雜湊值為 -1,移動後就變為 0,即該字母由數量缺少變成數量滿足需求,則 dif 減一;其餘數值情況對 dif 沒有影響。
點選檢視程式碼
class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        dic=collections.Counter(p)
        dic=dict.fromkeys(dic,0)
        n,m=len(s),len(p)
        res=[]
        if n<m:
            return res
        #統計字母數量並初始化差值
        for i in range(m):
            if dic.get(s[i])!=None:
                dic[s[i]]+=1
            if dic.get(p[i])!=None:
                dic[p[i]]-=1
        dif=[x!=0 for x in dic.values()].count(True)
    
        for i in range(n-m+1):
            #判斷視窗中每種字母的數量與字串 p 中每種字母的數量是否相等
            if dif==0:
                res.append(i)
            #視窗最左邊字母退出視窗,更新雜湊與差值
            if dic.get(s[i])!=None:
                if dic[s[i]]==1:
                    dif-=1
                elif dic[s[i]]==0:
                    dif+=1
                dic[s[i]]-=1
            #新字母進入視窗,更新雜湊與差值
            if i+m<n and dic.get(s[i+m])!=None:
                if dic[s[i+m]]==-1:
                    dif-=1
                elif dic[s[i+m]]==0:
                    dif+=1               
                dic[s[i+m]]+=1               
        return res
 

[複雜度分析]

  • 時間複雜度:T(n)=O(m+n+c),其中 n 為字串 s 的長度,m 為字串 p 的長度,c 為所有可能的字元數。
    統計字串 p 字母數量為 O(m),統計字母數量構造雜湊為 O(m),初始化差值為 O(c),視窗滑動遍歷為O(n-m),遍歷過程中判斷條件為O(1)。
  • 空間複雜度:S(n)=O(c)。
     

可變視窗

1. 題目連結:最長和諧子序列

[題目分析]:和諧子序列本質:由若干個元素構成,但一共只有兩種元素,且這兩種元素大小相差一

方法一:排序+滑動視窗

[演算法]:因為最終結果只需求序列的長度,可以改變元素順序。因此可以先對原序列排序,排序後相鄰元素相差小,從而維護一個大小不定的視窗,左邊界不變,右邊界移動,但要保證視窗中的元素只有兩種,其差值為1,只要當不滿足此條件,則右邊界不變,移動左邊界直到重新滿足條件。對於滿足條件的子序列更新其長度。

點選檢視程式碼
class Solution:
    def findLHS(self, nums: List[int]) -> int:
        nums.sort()
        l,n=0,len(nums)
        res=0
        for r in range(n):
            #當不滿足“和諧”條件後移動左邊界指標
            while nums[r]>nums[l]+1:
                l+=1
            #更新結果
            res=max(res,r-l+1) if nums[r]>nums[l] else res
        return res
 

[複雜度分析]

  • 時間複雜度:T(n)=O(nlogn),n 為nums陣列的長度。
    排序的複雜度 O(nlogn),遍歷尋找答案的時間複雜度 O(n),整體為 O(nlogn)。
  • 空間複雜度:S(n)=O(logn),即排序所需的棧空間。
     

方法二:雜湊

[演算法]:遍歷每一個元素,求得每一個元素與比其大 1 的元素的個數和,最終最大值為所求。通過雜湊的方法,也可求得最終的和諧子序列且不改變元素順序。

點選檢視程式碼
class Solution:
    def findLHS(self, nums: List[int]) -> int:
        c=collections.Counter(nums)
        res=0
        for num,cnt in c.items():
            if c.get(num+1):
                res=max(res,sum([cnt,c[num+1]]))
        return res
 

[複雜度分析]

  • 時間複雜度:T(n)=O(n),n 為陣列nums的長度。
    遍歷尋找答案的時間複雜度 O(n),雜湊查詢時間複雜度 O(1),整體為 O(n)。
  • 空間複雜度:S(n)=O(n)。雜湊長度不會超過 n。
     

2. 題目連結:無重複字元的最長子串

[題目分析]:使用雜湊+滑動視窗的方法,視窗大小不定,判定條件為視窗中的元素是否存在重複,因此使用可變視窗的演算法框架設計演算法。

[演算法]:左邊界初始化為0,右邊界移動,移動過程中,先更新判定條件,即修改雜湊表內容。修改後判斷條件是否滿足需求,不滿足則移動左邊界,同時修改判定條件直至滿足;若滿足則更新結果。

點選檢視程式碼
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        n=len(s)
        dic=collections.defaultdict(int)
        #初始化左邊界指標為0
        l=res=0
        for r in range(n):
            #更新判定條件
            dic[s[r]]+=1
            #不滿足條件,移動左邊界指標
            while dic[s[r]]>1:
                dic[s[l]]-=1
                l+=1
            #更新結果
            res=max(res,r-l+1)
        return res
 

[複雜度分析]

  • 時間複雜度:T(n)=O(n)。
  • 空間複雜度:S(n)=O(n)。
     

3. 題目連結:替換後的最長重複字元

[題目分析]:題目最終所求最長重複字元即為原字元的連續子串的長度,因此可以採用滑動視窗的方法。視窗大小不定,但視窗中一直存放滿足要求的子串(操作k次變成重複字串)。

[演算法]:根據可變視窗的演算法框架,需確定用什麼表示需要判定的條件以及如何在不滿足需求(判定條件不成立)時修改條件資訊。首先,使用雜湊表儲存每個字元已經出現的次數,每當視窗移動新字元鍵入視窗,則更新雜湊表(次數加 1)。其次,判定條件設定為一個需求量,表示當前視窗所有的字元長度減去當前字元中出現次數最多的字元個數的差值,該差值與可允許操作替換的次數 k 比較,當大於 k 時,說明視窗內字元不滿足條件,則需要縮小視窗,更新雜湊(將退出視窗的字元次數減 1),從而移動左邊界指標,再修改判定條件。最後,更新結果。

點選檢視程式碼
class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
        l,n=0,len(s)
        dic=defaultdict(int)
        res=0
        for r in range(n):
            #修改判定條件need
            dic[s[r]]+=1
            need=r-l+1-max(dic.values())
            #判定條件不滿足需求
            while need>k:
                #移動左邊界指標,更新雜湊
                if l<n and dic[s[l]]:
                    dic[s[l]]-=1
                l+=1
                #修改判定條件
                need=r-l+1-max(dic.values())
            #更新結果
            res=max(res,r-l+1)
        return res
 

[複雜度分析]

  • 時間複雜度:T(n)=O(nc),n 為字串 s 的長度,c 為雜湊的最長長度,即所有大寫字母個數26。
    遍歷尋找答案的時間複雜度 O(n),求當前視窗元素出現次數最大值的時間複雜度是 O(c),整體為 O(n
    c)。
  • 空間複雜度:S(n)=O(c)。
     

[雙指標解法]:

[雙指標]:滑動視窗是一種特殊的雙指標演算法,滑動視窗在滑動過程中,視窗中的元素保持著一定的特性,如上面例項中,視窗內元素的最大值與最小值始終相差一或視窗中的所有元素都只出現過一次等等。而雙指標意義更加廣泛一些,用兩個變數線上性結構上遍歷,根據元素的特性解決問題。

[問題分析]:通過滑動視窗,我們瞭解到,視窗的條件是以字串中的每一個位置作為右端點,然後找到其最遠的左端點的位置,滿足該區間(視窗)內除了出現次數最多的那一類字元之外,剩餘的字元(即非最長重複字元)數量不超過 k 個。在滑動視窗中,當不滿足視窗需求時,要一直移動左端點使得滿足條件。而在雙指標演算法中,當新進入視窗(區間)元素後仍滿足需求,則繼續移動右端點(即視窗變大);當新進入視窗(區間)元素後不滿足需求,左右端點同時移動(即視窗不變),使得區間一直保持滿足條件的最大值。

[演算法]:每次區間右移,更新右移位置的字元出現的次數,然後用它更新重複字元出現次數的歷史最大值,最後使用該最大值計算出區間內非最長重複字元的數量,以此判斷左指標是否需要右移即可。演算法中為什麼 r -l 代表最終的最大區間長度?在上面問題分析中已經說明,在區間移動過程中,區間 r-l 一直保持這滿足條件的最大值,即便 [l...r] 中的元素不滿足條件,但是其長度已經是對歷史最大長度的記錄,與其中間的元素特性沒有關係。

點選檢視程式碼
class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
        n=len(s)
        dic=defaultdict(int)
        l=r=maxn=0
        while r<n:
            dic[s[r]]+=1
            maxn=max(maxn,dic[s[r]])
            while r-l+1-maxn>k:
                if l<n and dic[s[l]]:
                    dic[s[l]]-=1
                l+=1
            r+=1
        return r-l
 

[複雜度分析]

  • 時間複雜度:T(n)=O(n),n 為字串 s 的長度。
  • 空間複雜度:S(n)=O(c),c 為雜湊的最長長度,即所有大寫字母個數26。
     

4. 題目連結:考試的最大困擾度

[題目分析]:該題原理和例項3完全相同,方法也可使用 滑動視窗 和 雙指標。

滑動視窗
class Solution:
    def maxConsecutiveAnswers(self, answerKey: str, k: int) -> int:        
        l,n=0,len(answerKey)
        dic={'T':0,'F':0}
        res=0
        for r in range(n):
            dic[answerKey[r]]+=1
            dif=min(dic.values())
            while dif>k:
                dic[answerKey[l]]-=1
                l+=1
                dif=min(dic.values())
            res=max(res,r-l+1)
        return res
 
雙指標
class Solution:
    def maxConsecutiveAnswers(self, answerKey: str, k: int) -> int:        
        n=len(answerKey)
        l=r=maxn=0
        dic={'T':0,'F':0}

        while r<n:
            dic[answerKey[r]]+=1
            maxn=max(maxn,dic[answerKey[r]])
            while r-l+1-maxn>k:
                dic[answerKey[l]]-=1
                l+=1
            r+=1
        return r-l