Dual Queues with Two Pointers
Lessons 1-3 used a fixed window size K. But what if the window size is dynamic -- determined by a constraint on the data itself?
That changes everything about how you manage the window. There's no K to tell you when to expire. Instead, you expand the window as long as some condition holds, and shrink it the moment that condition breaks.
The Problem
Find the longest subarray where max - min <= X.
Think about what this asks. You want a contiguous stretch of the array where the spread between the largest and smallest values stays within X. The window grows when adding the next element keeps the spread small enough. It shrinks when the spread blows past X.
To check the constraint efficiently at every step, you need O(1) access to both the current max and the current min. That means two monotonic queues running in parallel.
The Architecture
Here's the setup:
- maxDq: a decreasing deque. The front holds the index of the current window maximum.
- minDq: an increasing deque. The front holds the index of the current window minimum.
- left: a pointer that marks the start of the window. It only moves right.
- right: iterates through the array one element at a time.
For each new right:
- Eclipse on both deques independently. Push
a[right]through maxDq (popping smaller values from the back) and through minDq (popping larger values from the back). - Check the constraint:
a[maxDq.front()] - a[minDq.front()] > X? - If violated, advance
leftand expire the front of whichever deque(s) now point outside the window. - Record the window size.

Walkthrough: [1, 3, 2, 5, 1, 4], X = 2
Let's trace both deques together. I'll show values in parentheses next to indices.
right = 0, a[right] = 1:
Eclipse: both deques empty. Push 0 to both.
maxDq: [0(1)], minDq: [0(1)]. Max - min = 0 <= 2. Window [0, 0], size 1.
right = 1, a[right] = 3:
Eclipse maxDq: back is 0(1), 1 <= 3, pop it. Push 1. maxDq: [1(3)].
Eclipse minDq: back is 0(1), 1 >= 3? No. Push 1. minDq: [0(1), 1(3)].
Max - min = 3 - 1 = 2 <= 2. Window [0, 1], size 2.
right = 2, a[right] = 2:
Eclipse maxDq: back is 1(3), 3 <= 2? No. Push 2. maxDq: [1(3), 2(2)].
Eclipse minDq: back is 1(3), 3 >= 2? Yes, pop. Back is 0(1), 1 >= 2? No. Push 2. minDq: [0(1), 2(2)].
Max - min = 3 - 1 = 2 <= 2. Window [0, 2], size 3.
right = 3, a[right] = 5:
Eclipse maxDq: back is 2(2), pop. Back is 1(3), pop. Push 3. maxDq: [3(5)].
Eclipse minDq: back is 2(2), 2 >= 5? No. Push 3. minDq: [0(1), 2(2), 3(5)].
Max - min = 5 - 1 = 4 > 2. Constraint violated!
Shrink: left moves from 0 to 1. Expire: maxDq front is 3 >= 1, stays. minDq front is 0 < 1, pop it. Now minDq: [2(2), 3(5)].
Max - min = 5 - 2 = 3 > 2. Still violated.
Shrink: left moves to 2. Expire: maxDq front is 3 >= 2, stays. minDq front is 2 >= 2, stays. Max - min = 5 - 2 = 3 > 2. Still violated.
Shrink: left moves to 3. Expire: maxDq front is 3 >= 3, stays. minDq front is 2 < 3, pop it. minDq: [3(5)].
Max - min = 5 - 5 = 0 <= 2. Window [3, 3], size 1.
right = 4, a[right] = 1:
Eclipse maxDq: back is 3(5), 5 <= 1? No. Push 4. maxDq: [3(5), 4(1)].
Eclipse minDq: back is 3(5), 5 >= 1? Yes, pop. Push 4. minDq: [4(1)].
Max - min = 5 - 1 = 4 > 2. Violated.
Shrink: left moves to 4. Expire: maxDq front is 3 < 4, pop it. maxDq: [4(1)]. minDq front is 4, stays.
Max - min = 1 - 1 = 0 <= 2. Window [4, 4], size 1.
right = 5, a[right] = 4:
Eclipse maxDq: back is 4(1), pop. Push 5. maxDq: [5(4)].
Eclipse minDq: back is 4(1), 1 >= 4? No. Push 5. minDq: [4(1), 5(4)].
Max - min = 4 - 1 = 3 > 2. Violated.
Shrink: left moves to 5. Expire: maxDq front is 5, stays. minDq front is 4 < 5, pop it. minDq: [5(4)].
Max - min = 4 - 4 = 0 <= 2. Window [5, 5], size 1.
Answer: 3 (the window [0, 2] = subarray [1, 3, 2]).
The Key Insight: Synchronized Expiration, Independent Eclipse
Both deques share the same window [left, right]. That means expiration -- the front-pop that removes elements that slid out of the window -- is synchronized. When left advances, you check both fronts.
But the eclipse step is completely independent. maxDq pops values that are too small (they'll never be the max). minDq pops values that are too large (they'll never be the min). Each deque enforces its own monotonicity without caring about the other.
This separation is what makes it clean. You're not fighting two interleaved data structures. You're running two independent queues that happen to look at the same window.
Why It's O(N)
Count the operations:
- Each element enters maxDq at most once, leaves at most once: 2N operations.
- Each element enters minDq at most once, leaves at most once: 2N operations.
- The
leftpointer only moves forward, covering at most N positions.
Total work: O(4N) = O(N). The nested while loops don't change this -- they're bounded by the total number of deque operations across all iterations.
Generalizing the Pattern
The max-minus-min constraint isn't special. This dual-queue-plus-two-pointer pattern works for any condition that depends on the window's max and/or min, as long as it has one property: once the condition breaks, expanding the window further can only make it worse.
Examples:
- max - min <= X -- the classic version we just solved.
- max / min <= Y (with positive values) -- the ratio grows as the spread grows.
- Any condition that breaks monotonically -- once violated, the only fix is to shrink from the left. You'll never fix it by adding more elements on the right.
This "monotone violation" property is what makes the two-pointer approach correct. If adding an element could sometimes fix a broken constraint, you'd need to consider all possible windows, and two pointers wouldn't work.
Try It
The starter code reads N and X, then N integers. Fill in the eclipse steps for both deques. The shrink logic and expiration are already provided.
Input format: Line 1: N X Line 2: N integers
Output: the length of the longest subarray where max - min <= X.
Test with the example: 6 2 and 1 3 2 5 1 4. The answer should be 3.
Problems
| Problem | Link | Difficulty |
|---|---|---|
| LC 239 — Sliding Window Maximum | leetcode.com/problems/sliding-window-maximum | Hard |
| LC 862 — Shortest Subarray with Sum at Least K | leetcode.com/problems/shortest-subarray-with-sum-at-least-k | Hard |
| LC 1438 — Longest Subarray with Abs Diff <= Limit | leetcode.com/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit | Medium |
| LC 1499 — Max Value of Equation | leetcode.com/problems/max-value-of-equation | Hard |