Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Two Pointers & Sliding Window

Builds on Monotonic Deque and Longest Window, which you haven’t finished yet.

P2HardBuilt on Fixed Window~5 min speedrun

Dual Monotonic Deque (Longest Subarray Within a Max-Min Limit)

Built on
Monotonic DequeKeep a second deque for the minimum, and move the left end while the maximum minus the minimum is above the limit.
The problem

Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit (LC 1438)(opens leetcode.com)

Take the integer array nums and an integer limit. Look for the longest non-empty contiguous run of nums (a subarray) in which every pair of elements inside the run differs by at most limit.

Equivalently, the run's largest value minus its smallest value must be at most limit. Return the length of the longest such run.

Example 1
Input:nums = [8,2,4,7], limit = 4
Output:2
[2,4] has a gap of 2 and [4,7] has a gap of 3, both within the limit. Every run of three or more elements contains a pair more than 4 apart (for example 8 and 2 differ by 6), so the longest valid run has length 2.
80214273

Dual Monotonic Deque

sliding window ➔ two deques, one decreasing and one increasing ➔ the window's spread, read at both fronts at once
Spot it when: Every position of a sliding window must satisfy a condition on both its running maximum and its running minimum at once, such as a window that stays valid only while max - min stays within a limit, or a window you must count or measure by that joint condition.
Dual Monotonic Deque
Python
1from collections import deque
2from typing import List
3 
4def longest_subarray(nums: List[int], limit: int) -> int:
5 """Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit: two monotonic deques track the window's running max and min together.
6 Time: O(N) | Space: O(N)
7 """
8 # max_dq and min_dq hold indices, their values strictly decreasing and strictly increasing from
9 # front to back, so nums[max_dq[0]] is the window's max and nums[min_dq[0]] is its min.
10 max_dq: deque[int] = deque()
11 min_dq: deque[int] = deque()
12 left = 0
13 best = 0
14 
15 for right in range(len(nums)):
16 # Phase 1: Push & Maintain Monotonicity:
17 # Run the same back-eviction rule on both deques, then take in index right on each.
18 while max_dq and nums[max_dq[-1]] <= nums[right]:
19 max_dq.pop()
20 max_dq.append(right)
21 while min_dq and nums[min_dq[-1]] >= nums[right]:
22 min_dq.pop()
23 min_dq.append(right)
24 
25 # Phase 2 & 3: Contract the Boundary, Then Evict Stale Fronts:
26 # One outlier at right can invalidate many left positions at once, so this needs a while, not an if;
27 # each deque only evicted the indices it checked itself, so both fronts are tested every time left moves.
28 while nums[max_dq[0]] - nums[min_dq[0]] > limit:
29 left += 1
30 if max_dq[0] < left:
31 max_dq.popleft()
32 if min_dq[0] < left:
33 min_dq.popleft()
34 
35 # Phase 4: Harvest the Answer:
36 best = max(best, right - left + 1)
37 
38 return best
Invariant: max_dq holds the window's indices with strictly decreasing values and min_dq holds them with strictly increasing values, so nums[max_dq[0]] - nums[min_dq[0]] is always the window's exact spread.

Two Monotonic Deques read the same window side by side: max_dq strictly decreasing, so nums[max_dq[0]] is always the window's current maximum, and min_dq strictly increasing, so nums[min_dq[0]] is always the window's current minimum. Knowing both extremes at once, without rescanning the window, is what a single deque (the Monotonic Deque card) cannot give you, since one deque only tracks one side; a heap could track one side too, but it cannot cheaply drop a value in the middle that just aged out of the window, the way a deque's stale front does. While the window's spread, nums[max_dq[0]] - nums[min_dq[0]], is greater than limit, shrink the window from the left, moving left forward one step at a time. Each deque has only evicted the indices it has personally checked, so shrinking must test both fronts on every step: max_dq's front can still be fresh while min_dq's front has already fallen behind left, or the other way around. The trap is checking only one: if max_dq[0] < left: max_dq.popleft() must be followed by its own if min_dq[0] < left: min_dq.popleft(), because skipping the second line leaves a stale index at the front of the deque you did not check, and every later comparison then reads the wrong minimum or maximum for a window that has, in truth, already shrunk further.

