Sliding Window Maximum
Hard
๐ Problem Statement
You are given an array of integers nums and an integer k. 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 maximum sliding window.
๐ Example
Example 1:
Input:
nums = [1,3,-1,-3,5,3,6,7],k = 3Output:
[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
Example 2:
Input:
nums = [1],k = 1Output:
[1]
๐ง Key Insight
- A naive approach would be to find the maximum in each window, but that would be O(n*k) which is inefficient.
 - We need a data structure that can efficiently: 
- Add new elements as the window slides
 - Remove elements that are no longer in the window
 - Find the maximum element in the current window
 
 - A deque (double-ended queue) can be used to maintain a monotonic decreasing queue of indices.
 - We only keep elements that could potentially be the maximum in future windows.
 
๐งฎ Monotonic Queue Technique
The key idea is to maintain a deque of indices such that:
- Elements in the deque are in decreasing order of their values
 - The front of the deque always contains the index of the maximum element in the current window
 - When sliding the window, we remove indices that fall outside the window
 
๐ ๏ธ Solution
from collections import deque
def maxSlidingWindow(nums, k):
    result = []
    dq = deque()  # Will store indices
    
    for i in range(len(nums)):
        # Remove elements outside the window
        if dq and dq[0] < i - k + 1:
            dq.popleft()
        
        # Remove smaller elements as they won't be the maximum
        while dq and nums[dq[-1]] < nums[i]:
            dq.pop()
        
        # Add current element's index
        dq.append(i)
        
        # Add to result if we've reached window size
        if i >= k - 1:
            result.append(nums[dq[0]])
    
    return result
๐ Time Complexity: \(O(n)\) -> Each element is processed exactly once and each is added/removed from the deque at most once.
๐ฆ Space Complexity: \(O(k)\) -> The deque can contain at most k elements.
๐ก Explanation
- We use a deque to store indices of elements in the current window.
 - For each element at index i: 
- Remove indices that are outside the current window (i-k+1 to i)
 - Remove indices of smaller elements from the back of the deque (as they canโt be the maximum)
 - Add the current index to the deque
 - If weโve processed at least k elements, add the maximum (front of deque) to the result
 
 - The deque maintains a monotonically decreasing order of values, ensuring the maximum is always at the front.
 
๐ Visualization
Consider nums = [1,3,-1,-3,5,3,6,7] with k = 3:
- i=0: Add index 0 to deque. deque=[0]
 - i=1: Remove nothing (1 < 3). Add index 1. deque=[1]
 - i=2: 3 > -1, so keep 1 in deque. Add index 2. deque=[1,2] 
- Window complete, add nums[deque[0]] = nums[1] = 3 to result
 
 - i=3: Remove nothing. -1 > -3, so keep 2. Add index 3. deque=[1,2,3] 
- Add nums[1] = 3 to result
 
 - i=4: Remove index 1 (outside window). Remove 2,3 (smaller values). Add index 4. deque=[4] 
- Add nums[4] = 5 to result
 
 - i=5: Remove nothing. 5 > 3, so keep 4. Add index 5. deque=[4,5] 
- Add nums[4] = 5 to result
 
 - i=6: Remove index 4 (outside window). Remove 5 (smaller value). Add index 6. deque=[6] 
- Add nums[6] = 6 to result
 
 - i=7: Remove nothing. 6 < 7, so remove 6. Add index 7. deque=[7] 
- Add nums[7] = 7 to result
 
 
Final result: [3,3,5,5,6,7]
๐ Priority Queue Approach
Another way to solve this problem is using a priority queue (max heap). The idea is to:
- Keep a max heap of (value, index) pairs
 - Remove elements that are outside the current window
 - The top of the heap is always the maximum element in the current window
 
๐ ๏ธ Priority Queue Solution
import heapq
def maxSlidingWindow(nums, k):
    result = []
    # Use max heap (multiply by -1 since Python has min heap by default)
    heap = []
    
    for i in range(len(nums)):
        # Add current element to the heap
        heapq.heappush(heap, (-nums[i], i))
        
        # If we've reached window size
        if i >= k - 1:
            # Remove elements outside the current window
            while heap and heap[0][1] < i - k + 1:
                heapq.heappop(heap)
            
            # Add the maximum to result
            result.append(-heap[0][0])
    
    return result
๐ Time Complexity: $O(n \log n)$ -> Each element is pushed and popped from the heap at most once, and each operation takes O(log n) time.
๐ฆ Space Complexity: $O(n)$ -> In the worst case, all elements might be in the heap.
๐ก Priority Queue Explanation
- We use a max heap to store (value, index) pairs. Since Pythonโs heapq is a min heap, we negate the values to simulate a max heap.
 - For each element at index i: 
- Add the current element to the heap
 - If weโve processed at least k elements: 
- Remove elements that are outside the current window
 - The top of the heap is the maximum element in the current window
 
 
 - The priority queue approach is more intuitive but less efficient than the monotonic queue approach.
 
๐ Comparison of Approaches
| Approach | Time Complexity | Space Complexity | Advantages | Disadvantages | 
|---|---|---|---|---|
| Monotonic Queue | O(n) | O(k) | More efficient | Slightly more complex to understand | 
| Priority Queue | O(n log n) | O(n) | More intuitive | Less efficient | 
The monotonic queue approach is generally preferred due to its linear time complexity, but the priority queue approach is a good alternative if youโre more familiar with heap operations.