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•Stack

Builds on Next Greater Element, which you haven’t finished yet.

P3MediumBuilt on Next Greater Element~3 min speedrun

Monotonic Stack Greedy (Remove K Digits, Remove Duplicate Letters)

Built on
Next Greater ElementA pop is paid for by a budget of removals, or blocked because the popped item appears again later.
The problem

Remove K Digits (LC 402)(opens leetcode.com)

You are given a non-negative whole number written as a string of decimal digits, num, and a count k. Delete exactly k of its digits, leaving the others where they are relative to each other, so that the number that is left is as small as possible.

Return that number as a string, written the usual way: without leading zeros, and as "0" when no digit is left or only zeros are.

Example 1
Input:num = "1432219", k = 3
Output:"1219"
Deleting the 4, the 3 and one of the 2s leaves 1219; no other choice of three digits leaves a smaller number.
10413223241596removedremovedremoved

Lexicographic Stack

digits or letters ➔ pop a bigger stack top while deletions remain ➔ smallest result
Spot it when: You must delete characters from a sequence (a fixed number of them, or every repeated copy) and keep the rest in their order, so that what is left is the smallest (or largest) possible number or string.
Lexicographic Stack
Python
1def remove_k_digits(num: str, k: int) -> str:
2 """Remove K Digits: pop a bigger digit when a smaller one arrives, while removals remain.
3 Time: O(N) | Space: O(N)
4 """
5 stack: list[str] = [] # Digits kept so far, never decreasing while removals remain
6 
7 for digit in num:
8 # 1. Pop A Bigger Digit While Removals Remain:
9 # A bigger digit right before a smaller one is always worth deleting: it moves the smaller digit
10 # into an earlier place, and no later removal can lower that place more, so the pop never needs undoing.
11 while k > 0 and stack and stack[-1] > digit:
12 stack.pop()
13 k -= 1
14 stack.append(digit)
15 
16 # 2. Spend Any Removals Left Over On The Tail:
17 # With removals left, stack never decreased, so its last digits are its biggest; len(stack) - k keeps
18 # the slice correct even when k is 0.
19 stack = stack[:len(stack) - k]
20 
21 # 3. Write The Result As A Number:
22 # A number has no leading zeros, and no digits at all means the number is 0.
23 result = "".join(stack).lstrip("0")
24 return result or "0"
Invariant: While k > 0, the digits in stack never decrease from bottom to top, and every digit popped was bigger than a digit that came after it.

The leftmost place that changes decides which of two numbers is smaller, so walk num once with a stack of kept digits: while removals remain (k > 0) and stack[-1] > digit, pop the bigger digit and spend one removal, then push digit. A bigger digit right before a smaller one is the best removal there is: deleting it puts a smaller digit in the earliest place that can still change, and no later removal can lower that place more, so a pop never has to be undone. While removals remain, every push keeps stack from decreasing, so removals left over after the loop come off the end, stack[:len(stack) - k], where the biggest digits sit. The trap is how a number is written: strip the leading zeros with "".join(stack).lstrip("0") and return "0" when nothing is left, because popping the 1 of "10200" leaves "0200", which is the number 200.

See it run

Remove K Digits, step by step

k 2 · stack empty

k 2 · stack 1

k 1 · stack empty

k 1 · stack 0

k 1 · stack 0 2

k 1 · stack 0 2 3

k 1 · stack 0 2 3 4

k 1 · stack 0 2 3

k 0 · stack 0 2 3 · result 23

k 0 · stack 0 2 3 · answer 23

The top row is num. The bottom row is stack, filled from column 0, with the bottom of the stack on the left. Empty slots are unused.

  • digit the digit of num that the loop is reading
  • pop a bigger digit taken off the top of stack, which costs one removal
  • push the digit that was just pushed onto stack
  • cut a digit of the tail that is removed because k is still above 0
  • strip a leading zero that lstrip removes

Start: num = "10234" and k = 2. stack is empty.

Push 1. stack is empty, so there is nothing to pop.

stack[-1] = 1 is bigger than digit 0, and k was 2. Pop 1, so k becomes 1.

Push 0. stack is empty, so there is nothing to pop.