See it run

Longest Subarray With a Spread Limit: longest_subarray([8, 2, 4, 7], limit = 4)

left 0 · right - · max_dq [] · min_dq [] · best 0

left 0 · right 0 · max_dq [0] · min_dq [0] · best 0

left 0 · right 0 · max_dq [0] · min_dq [0] · best 1

left 0 · right 1 · max_dq [0, 1] · min_dq [1] · best 1

left 1 · right 1 · max_dq [1] · min_dq [1] · best 1

left 1 · right 1 · max_dq [1] · min_dq [1] · best 1

left 1 · right 2 · max_dq [2] · min_dq [1, 2] · best 1

left 1 · right 2 · max_dq [2] · min_dq [1, 2] · best 2

left 1 · right 3 · max_dq [3] · min_dq [1, 2, 3] · best 2

left 2 · right 3 · max_dq [3] · min_dq [2, 3] · best 2

left 2 · right 3 · max_dq [3] · min_dq [2, 3] · best 2

The top row is nums. The next two rows show the values of the indices that max_dq and min_dq hold, each under its own column.

  • left the first index of the window
  • right the index that was just read
  • left, right left and right are at the same index
  • in max_dq an index kept in max_dq, values falling from front to back
  • in min_dq an index kept in min_dq, values rising from front to back

max_dq and min_dq are empty, left is 0 and best is 0. The loop reads one index right at a time.

max_dq pops nothing and appends index 0 (value 8). min_dq pops nothing and appends index 0 too.

The spread 8 - 8 = 0 is at most limit 4. The window has 1 value, so best = 1.

max_dq pops nothing and appends index 1 (value 2). min_dq pops index 0 and appends index 1 too.

The spread 8 - 2 = 6 is above limit 4, so left becomes 1. max_dq[0] = 0 is below left and leaves, but min_dq[0] = 1 is not, so each front needs its own check.

The spread 2 - 2 = 0 is at most limit 4. The window has 1 value, so best = 1.

max_dq pops index 1 and appends index 2 (value 4). min_dq pops nothing and appends index 2 too.

The spread 4 - 2 = 2 is at most limit 4. The window has 2 values, so best = 2.

max_dq pops index 2 and appends index 3 (value 7). min_dq pops nothing and appends index 3 too.

The spread 7 - 2 = 5 is above limit 4, so left becomes 2. min_dq[0] = 1 is below left and leaves, but max_dq[0] = 3 is not, so each front needs its own check.

The spread 7 - 4 = 3 is at most limit 4. The window has 2 values, so best = 2.

