← 返回题目列表

移掉 K 位数字如何用贪心单调栈得到最小数?

中等 第 23 / 29 题 更新于 2026/08/01
贪心单调栈字符串移掉K位数字

简化版

移掉 K 位数字的贪心原则是:越靠前的高位越重要,只要当前数字比栈顶小,并且还可以删除,就删除栈顶,让更小的数字提前。最后如果还没删够,就从末尾继续删,再去掉前导零。

详细版

把结果看成一个单调递增趋势的栈。遍历数字字符 c,当 stack 非空、k > 0stack.peek() > c 时,弹出栈顶,因为用当前更小的 c 替换前面的较大数字,会让整体数值更小。之后把 c 入栈。

遍历结束后,如果 k 仍大于 0,说明原数字整体已经递增,此时只能从尾部删除。构造答案时去掉前导零;如果结果为空,返回 "0"。时间复杂度 O(n),空间复杂度 O(n)

完整版教学

一、为什么高位优先决定大小

十进制数字比较大小时,越靠左的位权越大。比如 14321322,只看第二位就能判断 1322 更小,因为百位从 4 变成了 3,后面个位十位再怎么变化都很难抵消这个影响。

1432
1322
 ^  这里 4 > 3,所以第二个更小

因此这题的贪心核心不是“删最大的数字”,而是“在尽可能靠前的位置删掉破坏变小机会的较大数字”。

二、为什么遇到更小数字要弹出前面的大数字

假设栈顶是 4,当前字符是 3,并且还能删除。把 4 留在前面会让结果前缀更大;删除 43 提前,得到的数一定不差。

局部选择结果趋势
保留前面的 4高位较大
删除 4,让 3 前移高位变小

记忆钩子:这题不是找全局最大位删除,而是维护“前缀尽量小”。

这个交换理由是贪心正确性的来源:一旦当前位置能用更小数字替换前面的较大数字,就应该马上做。

三、单调栈状态怎么维护

栈里保存当前构造出的结果前缀。遍历每个字符 c

while 栈非空 && k > 0 && 栈顶 > c:
    弹出栈顶,k--
把 c 入栈

这样做会让栈尽量保持从左到右不下降。不是绝对严格递增,因为当 k 用完后,后续字符只能全部保留。

num = 1432219, k = 3
读到 3 时弹出 4
读到 2 时弹出 3
再读到 2 时弹出前一个 2? 不弹,因为相等不需要删

相等时不弹,通常能保留更靠前的数字,给后面更多删除机会。

四、为什么递增数字要从末尾删

如果遍历完后 k 还没用完,说明栈中没有出现“前大后小”的机会,数字整体类似递增。此时删除越靠后的数字,对高位影响越小,结果最小。

num = 123456, k = 2
删前面:3456 或 12456 之类会明显变大
删后面:1234 最小

公式化地看,前缀越短且越保留小数字,整体越小。因此剩余删除次数应该从栈尾消耗。

五、代码模板

String removeKdigits(String num, int k) {
    Deque<Character> stack = new ArrayDeque<>();
    for (char c : num.toCharArray()) {
        while (!stack.isEmpty() && k > 0 && stack.peekLast() > c) {
            stack.pollLast();
            k--;
        }
        stack.addLast(c);
    }
    while (k > 0 && !stack.isEmpty()) {
        stack.pollLast();
        k--;
    }
    StringBuilder sb = new StringBuilder();
    boolean leadingZero = true;
    for (char c : stack) {
        if (leadingZero && c == '0') continue;
        leadingZero = false;
        sb.append(c);
    }
    return sb.length() == 0 ? "0" : sb.toString();
}

这里用 ArrayDequeStack 更现代。注意最后去前导零,不要在入栈时随便跳过 0,因为中间的 0 可能是最优结果的一部分。

六、用例子完整推演

num="1432219", k=3

读 1:栈 [1]
读 4:栈 [1,4]
读 3:弹 4,栈 [1,3],k=2
读 2:弹 3,栈 [1,2],k=1
读 2:相等不弹,栈 [1,2,2]
读 1:弹 2,栈 [1,2,1],k=0
读 9:栈 [1,2,1,9]

结果是 1219。每一次弹出都发生在“当前位能让更小数字提前”的瞬间。

七、常见误区与追问

  • 误区:每次删除当前最大的数字。 最大数字如果在低位,删除它未必比删除高位较大数字更优。
  • 误区:相等数字也弹出。 相等时弹出不会让当前位更小,还可能浪费删除次数。
  • 误区:入栈时直接丢弃所有 0。 只有前导零需要去掉,中间的 0 可能非常关键。
  • 追问:为什么栈是单调递增趋势? 因为一旦前面数字大于后面数字,就优先删除前面的大数字。
  • 追问:如果 k 等于数字长度怎么办? 最终栈为空,返回 "0"
  • 追问:复杂度为什么是 O(n) 每个字符最多入栈一次、出栈一次。

八、加强记忆

移掉 K 位数字记成“高位越小越好”。遍历时只要当前数字能打败前面的栈顶,就把栈顶删掉;删不动了再入栈。遍历完还没删够,说明没有下降机会,只能从末尾删。最后别忘了前导零和空串返回 0,这两个边界经常扣分。