Push 2. stack[-1] = 0 is not bigger than 2, so nothing is popped.

Push 3. stack[-1] = 2 is not bigger than 3, so nothing is popped.

Push 4. stack[-1] = 3 is not bigger than 4, so nothing is popped.

k = 1 removal is left. Cut the last 1 digit of stack: 4.

The kept digits read "023". lstrip("0") removes the leading 0, so the result is "23".

Done: remove_k_digits("10234", 2) returns "23".

num
01
10
22
33
44
stack
1 / 10
Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Returning the kept digits with leading zeros

    Mistake
    result = "".join(stack)
    Fix
    result = "".join(stack).lstrip("0")

    What happens: Removing a digit can leave a zero at the front. For "10200" with k = 1, the loop pops the 1, and the kept digits read "0200". A number is written without leading zeros. lstrip("0") removes them.

    Example: num = "10200", k = 1
    Mistake: returns '0200'Fix: returns '200'
  2. 2

    Gotcha 2: Popping with if instead of while

    Mistake
    if k > 0 and stack and stack[-1] > digit:
    Fix
    while k > 0 and stack and stack[-1] > digit:

    What happens: One small digit may need to remove several bigger digits before it. With if, each new digit pops at most one digit. For "9991" with k = 3, the 1 pops one 9. The other two removals are spent on the tail, and the 1 is cut off.

    Example: num = "9991", k = 3
    Mistake: returns '9'Fix: returns '1'
  3. 3

    Gotcha 3: Popping on stack[-1] >= digit

    Mistake
    while k > 0 and stack and stack[-1] >= digit:
    Fix
    while k > 0 and stack and stack[-1] > digit:

    What happens: Removing a digit that equals the next digit does not make the number smaller at that place. It only spends a removal. For "1121" with k = 1, >= spends the removal on the first 1. The larger 2 then stays.

    Example: num = "1121", k = 1
    Mistake: returns '121'Fix: returns '111'
  4. 4

    Gotcha 4: Cutting the tail with stack[:-k]

    Mistake
    stack = stack[:-k]
    Fix
    stack = stack[:len(stack) - k]

    What happens: When num already rises, the loop pops nothing, and the removals left over come off the end. That part works with both forms. But when k is 0 after the loop, stack[:-0] is stack[:0], which is empty. Every kept digit is lost.

    Example: num = "21", k = 1
    Mistake: returns '0'Fix: returns '1'
  5. 5

    Gotcha 5: Returning an empty string when no digit is left

    Mistake
    return result
    Fix
    return result or "0"

    What happens: When every digit is removed, or only zeros are left, result is the empty string "". The number is then 0. The function must return "0", not "".

    Example: num = "10", k = 2
    Mistake: returns ''Fix: returns '0'
Not this one if…
  • Every item needs its own answer, the first larger item after it, as in Daily Temperatures (LC 739): nothing is deleted and there is no budget of removals, so a pop records an answer instead of shrinking the result. See Next Greater Element

Deep Dive

What a pop costs here, and why it is correct

This card keeps the stack from Next Greater Element and changes what a pop means. In Next Greater Element, a larger item arrives and every smaller item on top of stack is popped for free. Here, a pop deletes a digit from the answer, so each pop uses one removal from the budget k. When k is 0, no digit can be popped, even if it is bigger than the new digit.

Why pop a bigger digit when a smaller digit arrives? Two numbers with the same number of digits are compared from the left. The first place where they differ decides which one is smaller. Suppose stack[-1] is bigger than digit. Deleting stack[-1] moves digit into that place, and a smaller digit now sits in the earliest place that can still change. No other removal can lower that place more, so the pop never has to be undone.

The pop uses > and not >=. If stack[-1] equals digit, deleting it changes nothing at that place. It only spends one removal. For "1121" with k = 1, the first two 1 digits are not popped, because each one equals the digit after it. The 2 is popped when the last 1 arrives, and the answer is "111". With >=, the second 1 would be popped instead, and the answer would be "121".

Example: in Remove K Digits (LC 402), num = "1432219" and k = 3 give "1219". The 4 is popped by the 3, the 3 is popped by the first 2, and the second 2 is popped by the 1. That is three pops, so k reaches 0. Then the 9 is pushed.