nums
08
12
24
37
max_dq
min_dq
1 / 11
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Shrinking checks only max_dq's stale front, never min_dq's

    Mistake
    while nums[max_dq[0]] - nums[min_dq[0]] > limit:
        left += 1
        if max_dq[0] < left:
            max_dq.popleft()
    Fix
    while nums[max_dq[0]] - nums[min_dq[0]] > limit:
        left += 1
        if max_dq[0] < left:
            max_dq.popleft()
        if min_dq[0] < left:
            min_dq.popleft()

    What happens: min_dq's own front is never removed, so nums[min_dq[0]] keeps reading an old minimum that is no longer inside the window. The comparison nums[max_dq[0]] - nums[min_dq[0]] > limit then stays true for a window that is already valid, so left keeps advancing past every index max_dq still holds. Once max_dq empties, nums[max_dq[0]] has no front to read.

    Example: nums = [4, 2, 2, 2, 4, 4, 2, 2], limit = 0
    Mistake: raises IndexErrorFix: returns 3
  2. 2

    Gotcha 2: Writing >= instead of <= when popping max_dq's back

    Mistake
    while max_dq and nums[max_dq[-1]] >= nums[right]:
    Fix
    while max_dq and nums[max_dq[-1]] <= nums[right]:

    What happens: max_dq must pop every value not larger than nums[right], so its values strictly decrease from front to back and the front stays the true maximum. With >=, max_dq instead pops values that are not smaller than nums[right], which keeps its values increasing. The front then becomes the smallest value seen, not the largest, so the whole window is measured against the wrong number.

    Example: nums = [8, 2, 4, 7], limit = 4
    Mistake: returns 4Fix: returns 2
  3. 3

    Gotcha 3: Evicting a stale front on <= instead of <

    Mistake
    if max_dq[0] <= left:
        max_dq.popleft()
    if min_dq[0] <= left:
        min_dq.popleft()
    Fix
    if max_dq[0] < left:
        max_dq.popleft()
    if min_dq[0] < left:
        min_dq.popleft()

    What happens: The window after left += 1 is nums[left .. right], so index left itself is still inside it. With <=, a front sitting exactly at left is evicted anyway, even though its index is still in the window. When that front is the only index a deque holds, the deque empties, and the next comparison has no front left to read.

    Example: nums = [10, 1, 2, 4, 7, 2], limit = 5
    Mistake: raises IndexErrorFix: returns 4
  4. 4

    Gotcha 4: Storing values instead of indices in max_dq and min_dq

    What happens: Shrinking the window needs to know whether a deque's front index has fallen behind left, which only an index can answer. A stored value cannot say which position it came from, so two equal values at different positions could not be told apart, and a front that is still inside the window could be evicted by mistake alongside one that has already left it.

Not this one if…
  • Every window only needs its maximum, as in Sliding Window Maximum (LC 239): one Monotonic Deque is enough, since nothing in the problem also depends on the window's minimum. See Monotonic Deque

Deep Dive

Why the window needs two deques

The Monotonic Deque card tracks one end of the window: the maximum. The rule in this card needs both ends at once. The window nums[left..right] is valid when its maximum minus its minimum, the spread, is at most limit.

max_dq holds indices whose values strictly decrease, so nums[max_dq[0]] is the maximum. min_dq holds indices whose values strictly increase, so nums[min_dq[0]] is the minimum. Both deques hold only indices of the current window. So nums[max_dq[0]] - nums[min_dq[0]] is the exact spread of the window, and it is read in O(1).

A heap can track one end, but it cannot cheaply remove a value from the middle that has just left the window. A deque only ever removes from its two ends, and the value that expires is always at the front.

Tracing [8, 2, 4, 7] with limit = 4

At right = 1, the value 2 is smaller than 8. min_dq pops index 0 and holds [1], and max_dq holds [0, 1]. The spread is 8 - 2 = 6, which is above 4, so left becomes 1. The front of max_dq is index 0, which is below left, so it leaves. The front of min_dq is index 1, which stays. The window is [2], and best stays 1.

At right = 2, the value 4 pops index 1 from max_dq. min_dq holds [1, 2]. The spread is 4 - 2 = 2, so the window [2, 4] is valid and best becomes 2.

At right = 3, the value 7 pops index 2 from max_dq. min_dq holds [1, 2, 3]. The spread is 7 - 2 = 5, which is above 4, so left becomes 2. This time the front of max_dq, index 3, stays, and the front of min_dq, index 1, is below left and leaves. The spread is now 7 - 4 = 3, and best stays 2.

The result is 2, the answer of Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit (LC 1438), Example 1.

Why both fronts need their own check

Each deque removes an index from its front only when the code tests that deque. When left moves, the stale index can be at the front of max_dq, at the front of min_dq, at both, or at neither. The trace shows two of these cases: max_dq at right = 1, and min_dq at right = 3.

The trap is to test only max_dq. Then a stale index stays at the front of min_dq, and nums[min_dq[0]] reads a value that is no longer in the window. For nums = [8, 2, 4, 7] and limit = 4, the spread at right = 3 stays at 7 - 2 = 5. The loop keeps moving left until max_dq is empty, and nums[max_dq[0]] raises IndexError.

