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
HardLC 239

Sliding Window Maximum (LeetCode 239)

Target Frequency:GoogleAmazonMetaApple

Take the integer array nums and look at it through a frame exactly k elements wide. The frame starts over the first k elements and moves right by one element at a time, until it covers the last k.

For every position of the frame, write down the largest value inside it. Return these maxima as a list, from the leftmost position of the frame to the rightmost: len(nums) - k + 1 values in all.

Worked Examples

Example 1
Input:nums = [1,3,-1,-3,5,3,6,7], k = 3
Output:[3,3,5,5,6,7]
1031-12-3354356677
Explanation: The six windows and their maxima: [1,3,-1] -> 3, [3,-1,-3] -> 3, [-1,-3,5] -> 5, [-3,5,3] -> 5, [5,3,6] -> 6, [3,6,7] -> 7.
Example 2
Input:nums = [1], k = 1
Output:[1]
10
Explanation: There is one window, [1], and its maximum is 1.

⚖️Formal Constraints & Bounds

  • 1 <= nums.length <= 105

  • -104 <= nums[i] <= 104

  • 1 <= k <= nums.length

Deep-Dive & Conceptual Insights

Core Insight

A value that arrives later and is at least as large makes every older, smaller-or-equal value useless: the newer one stays in the window at least as long. Dropping those from the back leaves indices whose values strictly decrease, so the front is always the window's maximum. The window can only ever be oversized by one index, so left advances with a plain if, and the front leaves exactly when dq[0] < left, tested only after left has already moved.

Real-World Scenario & Production Applications

Monitoring dashboards that plot the peak latency or the highest price of the last few minutes, updated every tick, and signal processing that follows the loudest sample of a moving block of audio.

Subproblems & Recurrence Decomposition3 Phases

🧩Subproblem 1: Pop What the Newcomer Beats, Then Push It

Every older value <= nums[right] can never be a window's maximum again, because nums[right] is at least as large and stays longer. Pop them from the back, then push right: the values in dq strictly decrease from front to back.

Mathematical Recurrence / Code Invariant
while dq and nums[dq[-1]] <= nums[right]:
    dq.pop()
dq.append(right)

Step-by-Step Execution Trace Table

rightnums[right]Back: pop while nums[dq[-1]] <= nums[right]dq after dq.append(right)left after the boundary checkFront: dq[0] < left?res.append(nums[dq[0]])
01—[0] (values 1)0no— (not size k yet)
13pop 0 (1 <= 3)[1] (values 3)0no— (not size k yet)
2-1—[1, 2] (values 3, -1)0no3
3-3—[1, 2, 3] (values 3, -1, -3)1no3
45pop 3 (-3 <= 5), 2 (-1 <= 5), 1 (3 <= 5)[4] (values 5)2no5
53—[4, 5] (values 5, 3)3no5
66pop 5 (3 <= 6), 4 (5 <= 6)[6] (values 6)4no6
77pop 6 (6 <= 7)[7] (values 7)5no7

The back-eviction loop happens to clear every stale index before the front check runs on this example, so the front check never fires here; see the card's first gotcha (nums = [9, 8, 7, 6, 5, 4], k = 2) for a trace where it does.

Code Shape
Archetype Code Shape
Python
1dq = deque()
2res = [] # indices, values strictly decreasing
3left = 0
4for right in range(len(nums)):
5 while dq and nums[dq[-1]] <= nums[right]:
6 dq.pop()
7 dq.append(right)
8 if right - left + 1 > k:
9 left += 1
10 if dq and dq[0] < left:
11 dq.popleft()
12 if right - left + 1 == k:
13 res.append(nums[dq[0]])
14return res

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

Re-scanning all k values of the window at every step is the obvious approach, and also the slow one. A heap would give the maximum faster, but it can't cheaply drop the value that just left the window. A monotonic deque keeps only the values that can still be a window's maximum, as indices whose values strictly decrease from front to back, so the answer is always at the front.

📐 What a Monotonic Deque Is