One trace: the leftover removals and the leading zero

The board replays num = "10234" with k = 2. The top row is num. The bottom row is stack.

The digit 0 arrives while stack holds 1. The 1 is bigger, so it is popped and k becomes 1. Then 0, 2, 3 and 4 are pushed. No pop is possible for them, because each new digit is not smaller than the top.

After the loop, k is still 1. The tail step cuts the last digit with stack[:len(stack) - k]. The digits in stack rise from left to right, so the last digit is the biggest. stack becomes 0 2 3.

The kept digits read "023". A number does not start with a zero, so lstrip("0") removes it and the result is "23". This is the step marked as the trap on the board. Every other choice of two digits to delete gives a larger number. For example, deleting 1 and 0 gives "234".

If every digit is removed or stripped, the result is the empty string. result or "0" returns "0" instead. For "10" with k = 2, the answer is "0".

A second reason to block or allow a pop

Some problems have no budget k. Instead, a pop is allowed only when the popped item appears again later, so removing it now loses nothing. This is the other rule that the card adds on top of Next Greater Element.

Remove Duplicate Letters (LC 316) keeps one copy of each letter in the smallest order. Store last[c], the last index of each letter in s. At index i, skip the letter if it is already in stack. Otherwise pop stack[-1] while stack[-1] > c and last[stack[-1]] > i, then push c.

Example: for s = "bcabc", the letter a at index 2 pops c, because last["c"] is 4. It then pops b, because last["b"] is 3. Both letters come back later. The stack ends as "abc". Without the last test, the loop would also pop a letter that never comes back, and that letter would be lost.

Find the Most Competitive Subsequence (LC 1673) uses the digit rule with a different budget. The number of items that may be removed is len(nums) - k, and the stack may not grow past k items. For nums = [3, 5, 2, 6] and k = 2, the answer is [2, 6].

The cost, and the budget when k is large

Every digit is pushed once and popped at most once. The tail cut, join and lstrip each pass over stack once. So the time is O(N) for N digits, and stack holds at most N digits, so the space is O(N).

The inner while loop does not make the time O(N^2). The total number of pops over the whole run can never be more than the number of pushes, which is N.

When num already rises, as in "123456789" with k = 3, the loop pops nothing. The three removals are spent on the tail, and the answer is "123456". When num falls, as in "9991" with k = 3, the 1 pops three 9 digits and the answer is "1".

Use stack[:len(stack) - k] and not stack[:-k]. When k is 0 after the loop, stack[:-0] is stack[:0], which is empty, and every kept digit is lost.

60-Second Pitch

"Two numbers of the same length compare from the left, so the first place that differs decides. I walk num once with a stack of kept digits. When a smaller digit arrives and the top is bigger, I pop the top and spend one removal from k, because that puts a smaller digit in the earliest place that can change. This is the Next Greater Element stack with a budget. A pop never has to be undone. If removals are left after the loop, I cut them from the end, where the biggest digits are. Each digit is pushed once and popped at most once, so the time is O(N) and the space is O(N). Last, I strip the leading zeros and return "0" if nothing is left."

Common Questions

Why is it correct to pop the bigger digit as soon as a smaller digit arrives?

The earliest place that differs decides which number is smaller. A bigger digit directly before a smaller digit is in that early place. Deleting it moves the smaller digit there, and no later removal can lower that place more. So the pop never needs to be undone.

How is this different from Next Greater Element?

The stack and the pop loop look the same. In Next Greater Element, a pop is free and gives the popped item its answer. Here a pop deletes a digit, so it costs one removal from k. The loop also stops popping when k is 0.

What do you do with the removals that are left after the loop?

Cut them from the end of stack. If removals remain, no pop happened for them, so stack rises from left to right and its last digits are the biggest. Use stack[:len(stack) - k], which is also correct when k is 0.

Why strip leading zeros, and what if nothing is left?

The answer is a number, and a number is written without leading zeros. Popping the 1 of "10200" leaves "0200", which is the number 200. If the result is empty, return "0".

How does Remove Duplicate Letters (LC 316) change the rule?

There is no budget k. A pop is allowed when stack[-1] > c and the popped letter appears again later, which last[stack[-1]] > i tests. A letter that is already in stack is skipped. The stack shape and the O(N) cost stay the same.