The test is < left, not <= left. The window is nums[left..right], so index left is inside it. With <=, a front at left leaves too early, and a deque can become empty while the window still holds values.

The shrink step is a while. One new outlier can raise the spread so much that left must move several steps before the spread fits. The while keeps the window valid at the end of every step.

The cost, and the condition for using it

Each index is appended once to each deque and popped at most once from each. left and right only move forward. So the time is O(N). In the worst case, a deque holds the whole window, so the space is O(N).

Use this card when a window is valid only if a condition on both its maximum and its minimum holds, and a longer window can only make the spread larger. Then moving left forward is the only repair. If the condition looks at only one end, the Monotonic Deque card is enough.

60-Second Pitch

"The window is valid when its maximum minus its minimum is at most limit. So I need both extremes of the window at every step, without a scan. I keep two deques of indices. max_dq has strictly decreasing values, so its front is the maximum. min_dq has strictly increasing values, so its front is the minimum. For each right, I add the index to both deques. While the spread is above limit, I move left forward, and I check the front of each deque separately. Each deque drops its front when that index is below left. Every index enters and leaves each deque once, so the time is O(N). The deques can hold the whole window, so the space is O(N)."

Common Questions

What is the time and space complexity?

The time is O(N), because each index enters and leaves each deque at most once, and left and right only move forward. The space is O(N) in the worst case, when a deque holds the whole window.

Why use two deques and not a heap or a sorted structure?

Two heaps cost O(log N) per step, and a value that left the window stays inside a heap until it reaches the top. A balanced tree or a sorted container can delete it at O(log N) per step, so the total is O(N log N). The deques reach O(N), because the expired index is always at a front.

Why must the loop check both fronts after every left += 1?

Each deque removes an index only when the code tests it. One deque can have a stale front while the other has a fresh one. If the code tests only one, the other reads an extreme value from outside the window, and the spread is wrong.

How would you count the subarrays with a spread within the limit?

Keep the same loop and add right - left + 1 to a counter after the shrink loop. Every start from left to right gives a subarray whose spread is at most limit, because a shorter window cannot have a larger spread.

What changes if the limit applies to the sum and not to the spread?

Then one number is enough, and you can use a plain sliding window. The two deques are for conditions on the extremes. A sum or a count of values is a single running number.

SWE Reasoning

Spot it

  • A window is valid only while its maximum minus its minimum stays within a limit.
  • The answer is the longest such subarray, or the number of such subarrays.
  • Adding an element can only keep or raise the spread, so shrinking from the left is the only repair.
  • Both extremes are needed at the same time, so one deque is not enough.

The invariant, step by step

  1. max_dq and min_dq are empty, left = 0 and best = 0.
  2. At each right, max_dq pops every back value that is at most nums[right], and min_dq pops every back value that is at least nums[right]. Both then append right.
  3. The values in max_dq strictly decrease, and the values in min_dq strictly increase. So the two fronts are the maximum and the minimum of the window.
  4. While nums[max_dq[0]] - nums[min_dq[0]] > limit, left += 1 makes the window smaller.
  5. After each left += 1, each deque tests its own front. If max_dq[0] < left or min_dq[0] < left, that front is removed.
  6. When the loop ends, the spread is at most limit, so right - left + 1 is a valid window length and best = max(best, right - left + 1) records it.

Traps

  • Testing only one front. The other deque keeps a stale index. Its extreme value is wrong, and the loop can run until a deque is empty.
  • Writing >= for the back of max_dq. The values of max_dq then increase, and the front is the smallest value, not the largest.
  • Evicting on <= left. Index left is still in the window.
  • Using if for the shrink step. One outlier can need several moves of left.
  • Storing values. A value cannot tell the code whether it has left the window.

Practice

Canonical LeetCode:Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit (LC 1438)Continuous Subarrays (LC 2762)Count Subarrays With Cost Less Than or Equal to K (LC 3835)
Theory Context•Two Pointers & Sliding Window

