Builds on Next Greater Element, which you haven’t finished yet.
Monotonic Stack Greedy (Remove K Digits, Remove Duplicate Letters)
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.
4, the 3 and one of the 2s leaves 1219; no other choice of three digits leaves a smaller number.Lexicographic Stack
digits or letters ➔ pop a bigger stack top while deletions remain ➔ smallest result| 1 | def 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" |
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 2stack empty
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.
digitthe digit of num that the loop is readingpopa bigger digit taken off the top of stack, which costs one removalpushthe digit that was just pushed onto stackcuta digit of the tail that is removed because k is still above 0stripa leading zero that lstrip removes
Start: num = "10234" and k = 2. stack is empty.
Gotcha 1 of 5
- The trap
Gotcha 1: Returning the kept digits with leading zeros
Mistakeresult = "".join(stack)Fixresult = "".join(stack).lstrip("0")What happens: Removing a digit can leave a zero at the front. For
"10200"withk = 1, the loop pops the1, and the kept digits read"0200". A number is written without leading zeros.lstrip("0")removes them.Example: num = "10200", k = 1Mistake: returns '0200'Fix: returns '200'
- 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 whenkis 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, sostackrises from left to right and its last digits are the biggest. Usestack[:len(stack) - k], which is also correct whenkis 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
1of"10200"leaves"0200", which is the number200. 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 whenstack[-1] > cand the popped letter appears again later, whichlast[stack[-1]] > itests. A letter that is already instackis skipped. The stack shape and theO(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
stackstarts empty, andkis the number of removals still allowed.- For each
digitinnum, the loop popsstack[-1]whilek > 0andstack[-1] > digit. Each pop lowerskby 1. - The loop then pushes
digit. The digits instacknever decrease from bottom to top, as long as removals remain. - 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 whenkis 0. "".join(stack).lstrip("0")writes the number without leading zeros, andresult or "0"handles the empty result.
Traps
- Leading zeros. For
"10200"withk = 1, the loop pops the1, and the kept digits read"0200". The answer must be"200", so strip the zeros. - Empty result. For
"10"withk = 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"withk = 3, a single pop leaves"9"instead of"1". - Pop on
>=. An equal digit does not lower the number at that place. For"1121"withk = 1, popping the equal1gives"121", not"111". - Tail cut with
stack[:-k]. Whenkis 0 this returns an empty list. Usestack[:len(stack) - k]. - Wrong blocker in Remove Duplicate Letters (LC 316). Without the
last[stack[-1]] > itest, a letter that never appears again is popped and lost.
Practice
Builds on Next Greater Element, which you haven’t finished yet.
Monotonic Stack Greedy (Remove K Digits, Remove Duplicate Letters)
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.
4, the 3 and one of the 2s leaves 1219; no other choice of three digits leaves a smaller number.Lexicographic Stack
digits or letters ➔ pop a bigger stack top while deletions remain ➔ smallest result| 1 | def 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" |
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 2stack empty
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.
digitthe digit of num that the loop is readingpopa bigger digit taken off the top of stack, which costs one removalpushthe digit that was just pushed onto stackcuta digit of the tail that is removed because k is still above 0stripa leading zero that lstrip removes
Start: num = "10234" and k = 2. stack is empty.
Gotcha 1 of 5
- The trap
Gotcha 1: Returning the kept digits with leading zeros
Mistakeresult = "".join(stack)Fixresult = "".join(stack).lstrip("0")What happens: Removing a digit can leave a zero at the front. For
"10200"withk = 1, the loop pops the1, and the kept digits read"0200". A number is written without leading zeros.lstrip("0")removes them.Example: num = "10200", k = 1Mistake: returns '0200'Fix: returns '200'
- 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 whenkis 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, sostackrises from left to right and its last digits are the biggest. Usestack[:len(stack) - k], which is also correct whenkis 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
1of"10200"leaves"0200", which is the number200. 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 whenstack[-1] > cand the popped letter appears again later, whichlast[stack[-1]] > itests. A letter that is already instackis skipped. The stack shape and theO(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
stackstarts empty, andkis the number of removals still allowed.- For each
digitinnum, the loop popsstack[-1]whilek > 0andstack[-1] > digit. Each pop lowerskby 1. - The loop then pushes
digit. The digits instacknever decrease from bottom to top, as long as removals remain. - 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 whenkis 0. "".join(stack).lstrip("0")writes the number without leading zeros, andresult or "0"handles the empty result.
Traps
- Leading zeros. For
"10200"withk = 1, the loop pops the1, and the kept digits read"0200". The answer must be"200", so strip the zeros. - Empty result. For
"10"withk = 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"withk = 3, a single pop leaves"9"instead of"1". - Pop on
>=. An equal digit does not lower the number at that place. For"1121"withk = 1, popping the equal1gives"121", not"111". - Tail cut with
stack[:-k]. Whenkis 0 this returns an empty list. Usestack[:len(stack) - k]. - Wrong blocker in Remove Duplicate Letters (LC 316). Without the
last[stack[-1]] > itest, a letter that never appears again is popped and lost.