滑動視窗 — 實戰題解

陣列與字串優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值 更新於 Nov 12, 2026
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。

範圍 — 不屬於任何模板錨定題的滑動視窗 LeetCode 實作題,每題一張題目卡,依它歸約到的主文件模板歸檔;模板、概念與決策表都留在滑動視窗主文件。 另見:sliding_window.md — 每個範例歸約到的七個標準模板,以及在它們之間做選擇的決策表;sliding_window_advanced.md — 較少見的視窗型態(帶重置的恰好 K、單字層級、同時處理多個視窗);hash_map.md — 這些視窗大多帶著的頻率表;2_pointers_examples.md — 相向指標的題目集。

LeetCode 題目清單

總覽

這裡的每一題,都是 sliding_window.md 裡某個模板換掉一個欄位:視窗狀態、合法性判斷,或記錄什麼。每張卡都寫出它是哪個模板、多出哪一步,所以請先讀模板。模板自己的錨定題(LC 643、1004、3、209、76、992、4067)在那邊的卡片裡解,這裡不重複。

索引

模式 本頁題目(由易到難) 核心 ⭐
模板 1 — 固定大小視窗 LC 1151 —
模板 3 — 滿足 P 的最長視窗 LC 424 · LC 1838 LC 424
模板 6 — 用至多相減求恰好 K(它的計數欄位) LC 413 · LC 713 —

下面這些滑動視窗題在別份文件裡解,用的是同一個做法,所以這裡不重複。

LC 題目 放在
438 Find All Anagrams in a String hash_map § 模板 5 — 用字母計數比對的固定視窗
567 Permutation in String 2_pointers § 模板 8 — 同一個視窗,第一次相符就回傳
763 Partition Labels string § 模板 8 — 在最後出現位置貪心切開,不是會收縮的視窗
1004 Max Consecutive Ones III sliding_window § 模板 2 — 這個模板的錨定題

固定大小視窗範例

Minimum Swaps to Group All 1’s Together — LC 1151

關鍵 — 所有的 1 最後會落在某個寬度恰為 ones = sum(data) 的區塊裡,區塊中每個 0 都要花一次交換。所以答案是 ones −(任一寬度為 ones 的視窗中最多的 1 數)。

歸約到:sliding_window § 模板 1 — 視窗寬度由輸入推得(ones),分數是 1 的個數而不是總和。

python
# python
# LC 1151 - Minimum Swaps to Group All 1's Together
# IDEA: fixed window of width ones = sum(data); the window holding the most 1s needs the fewest swaps
# time = O(n), space = O(1)
def minSwaps(data):
    ones = sum(data)
    if ones == 0:
        return 0
    in_window = 0                         # 1s inside the current window
    best = 0
    for right in range(len(data)):
        in_window += data[right]
        if right >= ones:                 # window is one too wide: drop the cell that fell off
            in_window -= data[right - ones]
        if right >= ones - 1:             # window is exactly `ones` wide
            best = max(best, in_window)
    return ones - best

陷阱

  • 全是 0([0, 0, 0]):ones = 0 給出寬度為 0 的視窗,而 data[right - 0] 會把剛加入的格子又移掉;在迴圈前就回傳 0。

相似題 — LC 2134 (環狀陣列上的同一題:在 data + data 上跑視窗)

最長視窗範例

Longest Repeating Character Replacement — LC 424 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

關鍵 — 一個視窗最多換 k 個字元就能變成同一個字母,若且唯若 len − maxFreq ≤ k。收縮時從不調低 maxFreq:過期的值可能讓視窗停在不合法的區段上,但那時視窗只會平移(不會變長),所以 best 只有在真的出現更高的 maxFreq、可能有更長合法視窗時才會變大。

歸約到:sliding_window § 模板 3 — 合法性判斷改成 len − maxFreq ≤ k,而不是「沒有重複」。