SWE Reasoning

Spot it

  • You must delete a fixed number of characters, or all repeated copies, and keep the others in their original order.
  • The result must be the smallest or the largest possible number or string.
  • Two choices of the same length are compared from the left, so the earliest place that differs decides the result.
  • Trying every set of removals would take exponential time, but each decision depends only on the last kept character and the next one.
  • The statement says the result is a number, so leading zeros and an empty result need a rule.

The invariant, step by step

  1. stack starts empty, and k is the number of removals still allowed.
  2. For each digit in num, the loop pops stack[-1] while k > 0 and stack[-1] > digit. Each pop lowers k by 1.
  3. The loop then pushes digit. The digits in stack never decrease from bottom to top, as long as removals remain.
  4. When the loop ends, any removals left over come off the end, because the last digits of a rising stack are the biggest. stack[:len(stack) - k] does this, and it is also correct when k is 0.
  5. "".join(stack).lstrip("0") writes the number without leading zeros, and result or "0" handles the empty result.

Traps

  • Leading zeros. For "10200" with k = 1, the loop pops the 1, and the kept digits read "0200". The answer must be "200", so strip the zeros.
  • Empty result. For "10" with k = 2, nothing is left. Return "0", not the empty string.
  • Pop with if. One small digit may need to pop several bigger digits. For "9991" with k = 3, a single pop leaves "9" instead of "1".
  • Pop on >=. An equal digit does not lower the number at that place. For "1121" with k = 1, popping the equal 1 gives "121", not "111".
  • Tail cut with stack[:-k]. When k is 0 this returns an empty list. Use stack[:len(stack) - k].
  • Wrong blocker in Remove Duplicate Letters (LC 316). Without the last[stack[-1]] > i test, a letter that never appears again is popped and lost.

Practice

Canonical LeetCode:Remove K Digits (LC 402)Remove Duplicate Letters (LC 316)Smallest Subsequence of Distinct Characters (LC 1081)Find the Most Competitive Subsequence (LC 1673)
Theory Context•Stack

Builds on Next Greater Element, which you haven’t finished yet.

P3MediumBuilt on Next Greater Element~3 min speedrun

Monotonic Stack Greedy (Remove K Digits, Remove Duplicate Letters)

Built on
Next Greater ElementA pop is paid for by a budget of removals, or blocked because the popped item appears again later.
The problem

Remove K Digits (LC 402)(opens leetcode.com)

You are given a non-negative whole number written as a string of decimal digits, num, and a count k. Delete exactly k of its digits, leaving the others where they are relative to each other, so that the number that is left is as small as possible.

Return that number as a string, written the usual way: without leading zeros, and as "0" when no digit is left or only zeros are.

Example 1
Input:num = "1432219", k = 3
Output:"1219"
Deleting the 4, the 3 and one of the 2s leaves 1219; no other choice of three digits leaves a smaller number.
10413223241596removedremovedremoved

Lexicographic Stack

digits or letters ➔ pop a bigger stack top while deletions remain ➔ smallest result
Spot it when: You must delete characters from a sequence (a fixed number of them, or every repeated copy) and keep the rest in their order, so that what is left is the smallest (or largest) possible number or string.
Lexicographic Stack
Python
1def remove_k_digits(num: str, k: int) -> str:
2 """Remove K Digits: pop a bigger digit when a smaller one arrives, while removals remain.
3 Time: O(N) | Space: O(N)
4 """
5 stack: list[str] = [] # Digits kept so far, never decreasing while removals remain
6 
7 for digit in num:
8 # 1. Pop A Bigger Digit While Removals Remain:
9 # A bigger digit right before a smaller one is always worth deleting: it moves the smaller digit
10 # into an earlier place, and no later removal can lower that place more, so the pop never needs undoing.
11 while k > 0 and stack and stack[-1] > digit:
12 stack.pop()
13 k -= 1
14 stack.append(digit)
15 
16 # 2. Spend Any Removals Left Over On The Tail:
17 # With removals left, stack never decreased, so its last digits are its biggest; len(stack) - k keeps
18 # the slice correct even when k is 0.
19 stack = stack[:len(stack) - k]
20 
21 # 3. Write The Result As A Number:
22 # A number has no leading zeros, and no digits at all means the number is 0.
23 result = "".join(stack).lstrip("0")
24 return result or "0"
Invariant: While k > 0, the digits in stack never decrease from bottom to top, and every digit popped was bigger than a digit that came after it.

