LeetCode 402. 移掉 K 位数字
题目描述

题意分析
从十进制数字字符串
num中恰好删除k个字符,让剩余字符表示的非负整数尽可能小。只能删除,不能交换或重新排序,因此保留下来的字符必须维持原来的相对顺序。设原长度为
n,删除完成后先保留n - k个字符,再去掉前导零作为输出格式整理;去前导零不占删除额度。若全部字符都被删除,或者剩余字符全是零,返回"0"。字符串最长可达10^5位,需要直接按字符处理。
解法:单调栈构造最小数字
核心思路
[!blue]
所有候选结果在去前导零之前长度都相同,数值大小由最靠左的不同数字决定。因此,删除时应优先改善高位,而不是只考虑删掉整个字符串中数值最大的字符。
从左到右扫描,用栈保存暂时保留的数字。如果栈顶大于当前数字,且还有删除额度,就删除栈顶,让更小的当前数字向前靠。这会降低最早发生变化的位置,后面的数字再大也无法抵消这个高位优势。弹出后,新栈顶也可能比当前数字大,因此必须持续比较,而不是只删除一次。
栈顶与当前数字相等时,删掉前者不会改善这一位,却会消耗可以留给后面更大数字的额度,所以只在严格大于时弹栈。只要删除额度没有用完,栈内数字就保持非递减;额度耗尽后,后续字符必须全部保留,此时不再强求整个栈有序。
扫描结束若仍有删除额度,说明保留序列已经非递减,没有可以通过删除下降处来改善的高位。此时从末尾删最合适:删除前面的较小数字只会让后面不更小的数字提前,无法得到更小结果。
扫描中的弹栈和最后的删尾合计恰好消耗原来的
k次删除。随后才跳过前导零;如果没有有效数字,统一返回"0"。这样选取最小子序列与输出格式处理各自独立,不会把前导零误算成额外删除。
解题步骤
- 创建字符栈,从左到右读取当前数字
ch。- 当剩余
k > 0、栈非空且栈顶大于ch时,反复弹出栈顶,并将k减一。- 把当前数字压入栈中;删除额度耗尽后,继续按原顺序保留后面的数字。
- 扫描结束后,从栈尾再删除剩余的
k个字符。- 跳过栈中的前导零,将余下字符作为结果;若余下部分为空,返回
"0"。
代码实现
class Solution {
public String removeKdigits(String num, int k) {
StringBuilder stack = new StringBuilder();
for (int i = 0; i < num.length(); i++) {
char ch = num.charAt(i);
// 删除额度还在时,让较小数字连续淘汰前面较大的高位。
while (k > 0 && stack.length() > 0 && stack.charAt(stack.length() - 1) > ch) {
stack.deleteCharAt(stack.length() - 1);
k--;
}
stack.append(ch);
}
// 扫描后仍有额度,从末尾删除以保持前缀最小。
stack.setLength(stack.length() - k);
int idx = 0;
while (idx < stack.length() && stack.charAt(idx) == '0') {
idx++;
}
String res = stack.substring(idx);
return res.length() == 0 ? "0" : res;
}
}
func removeKdigits(num string, k int) string {
stack := make([]byte, 0, len(num))
for i := 0; i < len(num); i++ {
ch := num[i]
// 删除额度还在时,让较小数字连续淘汰前面较大的高位。
for k > 0 && len(stack) > 0 && stack[len(stack)-1] > ch {
stack = stack[:len(stack)-1]
k--
}
stack = append(stack, ch)
}
// 扫描后仍有额度,从末尾删除以保持前缀最小。
stack = stack[:len(stack)-k]
idx := 0
for idx < len(stack) && stack[idx] == '0' {
idx++
}
if idx == len(stack) {
return "0"
}
return string(stack[idx:])
}
复杂度分析
设字符串长度为 $n$。
- 时间复杂度:$O(n)$。虽然扫描中嵌套了弹栈循环,每个字符仍只入栈一次、最多出栈一次;去前导零和构造结果也只需线性时间。
- 辅助空间复杂度:$O(n)$,用于保存暂时保留的数字。
关键点总结
[!green]
- 固定长度的数字串优先比较高位,遇到较小数字时应优先淘汰它前面的较大数字。
- 单调性只在还有删除额度时维护,额度耗尽后不能再弹栈。
- 剩余额度从末尾消耗,完成恰好删除后再去前导零。
易错点总结
[!yellow]
- 弹栈只用
if,会漏掉一个小数字能够连续替代的多个较大前置数字,应使用循环。- 比较条件写成
>=,会在相等数字上提前浪费删除额度,可能无力删除后续真正更大的数字。- 扫描结束忘记删去剩余额度,会得到长度超过要求的结果,尤其是在原序列非递减时。
- 在贪心扫描中忽略零却没有同步维护删除计数,会混淆“删除字符”和“省略前导零”两件事。
- 全部删空或只剩零时应返回
"0";不能直接返回空字符串,也不能把长字符串转换成普通整数。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 316. 去除重复字母 | 中等 | 同样用单调栈弹出不利前缀,本题由删除次数限制弹栈;316要求原串每种字母恰好保留一次,既要判重,又要确认被弹字母之后还能补回。 |
| 321. 拼接最大数 | 困难 | 固定长度最优子序列的选取相同,原题取最大并需合并两条来源。 |
| 1081. 不同字符的最小子序列 | 中等 | 保持单调候选并在后续仍可补足时弹出较差元素;本题删除固定数量的较大前缀数字,该题选择每种不同字符一次的最小字典序子序列。 |
| 1673. 找出最具竞争力的子序列 | 中等 | 保持单调候选并在后续仍可补足时弹出较差元素;本题删除固定数量的较大前缀数字,该题保留固定长度的最具竞争力子序列。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!