python
# python
# LC 424 - Longest Repeating Character Replacement
# IDEA: grow right; while the window needs more than k replacements, shrink left; maxFreq only rises
# time = O(n), space = O(1)   (26 uppercase letters)
def characterReplacement(s, k):
    count = {}
    left = 0
    max_freq = 0                          # highest count of one letter seen in any window so far
    best = 0
    for right in range(len(s)):
        ch = s[right]
        count[ch] = count.get(ch, 0) + 1
        max_freq = max(max_freq, count[ch])
        while (right - left + 1) - max_freq > k:   # more than k letters to replace
            count[s[left]] -= 1
            left += 1
        best = max(best, right - left + 1)
    return best
java
// java
// LC 424 - Longest Repeating Character Replacement
// IDEA: grow right; while the window needs more than k replacements, shrink left; maxFreq only rises
// time = O(n), space = O(1)
public int characterReplacement(String s, int k) {
    int[] count = new int[26];
    int left = 0;
    int maxFreq = 0;
    int best = 0;
    for (int right = 0; right < s.length(); right++) {
        int ch = s.charAt(right) - 'A';
        count[ch]++;
        maxFreq = Math.max(maxFreq, count[ch]);
        while ((right - left + 1) - maxFreq > k) {   // more than k replacements needed
            count[s.charAt(left) - 'A']--;
            left++;
        }
        best = Math.max(best, right - left + 1);
    }
    return best;
}

第二種做法 — 每個字母一個視窗。 對每個不同的字母各跑一次模板 3,問「這個視窗能不能全變成 letter?」。它是 O(26·n) 而不是 O(n),但沒有過期的 maxFreq 要解釋,所以在壓力下比較容易說明它是對的。

python
# python
# LC 424 - Longest Repeating Character Replacement (one window per letter)
# IDEA: for each letter, the longest window whose non-letter count is <= k
# time = O(26 * n), space = O(26)
def characterReplacement_per_letter(s, k):
    best = 0
    for letter in set(s):
        left = 0
        matches = 0                       # copies of `letter` inside the window
        for right in range(len(s)):
            if s[right] == letter:
                matches += 1
            while (right - left + 1) - matches > k:
                if s[left] == letter:
                    matches -= 1
                left += 1
            best = max(best, right - left + 1)
    return best

陷阱

  • 在收縮之前就記錄 best,量到的是不合法的視窗:"ABAB", k = 0 會回傳 2 而不是 1。
  • 每一步重算 max(count.values()) 也是對的,但每步要 O(26);收縮時調低 max_freq 沒有任何好處,因為過期的值永遠不會把 best 灌大。

相似題 — LC 1004 (二元字母表:數 0 的個數,取代 len − maxFreq) · LC 2024

Frequency of the Most Frequent Element — LC 1838

關鍵 — 排序之後,一個視窗最便宜的目標值就是它最右邊的值,把整個視窗補到這個值的成本是 nums[right] · len − windowSum。視窗往右變大時這個成本只會增加,所以它是模板 3 的合法性判斷。

歸約到:sliding_window § 模板 3 — 先做 O(n log n) 排序,合法性是 nums[right] · len − windowSum ≤ k。

python
# python
# LC 1838 - Frequency of the Most Frequent Element
# IDEA: sort; the longest window whose cost to level up to nums[right] is <= k
# time = O(n log n), space = O(n)   (the sorted copy; the window itself is O(1))
def maxFrequency(nums, k):
    nums = sorted(nums)
    left = 0
    window_sum = 0
    best = 0
    for right in range(len(nums)):
        window_sum += nums[right]
        # raising every value in [left, right] to nums[right] costs this many increments
        while nums[right] * (right - left + 1) - window_sum > k:
            window_sum -= nums[left]
            left += 1
        best = max(best, right - left + 1)
    return best
java
// java
// LC 1838 - Frequency of the Most Frequent Element
// IDEA: sort; the longest window whose cost to level up to nums[right] is <= k
// time = O(n log n), space = O(1) extra
public int maxFrequency(int[] nums, int k) {
    Arrays.sort(nums);
    int left = 0;
    long windowSum = 0;
    int best = 0;
    for (int right = 0; right < nums.length; right++) {
        windowSum += nums[right];
        // cost to raise every value in the window to nums[right]
        while ((long) nums[right] * (right - left + 1) - windowSum > k) {
            windowSum -= nums[left];
            left++;
        }
        best = Math.max(best, right - left + 1);
    }
    return best;
}