The leftmost place that changes decides which of two numbers is smaller, so walk num once with a stack of kept digits: while removals remain (k > 0) and stack[-1] > digit, pop the bigger digit and spend one removal, then push digit. A bigger digit right before a smaller one is the best removal there is: deleting it puts a smaller digit in the earliest place that can still change, and no later removal can lower that place more, so a pop never has to be undone. While removals remain, every push keeps stack from decreasing, so removals left over after the loop come off the end, stack[:len(stack) - k], where the biggest digits sit. The trap is how a number is written: strip the leading zeros with "".join(stack).lstrip("0") and return "0" when nothing is left, because popping the 1 of "10200" leaves "0200", which is the number 200.

See it run

Remove K Digits, step by step

k 2 · stack empty

k 2 · stack 1

k 1 · stack empty

k 1 · stack 0

k 1 · stack 0 2

k 1 · stack 0 2 3

k 1 · stack 0 2 3 4

k 1 · stack 0 2 3

k 0 · stack 0 2 3 · result 23

k 0 · stack 0 2 3 · answer 23

The top row is num. The bottom row is stack, filled from column 0, with the bottom of the stack on the left. Empty slots are unused.

  • digit the digit of num that the loop is reading
  • pop a bigger digit taken off the top of stack, which costs one removal
  • push the digit that was just pushed onto stack
  • cut a digit of the tail that is removed because k is still above 0
  • strip a leading zero that lstrip removes

Start: num = "10234" and k = 2. stack is empty.

Push 1. stack is empty, so there is nothing to pop.

stack[-1] = 1 is bigger than digit 0, and k was 2. Pop 1, so k becomes 1.

Push 0. stack is empty, so there is nothing to pop.

Push 2. stack[-1] = 0 is not bigger than 2, so nothing is popped.

Push 3. stack[-1] = 2 is not bigger than 3, so nothing is popped.

Push 4. stack[-1] = 3 is not bigger than 4, so nothing is popped.

k = 1 removal is left. Cut the last 1 digit of stack: 4.

The kept digits read "023". lstrip("0") removes the leading 0, so the result is "23".

Done: remove_k_digits("10234", 2) returns "23".

num
01
10
22
33
44
stack
1 / 10
Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Returning the kept digits with leading zeros

    Mistake
    result = "".join(stack)
    Fix
    result = "".join(stack).lstrip("0")

    What happens: Removing a digit can leave a zero at the front. For "10200" with k = 1, the loop pops the 1, and the kept digits read "0200". A number is written without leading zeros. lstrip("0") removes them.

    Example: num = "10200", k = 1
    Mistake: returns '0200'Fix: returns '200'
  2. 2

    Gotcha 2: Popping with if instead of while

    Mistake
    if k > 0 and stack and stack[-1] > digit:
    Fix
    while k > 0 and stack and stack[-1] > digit:

    What happens: One small digit may need to remove several bigger digits before it. With if, each new digit pops at most one digit. For "9991" with k = 3, the 1 pops one 9. The other two removals are spent on the tail, and the 1 is cut off.

    Example: num = "9991", k = 3
    Mistake: returns '9'Fix: returns '1'
  3. 3

    Gotcha 3: Popping on stack[-1] >= digit

    Mistake
    while k > 0 and stack and stack[-1] >= digit:
    Fix
    while k > 0 and stack and stack[-1] > digit:

    What happens: Removing a digit that equals the next digit does not make the number smaller at that place. It only spends a removal. For "1121" with k = 1, >= spends the removal on the first 1. The larger 2 then stays.

    Example: num = "1121", k = 1
    Mistake: returns '121'Fix: returns '111'
  4. 4

    Gotcha 4: Cutting the tail with stack[:-k]

    Mistake
    stack = stack[:-k]
    Fix
    stack = stack[:len(stack) - k]

    What happens: When num already rises, the loop pops nothing, and the removals left over come off the end. That part works with both forms. But when k is 0 after the loop, stack[:-0] is stack[:0], which is empty. Every kept digit is lost.

    Example: num = "21", k = 1
    Mistake: returns '0'Fix: returns '1'
  5. 5

    Gotcha 5: Returning an empty string when no digit is left

    Mistake
    return result
    Fix
    return result or "0"

    What happens: When every digit is removed, or only zeros are left, result is the empty string "". The number is then 0. The function must return "0", not "".

    Example: num = "10", k = 2
    Mistake: returns ''Fix: returns '0'