A monotonic deque is a double-ended queue of array indices. A double-ended queue can add and remove items at both ends in O(1) time; Python's collections.deque is one. Three rules hold after every step:

  1. Index order. The indices in dq increase from front to back. They are not always next to each other, because some indices were removed. The front is the oldest index, and the back is the newest.
  2. Value order. The values at those indices move in one direction only, with no two equal. For a maximum, they strictly decrease from front to back, so nums[dq[0]] is the largest. For a minimum, they strictly increase, so nums[dq[0]] is the smallest.
  3. Only candidates stay. Every index in dq can still be the answer for the current window or a later one. An index leaves in one of two ways:
    • From the back, when a newer index arrives whose value is at least as large (for a maximum) or at most as large (for a minimum). The newer index stays in every later window that the older index is in, so the older index can never be the answer again.
    • From the front, when the problem says the front index can no longer be used. In Sliding Window Maximum (LC 239), that happens once the window's own left pointer passes it: dq[0] < left, tested only after left has already advanced.

Each index enters dq once and leaves it at most once, so one pass over the array costs O(N).

🔁 The Same Deque Beyond the Window Maximum

The three rules stay the same. Three choices change from problem to problem.

What changesSliding Window Maximum (LC 239)Window minimumJump Game VI (LC 1696)
The values the indices point tonums[right]nums[i]score[i], the best total when standing on index i
Value order, front to backstrictly decreasingstrictly increasingstrictly decreasing
Back pop testnums[dq[-1]] <= nums[right]nums[dq[-1]] >= nums[i]score[dq[-1]] <= score[i]
When the front leavesdq[0] < left, after left advancesdq[0] <= i - kdq[0] < i - k

The front test depends on how the problem defines its window. Sliding Window Maximum (LC 239) tracks the window's own left edge in an explicit left pointer: once the window holds more than k indices, left moves forward one step, and only then is the front tested against it, because testing it against left's old value misses the index that just fell behind. In Jump Game VI (LC 1696), the indices you can jump from are i - k to i - 1, so index i - k can still be used, and only indices below it leave. Constrained Subsequence Sum (LC 1425) uses the same deque as Jump Game VI (LC 1696). Shortest Subarray with Sum at Least K (LC 862) uses the same three rules on prefix sums, with its own test for when the front leaves.

🏆 The Analogy: A Leaderboard That Removes Everyone Who Can No Longer Win

Imagine a live leaderboard of "candidates for tallest person in the room." Every time a new person walks in, you don't just add them: you first remove everyone already on the leaderboard who is not taller than the newcomer, because they will leave the room first and can never be the tallest while the newcomer is still there. The leaderboard therefore stays sorted tallest-to-shortest from front to back, and the front is always, instantly, the tallest person in the room. When someone's time is up (they slide out of the window), you only need to check whether they are the person at the front.

🔑 The Key Idea in the Code

The deque stores indices so the front check can tell when the leader has left: once the window holds more than k indices, left moves forward one step, and the test dq[0] < left must run after that move, against left's new value, or the index that just fell behind stays at the front for one window too long. Each index is pushed once and popped at most once across the whole array, which keeps the total work at O(N) instead of O(N·k).

  • Popping the front on dq[0] < i - k

  • Writing the front cutoff as k - i instead of i - k

  • Storing values in dq instead of indices

  • Appending to res before the first window is full

  • Keeping the <= pop test for a window minimum

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

Pattern Recognition Signals

The 10-second spot

The statement moves a window of fixed width k one step at a time and asks for the largest value in every window. Every step adds one value and drops one, and each step needs the current maximum: rescanning costs k per step and a heap can't cheaply drop the value that left. A maximum over a window that slides is the Monotonic Deque.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

Decision at each right: pop the back while nums[dq[-1]] <= nums[right], push right, advance left with if right - left + 1 > k: left += 1, pop the front with if dq[0] < left: dq.popleft(), and read nums[dq[0]] once right - left + 1 == k.

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • left must advance before the front is tested: the test is against the window's new edge (gotcha 1 has the failing input).

  • dq.append(right): push the index, not the value (gotcha 4).

  • if right - left + 1 > k, not >= k: shrinking one step early collapses the window below size k forever (gotcha 2).

  • nums[dq[-1]] <= nums[right]: flip to >= only for a window minimum (gotcha 5).

The 60-Second Interview Pitch

Say this out loud before you type a single line

