目录

题目描述

402. 移掉 K 位数字

image-20241103153952322

题意分析

给定用字符串表示的非负整数 num 和整数 k,从中恰好移除 k 位数字(不是至多),使剩下的数字按原有相对顺序拼起来最小,以字符串形式返回。

「保持相对顺序」意味着这是在选一个长度为 n - k子序列,而不是任意重排;比较的是数值大小,但由于剩余长度固定,数值比较可以退化为字符串的字典序比较。

约束信号:num 长度可达 $10^5$,逐个枚举删哪 k 位的组合数是天文数字,必须线性或近线性解决;num 不含前导零,但删除之后剩余串可能以 0 开头。

边界:结果若有前导零要去掉("10200" 删 1 位得 "0200",应输出 "200");k 等于长度时所有位都被删光,以及去零后为空串的情形,都要返回 "0" 而不是空字符串。

解法:单调栈构造最小数字

核心思路

问题关键:删除后保留的是长度固定为 n - k 的子序列。长度相同时,数值越小等价于字典序越小,因此越高位的数字越值得优先优化。

为什么选单调栈:扫描到更小的数字时,如果前面紧邻保留的数字更大,删除这个较大的栈顶就能让当前数字提前一位,结果一定更小。栈允许从末尾连续反悔:只要还有删除额度且栈顶大于当前数字,就弹出栈顶;每个字符最多进出栈一次。

不变量与正确性:处理每个字符后,栈中保留原相对顺序,并且在还有删除额度时不存在可继续消除的降序栈顶。弹掉较大的高位、换成当前较小数字,会得到同长度但字典序更小的前缀,因此最优解不会需要被弹出的字符。若扫描结束仍有额度,说明栈整体非递减,此时删除越靠后的数字影响越小,应从尾部删除。最后再统一去掉前导零。

解题步骤

  • 用字符数组或 StringBuilder 充当栈,从左到右扫描数字。
  • k > 0 且栈顶大于当前字符时持续弹栈,并减少删除额度;必须用 while,因为一个小数字可能连续淘汰多个高位大数字。
  • 将当前字符入栈,继续处理后续位置。
  • 扫描结束若 k > 0,直接从栈尾截掉 k 位,保证恰好删除指定数量。
  • 跳过结果的前导零;若全部删空或只剩零,返回 "0"
  • "1432219"k = 3 中依次弹掉 4、3、2,栈最终为 "1219""10200"k = 1 得到 "0200",规范化后返回 "200"

代码实现

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:])
}

复杂度分析

  • 时间复杂度:$O(n)$。每个字符入栈一次、最多出栈一次,去前导零也只扫描一次。
  • 空间复杂度:$O(n)$,用于保存单调栈。

关键点总结

  • 定长子序列比较大小时,先出现的不同数字决定结果,所以优先优化高位。
  • 弹栈条件必须同时满足:还有额度、栈非空、栈顶严格大于当前数字。
  • 扫描后剩余额度要从尾部消耗,否则没有做到“恰好删除 k 位”。
  • 构造结果与规范化输出分开处理:先完成贪心,再去前导零并处理空串。

易错点总结

  • 漏删剩余额度"112"k = 1 全程不会弹栈,最后必须删尾得到 "11"
  • 弹栈条件写成 >="112"k = 1 会过早删掉相等的 1,得到较大的 "12"
  • 只用 if 弹一次"1230"k = 3 中字符 0 需要连续弹掉 3、2、1;只弹一次会错误保留高位。
  • 忘记前导零和空串"10200"k = 1 应返回 "200";全部删完时应返回 "0"
  • 转换成整数处理num 最长可达 $10^5$,任何整数类型都会溢出,应始终按字符操作。

相似题目

题目 难度 考察点
316. 去除重复字母 中等 单调栈 + 剩余计数:删除量不定,字符不能重复且必须都出现
1081. 不同字符的最小子序列 中等 与 316 完全同题,换题面验证同一套栈贪心
1673. 找出最具竞争力的子序列 中等 本题的数组版:保留恰好 k 个元素,配额换算方向相反
321. 拼接最大数 困难 两个数组各取子序列后归并求最大,单调栈作为子过程复用
456. 132 模式 中等 单调栈找结构模式而非构造子序列,栈的单调方向相反
739. 每日温度 中等 单调栈入门:求下一个更大元素的距离,无贪心构造成分