Not this one if…
  • Every item needs its own answer, the first larger item after it, as in Daily Temperatures (LC 739): nothing is deleted and there is no budget of removals, so a pop records an answer instead of shrinking the result. See Next Greater Element

Deep Dive

What a pop costs here, and why it is correct

This card keeps the stack from Next Greater Element and changes what a pop means. In Next Greater Element, a larger item arrives and every smaller item on top of stack is popped for free. Here, a pop deletes a digit from the answer, so each pop uses one removal from the budget k. When k is 0, no digit can be popped, even if it is bigger than the new digit.

Why pop a bigger digit when a smaller digit arrives? Two numbers with the same number of digits are compared from the left. The first place where they differ decides which one is smaller. Suppose stack[-1] is bigger than digit. Deleting stack[-1] moves digit into that place, and a smaller digit now sits in the earliest place that can still change. No other removal can lower that place more, so the pop never has to be undone.

The pop uses > and not >=. If stack[-1] equals digit, deleting it changes nothing at that place. It only spends one removal. For "1121" with k = 1, the first two 1 digits are not popped, because each one equals the digit after it. The 2 is popped when the last 1 arrives, and the answer is "111". With >=, the second 1 would be popped instead, and the answer would be "121".

Example: in Remove K Digits (LC 402), num = "1432219" and k = 3 give "1219". The 4 is popped by the 3, the 3 is popped by the first 2, and the second 2 is popped by the 1. That is three pops, so k reaches 0. Then the 9 is pushed.

One trace: the leftover removals and the leading zero

The board replays num = "10234" with k = 2. The top row is num. The bottom row is stack.

The digit 0 arrives while stack holds 1. The 1 is bigger, so it is popped and k becomes 1. Then 0, 2, 3 and 4 are pushed. No pop is possible for them, because each new digit is not smaller than the top.

After the loop, k is still 1. The tail step cuts the last digit with stack[:len(stack) - k]. The digits in stack rise from left to right, so the last digit is the biggest. stack becomes 0 2 3.

The kept digits read "023". A number does not start with a zero, so lstrip("0") removes it and the result is "23". This is the step marked as the trap on the board. Every other choice of two digits to delete gives a larger number. For example, deleting 1 and 0 gives "234".

If every digit is removed or stripped, the result is the empty string. result or "0" returns "0" instead. For "10" with k = 2, the answer is "0".

A second reason to block or allow a pop

Some problems have no budget k. Instead, a pop is allowed only when the popped item appears again later, so removing it now loses nothing. This is the other rule that the card adds on top of Next Greater Element.

Remove Duplicate Letters (LC 316) keeps one copy of each letter in the smallest order. Store last[c], the last index of each letter in s. At index i, skip the letter if it is already in stack. Otherwise pop stack[-1] while stack[-1] > c and last[stack[-1]] > i, then push c.

Example: for s = "bcabc", the letter a at index 2 pops c, because last["c"] is 4. It then pops b, because last["b"] is 3. Both letters come back later. The stack ends as "abc". Without the last test, the loop would also pop a letter that never comes back, and that letter would be lost.

Find the Most Competitive Subsequence (LC 1673) uses the digit rule with a different budget. The number of items that may be removed is len(nums) - k, and the stack may not grow past k items. For nums = [3, 5, 2, 6] and k = 2, the answer is [2, 6].

The cost, and the budget when k is large

Every digit is pushed once and popped at most once. The tail cut, join and lstrip each pass over stack once. So the time is O(N) for N digits, and stack holds at most N digits, so the space is O(N).