"I'll use a Monotonic Deque. I keep a deque of indices whose values strictly decrease from front to back, so the front is always the largest value still in the window, plus an explicit left pointer for the window's own left edge. For each index right, I first pop from the back every index whose value is not larger than nums of right: the newcomer is at least as big and stays in the window longer, so those values can never be a maximum again. I push right. The window can only ever be oversized by one index, since exactly one enters per step, so I advance left with a plain if once the window exceeds size k. Only then do I test the front against left: a value that already fell behind the window's new edge gets popped. Once the window holds exactly k elements, I append the front's value to the answer. The trap is that order: left must move before the front is tested, or an old maximum survives one window too long. Every index is pushed once and popped at most once, so it's O(N) time and O(K) space for the deque, output not counted."

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N)

Look at the code: for right in range(len(nums)) runs N times. Inside it, dq.append(right) is one push, if right - left + 1 > k: left += 1 and if dq and dq[0] < left: dq.popleft() are two checks and at most one pop, and if right - left + 1 == k with res.append(nums[dq[0]]) is one check and one append. The inner while dq and nums[dq[-1]] <= nums[right] can run many times in one step, but each dq.pop() removes an index that was appended earlier, and every index is appended exactly once, so over the whole run the while pops at most N times in total. That makes at most N appends, N pops (front and back together) and N failed while checks: O(N).

SPACE COMPLEXITY

O(K)

Look at the allocations: dq only holds indices inside the window nums[left .. right], so it never has more than K entries. res holds the N - K + 1 answers, which is the output and is counted separately. There is no recursion.

Formal Recurrence Relation

T(N)=N⏟appends+≤N⏟pops+N⏟failed while checks=O(N)T(N) = \underbrace{N}_{\text{appends}} + \underbrace{\le N}_{\text{pops}} + \underbrace{N}_{\text{failed while checks}} = O(N)T(N)=appendsN​​+pops≤N​​+failed while checksN​​=O(N)

Derivation Progression

Outer loop

for right in range(len(nums)): N passes, each with one append, one boundary check, one front check and one window check

Everything outside the inner while costs a constant amount per index.

Back pops, counted over the whole run

total dq.pop() + dq.popleft() <= total dq.append(right) = N

An index can only be popped after it was appended, and each index is appended once, so the inner while pops at most N times across all passes together, not N per pass.

Total

N + N + N = O(N)

Appends, pops and the one failing while check per pass add up to a constant times N.

Variable Definitions

NNN

len(nums), the number of values

KKK

k, the window size (1 <= K <= N)

Memory Architecture & Bounds

🟣 Call Stack

O(1): no recursion

🔵 Auxiliary Heap

O(K): dq holds only indices inside the current window

🟢 Output Space

O(N - K + 1): res, one maximum per window (output, not counted)

Boundary Best / Worst Cases

Best Case

O(N)O(N)O(N): strictly decreasing input, nothing is popped from the back and the front leaves once per step

Average Case

O(N)O(N)O(N)

Worst Case

O(N)O(N)O(N): strictly increasing input, each new value pops the one before it

Pointer Invariant Transition Progression

Pointer Invariant Transition Progression
Synthesizing vector architecture diagram...
Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "a frame exactly k elements wide", "write down the largest value inside it". Every step adds one value and drops one, and every step needs the current maximum: the maximum of a window that slides is the Monotonic Deque, not a rescan and not a heap.

CONSTRAINTS & BOUNDS

1≤N≤1051 \le N \le 10^51≤N≤105, 1≤k≤N1 \le k \le N1≤k≤N, values in [−104,104][-10^4, 10^4][−104,104]. Rescanning every window is N×kN \times kN×k, up to about 2.5×1092.5 \times 10^92.5×109 comparisons when k=N/2k = N/2k=N/2. A heap gives O(Nlog⁡k)O(N \log k)O(Nlogk) but cannot cheaply drop the value that just left, so it needs lazy deletion. The deque does at most 2N2N2N pushes and pops and holds at most kkk indices, so it also works on a stream where only the last kkk values are kept.

FAANG PRODUCTION TRAPS & EDGE CASES

On a time-based window (the last 5 minutes rather than the last k samples), several samples can expire at once, so the single front check becomes a while loop over expired timestamps. With samples arriving out of order, the deque's increasing-position order breaks, so late samples must be buffered or dropped before they reach it.

Core Algorithmic State Invariants

  1. Only the Front Can Be Too Old

dq holds indices in increasing order, so the oldest index still stored is always at the front, and the window nums[i - k + 1 .. i] loses exactly one index per step. That is why a single if suffices at the front, while the back needs a while: many older values can be dominated by one newcomer.