Builds on Monotonic Deque and Longest Window, which you haven’t finished yet.

P2HardBuilt on Fixed Window~5 min speedrun

Dual Monotonic Deque (Longest Subarray Within a Max-Min Limit)

Built on
Monotonic DequeKeep a second deque for the minimum, and move the left end while the maximum minus the minimum is above the limit.
The problem

Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit (LC 1438)(opens leetcode.com)

Take the integer array nums and an integer limit. Look for the longest non-empty contiguous run of nums (a subarray) in which every pair of elements inside the run differs by at most limit.

Equivalently, the run's largest value minus its smallest value must be at most limit. Return the length of the longest such run.

Example 1
Input:nums = [8,2,4,7], limit = 4
Output:2
[2,4] has a gap of 2 and [4,7] has a gap of 3, both within the limit. Every run of three or more elements contains a pair more than 4 apart (for example 8 and 2 differ by 6), so the longest valid run has length 2.
80214273

Dual Monotonic Deque

sliding window ➔ two deques, one decreasing and one increasing ➔ the window's spread, read at both fronts at once
Spot it when: Every position of a sliding window must satisfy a condition on both its running maximum and its running minimum at once, such as a window that stays valid only while max - min stays within a limit, or a window you must count or measure by that joint condition.
Dual Monotonic Deque
Python
1from collections import deque
2from typing import List
3 
4def longest_subarray(nums: List[int], limit: int) -> int:
5 """Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit: two monotonic deques track the window's running max and min together.
6 Time: O(N) | Space: O(N)
7 """
8 # max_dq and min_dq hold indices, their values strictly decreasing and strictly increasing from
9 # front to back, so nums[max_dq[0]] is the window's max and nums[min_dq[0]] is its min.
10 max_dq: deque[int] = deque()
11 min_dq: deque[int] = deque()
12 left = 0
13 best = 0
14 
15 for right in range(len(nums)):
16 # Phase 1: Push & Maintain Monotonicity:
17 # Run the same back-eviction rule on both deques, then take in index right on each.
18 while max_dq and nums[max_dq[-1]] <= nums[right]:
19 max_dq.pop()
20 max_dq.append(right)
21 while min_dq and nums[min_dq[-1]] >= nums[right]:
22 min_dq.pop()
23 min_dq.append(right)
24 
25 # Phase 2 & 3: Contract the Boundary, Then Evict Stale Fronts:
26 # One outlier at right can invalidate many left positions at once, so this needs a while, not an if;
27 # each deque only evicted the indices it checked itself, so both fronts are tested every time left moves.
28 while nums[max_dq[0]] - nums[min_dq[0]] > limit:
29 left += 1
30 if max_dq[0] < left:
31 max_dq.popleft()
32 if min_dq[0] < left:
33 min_dq.popleft()
34 
35 # Phase 4: Harvest the Answer:
36 best = max(best, right - left + 1)
37 
38 return best
Invariant: max_dq holds the window's indices with strictly decreasing values and min_dq holds them with strictly increasing values, so nums[max_dq[0]] - nums[min_dq[0]] is always the window's exact spread.

Two Monotonic Deques read the same window side by side: max_dq strictly decreasing, so nums[max_dq[0]] is always the window's current maximum, and min_dq strictly increasing, so nums[min_dq[0]] is always the window's current minimum. Knowing both extremes at once, without rescanning the window, is what a single deque (the Monotonic Deque card) cannot give you, since one deque only tracks one side; a heap could track one side too, but it cannot cheaply drop a value in the middle that just aged out of the window, the way a deque's stale front does. While the window's spread, nums[max_dq[0]] - nums[min_dq[0]], is greater than limit, shrink the window from the left, moving left forward one step at a time. Each deque has only evicted the indices it has personally checked, so shrinking must test both fronts on every step: max_dq's front can still be fresh while min_dq's front has already fallen behind left, or the other way around. The trap is checking only one: if max_dq[0] < left: max_dq.popleft() must be followed by its own if min_dq[0] < left: min_dq.popleft(), because skipping the second line leaves a stale index at the front of the deque you did not check, and every later comparison then reads the wrong minimum or maximum for a window that has, in truth, already shrunk further.