The inner while loop does not make the time O(N^2). The total number of pops over the whole run can never be more than the number of pushes, which is N.

When num already rises, as in "123456789" with k = 3, the loop pops nothing. The three removals are spent on the tail, and the answer is "123456". When num falls, as in "9991" with k = 3, the 1 pops three 9 digits and the answer is "1".

Use stack[:len(stack) - k] and not stack[:-k]. When k is 0 after the loop, stack[:-0] is stack[:0], which is empty, and every kept digit is lost.

60-Second Pitch

"Two numbers of the same length compare from the left, so the first place that differs decides. I walk num once with a stack of kept digits. When a smaller digit arrives and the top is bigger, I pop the top and spend one removal from k, because that puts a smaller digit in the earliest place that can change. This is the Next Greater Element stack with a budget. A pop never has to be undone. If removals are left after the loop, I cut them from the end, where the biggest digits are. Each digit is pushed once and popped at most once, so the time is O(N) and the space is O(N). Last, I strip the leading zeros and return "0" if nothing is left."

Common Questions

Why is it correct to pop the bigger digit as soon as a smaller digit arrives?

The earliest place that differs decides which number is smaller. A bigger digit directly before a smaller digit is in that early place. Deleting it moves the smaller digit there, and no later removal can lower that place more. So the pop never needs to be undone.

How is this different from Next Greater Element?

The stack and the pop loop look the same. In Next Greater Element, a pop is free and gives the popped item its answer. Here a pop deletes a digit, so it costs one removal from k. The loop also stops popping when k is 0.

What do you do with the removals that are left after the loop?

Cut them from the end of stack. If removals remain, no pop happened for them, so stack rises from left to right and its last digits are the biggest. Use stack[:len(stack) - k], which is also correct when k is 0.

Why strip leading zeros, and what if nothing is left?

The answer is a number, and a number is written without leading zeros. Popping the 1 of "10200" leaves "0200", which is the number 200. If the result is empty, return "0".

How does Remove Duplicate Letters (LC 316) change the rule?

There is no budget k. A pop is allowed when stack[-1] > c and the popped letter appears again later, which last[stack[-1]] > i tests. A letter that is already in stack is skipped. The stack shape and the O(N) cost stay the same.

SWE Reasoning

Spot it

  • You must delete a fixed number of characters, or all repeated copies, and keep the others in their original order.
  • The result must be the smallest or the largest possible number or string.
  • Two choices of the same length are compared from the left, so the earliest place that differs decides the result.
  • Trying every set of removals would take exponential time, but each decision depends only on the last kept character and the next one.
  • The statement says the result is a number, so leading zeros and an empty result need a rule.

The invariant, step by step

  1. stack starts empty, and k is the number of removals still allowed.
  2. For each digit in num, the loop pops stack[-1] while k > 0 and stack[-1] > digit. Each pop lowers k by 1.
  3. The loop then pushes digit. The digits in stack never decrease from bottom to top, as long as removals remain.
  4. When the loop ends, any removals left over come off the end, because the last digits of a rising stack are the biggest. stack[:len(stack) - k] does this, and it is also correct when k is 0.
  5. "".join(stack).lstrip("0") writes the number without leading zeros, and result or "0" handles the empty result.

Traps

  • Leading zeros. For "10200" with k = 1, the loop pops the 1, and the kept digits read "0200". The answer must be "200", so strip the zeros.
  • Empty result. For "10" with k = 2, nothing is left. Return "0", not the empty string.
  • Pop with if. One small digit may need to pop several bigger digits. For "9991" with k = 3, a single pop leaves "9" instead of "1".
  • Pop on >=. An equal digit does not lower the number at that place. For "1121" with k = 1, popping the equal 1 gives "121", not "111".
  • Tail cut with stack[:-k]. When k is 0 this returns an empty list. Use stack[:len(stack) - k].
  • Wrong blocker in Remove Duplicate Letters (LC 316). Without the last[stack[-1]] > i test, a letter that never appears again is popped and lost.

Practice

Canonical LeetCode:Remove K Digits (LC 402)Remove Duplicate Letters (LC 316)Smallest Subsequence of Distinct Characters (LC 1081)Find the Most Competitive Subsequence (LC 1673)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•