Sliding Window Maximum (滑動窗口最大值) 🔴 Hard¶
📌 LeetCode #239 — 題目連結 | NeetCode 解說
1. 🧐 Problem Dissection (釐清問題)¶
題目給一個整數陣列 nums,以及一個窗口大小 k。 這個窗口從最左邊移動到最右邊,每次只移動一步。 我們只能看到窗口內的 k 個數字。 請回傳 每一個位置時窗口內的最大值。
- Input:
nums = [1,3,-1,-3,5,3,6,7], k = 3 - Output:
[3,3,5,5,6,7] [1,3,-1], -3, 5, 3, 6, 7 -> max: 3- 1,
[3,-1,-3], 5, 3, 6, 7 -> max: 3 - 1, 3,
[-1,-3,5], 3, 6, 7 -> max: 5 - ...
- Constraints:
- \(1 <= k <= nums.length <= 10^5\).
- 題目要求 \(O(n)\) 的解法。
2. 🐢 Brute Force Approach (暴力解)¶
對於每一個窗口,遍歷這 k 個元素找最大值。
- Time: \(O(n \cdot k)\)。
- Result: 當
k很大時 (例如 \(k \approx n/2\)),會變成 \(O(n^2)\) -> TLE。
3. 💡 The "Aha!" Moment (優化)¶
我們需要一個這要在 \(O(1)\) 時間內取得最大值,同時支援 \(O(1)\) 的「加入」與「移除」操作。 Heap? 移除任意元素需要 \(O(k)\) 或 tricky \(O(\log k)\) mapping。 BST? \(O(\log k)\)。
這題的最佳解是 Monotonically Decreasing Deque (單調遞減雙端佇列)。
核心邏輯: Deque 裡存的一定是 候選的最大值 的 Index。 並且我們保持 Deque 裡的元素對應的數值是 單調遞減 的。
deque.front()永遠是當前窗口的最大值。- 當新元素
nums[i]進來時: - Pop Small Elements: 如果
nums[i]比deque.back()還大,那deque.back()裡的那個數字這輩子都不可能成為最大值了(因為nums[i]比它晚進來,還比它大,會壓死它)。直接踢掉pop_back()。重複直到 Deque 單調或空。 - Add New Element: 把
i加進push_back()。 - Pop Outdated Elements: 如果
deque.front()的 index 已經超出窗口範圍 (i - k),就踢掉pop_front()。 - Record Result: 只要
i >= k-1(窗口成形後),deque.front()就是當前答案。
🎬 Visualization (演算法視覺化)¶
4. 💻 Implementation (程式碼)¶
Approach: Monotonic Deque¶
#include <vector>
#include <deque>
using namespace std;
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
// Deque 存的是 index,不是 value
deque<int> dq;
vector<int> res;
for (int i = 0; i < nums.size(); i++) {
// 1. 移除過期元素 (超出窗口左界)
if (!dq.empty() && dq.front() == i - k) {
dq.pop_front();
}
// 2. 維護單調性 (移除比當前元素小的所有候選人)
while (!dq.empty() && nums[dq.back()] < nums[i]) {
dq.pop_back();
}
// 3. 加入新元素
dq.push_back(i);
// 4. 記錄答案 (當窗口填滿 k 個後開始記錄)
if (i >= k - 1) {
res.push_back(nums[dq.front()]);
}
}
return res;
}
};
Python Reference¶
from collections import deque
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
dq = deque()
res = []
for i, n in enumerate(nums):
while dq and nums[dq[-1]] < n:
dq.pop()
dq.append(i)
if dq[0] == i - k:
dq.popleft()
if i >= k - 1:
res.append(nums[dq[0]])
return res
5. 📝 Detailed Code Comments (詳細註解)¶
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
// 使用 Deque (Double Ended Queue)
// 裡面存的是 Index。
// 性質:對應的 nums[index] 是單調嚴格遞減的。
// 例如: deque裡是 [5, 2] (假設是 indices),這代表 nums[5] > nums[2]。
// 且 nums[5] 是目前窗口最大值。
deque<int> dq;
vector<int> result;
for (int i = 0; i < nums.size(); i++) {
// Step 1: Remove Out-of-bound indices
// 窗口範圍是 [i-k+1, i]。如果 front 是 i-k,說明它過期了。
if (!dq.empty() && dq.front() <= i - k) {
dq.pop_front();
}
// Step 2: Remove smaller elements from back
// 如果新來的 nums[i] 很大,那它前面那些比它小的「老元素」就沒用了。
// 因為 nums[i] 比它們晚過期,數值又比它們大,所以它們永遠沒機會翻身。
while (!dq.empty() && nums[dq.back()] < nums[i]) {
dq.pop_back();
}
// Step 3: Add current index
dq.push_back(i);
// Step 4: Add to result
// 當我們至少有 k 個元素 (i >= k-1) 時,front 就是最大值
if (i >= k - 1) {
result.push_back(nums[dq.front()]);
}
}
return result;
}
};
6. 📊 Rigorous Complexity Analysis (複雜度分析)¶
- Time Complexity: \(O(n)\)
- 每個元素被 push 進 Deque 一次。
- 每個元素被 pop 出 Deque 最多一次。
- 總操作次數是 \(2n\),所以是線性時間。
- Space Complexity: \(O(k)\)
- Deque 最多同時儲存 \(k\) 個元素 (在 Input 是 Strictly Decreasing
[5,4,3,2,1]的最差情況下)。 - Output space \(O(n-k+1)\) 不算在 auxiliary space 中。
7. 💼 Interview Tips (面試技巧)¶
🎯 Follow-up 問題¶
面試官可能會問的延伸問題:
- 如果需要滑動視窗最小值?
- 兩者如何組合?
🚩 常見錯誤 (Red Flags)¶
避免這些會讓面試官扣分的錯誤:
- ⚠️ Deque 維護邏輯錯誤
- ⚠️ 沒有移除過期元素
✨ 加分項 (Bonus Points)¶
這些會讓你脫穎而出:
- 💎 Monotonic Deque
- 💎 解釋單調遞減的必要性
📚 Related Problems (相關題目)¶
站內相關¶
進階挑戰¶
- Sliding Window Median — LeetCode