See it run

Longest Subarray With a Spread Limit: longest_subarray([8, 2, 4, 7], limit = 4)

left 0 · right - · max_dq [] · min_dq [] · best 0

left 0 · right 0 · max_dq [0] · min_dq [0] · best 0

left 0 · right 0 · max_dq [0] · min_dq [0] · best 1

left 0 · right 1 · max_dq [0, 1] · min_dq [1] · best 1

left 1 · right 1 · max_dq [1] · min_dq [1] · best 1

left 1 · right 1 · max_dq [1] · min_dq [1] · best 1

left 1 · right 2 · max_dq [2] · min_dq [1, 2] · best 1

left 1 · right 2 · max_dq [2] · min_dq [1, 2] · best 2

left 1 · right 3 · max_dq [3] · min_dq [1, 2, 3] · best 2

left 2 · right 3 · max_dq [3] · min_dq [2, 3] · best 2

left 2 · right 3 · max_dq [3] · min_dq [2, 3] · best 2

The top row is nums. The next two rows show the values of the indices that max_dq and min_dq hold, each under its own column.

  • left the first index of the window
  • right the index that was just read
  • left, right left and right are at the same index
  • in max_dq an index kept in max_dq, values falling from front to back
  • in min_dq an index kept in min_dq, values rising from front to back

max_dq and min_dq are empty, left is 0 and best is 0. The loop reads one index right at a time.

max_dq pops nothing and appends index 0 (value 8). min_dq pops nothing and appends index 0 too.

The spread 8 - 8 = 0 is at most limit 4. The window has 1 value, so best = 1.

max_dq pops nothing and appends index 1 (value 2). min_dq pops index 0 and appends index 1 too.

The spread 8 - 2 = 6 is above limit 4, so left becomes 1. max_dq[0] = 0 is below left and leaves, but min_dq[0] = 1 is not, so each front needs its own check.

The spread 2 - 2 = 0 is at most limit 4. The window has 1 value, so best = 1.

max_dq pops index 1 and appends index 2 (value 4). min_dq pops nothing and appends index 2 too.

The spread 4 - 2 = 2 is at most limit 4. The window has 2 values, so best = 2.

max_dq pops index 2 and appends index 3 (value 7). min_dq pops nothing and appends index 3 too.

The spread 7 - 2 = 5 is above limit 4, so left becomes 2. min_dq[0] = 1 is below left and leaves, but max_dq[0] = 3 is not, so each front needs its own check.

The spread 7 - 4 = 3 is at most limit 4. The window has 2 values, so best = 2.

