Loading learning content...
Imagine you're building a real-time stock trading system. Every second, you receive a new stock price, and you need to display the maximum price over the last 60 seconds. As new prices arrive, old prices fall outside your window, and you must continuously update the maximum.
This is the sliding window maximum problem—one of the most elegant problems in algorithmic design that sits at the intersection of array processing, window techniques, and specialized data structures. It appears in countless real-world applications: monitoring systems, signal processing, financial analytics, and gaming leaderboards.
What makes this problem fascinating is that the naive approach is deceptively simple, yet achieving optimal efficiency requires a profound insight about how we track and discard candidate values.
By the end of this page, you will deeply understand the sliding window maximum problem—its formal definition, why it matters in practice, why naive approaches fail at scale, and why we need a specialized technique to solve it efficiently. This foundation is essential before we introduce the monotonic deque solution.
Let's establish the problem formally before exploring solutions.
The Sliding Window Maximum Problem:
Given an array nums of n elements and a window size k, find the maximum element in every contiguous subarray (window) of size k.
Input:
nums = [a₀, a₁, a₂, ..., aₙ₋₁] of n elementsk where 1 ≤ k ≤ nOutput:
n - k + 1 elements, where the i-th element is the maximum of nums[i..i+k-1]Example:
nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
Window positions: Maximum:
[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
Output: [3, 3, 5, 5, 6, 7]
The key insight is that the window slides one position at a time. When moving from window [i, i+k-1] to [i+1, i+k], exactly one element (nums[i]) leaves the window and exactly one element (nums[i+k]) enters. This incremental change is what makes optimization possible—we shouldn't need to recompute the maximum from scratch each time.
Constraints to Consider:
In competitive programming and real-world systems, you'll encounter various constraint ranges:
| Constraint Level | n range | k range | Implications |
|---|---|---|---|
| Small | n ≤ 10³ | k ≤ n | Naive O(nk) is acceptable |
| Medium | n ≤ 10⁵ | k ≤ n | Need O(n log k) or better |
| Large | n ≤ 10⁶ | k ≤ n | Must achieve O(n) |
| Extreme | n ≤ 10⁷ | k ≤ n | O(n) with low constant factor |
This module focuses on achieving the optimal O(n) solution that works across all constraint levels.
The sliding window maximum problem isn't merely an academic exercise—it represents a fundamental pattern that appears throughout software engineering and algorithm design. Understanding this problem deeply equips you with techniques applicable far beyond the specific problem statement.
The Pattern Recognition Value:
Beyond direct applications, this problem teaches a critical pattern: how to efficiently maintain aggregate information (maximum, minimum, sum) over a sliding window. Once you master the monotonic deque technique here, you can apply similar reasoning to:
This pattern appears in LeetCode problems, system design interviews, and production code alike.
The sliding window maximum problem is a favorite at FAANG companies because it tests multiple skills simultaneously: understanding the problem constraints, recognizing that naive solutions won't scale, knowledge of specialized data structures, and ability to reason about amortized complexity. Mastering this problem signals algorithmic maturity.
Before optimizing, let's understand the straightforward approach and analyze why it falls short.
The Brute Force Algorithm:
For each possible window position, scan all k elements to find the maximum:
function slidingWindowMaxNaive(nums, k):
n = length(nums)
result = new array of size (n - k + 1)
for i from 0 to n - k:
maxVal = nums[i]
for j from i + 1 to i + k - 1:
if nums[j] > maxVal:
maxVal = nums[j]
result[i] = maxVal
return result
This approach is intuitive and correct. For each of the n - k + 1 windows, we perform k comparisons.
1234567891011121314151617181920212223242526
function maxSlidingWindowNaive(nums: number[], k: number): number[] { const n = nums.length; const result: number[] = []; // For each window starting position for (let i = 0; i <= n - k; i++) { let maxVal = nums[i]; // Scan all k elements in the window for (let j = i + 1; j < i + k; j++) { if (nums[j] > maxVal) { maxVal = nums[j]; } } result.push(maxVal); } return result;} // Example usageconst nums = [1, 3, -1, -3, 5, 3, 6, 7];const k = 3;console.log(maxSlidingWindowNaive(nums, k)); // Output: [3, 3, 5, 5, 6, 7]Complexity Analysis:
| Aspect | Complexity | Explanation |
|---|---|---|
| Time | O(n × k) | For each of (n-k+1) windows, scan k elements |
| Space | O(n - k + 1) | Output array storage |
Why This Fails at Scale:
Consider the worst-case scenarios for large inputs:
For real-time systems processing millions of data points with large windows, this approach is completely impractical.
The fundamental issue is that we're doing redundant work. When the window slides from position i to i+1, we throw away all knowledge about the previous window and start fresh. But the windows [i, i+k-1] and [i+1, i+k] share k-1 elements! We should be able to leverage this overlap instead of recomputing from scratch.
Before arriving at the optimal solution, let's explore some natural optimization attempts and understand why they don't quite work. This analysis builds intuition for what the correct approach must achieve.
The tracking approach fails because maximum is not an incrementally reducible operation. Unlike sum (where removing x means subtracting x), finding a new maximum after removing the current maximum requires examining all remaining candidates.
1234567891011121314151617181920212223242526272829303132
// Using a max-heap with lazy deletion// This achieves O(n log k) but not O(n) import { MaxPriorityQueue } from '@datastructures-js/priority-queue'; function maxSlidingWindowHeap(nums: number[], k: number): number[] { const result: number[] = []; // Store [value, index] pairs in max-heap const maxHeap = new MaxPriorityQueue<[number, number]>({ compare: (a, b) => b[0] - a[0] // Max by value }); for (let i = 0; i < nums.length; i++) { // Add new element maxHeap.enqueue([nums[i], i]); // Remove elements outside window (lazy deletion) while (maxHeap.front()[1] <= i - k) { maxHeap.dequeue(); } // Once we have k elements, record maximum if (i >= k - 1) { result.push(maxHeap.front()[0]); } } return result;} // Complexity: O(n log k) time, O(k) space// Better than naive, but can we do better?The O(n log k) Heap-Based Approach:
Using a max-heap with lazy deletion, we can achieve O(n log k) time:
This is a valid solution for medium-sized inputs, but for truly large inputs (n = 10⁷, k = 10⁶), the log factor becomes significant.
The Question Remains:
Can we do better than O(n log k)? Can we achieve O(n) time regardless of window size?
The answer is yes—using a technique called the monotonic deque.
The breakthrough comes from realizing we don't need to track ALL elements in the window—only those that COULD POTENTIALLY become the maximum. Many elements enter the window but can never be the answer because a larger element entered after them and will leave after them. We can safely ignore these "dominated" elements.
Let's trace through a detailed example to build intuition for what candidates matter and which can be discarded.
Example: nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
| Step | Window | Elements | Maximum | Key Observation |
|---|---|---|---|---|
| 1 | [0,2] | [1, 3, -1] | 3 | 3 dominates 1 and -1 (later, larger) |
| 2 | [1,3] | [3, -1, -3] | 3 | 3 still in window, still maximum |
| 3 | [2,4] | [-1, -3, 5] | 5 | 5 enters and dominates everything |
| 4 | [3,5] | [-3, 5, 3] | 5 | 5 still in window; 3 can't beat it |
| 5 | [4,6] | [5, 3, 6] | 6 | 6 enters and dominates 5 and 3 |
| 6 | [5,7] | [3, 6, 7] | 7 | 7 enters and becomes new maximum |
Critical Observation — The Domination Principle:
When element nums[j] enters the window, consider any earlier element nums[i] where i < j:
nums[i] ≤ nums[j]: Element nums[i] can never be the maximum while nums[j] is in the windownums[j] entered later and is at least as largenums[i] leaves the window, nums[j] will still be there (since j > i)nums[i] is "dominated" and can be discardedExample Trace:
Processing nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
i=0: See 1. Window: [1]. Candidates: [1]
i=1: See 3. 3 > 1, so 1 is dominated. Candidates: [3]
i=2: See -1. -1 < 3, keep both. Candidates: [3, -1]
Window complete! Max = 3
i=3: See -3. -3 < -1, keep all. Candidates: [3, -1, -3]
But wait! 3's index is 1, window starts at 1. OK.
Max = 3
i=4: See 5. 5 > -3, 5 > -1, 5 > 3! All dominated.
Candidates: [5]
3's index was 1, now outside window [2,4]. Would remove anyway.
Max = 5
i=5: See 3. 3 < 5, keep both. Candidates: [5, 3]
Max = 5
i=6: See 6. 6 > 3, 6 > 5! Candidates: [6]
Max = 6
i=7: See 7. 7 > 6! Candidates: [7]
Max = 7
At any point, we only need to maintain candidates in DECREASING order. When a new element arrives, we remove all candidates smaller than it (they're dominated). What remains is a decreasing sequence where the front is always the maximum. This is exactly what a monotonic deque provides!
The sliding window maximum problem has several important variants. Understanding these variants helps you recognize the pattern in different contexts and apply the same technique.
| Variant | Description | Modification Needed |
|---|---|---|
| Sliding Window Minimum | Find minimum instead of maximum | Use monotonically increasing deque |
| Variable Window Size | Window size changes over time | Track entry times, check bounds dynamically |
| Multiple Windows | Track max for windows of sizes k₁, k₂, ... | Use separate deques or segment trees |
| 2D Sliding Window | Max in k×k rectangle sliding over matrix | Apply 1D technique row-wise, then column-wise |
| Circular Array | Array wraps around | Double the array or use modular arithmetic |
| Weighted Maximum | Elements have weights affecting priority | Combine with priority queue techniques |
Common LeetCode Problems Using This Technique:
| Problem | Difficulty | Core Technique |
|---|---|---|
| 239. Sliding Window Maximum | Hard | Direct application |
| 1438. Longest Subarray with Abs Diff ≤ Limit | Medium | Two monotonic deques |
| 862. Shortest Subarray with Sum ≥ K | Hard | Monotonic deque with prefix sums |
| 1499. Max Value of Equation | Hard | Monotonic deque optimization |
| 1696. Jump Game VI | Medium | DP with monotonic deque |
| 2398. Max Number of Robots | Hard | Monotonic deque + binary search |
Mastering the base problem prepares you for all these variations.
Once you recognize this pattern, you'll see it everywhere. Any time you need to track an extremum (max/min) or monotonic property over a sliding window, a monotonic deque is likely the optimal approach. This single technique unlocks dozens of seemingly unrelated problems.
We've established a solid foundation for understanding the sliding window maximum problem. Let's consolidate what we've learned:
What's Next:
In the next page, we'll introduce the deque (double-ended queue) as our tool for maintaining candidate maximum values. We'll see how a deque's ability to efficiently add/remove from both ends perfectly matches the requirements of our sliding window problem.
We'll explore:
You now understand the sliding window maximum problem, why naive approaches fail, and the key insight about element domination. This foundation prepares you for the elegant monotonic deque solution coming next.