代码改变世界

单调栈的“贪心”艺术:精雕细琢,打造「最小可能」的数字 - 实践

2025-11-29 18:36  tlnshuju  阅读(0)  评论(0)    收藏  举报

哈喽各位,我是前端小L。

我们的单调栈工具箱已经很丰富了:我们用“单调递增栈”找到了最小值边界(LC 84, 907),用“单调递减栈”找到了最大值边界(LC 503, 739)。这些都是“分析型”的应用。

今天,我们的角色从“分析师”转变为“雕刻家”。我们将得到一个数字字符串和一把“锤子”(可以移除 k 个数字),我们的目标是,精心地敲掉 k 个数字,让我们手中的“石料”(数字字符串)变成一座最“小”的雕塑(最小的可能数字)。

力扣 402. 移掉 K 位数字

https://leetcode.cn/problems/remove-k-digits/

题目分析:

  • 输入:一个非负整数 num 的字符串表示,一个要移除的数字个数 k

  • 目标:返回移除 k 个数字后,所能得到的最小可能的新数字(以字符串形式)。

例子:num = "1432219", k = 3

  • 移除 4, 3, 2 -> 1219

  • 移除 1, 4, 3 -> 2219

  • 移除 4, 3, 9 -> 1221 ... 显然 1219 是最小的。

核心洞察:高位的“大数”是首要敌人

如何让一个数字变得尽可能小?关键在于让它的高位(左侧)数字尽可能小。12... 永远比 21... 小。

这给了我们一个清晰的贪心策略:从左到右扫描数字。当我们遇到一个数字 current,它比它前一个数字 prev时,我们就发现了一个“山峰”(prev > current)。这个 prev 是一个在高位的、相对较大的数字,它阻碍了数字变小。如果我们还有移除名额(k > 0),我们应该立即移除 prev

例如 num = "143...", k = 1

  • 看到 1

  • 看到 44 > 1,暂时安全。

  • 看到 33 < 4!我们发现了一个“山峰” 413... 显然优于 14...。我们应该立即移除 4,并消耗一个 k 名额。

“Aha!”时刻:单调栈,贪心策略的完美载体!

这个“如果 current < prev,就移除 prev”的逻辑,我们该如何高效实现呢? 这不就是单调递增栈的“翻版”吗!

大家希望构建一个“结果”栈,这个栈从底到顶是单调递增的(高位小,低位大)。 当我们遍历 num 中的每个数字 d 时:

  1. “雕刻”时刻 (贪心移除):不断查看栈顶元素 stack.top()

    • 如果 !s.empty() && k > 0 && d < s.top()

      • 找到了!当前数字 d 比栈顶(前一个我们暂时保留的数)要小。

      • “坏”的,大家就是栈顶这个“高位大数”贪心地移除它

      • s.pop();

      • k--;

    • 继续查看新的栈顶,重复此过程,直到栈恢复递增性(或 k=0 或栈空)。

  2. “保留”时刻 (入栈)

    • s.push(d);

    • 将当前数字 d 压入栈,作为“候选”结果的一部分,它也将接受后续数字的考验。

棘手的“后处理”:三个关键细节

用单调栈处理完一遍,还剩三个细节需要“收尾”:

  1. k 还没用完怎么办?

    • 比如 num = "12345", k = 2。栈会变成 [1, 2, 3, 4, 5]k 还是2。

    • 这说明原数组已经是从左到右递增了,是最优结构。大家被迫要移除数字时,只能从末尾(低位)移除,对结果的影响最小。

    • 操作while (k > 0) { s.pop(); k--; }

  2. 前导零怎么办?

    • 比如 num = "10200", k = 1

    • 栈会变成 ['0', '2', '0', '0']。(1 会被 0 弹掉)。

    • 最终结果是 "0200",但应该是 "200"

    • 操作:在从栈构建字符串后,需要一个循环来去除前导零

  3. 结果为空怎么办?

    • 比如 num = "10", k = 2

    • 10 弹掉。栈剩 [0]

    • 后处理第一步,k=10 被弹掉。栈空了。

    • 操作:如果最终结果字符串为空,应返回 "0"

代码实现 (使用 string 作为栈,更易处理)

用一个 string (或 vector<char>) 来充当栈,可以更方便地处理这三个“后处理”问题。

#include 
#include 
#include  // for reverse
using namespace std;
class Solution {
public:
    string removeKdigits(string num, int k) {
        string s; // 使用 string 作为栈,它就是我们构建的结果
        for (char d : num) {
            // 贪心移除:当k>0 且 栈顶元素 > 当前元素
            while (!s.empty() && k > 0 && s.back() > d) {
                s.pop_back();
                k--;
            }
            s.push_back(d);
        }
        // 1. k 还没用完,从末尾移除
        while (k > 0 && !s.empty()) {
            s.pop_back();
            k--;
        }
        // 2. 处理前导零
        int i = 0;
        // i < s.length() - 1 是为了防止 "0" 本身被移除
        while (i < (int)s.length() - 1 && s[i] == '0') {
            i++;
        }
        string result = s.substr(i);
        // 3. 处理结果为空
        return result.empty() ? "0" : result;
    }
};

总结:单调栈的“构造”之力

今天,我们解锁了单调栈的第三大核心能力——贪心构造。 它让我们深刻地理解到:

单调(递增)栈,是建立“保留高位小数、移除高位大数”这一贪心策略的完美数据结构。

它通过 while (d < stack.top()) 这个简单的比较,在 O(n) 的时间内,为我们完成了所有“高位大数”的“雕刻”工作。

从“找边界”到“算贡献”,再到今天的“贪心构造”,我们的单调栈工具箱已经日益强大。下一次,它又会给我们带来什么惊喜呢?

下期见!