nums
08
12
24
37
max_dq
min_dq
1 / 11
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Shrinking checks only max_dq's stale front, never min_dq's

    Mistake
    while nums[max_dq[0]] - nums[min_dq[0]] > limit:
        left += 1
        if max_dq[0] < left:
            max_dq.popleft()
    Fix
    while nums[max_dq[0]] - nums[min_dq[0]] > limit:
        left += 1
        if max_dq[0] < left:
            max_dq.popleft()
        if min_dq[0] < left:
            min_dq.popleft()

    What happens: min_dq's own front is never removed, so nums[min_dq[0]] keeps reading an old minimum that is no longer inside the window. The comparison nums[max_dq[0]] - nums[min_dq[0]] > limit then stays true for a window that is already valid, so left keeps advancing past every index max_dq still holds. Once max_dq empties, nums[max_dq[0]] has no front to read.

    Example: nums = [4, 2, 2, 2, 4, 4, 2, 2], limit = 0
    Mistake: raises IndexErrorFix: returns 3
  2. 2

    Gotcha 2: Writing >= instead of <= when popping max_dq's back

    Mistake
    while max_dq and nums[max_dq[-1]] >= nums[right]:
    Fix
    while max_dq and nums[max_dq[-1]] <= nums[right]:

    What happens: max_dq must pop every value not larger than nums[right], so its values strictly decrease from front to back and the front stays the true maximum. With >=, max_dq instead pops values that are not smaller than nums[right], which keeps its values increasing. The front then becomes the smallest value seen, not the largest, so the whole window is measured against the wrong number.

    Example: nums = [8, 2, 4, 7], limit = 4
    Mistake: returns 4Fix: returns 2
  3. 3

    Gotcha 3: Evicting a stale front on <= instead of <

    Mistake
    if max_dq[0] <= left:
        max_dq.popleft()
    if min_dq[0] <= left:
        min_dq.popleft()
    Fix
    if max_dq[0] < left:
        max_dq.popleft()
    if min_dq[0] < left:
        min_dq.popleft()

    What happens: The window after left += 1 is nums[left .. right], so index left itself is still inside it. With <=, a front sitting exactly at left is evicted anyway, even though its index is still in the window. When that front is the only index a deque holds, the deque empties, and the next comparison has no front left to read.

    Example: nums = [10, 1, 2, 4, 7, 2], limit = 5
    Mistake: raises IndexErrorFix: returns 4
  4. 4

    Gotcha 4: Storing values instead of indices in max_dq and min_dq

    What happens: Shrinking the window needs to know whether a deque's front index has fallen behind left, which only an index can answer. A stored value cannot say which position it came from, so two equal values at different positions could not be told apart, and a front that is still inside the window could be evicted by mistake alongside one that has already left it.

Not this one if…
  • Every window only needs its maximum, as in Sliding Window Maximum (LC 239): one Monotonic Deque is enough, since nothing in the problem also depends on the window's minimum. See Monotonic Deque

Deep Dive

Why the window needs two deques

The Monotonic Deque card tracks one end of the window: the maximum. The rule in this card needs both ends at once. The window nums[left..right] is valid when its maximum minus its minimum, the spread, is at most limit.

max_dq holds indices whose values strictly decrease, so nums[max_dq[0]] is the maximum. min_dq holds indices whose values strictly increase, so nums[min_dq[0]] is the minimum. Both deques hold only indices of the current window. So nums[max_dq[0]] - nums[min_dq[0]] is the exact spread of the window, and it is read in O(1).

A heap can track one end, but it cannot cheaply remove a value from the middle that has just left the window. A deque only ever removes from its two ends, and the value that expires is always at the front.

Tracing [8, 2, 4, 7] with limit = 4

At right = 1, the value 2 is smaller than 8. min_dq pops index 0 and holds [1], and max_dq holds [0, 1]. The spread is 8 - 2 = 6, which is above 4, so left becomes 1. The front of max_dq is index 0, which is below left, so it leaves. The front of min_dq is index 1, which stays. The window is [2], and best stays 1.

At right = 2, the value 4 pops index 1 from max_dq. min_dq holds [1, 2]. The spread is 4 - 2 = 2, so the window [2, 4] is valid and best becomes 2.

At right = 3, the value 7 pops index 2 from max_dq. min_dq holds [1, 2, 3]. The spread is 7 - 2 = 5, which is above 4, so left becomes 2. This time the front of max_dq, index 3, stays, and the front of min_dq, index 1, is below left and leaves. The spread is now 7 - 4 = 3, and best stays 2.

The result is 2, the answer of Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit (LC 1438), Example 1.

Why both fronts need their own check

Each deque removes an index from its front only when the code tests that deque. When left moves, the stale index can be at the front of max_dq, at the front of min_dq, at both, or at neither. The trace shows two of these cases: max_dq at right = 1, and min_dq at right = 3.

The trap is to test only max_dq. Then a stale index stays at the front of min_dq, and nums[min_dq[0]] reads a value that is no longer in the window. For nums = [8, 2, 4, 7] and limit = 4, the spread at right = 3 stays at 7 - 2 = 5. The loop keeps moving left until max_dq is empty, and nums[max_dq[0]] raises IndexError.

