Problem overview
The method removeKdigits receives num, a string of digits that stands for a non-negative number, and an integer k. It must delete exactly k digits, keep the rest in their original order, and return the smallest number that can result, as a string without leading zeros. If nothing is left, or only zeros, the answer is "0".
With "1432219" and k = 3, removing the 4, the 3 and one of the 2s leaves "1219". With "10200" and k = 1, removing the 1 leaves "0200", which is written as "200". With "10" and k = 2 every digit goes, and the answer is "0". Since num can have 100,000 digits, the answer has to be built string-wise: it is not a number that fits into an Int or even a Long.
Approach: drop a digit when a smaller one follows
All results have the same length (num.length - k digits, before zeros are trimmed), so the smaller number is the one that is smaller at the first position where two candidates differ. That suggests a greedy rule: the leftmost digits matter most.
Look at two neighbours, a bigger digit followed by a smaller one, such as 4 then 3. If one of them has to go, removing the bigger one is better: the smaller digit moves into its place, and the number gets smaller at that position. Removing anything later could not do as well, since earlier positions weigh more.
So read the digits from left to right and keep the digits chosen so far on a stack. Before pushing a new digit c, pop every digit on top that is larger than c, as long as removals remain. After that the stack never decreases from bottom to top, except where the removal budget ran out.
If the digits only grow, as in 1234, nothing is popped and some removals are left over. The stack is then ascending, so its largest digits are at the top. Dropping from the top is the best use of the leftover removals.
Last, the stack may start with zeros, as in 0, 2, 0, 0 for "10200". Leading zeros are skipped when the string is written, and an empty result becomes "0".
Here is "1432219" with k = 3. The stack goes 1, then 1 4. The digit 3 pops the 4 (two removals left): 1 3. The next 2 pops the 3 (one left): 1 2. The second 2 isn't smaller than the top, so it is pushed: 1 2 2. The 1 pops one 2 and uses the last removal: 1 2 1. With no removals left, 9 is pushed, giving 1 2 1 9.
Step by step
- Copy
kintovar remaining. Parameters in Scala can't be reassigned, and the count has to go down with every removal. - Create the stack as an empty
ArrayBuffer[Char]in aval. Its end is the top. A buffer keeps the digits in the order they will be written, which amutable.Stack(it hands back its elements top first) would not. - Loop over the digits with
for (c <- num). AStringcan be traversed as a sequence ofChars. - Pop while a bigger digit is on top. The inner loop runs while
remaining > 0, the stack is not empty, andstack.last > c.stack.nonEmptygoes beforestack.last, becauselaston an empty buffer throws. Digit characters compare like the digits they show. - Remove the top with
stack.remove(stack.length - 1). - Count the removal with
remaining -= 1. When it reaches 0, the loop condition fails. - Push the digit with
stack.append(c). It may still be popped later by a smaller digit. - Use up leftover removals with a second loop,
while (remaining > 0). - Drop the top again with
remove(stack.length - 1). At this point the stack is non-decreasing, so the top is the biggest digit. - Count it with
remaining -= 1. - Delegate the output by ending with a call to
joinWithoutLeadingZeros(stack), whose value is the method's result. - Declare the helper as a
privatemethod that takes the buffer and returns aString. - Start with
startingZeros = true. This flag says that only zeros have been seen so far. - Create a
StringBuilderwithnew StringBuilder()to collect the output without copying the text for every digit. - Loop over the buffer with
for (ch <- stack), from the bottom, which is the front of the number. - Test
ch != '0'. The first digit other than zero ends the run of leading zeros. - Set
startingZeros = falseinside that test. The flag never goes back totrue, so zeros inside the number, like the two in 200, are kept. - Test
!startingZerosin a secondif. - Append
chtoresult. - Finish with
if (result.isEmpty) "0" else result.toString. In Scala anifis an expression, so it can be the method's last line.
The solution in Scala
class Solution {
def removeKdigits(num: String, k: Int): String = {
var remaining = k
val stack = scala.collection.mutable.ArrayBuffer[Char]()
for (c <- num) {
while (remaining > 0 && stack.nonEmpty && stack.last > c) {
stack.remove(stack.length - 1)
remaining -= 1
}
stack.append(c)
}
while (remaining > 0) {
stack.remove(stack.length - 1)
remaining -= 1
}
joinWithoutLeadingZeros(stack)
}
private def joinWithoutLeadingZeros(stack: scala.collection.mutable.ArrayBuffer[Char]): String = {
var startingZeros = true
val result = new StringBuilder()
for (ch <- stack) {
if (ch != '0') {
startingZeros = false
}
if (!startingZeros) {
result.append(ch)
}
}
if (result.isEmpty) "0" else result.toString
}
}
The Scala details worth a closer look:
- Comparing
Chars.stack.last > ccompares character codes. The codes of'0'to'9'are consecutive and in order, so a character comparison gives the same answer as comparing the digits. removetakes an index. On anArrayBuffer,remove(i)deletes the element at positioniand returns it. With the last index it is a constant-time pop. The result is ignored here.laston an empty buffer throwsNoSuchElementException. ThenonEmptytest in front of it, joined with&&, stops the evaluation beforelastruns.- Reading
for (c <- num). It runs the body once for each character, in order. It is a loop, not a collection builder, because there is noyield. private def. The helper is visible only inside the class. Its parameter type is written with the full package name, matching how the buffer is created inremoveKdigits.- The
ifexpression.if (result.isEmpty) "0" else result.toStringhas the value of whichever branch runs, and both branches areStrings.toStringcan be written without parentheses.
Complexity
Let n be the length of num.
Time: O(n). Each digit is pushed once and popped at most once, so the two loops together do at most about 2n stack operations. The helper walks the stack once.
Space: O(n). The stack can hold all the digits, and the StringBuilder holds up to as many.
Edge cases and common mistakes
- Removing everything. With
"10"andk = 2the stack ends up empty and the method returns"0". The same holds for"12345"withk = 5. - Only zeros left.
"100"withk = 1leaves the digits0, 0. Every zero is a leading zero, so the builder stays empty and the result is"0"."10001"withk = 4also gives"0". - Ascending digits.
"1234"withk = 2pops nothing in the main loop. The second loop drops 4 and 3, and the answer is"12". Without the second loop, the answer would be too long. - Equal neighbours.
"112"withk = 1gives"11". The comparison is strict (>), so a digit equal to the top is pushed and nothing is popped for it. Popping on equality would remove digits without making the number smaller. - Converting to a number. The input can be 100,000 digits long, far past
Long. Keep everything as characters. - Trimming zeros too early. Removing the zeros while the stack is built would change which digits are compared. The stack keeps them. Only the output step skips the leading ones.
- Checking
lastfirst. Writingstack.last > c && stack.nonEmptythrows on the very first digit, when the stack is empty.
Testing
"1432219",k = 3gives"1219"."10200",k = 1gives"200"."10",k = 2gives"0"."1234",k = 2gives"12"."112",k = 1gives"11"."5337",k = 2gives"33"."100",k = 1gives"0".