Theory Context•Two Pointers & Sliding Window
HardLC 239

Sliding Window Maximum (LeetCode 239)

Target Frequency:GoogleAmazonMetaApple

Take the integer array nums and look at it through a frame exactly k elements wide. The frame starts over the first k elements and moves right by one element at a time, until it covers the last k.

For every position of the frame, write down the largest value inside it. Return these maxima as a list, from the leftmost position of the frame to the rightmost: len(nums) - k + 1 values in all.

Worked Examples

Example 1
Input:nums = [1,3,-1,-3,5,3,6,7], k = 3
Output:[3,3,5,5,6,7]
1031-12-3354356677
Explanation: The six windows and their maxima: [1,3,-1] -> 3, [3,-1,-3] -> 3, [-1,-3,5] -> 5, [-3,5,3] -> 5, [5,3,6] -> 6, [3,6,7] -> 7.
Example 2
Input:nums = [1], k = 1
Output:[1]
10
Explanation: There is one window, [1], and its maximum is 1.

⚖️Formal Constraints & Bounds

  • 1 <= nums.length <= 105

  • -104 <= nums[i] <= 104

  • 1 <= k <= nums.length

Deep-Dive & Conceptual Insights

Core Insight

A value that arrives later and is at least as large makes every older, smaller-or-equal value useless: the newer one stays in the window at least as long. Dropping those from the back leaves indices whose values strictly decrease, so the front is always the window's maximum. The window can only ever be oversized by one index, so left advances with a plain if, and the front leaves exactly when dq[0] < left, tested only after left has already moved.

Real-World Scenario & Production Applications

Monitoring dashboards that plot the peak latency or the highest price of the last few minutes, updated every tick, and signal processing that follows the loudest sample of a moving block of audio.

Subproblems & Recurrence Decomposition3 Phases

🧩Subproblem 1: Pop What the Newcomer Beats, Then Push It

Every older value <= nums[right] can never be a window's maximum again, because nums[right] is at least as large and stays longer. Pop them from the back, then push right: the values in dq strictly decrease from front to back.

Mathematical Recurrence / Code Invariant
while dq and nums[dq[-1]] <= nums[right]:
    dq.pop()
dq.append(right)

Step-by-Step Execution Trace Table

rightnums[right]Back: pop while nums[dq[-1]] <= nums[right]dq after dq.append(right)left after the boundary checkFront: dq[0] < left?res.append(nums[dq[0]])
01—[0] (values 1)0no— (not size k yet)
13pop 0 (1 <= 3)[1] (values 3)0no— (not size k yet)
2-1—[1, 2] (values 3, -1)0no3
3-3—[1, 2, 3] (values 3, -1, -3)1no3
45pop 3 (-3 <= 5), 2 (-1 <= 5), 1 (3 <= 5)[4] (values 5)2no5
53—[4, 5] (values 5, 3)3no5
66pop 5 (3 <= 6), 4 (5 <= 6)[6] (values 6)4no6
77pop 6 (6 <= 7)[7] (values 7)5no7

The back-eviction loop happens to clear every stale index before the front check runs on this example, so the front check never fires here; see the card's first gotcha (nums = [9, 8, 7, 6, 5, 4], k = 2) for a trace where it does.

Code Shape
Archetype Code Shape
Python
1dq = deque()
2res = [] # indices, values strictly decreasing
3left = 0
4for right in range(len(nums)):
5 while dq and nums[dq[-1]] <= nums[right]:
6 dq.pop()
7 dq.append(right)
8 if right - left + 1 > k:
9 left += 1
10 if dq and dq[0] < left:
11 dq.popleft()
12 if right - left + 1 == k:
13 res.append(nums[dq[0]])
14return res

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

Re-scanning all k values of the window at every step is the obvious approach, and also the slow one. A heap would give the maximum faster, but it can't cheaply drop the value that just left the window. A monotonic deque keeps only the values that can still be a window's maximum, as indices whose values strictly decrease from front to back, so the answer is always at the front.

📐 What a Monotonic Deque Is

