title: Sliding Window Maximum slug: sliding-window-maximum difficulty: hard leetcode_id: 239 leetcode_url: https://leetcode.com/problems/sliding-window-maximum/ categories: - arrays - queue - heap patterns: - sliding-window - monotonic-stack function_signature: "def max_sliding_window(nums: list[int], k: int) -> list[int]:" test_cases: visible: - input: { nums: [1, 3, -1, -3, 5, 3, 6, 7], k: 3 } expected: [3, 3, 5, 5, 6, 7] - input: { nums: [1], k: 1 } expected: [1] - input: { nums: [1, -1], k: 1 } expected: [1, -1] hidden: - input: { nums: [9, 11], k: 2 } expected: [11] - input: { nums: [4, 3, 2, 1], k: 2 } expected: [4, 3, 2] - input: { nums: [1, 2, 3, 4], k: 2 } expected: [2, 3, 4] - input: { nums: [7, 7, 7, 7], k: 3 } expected: [7, 7] - input: { nums: [1, 3, 1, 2, 0, 5], k: 3 } expected: [3, 3, 2, 5] - input: { nums: [-7, -8, 7, 5, 7, 1, 6, 0], k: 4 } expected: [7, 7, 7, 7, 7] description: | You are given an array of integers `nums`, there is a sliding window of size `k` which is moving from the very left of the array to the very right. You can only see the `k` numbers in the window. Each time the sliding window moves right by one position. Return *the max sliding window*. constraints: | - `1 <= nums.length <= 10^5` - `-10^4 <= nums[i] <= 10^4` - `1 <= k <= nums.length` examples: - input: "nums = [1,3,-1,-3,5,3,6,7], k = 3" output: "[3,3,5,5,6,7]" explanation: | Window position Max --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7 - input: "nums = [1], k = 1" output: "[1]" explanation: "With a window size of 1, the maximum is always the single element in the window." explanation: intuition: | Imagine you're looking through a window of fixed size `k` that slides across an array. At each position, you need to report the largest value visible through that window. The naive approach would be to scan all `k` elements for each window position, but with up to `10^5` elements, this O(n×k) approach is too slow. The key insight is that **not all elements in the window matter**. If you see a large element, any smaller elements to its *left* within the window can never be the maximum — they'll leave the window before the larger element does. Think of it like a queue where people line up by strength: when a stronger person arrives, weaker people already in line might as well leave — they'll never be the strongest while the new person is there. This is a **monotonic decreasing queue**: elements are ordered from largest to smallest, and smaller elements get removed when a larger one enters. The front of this queue always holds the current maximum. When it slides out of the window (based on index), we remove it and the next largest becomes the answer. approach: | We solve this using a **Monotonic Decreasing Deque**: **Step 1: Initialise the deque** - `deque`: Stores *indices* (not values) of elements in decreasing order of their values - `result`: Collects the maximum for each window position   **Step 2: Process each element** For each index `i`: - **Remove expired indices**: If the front of the deque is outside the current window (`i - k`), pop it from the front - **Maintain monotonic order**: While the deque is not empty and `nums[i]` is greater than or equal to the value at the back index, pop from the back — these elements can never be the maximum - **Add current index**: Push `i` to the back of the deque   **Step 3: Record the maximum** - Once we've processed at least `k` elements (`i >= k - 1`), the front of the deque is the index of the maximum in the current window - Append `nums[deque[0]]` to the result   **Why store indices instead of values?** We need to know when an element has left the window. By storing indices, we can check if `deque[0] <= i - k` to determine if the maximum has expired. common_pitfalls: - title: Using a Max-Heap Without Index Tracking description: | A max-heap seems natural for finding maximums, but the heap may contain elements that have already left the window. You'd need to lazily remove them by checking indices, adding complexity. The deque approach is cleaner because we proactively remove elements when they're no longer useful *or* when they leave the window. wrong_approach: "Max-heap with values only" correct_approach: "Monotonic deque with indices" - title: O(n×k) Brute Force description: | Scanning all `k` elements for each window results in O(n×k) time. With `n = 10^5` and `k = 10^5`, this means 10^10 operations — far too slow. Each element should enter and leave the deque at most once, giving amortised O(1) per element and O(n) overall. wrong_approach: "Nested loop: for each window, scan all k elements" correct_approach: "Monotonic deque: each element processed once" - title: Forgetting to Remove Expired Elements description: | The deque front might hold an index from outside the current window. Always check if `deque[0] <= i - k` and pop it *before* recording the maximum. Failing to do this returns maximums from elements no longer in the window. wrong_approach: "Only removing from back, ignoring front expiration" correct_approach: "Check and pop front if index is outside window" - title: Storing Values Instead of Indices description: | If you store values, you can't tell when an element has left the window. Two elements might have the same value at different positions — you need indices to track window boundaries. wrong_approach: "deque stores nums[i] directly" correct_approach: "deque stores i (index), lookup nums[i] when needed" key_takeaways: - "**Monotonic deque pattern**: Maintain elements in sorted order; remove smaller elements when a larger one arrives" - "**Store indices, not values**: Enables tracking whether elements have left the sliding window" - "**Amortised O(1) per element**: Each element enters and exits the deque at most once" - "**Classic hard problem**: Appears frequently in interviews; mastering this unlocks many similar problems" time_complexity: "O(n). Each element is pushed and popped from the deque at most once, giving amortised O(1) per element." space_complexity: "O(k). The deque holds at most `k` indices (the current window size)." solutions: - approach_name: Monotonic Deque is_optimal: true code: | from collections import deque def max_sliding_window(nums: list[int], k: int) -> list[int]: # Deque stores indices in decreasing order of their values dq = deque() result = [] for i, num in enumerate(nums): # Remove indices outside the current window if dq and dq[0] <= i - k: dq.popleft() # Remove indices of smaller elements (they'll never be max) while dq and nums[dq[-1]] <= num: dq.pop() # Add current index dq.append(i) # Window is complete, record the maximum if i >= k - 1: result.append(nums[dq[0]]) return result explanation: | **Time Complexity:** O(n) — Each element is pushed and popped at most once. **Space Complexity:** O(k) — Deque holds at most k indices. The deque maintains indices in decreasing order of their values. When a new element arrives, we remove all smaller elements from the back (they can never be the maximum while the new element is in the window). The front always holds the current maximum's index. - approach_name: Heap with Lazy Deletion is_optimal: false code: | import heapq def max_sliding_window(nums: list[int], k: int) -> list[int]: # Max-heap stores (-value, index) for max extraction heap = [] result = [] for i in range(len(nums)): # Add current element (negate for max-heap behavior) heapq.heappush(heap, (-nums[i], i)) # Remove elements outside the window (lazy deletion) while heap[0][1] <= i - k: heapq.heappop(heap) # Record maximum once window is complete if i >= k - 1: result.append(-heap[0][0]) return result explanation: | **Time Complexity:** O(n log n) — Each push/pop is O(log n), and we may have up to n elements in heap. **Space Complexity:** O(n) — Heap may accumulate elements with lazy deletion. Uses a max-heap (simulated with negated values) to track the maximum. Elements outside the window are lazily removed only when they reach the top. While correct, this is less efficient than the deque approach due to logarithmic heap operations. - approach_name: Brute Force is_optimal: false code: | def max_sliding_window(nums: list[int], k: int) -> list[int]: result = [] n = len(nums) # For each window position for i in range(n - k + 1): # Find max in current window window_max = max(nums[i:i + k]) result.append(window_max) return result explanation: | **Time Complexity:** O(n × k) — For each of (n - k + 1) windows, we scan k elements. **Space Complexity:** O(1) — Only storing the result (not counting output). The straightforward approach: for each window position, scan all k elements to find the maximum. Simple to understand but too slow for large inputs. With n = k = 10^5, this would require 10^10 operations.