陷阱

  • right 從 1 開始又讓 windowSum = 0,會把 nums[0] 漏在總和外,每個成本都被高估:[1, 2, 4], k = 5 回傳 2 而不是 3。(這裡原本的 Java 版就是這樣。)
  • nums[i] = 10⁵, n = 10⁵ 時 nums[right] * len 會讓 int 溢位:用 long 相乘。

恰好 K 計數範例

Arithmetic Slices — LC 413

關鍵 — 一段結束在 i、有 run + 2 個元素的等差段,恰好包含 run 個結束在 i 的等差切片(長度 3 … run + 2)。所以維護 run,每個延續這段的索引都把它加進答案,公差改變時把它歸 0。

歸約到:sliding_window § 模板 6(它的計數欄位) — 視窗是目前這一段,公差改變時是重置而不是收縮,而且每一步加的是 len − 2 而不是 len,因為一個切片至少要三個元素。

python
# python
# LC 413 - Arithmetic Slices
# IDEA: run = slices ending at i; it grows by 1 while the difference holds and resets when it breaks
# time = O(n), space = O(1)
def numberOfArithmeticSlices(nums):
    total = 0
    run = 0                               # arithmetic slices that end at index i
    for i in range(2, len(nums)):
        if nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]:
            run += 1                      # every slice ending at i-1, extended, plus the new length-3 one
            total += run
        else:
            run = 0                       # the difference changed: no slice crosses i-1..i
    return total

陷阱

  • 少於三個元素([1]、[1, 2]):迴圈一次都不跑,答案是 0。
  • 從每個 i 往回看到公差斷掉為止也是對的,但在一整段長等差([1, 2, …, n])上是 O(n²);run 把這個數往前帶,省掉回看。

相似題 — LC 446 (子序列而非子陣列:在公差的 map 上做 DP,見 dp.md)

Subarray Product Less Than K — LC 713

關鍵 — 值都是正數時,視窗變大乘積只會變大,所以「乘積 < k」本身就是一個至多條件:收縮之後,每個結束在 right、起點落在 [left, right] 的子陣列都合法,共 right − left + 1 個。

歸約到:sliding_window § 模板 6 — 只有 at_most 那一半;條件本來就是「至多」,所以不用相減。

python
# python
# LC 713 - Subarray Product Less Than K
# IDEA: shrink while product >= k; every start in [left, right] gives one valid subarray ending at right
# time = O(n), space = O(1)
def numSubarrayProductLessThanK(nums, k):
    if k <= 1:
        return 0                          # products of positive integers are never < 1
    left = 0
    product = 1
    count = 0
    for right in range(len(nums)):
        product *= nums[right]
        while product >= k:
            product //= nums[left]        # exact: nums[left] divides the product
            left += 1
        count += right - left + 1
    return count
java
// java
// LC 713 - Subarray Product Less Than K
// IDEA: shrink while product >= k; every start in [left, right] gives one valid subarray ending at right
// time = O(n), space = O(1)
public int numSubarrayProductLessThanK(int[] nums, int k) {
    if (k <= 1) {
        return 0;                       // no product of positive ints is < 1
    }
    int left = 0;
    long product = 1;
    int count = 0;
    for (int right = 0; right < nums.length; right++) {
        product *= nums[right];
        while (product >= k) {
            product /= nums[left];
            left++;
        }
        count += right - left + 1;      // subarrays ending at right, starting in [left, right]
    }
    return count;
}

陷阱

  • k ≤ 1:沒有任何視窗合法,少了這個防護 while 會把 left 推過 right、讀到陣列外:[1, 2, 3], k = 0 會丟出 IndexError 而不是回傳 0。
  • 記錄 max(len) 而不是累加 len,回答的是「最長」而不是「有幾個」。

相似題 — LC 992 (恰好 K 個相異:這個計數做兩次) · LC 2302