A monotonic deque is a double-ended queue of array indices. A double-ended queue can add and remove items at both ends in O(1) time; Python's collections.deque is one. Three rules hold after every step:

  1. Index order. The indices in dq increase from front to back. They are not always next to each other, because some indices were removed. The front is the oldest index, and the back is the newest.
  2. Value order. The values at those indices move in one direction only, with no two equal. For a maximum, they strictly decrease from front to back, so nums[dq[0]] is the largest. For a minimum, they strictly increase, so nums[dq[0]] is the smallest.
  3. Only candidates stay. Every index in dq can still be the answer for the current window or a later one. An index leaves in one of two ways:
    • From the back, when a newer index arrives whose value is at least as large (for a maximum) or at most as large (for a minimum). The newer index stays in every later window that the older index is in, so the older index can never be the answer again.
    • From the front, when the problem says the front index can no longer be used. In Sliding Window Maximum (LC 239), that happens once the window's own left pointer passes it: dq[0] < left, tested only after left has already advanced.

Each index enters dq once and leaves it at most once, so one pass over the array costs O(N).

🔁 The Same Deque Beyond the Window Maximum

The three rules stay the same. Three choices change from problem to problem.

What changesSliding Window Maximum (LC 239)Window minimumJump Game VI (LC 1696)
The values the indices point tonums[right]nums[i]score[i], the best total when standing on index i
Value order, front to backstrictly decreasingstrictly increasingstrictly decreasing
Back pop testnums[dq[-1]] <= nums[right]nums[dq[-1]] >= nums[i]score[dq[-1]] <= score[i]
When the front leavesdq[0] < left, after left advancesdq[0] <= i - kdq[0] < i - k

The front test depends on how the problem defines its window. Sliding Window Maximum (LC 239) tracks the window's own left edge in an explicit left pointer: once the window holds more than k indices, left moves forward one step, and only then is the front tested against it, because testing it against left's old value misses the index that just fell behind. In Jump Game VI (LC 1696), the indices you can jump from are i - k to i - 1, so index i - k can still be used, and only indices below it leave. Constrained Subsequence Sum (LC 1425) uses the same deque as Jump Game VI (LC 1696). Shortest Subarray with Sum at Least K (LC 862) uses the same three rules on prefix sums, with its own test for when the front leaves.

🏆 The Analogy: A Leaderboard That Removes Everyone Who Can No Longer Win

Imagine a live leaderboard of "candidates for tallest person in the room." Every time a new person walks in, you don't just add them: you first remove everyone already on the leaderboard who is not taller than the newcomer, because they will leave the room first and can never be the tallest while the newcomer is still there. The leaderboard therefore stays sorted tallest-to-shortest from front to back, and the front is always, instantly, the tallest person in the room. When someone's time is up (they slide out of the window), you only need to check whether they are the person at the front.

🔑 The Key Idea in the Code

The deque stores indices so the front check can tell when the leader has left: once the window holds more than k indices, left moves forward one step, and the test dq[0] < left must run after that move, against left's new value, or the index that just fell behind stays at the front for one window too long. Each index is pushed once and popped at most once across the whole array, which keeps the total work at O(N) instead of O(N·k).

  • Popping the front on dq[0] < i - k

  • Writing the front cutoff as k - i instead of i - k

  • Storing values in dq instead of indices

  • Appending to res before the first window is full

  • Keeping the <= pop test for a window minimum

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

Pattern Recognition Signals

The 10-second spot

The statement moves a window of fixed width k one step at a time and asks for the largest value in every window. Every step adds one value and drops one, and each step needs the current maximum: rescanning costs k per step and a heap can't cheaply drop the value that left. A maximum over a window that slides is the Monotonic Deque.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

Decision at each right: pop the back while nums[dq[-1]] <= nums[right], push right, advance left with if right - left + 1 > k: left += 1, pop the front with if dq[0] < left: dq.popleft(), and read nums[dq[0]] once right - left + 1 == k.

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • left must advance before the front is tested: the test is against the window's new edge (gotcha 1 has the failing input).

  • dq.append(right): push the index, not the value (gotcha 4).

  • if right - left + 1 > k, not >= k: shrinking one step early collapses the window below size k forever (gotcha 2).

  • nums[dq[-1]] <= nums[right]: flip to >= only for a window minimum (gotcha 5).

The 60-Second Interview Pitch

Say this out loud before you type a single line