The test is < left, not <= left. The window is nums[left..right], so index left is inside it. With <=, a front at left leaves too early, and a deque can become empty while the window still holds values.

The shrink step is a while. One new outlier can raise the spread so much that left must move several steps before the spread fits. The while keeps the window valid at the end of every step.

The cost, and the condition for using it

Each index is appended once to each deque and popped at most once from each. left and right only move forward. So the time is O(N). In the worst case, a deque holds the whole window, so the space is O(N).

Use this card when a window is valid only if a condition on both its maximum and its minimum holds, and a longer window can only make the spread larger. Then moving left forward is the only repair. If the condition looks at only one end, the Monotonic Deque card is enough.

60-Second Pitch

"The window is valid when its maximum minus its minimum is at most limit. So I need both extremes of the window at every step, without a scan. I keep two deques of indices. max_dq has strictly decreasing values, so its front is the maximum. min_dq has strictly increasing values, so its front is the minimum. For each right, I add the index to both deques. While the spread is above limit, I move left forward, and I check the front of each deque separately. Each deque drops its front when that index is below left. Every index enters and leaves each deque once, so the time is O(N). The deques can hold the whole window, so the space is O(N)."

Common Questions

What is the time and space complexity?

The time is O(N), because each index enters and leaves each deque at most once, and left and right only move forward. The space is O(N) in the worst case, when a deque holds the whole window.

Why use two deques and not a heap or a sorted structure?

Two heaps cost O(log N) per step, and a value that left the window stays inside a heap until it reaches the top. A balanced tree or a sorted container can delete it at O(log N) per step, so the total is O(N log N). The deques reach O(N), because the expired index is always at a front.

Why must the loop check both fronts after every left += 1?

Each deque removes an index only when the code tests it. One deque can have a stale front while the other has a fresh one. If the code tests only one, the other reads an extreme value from outside the window, and the spread is wrong.

How would you count the subarrays with a spread within the limit?

Keep the same loop and add right - left + 1 to a counter after the shrink loop. Every start from left to right gives a subarray whose spread is at most limit, because a shorter window cannot have a larger spread.

What changes if the limit applies to the sum and not to the spread?

Then one number is enough, and you can use a plain sliding window. The two deques are for conditions on the extremes. A sum or a count of values is a single running number.

SWE Reasoning

Spot it

  • A window is valid only while its maximum minus its minimum stays within a limit.
  • The answer is the longest such subarray, or the number of such subarrays.
  • Adding an element can only keep or raise the spread, so shrinking from the left is the only repair.
  • Both extremes are needed at the same time, so one deque is not enough.

The invariant, step by step

  1. max_dq and min_dq are empty, left = 0 and best = 0.
  2. At each right, max_dq pops every back value that is at most nums[right], and min_dq pops every back value that is at least nums[right]. Both then append right.
  3. The values in max_dq strictly decrease, and the values in min_dq strictly increase. So the two fronts are the maximum and the minimum of the window.
  4. While nums[max_dq[0]] - nums[min_dq[0]] > limit, left += 1 makes the window smaller.
  5. After each left += 1, each deque tests its own front. If max_dq[0] < left or min_dq[0] < left, that front is removed.
  6. When the loop ends, the spread is at most limit, so right - left + 1 is a valid window length and best = max(best, right - left + 1) records it.

Traps

  • Testing only one front. The other deque keeps a stale index. Its extreme value is wrong, and the loop can run until a deque is empty.
  • Writing >= for the back of max_dq. The values of max_dq then increase, and the front is the smallest value, not the largest.
  • Evicting on <= left. Index left is still in the window.
  • Using if for the shrink step. One outlier can need several moves of left.
  • Storing values. A value cannot tell the code whether it has left the window.

Practice

Canonical LeetCode:Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit (LC 1438)Continuous Subarrays (LC 2762)Count Subarrays With Cost Less Than or Equal to K (LC 3835)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•