滑動視窗 — 實戰題解
範圍 — 不屬於任何模板錨定題的滑動視窗 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
# 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
# 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
// 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
# 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
# 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
// 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
# 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
# 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
// 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