"I'll use a Monotonic Deque. I keep a deque of indices whose values strictly decrease from front to back, so the front is always the largest value still in the window, plus an explicit left pointer for the window's own left edge. For each index right, I first pop from the back every index whose value is not larger than nums of right: the newcomer is at least as big and stays in the window longer, so those values can never be a maximum again. I push right. The window can only ever be oversized by one index, since exactly one enters per step, so I advance left with a plain if once the window exceeds size k. Only then do I test the front against left: a value that already fell behind the window's new edge gets popped. Once the window holds exactly k elements, I append the front's value to the answer. The trap is that order: left must move before the front is tested, or an old maximum survives one window too long. Every index is pushed once and popped at most once, so it's O(N) time and O(K) space for the deque, output not counted."

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N)

Look at the code: for right in range(len(nums)) runs N times. Inside it, dq.append(right) is one push, if right - left + 1 > k: left += 1 and if dq and dq[0] < left: dq.popleft() are two checks and at most one pop, and if right - left + 1 == k with res.append(nums[dq[0]]) is one check and one append. The inner while dq and nums[dq[-1]] <= nums[right] can run many times in one step, but each dq.pop() removes an index that was appended earlier, and every index is appended exactly once, so over the whole run the while pops at most N times in total. That makes at most N appends, N pops (front and back together) and N failed while checks: O(N).

SPACE COMPLEXITY

O(K)

Look at the allocations: dq only holds indices inside the window nums[left .. right], so it never has more than K entries. res holds the N - K + 1 answers, which is the output and is counted separately. There is no recursion.

Formal Recurrence Relation

T(N)=N⏟appends+≤N⏟pops+N⏟failed while checks=O(N)T(N) = \underbrace{N}_{\text{appends}} + \underbrace{\le N}_{\text{pops}} + \underbrace{N}_{\text{failed while checks}} = O(N)T(N)=appendsN​​+pops≤N​​+failed while checksN​​=O(N)

Derivation Progression

Outer loop

for right in range(len(nums)): N passes, each with one append, one boundary check, one front check and one window check

Everything outside the inner while costs a constant amount per index.

Back pops, counted over the whole run

total dq.pop() + dq.popleft() <= total dq.append(right) = N

An index can only be popped after it was appended, and each index is appended once, so the inner while pops at most N times across all passes together, not N per pass.

Total

N + N + N = O(N)

Appends, pops and the one failing while check per pass add up to a constant times N.

Variable Definitions

NNN

len(nums), the number of values

KKK

k, the window size (1 <= K <= N)

Memory Architecture & Bounds

🟣 Call Stack

O(1): no recursion

🔵 Auxiliary Heap

O(K): dq holds only indices inside the current window

🟢 Output Space

O(N - K + 1): res, one maximum per window (output, not counted)

Boundary Best / Worst Cases

Best Case

O(N)O(N)O(N): strictly decreasing input, nothing is popped from the back and the front leaves once per step

Average Case

O(N)O(N)O(N)

Worst Case

O(N)O(N)O(N): strictly increasing input, each new value pops the one before it

Pointer Invariant Transition Progression

Pointer Invariant Transition Progression
Synthesizing vector architecture diagram...
Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "a frame exactly k elements wide", "write down the largest value inside it". Every step adds one value and drops one, and every step needs the current maximum: the maximum of a window that slides is the Monotonic Deque, not a rescan and not a heap.

CONSTRAINTS & BOUNDS

1≤N≤1051 \le N \le 10^51≤N≤105, 1≤k≤N1 \le k \le N1≤k≤N, values in [−104,104][-10^4, 10^4][−104,104]. Rescanning every window is N×kN \times kN×k, up to about 2.5×1092.5 \times 10^92.5×109 comparisons when k=N/2k = N/2k=N/2. A heap gives O(Nlog⁡k)O(N \log k)O(Nlogk) but cannot cheaply drop the value that just left, so it needs lazy deletion. The deque does at most 2N2N2N pushes and pops and holds at most kkk indices, so it also works on a stream where only the last kkk values are kept.

FAANG PRODUCTION TRAPS & EDGE CASES

On a time-based window (the last 5 minutes rather than the last k samples), several samples can expire at once, so the single front check becomes a while loop over expired timestamps. With samples arriving out of order, the deque's increasing-position order breaks, so late samples must be buffered or dropped before they reach it.

Core Algorithmic State Invariants

  1. Only the Front Can Be Too Old

dq holds indices in increasing order, so the oldest index still stored is always at the front, and the window nums[i - k + 1 .. i] loses exactly one index per step. That is why a single if suffices at the front, while the back needs a while: many older values can be dominated by one newcomer.

© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•