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).

12 Core Algorithmic Patterns & 168 Practice Problems

  • 1. Two Pointers (9 Paradigms, 32 Problems): Converging pointers, sorted pair sums, container with most water, trapping rain water, 3Sum, plus the Sliding Window and Fast & Slow Pointers paradigms (Floyd cycle detection, monotonic window invariants, longest substrings, minimum window).
  • 2. Binary Search (8 Paradigms, 12 Problems): Monotonic predicate partitioning, boundary searching, rotated arrays, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 8 Problems): Bitmasking, XOR tricks, counting set bits, subset enumeration via bitmasks.
  • 4. Math & Geometry (5 Paradigms, 10 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (8 Paradigms, 17 Problems): Path sums, lowest common ancestor, tree diameter, subtree serialization, validating BSTs.
  • 6. Tree/Graph Breadth-First Search (4 Paradigms, 11 Problems): Level-order traversals, shortest path, rotting oranges, word ladders.
  • 7. Graphs (6 Paradigms, 14 Problems): Topological sort, cycle detection, Dijkstra, bipartite validation, network delay.
  • 8. Backtracking (5 Paradigms, 13 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens.
  • 9. Dynamic Programming (7 Paradigms, 14 Problems): Memoization vs tabulation, knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (8 Paradigms, 10 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (5 Paradigms, 13 Problems): Trie, Union-Find, LRU Cache, LFU Cache, Monotonic Stacks.
  • 12. Intervals & Stack / Miscellaneous (7 Paradigms, 14 Problems): Merge intervals, daily temperatures, largest rectangle in histogram, trapping rain water via stack.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Multi-language Rosetta Stone contrasting abstract invariants with concrete solutions across Python, C#, Java, TypeScript, C++, Go, and Rust.
  2. Stage 2 (Active Recall): Reconstruct algorithmic template invariants from memory with real-time feedback before looking at solutions.
  3. Stage 3 (Senior SWE AI Mock Coach): Simulated senior mock interview evaluating Big-O space/time tradeoffs, edge cases, and code reviews in Monaco Editor.
  4. Stage 4 (Solve on Your Own): Timed sandbox challenges verified against automated test suites in Python, C#, Java, and TypeScript.

Equipped with SM-2 Spaced Repetition Review Hub, Studio Cockpit workspace layout, and interactive study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Compute Coins: 40 free coins upon signup, +20 daily login bonus, +25 referral bonus.
  • 24-Hour AI Coaching Pass: 5 compute coins unlocks unlimited senior SWE AI coaching for a full 24 hours.
  • BYOK (Bring Your Own Key): Completely free unlimited AI coaching if using your own Gemini/OpenAI API key.
  • Refund & Subscription Policy: No recurring charges, no subscription traps, and no paid paywalls. Free tier provides full learning path.
  • Platform Operator: Hi👋WebEnterprise Inc. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

180Items
Theory Context•Miscellaneous & Sweeps
MediumLC 402

Remove K Digits (LeetCode 402)

You will see how one stack that pops bigger digits turns "delete k digits" into a single left-to-right pass.

Target Frequency:AmazonGoogleMicrosoft

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.

Worked Examples

Example 1
Input:num = "1432219", k = 3
Output:"1219"
10413223241596removedremovedremoved
Explanation: Deleting the `4`, the `3` and one of the `2`s leaves `1219`; no other choice of three digits leaves a smaller number.
Example 2
Input:num = "10200", k = 1
Output:"200"
1001220304removed
Explanation: Deleting the leading `1` leaves the digits `0200`, which is the number `200`: the answer is written without its leading zero.
Example 3
Input:num = "10", k = 2
Output:"0"
1001removedremoved
Explanation: Both digits are deleted. Nothing is left, and an empty number counts as `0`.

⚖️Formal Constraints & Bounds

  • 1 <= k <= num.length <= 105

  • num consists of only digits.

  • num does not have any leading zeros except for the zero itself.

Deep-Dive & Conceptual Insights

Why It Works & Core Invariant

The leftmost place that changes decides which number is smaller, so a bigger digit sitting right before a smaller one is always worth deleting: a stack pops such digits while removals remain, and any removals left over come off the end.

Real-World Scenario & Production Applications

Identifiers, version strings and sort keys are compared the way numbers are, from the left: the first place that differs decides. Whenever a system must shorten such a sequence by a fixed number of characters and keep the rest in order (trimming a key to a length budget, sampling a sequence down), the best result spends every deletion on the earliest place it can lower, which is exactly what the pop-bigger stack does in one pass.

Step-by-Step Execution Trace Table

Example 2, num = "10200", k = 1 (the trap case: leading zeros):

digitwhile k > 0 and stack and stack[-1] > digitWhat the code doesstack afterk after
1stack is empty: nopush 1["1"]1
0"1" > "0": yes, then k is 0: nopop 1, k -= 1, push 0["0"]0
2k is 0: nopush 2["0", "2"]0
0k is 0: nopush 0["0", "2", "0"]0
0k is 0: nopush 0["0", "2", "0", "0"]0
after the loopstack[:4 - 0] keeps every digit["0", "2", "0", "0"]0
"0200".lstrip("0") gives "200": without it the answer would be "0200"
Scroll horizontally to see all columns, or expand to full screen

return result or "0" gives "200".

Core Invariant Specification & Code Shape
Archetype Code Shape
Python
1Numbers of the same length are compared from the left, so the earliest digit you can lower matters most: a bigger digit right before a smaller one is always worth deleting.
2Keep a `stack` of the digits you keep. While `k > 0` it never decreases from bottom to top: before pushing `digit`, pop every `stack[-1] > digit` and spend one removal for each.
3Loop `for digit in num:` with `while k > 0 and stack and stack[-1] > digit:` popping and doing `k -= 1`, then `stack.append(digit)`. After the loop, `stack = stack[:len(stack) - k]` spends the removals left over on the tail.
4The trap: the number is written without leading zeros. Return `"".join(stack).lstrip("0") or "0"`: `"10200"` with `k = 1` leaves `"0200"`, which must be `"200"`, and an empty result is `"0"`.

Target: Remove K Digits (LeetCode 402). `stack` holds the digits kept so far; its bottom is the most significant place.

Boundary Model: Sorted Coordinate Sweep-Line & Monotonic Stack

Intervals sorted by start coordinate; monotonic stack preserves strictly increasing or decreasing element values.

Loop Invariant Termination

Sweep: if curr.start <= prev.end: merge; Stack: while stack and nums[i] >= stack[-1]: stack.pop().

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

Two numbers with the same number of digits are compared from the left: the first place where they differ decides. So when you must delete k digits, the digit to delete first is the one whose removal lowers the earliest place. The Lexicographic Stack finds those digits in one pass. It keeps the digits it wants on a stack; when a new digit is smaller than the digit on top and removals remain, the top digit goes, because the smaller digit then moves one place to the left. It keeps popping while that holds, and then pushes digit.

🧗 The Analogy: A Line of Climbers Sorted by Height

Climbers arrive one by one to form a rope team, and you may send k of them home, but never reorder them. You want the team to look as short as possible from the front. When a shorter climber arrives while a taller one stands just ahead, send the taller one home, and keep doing it while you still can: the shorter climber moves forward. When you run out of arrivals with home tickets left, the team already rises from front to back, so the tallest climbers are the last ones: send them home from the back.

🪄 The Mathematical Harmony / Magic Trick
Code / Blueprint
for digit in num:
while k > 0 and stack and stack[-1] > digit:
stack.pop()
k -= 1
stack.append(digit)
stack = stack[:len(stack) - k]
return "".join(stack).lstrip("0") or "0"
 

A pop never has to be undone: once a smaller digit takes an earlier place, no set of later deletions can put a smaller digit there. The last line is the trap: the answer is a number, so it is written without leading zeros, and an empty result is "0".

💡 Summary

Pop stack[-1] > digit while k > 0, push digit, cut the tail with stack[:len(stack) - k], and return "".join(stack).lstrip("0") or "0". Each digit is pushed once and popped at most once: O(N)O(N)O(N) time, O(N)O(N)O(N) space.

  • Leading zeros and an empty result (the trap): "".join(stack) can start with zeros or be empty: popping the 1 of "10200" leaves "0200". Strip them with .lstrip("0") and return result or "0", so the answers are "200" and, for "10" with k = 2, "0".

  • Removals left over: when num already rises, as in "12345" with k = 2, the loop pops nothing. Cut the tail with stack[:len(stack) - k]; stack[:-k] empties the stack when k is 0.

  • if instead of while: one small digit may have to clear several bigger ones: "9991" with k = 3 must pop all three 9s and return "1", not "9".

  • Popping equal digits: use stack[-1] > digit, not >=: "1121" with k = 1 must return "111"; popping the equal 1 wastes the removal and returns "121".

  • Deleting digits one at a time: rescanning from the left for the first digit bigger than the one after it is correct but O(N · k), about 5 * 109 steps at these limits; the stack does all k removals in one pass.

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

You will see how a senior engineer spots a deletion budget with a left-to-right comparison and defends the pop-bigger stack out loud.

Pattern Recognition Signals

The 10-second spot

"Remove k digits" to get "the smallest possible integer", with the remaining digits kept in their order: a budget of deletions and a result that is compared from the left. The earliest place that changes decides the comparison, so each new digit should knock out the bigger digits in front of it while the budget lasts: the Lexicographic Stack.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

While k > 0, the digits in stack never decrease from bottom to top: each digit first pops every stack[-1] > digit (one removal each), then is pushed. After the loop, stack[:len(stack) - k] spends the removals left over on the tail, and "".join(stack).lstrip("0") writes the number.

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • "".join(stack).lstrip("0"), then return result or "0": popping the 1 of "10200" leaves "0200", which is the number 200, and "10" with k = 2 leaves nothing, which is "0".

  • Removals left over: on "12345" with k = 2 nothing is ever popped, so cut the tail with stack[:len(stack) - k]; stack[:-k] would empty the whole stack when k is 0.

  • while, not if: in "9991" with k = 3 the 1 must pop all three 9s; one pop per digit returns "9".

  • Pop on stack[-1] > digit, not >=: popping an equal digit wastes a removal, so "1121" with k = 1 would give "121" instead of "111".

The 60-Second Interview Pitch

Say this out loud before you type a single line

I'd use a Lexicographic Stack. Two numbers of the same length are compared from the left, so the earliest digit I can lower matters most. I walk the digits once and keep a stack of the digits I'm keeping. When the new digit is smaller than the top of the stack and I still have removals, I pop the top and spend one removal, and I keep popping while that holds; then I push the new digit. A bigger digit right before a smaller one is always the best deletion, so no pop ever has to be undone. If removals are left at the end, the stack never decreases, so I cut them off the tail. The trap is the output: I strip the leading zeros, and return zero if nothing is left. Each digit is pushed once and popped at most once, so it's O(N) time and O(N) space.

So: the earliest place decides, so pop bigger digits while removals remain; cut the tail with what is left, then strip the leading zeros.

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N)

Look at the code: for digit in num runs N times, and each digit is pushed once by stack.append(digit). The inner while can pop several digits at one step, but every pop removes a digit that was pushed earlier, and a digit is popped at most once, so all the pops of the whole run add up to at most N (in fact at most k). The slice stack[:len(stack) - k], the join and lstrip("0") each walk at most N characters once. Total: O(N).

SPACE COMPLEXITY

O(N)

stack holds at most N digits (all of them when nothing is popped), and the slice and the joined result are at most N characters each. Space: O(N), the returned string included.

Formal Recurrence Relation

T(N) = N pushes + at most N pops + O(N) to cut, join and strip = O(N)

Look at the code: for digit in num runs N times, and each digit is pushed once by stack.append(digit). The inner while can pop several digits at one step, but every pop removes a digit that was pushed earlier, and a digit is popped at most once, so all the pops of the whole run add up to at most N (in fact at most k). The slice stack[:len(stack) - k], the join and lstrip("0") each walk at most N characters once. Total: O(N).

Derivation Progression

Digit loop

N iterations

for digit in num reads each digit once and pushes it once with stack.append(digit).

Pops (the inner while)

at most N in total

Each pop removes a digit pushed earlier, and a digit is popped at most once, so the while runs at most N extra times over the whole run (and at most k times).

Cut, join, strip

O(N)

stack[:len(stack) - k], "".join(stack) and .lstrip("0") each walk at most N characters.

Total

O(N)

N pushes, at most N pops and three linear passes.

Variable Definitions

NNN

Number of digits, len(num) (at most 105)

kkk

Digits still to delete (at most N)

Memory Architecture & Bounds

🟣 Call Stack

O(1) Iterative, no recursion

🔵 Auxiliary Heap

O(N): stack holds at most N digits

🟢 Output Space

O(N): the returned string

Boundary Best / Worst Cases

Best Case

O(N)O(N)O(N): every digit is still read and pushed once, even when nothing is popped

Average Case

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

Worst Case

O(N)O(N)O(N): at most N pops in total, however they are spread over the digits

Recurrence Tree Topology

Recurrence Tree Topology
Synthesizing vector architecture diagram...
Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "remove k digits", "the smallest possible integer". A fixed budget of deletions, the rest kept in order, and a result compared from the left: the Lexicographic Stack, which pops a bigger item whenever a smaller one arrives and the budget allows.

CONSTRAINTS & BOUNDS

N≤105N \le 10^5N≤105 digits and k≤Nk \le Nk≤N. Deleting one digit at a time with a rescan from the left is O(N⋅k)O(N \cdot k)O(N⋅k), about 5⋅1095 \cdot 10^95⋅109 steps at k=N/2k = N / 2k=N/2; the stack pushes each digit once and pops it at most once: O(N)O(N)O(N) time, O(N)O(N)O(N) memory.

FAANG PRODUCTION TRAPS & EDGE CASES

The answer is a number, so "".join(stack).lstrip("0") and "0" for an empty result. The removals left over come off the tail. On a stream, a digit that is pushed may still be popped by a later, smaller digit while the budget lasts, so nothing can be emitted before the budget is spent or the input ends: the stack is the buffer.

Core Algorithmic State Invariants

1. The Earliest Place Decides

Two numbers of the same length are compared from the left, so `while k > 0 and stack and stack[-1] > digit` pops the bigger digit: a smaller digit moves into an earlier place, and no later removal can do better there.

2. Leftovers Go at the End, Zeros Go at the Front

With removals left, `stack` never decreased, so `stack[:len(stack) - k]` cuts its biggest digits off the tail. Then `"".join(stack).lstrip("0")` drops the leading zeros, and `return result or "0"` covers an empty result.

3. Each Digit Pushed Once, Popped at Most Once

The `while` inside the `for` pops at most N digits over the whole run, so the pass is O(N) time, with O(N) space for `stack`.

Theory Context•Miscellaneous & Sweeps
MediumLC 402

Remove K Digits (LeetCode 402)

You will see how one stack that pops bigger digits turns "delete k digits" into a single left-to-right pass.

Target Frequency:AmazonGoogleMicrosoft

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.

Worked Examples

Example 1
Input:num = "1432219", k = 3
Output:"1219"
10413223241596removedremovedremoved
Explanation: Deleting the `4`, the `3` and one of the `2`s leaves `1219`; no other choice of three digits leaves a smaller number.
Example 2
Input:num = "10200", k = 1
Output:"200"
1001220304removed
Explanation: Deleting the leading `1` leaves the digits `0200`, which is the number `200`: the answer is written without its leading zero.
Example 3
Input:num = "10", k = 2
Output:"0"
1001removedremoved
Explanation: Both digits are deleted. Nothing is left, and an empty number counts as `0`.

⚖️Formal Constraints & Bounds

  • 1 <= k <= num.length <= 105

  • num consists of only digits.

  • num does not have any leading zeros except for the zero itself.

Deep-Dive & Conceptual Insights

Why It Works & Core Invariant

The leftmost place that changes decides which number is smaller, so a bigger digit sitting right before a smaller one is always worth deleting: a stack pops such digits while removals remain, and any removals left over come off the end.

Real-World Scenario & Production Applications

Identifiers, version strings and sort keys are compared the way numbers are, from the left: the first place that differs decides. Whenever a system must shorten such a sequence by a fixed number of characters and keep the rest in order (trimming a key to a length budget, sampling a sequence down), the best result spends every deletion on the earliest place it can lower, which is exactly what the pop-bigger stack does in one pass.

Step-by-Step Execution Trace Table

Example 2, num = "10200", k = 1 (the trap case: leading zeros):

digitwhile k > 0 and stack and stack[-1] > digitWhat the code doesstack afterk after
1stack is empty: nopush 1["1"]1
0"1" > "0": yes, then k is 0: nopop 1, k -= 1, push 0["0"]0
2k is 0: nopush 2["0", "2"]0
0k is 0: nopush 0["0", "2", "0"]0
0k is 0: nopush 0["0", "2", "0", "0"]0
after the loopstack[:4 - 0] keeps every digit["0", "2", "0", "0"]0
"0200".lstrip("0") gives "200": without it the answer would be "0200"
Scroll horizontally to see all columns, or expand to full screen

return result or "0" gives "200".

Core Invariant Specification & Code Shape
Archetype Code Shape
Python
1Numbers of the same length are compared from the left, so the earliest digit you can lower matters most: a bigger digit right before a smaller one is always worth deleting.
2Keep a `stack` of the digits you keep. While `k > 0` it never decreases from bottom to top: before pushing `digit`, pop every `stack[-1] > digit` and spend one removal for each.
3Loop `for digit in num:` with `while k > 0 and stack and stack[-1] > digit:` popping and doing `k -= 1`, then `stack.append(digit)`. After the loop, `stack = stack[:len(stack) - k]` spends the removals left over on the tail.
4The trap: the number is written without leading zeros. Return `"".join(stack).lstrip("0") or "0"`: `"10200"` with `k = 1` leaves `"0200"`, which must be `"200"`, and an empty result is `"0"`.

Target: Remove K Digits (LeetCode 402). `stack` holds the digits kept so far; its bottom is the most significant place.

Boundary Model: Sorted Coordinate Sweep-Line & Monotonic Stack

Intervals sorted by start coordinate; monotonic stack preserves strictly increasing or decreasing element values.

Loop Invariant Termination

Sweep: if curr.start <= prev.end: merge; Stack: while stack and nums[i] >= stack[-1]: stack.pop().

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

Two numbers with the same number of digits are compared from the left: the first place where they differ decides. So when you must delete k digits, the digit to delete first is the one whose removal lowers the earliest place. The Lexicographic Stack finds those digits in one pass. It keeps the digits it wants on a stack; when a new digit is smaller than the digit on top and removals remain, the top digit goes, because the smaller digit then moves one place to the left. It keeps popping while that holds, and then pushes digit.

🧗 The Analogy: A Line of Climbers Sorted by Height

Climbers arrive one by one to form a rope team, and you may send k of them home, but never reorder them. You want the team to look as short as possible from the front. When a shorter climber arrives while a taller one stands just ahead, send the taller one home, and keep doing it while you still can: the shorter climber moves forward. When you run out of arrivals with home tickets left, the team already rises from front to back, so the tallest climbers are the last ones: send them home from the back.

🪄 The Mathematical Harmony / Magic Trick
Code / Blueprint
for digit in num:
while k > 0 and stack and stack[-1] > digit:
stack.pop()
k -= 1
stack.append(digit)
stack = stack[:len(stack) - k]
return "".join(stack).lstrip("0") or "0"
 

A pop never has to be undone: once a smaller digit takes an earlier place, no set of later deletions can put a smaller digit there. The last line is the trap: the answer is a number, so it is written without leading zeros, and an empty result is "0".

💡 Summary

Pop stack[-1] > digit while k > 0, push digit, cut the tail with stack[:len(stack) - k], and return "".join(stack).lstrip("0") or "0". Each digit is pushed once and popped at most once: O(N)O(N)O(N) time, O(N)O(N)O(N) space.

  • Leading zeros and an empty result (the trap): "".join(stack) can start with zeros or be empty: popping the 1 of "10200" leaves "0200". Strip them with .lstrip("0") and return result or "0", so the answers are "200" and, for "10" with k = 2, "0".

  • Removals left over: when num already rises, as in "12345" with k = 2, the loop pops nothing. Cut the tail with stack[:len(stack) - k]; stack[:-k] empties the stack when k is 0.

  • if instead of while: one small digit may have to clear several bigger ones: "9991" with k = 3 must pop all three 9s and return "1", not "9".

  • Popping equal digits: use stack[-1] > digit, not >=: "1121" with k = 1 must return "111"; popping the equal 1 wastes the removal and returns "121".

  • Deleting digits one at a time: rescanning from the left for the first digit bigger than the one after it is correct but O(N · k), about 5 * 109 steps at these limits; the stack does all k removals in one pass.

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

You will see how a senior engineer spots a deletion budget with a left-to-right comparison and defends the pop-bigger stack out loud.

Pattern Recognition Signals

The 10-second spot

"Remove k digits" to get "the smallest possible integer", with the remaining digits kept in their order: a budget of deletions and a result that is compared from the left. The earliest place that changes decides the comparison, so each new digit should knock out the bigger digits in front of it while the budget lasts: the Lexicographic Stack.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

While k > 0, the digits in stack never decrease from bottom to top: each digit first pops every stack[-1] > digit (one removal each), then is pushed. After the loop, stack[:len(stack) - k] spends the removals left over on the tail, and "".join(stack).lstrip("0") writes the number.

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • "".join(stack).lstrip("0"), then return result or "0": popping the 1 of "10200" leaves "0200", which is the number 200, and "10" with k = 2 leaves nothing, which is "0".

  • Removals left over: on "12345" with k = 2 nothing is ever popped, so cut the tail with stack[:len(stack) - k]; stack[:-k] would empty the whole stack when k is 0.

  • while, not if: in "9991" with k = 3 the 1 must pop all three 9s; one pop per digit returns "9".

  • Pop on stack[-1] > digit, not >=: popping an equal digit wastes a removal, so "1121" with k = 1 would give "121" instead of "111".

The 60-Second Interview Pitch

Say this out loud before you type a single line

I'd use a Lexicographic Stack. Two numbers of the same length are compared from the left, so the earliest digit I can lower matters most. I walk the digits once and keep a stack of the digits I'm keeping. When the new digit is smaller than the top of the stack and I still have removals, I pop the top and spend one removal, and I keep popping while that holds; then I push the new digit. A bigger digit right before a smaller one is always the best deletion, so no pop ever has to be undone. If removals are left at the end, the stack never decreases, so I cut them off the tail. The trap is the output: I strip the leading zeros, and return zero if nothing is left. Each digit is pushed once and popped at most once, so it's O(N) time and O(N) space.

So: the earliest place decides, so pop bigger digits while removals remain; cut the tail with what is left, then strip the leading zeros.

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N)

Look at the code: for digit in num runs N times, and each digit is pushed once by stack.append(digit). The inner while can pop several digits at one step, but every pop removes a digit that was pushed earlier, and a digit is popped at most once, so all the pops of the whole run add up to at most N (in fact at most k). The slice stack[:len(stack) - k], the join and lstrip("0") each walk at most N characters once. Total: O(N).

SPACE COMPLEXITY

O(N)

stack holds at most N digits (all of them when nothing is popped), and the slice and the joined result are at most N characters each. Space: O(N), the returned string included.

Formal Recurrence Relation

T(N) = N pushes + at most N pops + O(N) to cut, join and strip = O(N)

Look at the code: for digit in num runs N times, and each digit is pushed once by stack.append(digit). The inner while can pop several digits at one step, but every pop removes a digit that was pushed earlier, and a digit is popped at most once, so all the pops of the whole run add up to at most N (in fact at most k). The slice stack[:len(stack) - k], the join and lstrip("0") each walk at most N characters once. Total: O(N).

Derivation Progression

Digit loop

N iterations

for digit in num reads each digit once and pushes it once with stack.append(digit).

Pops (the inner while)

at most N in total

Each pop removes a digit pushed earlier, and a digit is popped at most once, so the while runs at most N extra times over the whole run (and at most k times).

Cut, join, strip

O(N)

stack[:len(stack) - k], "".join(stack) and .lstrip("0") each walk at most N characters.

Total

O(N)

N pushes, at most N pops and three linear passes.

Variable Definitions

NNN

Number of digits, len(num) (at most 105)

kkk

Digits still to delete (at most N)

Memory Architecture & Bounds

🟣 Call Stack

O(1) Iterative, no recursion

🔵 Auxiliary Heap

O(N): stack holds at most N digits

🟢 Output Space

O(N): the returned string

Boundary Best / Worst Cases

Best Case

O(N)O(N)O(N): every digit is still read and pushed once, even when nothing is popped

Average Case

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

Worst Case

O(N)O(N)O(N): at most N pops in total, however they are spread over the digits

Recurrence Tree Topology

Recurrence Tree Topology
Synthesizing vector architecture diagram...
Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "remove k digits", "the smallest possible integer". A fixed budget of deletions, the rest kept in order, and a result compared from the left: the Lexicographic Stack, which pops a bigger item whenever a smaller one arrives and the budget allows.

CONSTRAINTS & BOUNDS

N≤105N \le 10^5N≤105 digits and k≤Nk \le Nk≤N. Deleting one digit at a time with a rescan from the left is O(N⋅k)O(N \cdot k)O(N⋅k), about 5⋅1095 \cdot 10^95⋅109 steps at k=N/2k = N / 2k=N/2; the stack pushes each digit once and pops it at most once: O(N)O(N)O(N) time, O(N)O(N)O(N) memory.

FAANG PRODUCTION TRAPS & EDGE CASES

The answer is a number, so "".join(stack).lstrip("0") and "0" for an empty result. The removals left over come off the tail. On a stream, a digit that is pushed may still be popped by a later, smaller digit while the budget lasts, so nothing can be emitted before the budget is spent or the input ends: the stack is the buffer.

Core Algorithmic State Invariants

1. The Earliest Place Decides

Two numbers of the same length are compared from the left, so `while k > 0 and stack and stack[-1] > digit` pops the bigger digit: a smaller digit moves into an earlier place, and no later removal can do better there.

2. Leftovers Go at the End, Zeros Go at the Front

With removals left, `stack` never decreased, so `stack[:len(stack) - k]` cuts its biggest digits off the tail. Then `"".join(stack).lstrip("0")` drops the leading zeros, and `return result or "0"` covers an empty result.

3. Each Digit Pushed Once, Popped at Most Once

The `while` inside the `for` pops at most N digits over the whole run, so the pass is O(N) time, with O(N) space for `stack`.

Rosetta Dual-Monaco Comparison
Python 3
CANONICAL INVARIANT TEMPLATE
Loading...
CONCRETE: REMOVE K DIGITS (LEETCODE 402)
T = O(N)S = O(N)
Loading...
Pattern Implementation Mapping TableCanonical Invariant ⟷ Concrete Code ⟷ Engineering Rationale
Canonical InvariantConcrete CodeEngineering Rationale
A stack of kept items, in their original orderstack: list[str] = []`stack` holds the digits kept so far; its bottom is the most significant place.
Read the input once, left to rightfor digit in num:Each digit is compared with the kept digits in front of it exactly when it arrives.
Pop a bigger item while the deletion rule allows itwhile k > 0 and stack and stack[-1] > digit:Here the rule is the budget `k > 0`. A strict `>` keeps equal digits: popping one would spend a removal and lower nothing.
Each pop is one deletionstack.pop() k -= 1Dropping a bigger digit that sits before a smaller one lowers the earliest place that can still change.
Keep the new itemstack.append(digit)Every digit is pushed once; it may still be popped later by a smaller digit while removals remain.
Spend the budget left over on the tailstack = stack[:len(stack) - k]With removals left, `stack` never decreased, so its biggest digits are the last ones. `stack[:-k]` would be wrong when `k` is 0.
Write the result the way the problem compares itresult = "".join(stack).lstrip("0") return result or "0"The trap: a number has no leading zeros, and no digits at all means `0`.
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•