单调栈的“贪心”艺术:精雕细琢,打造「最小可能」的数字 - 实践
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。看到
4。4 > 1,暂时安全。看到
3。3 < 4!我们发现了一个“山峰”4。13...显然优于14...。我们应该立即移除4,并消耗一个k名额。
“Aha!”时刻:单调栈,贪心策略的完美载体!
这个“如果 current < prev,就移除 prev”的逻辑,我们该如何高效实现呢? 这不就是单调递增栈的“翻版”吗!
大家希望构建一个“结果”栈,这个栈从底到顶是单调递增的(高位小,低位大)。 当我们遍历 num 中的每个数字 d 时:
“雕刻”时刻 (贪心移除):不断查看栈顶元素
stack.top()。如果
!s.empty() && k > 0 && d < s.top():找到了!当前数字
d比栈顶(前一个我们暂时保留的数)要小。“坏”的,大家就是栈顶这个“高位大数”贪心地移除它!
s.pop();k--;
继续查看新的栈顶,重复此过程,直到栈恢复递增性(或
k=0或栈空)。
“保留”时刻 (入栈):
s.push(d);将当前数字
d压入栈,作为“候选”结果的一部分,它也将接受后续数字的考验。
棘手的“后处理”:三个关键细节
用单调栈处理完一遍,还剩三个细节需要“收尾”:
k还没用完怎么办?比如
num = "12345",k = 2。栈会变成[1, 2, 3, 4, 5],k还是2。这说明原数组已经是从左到右递增了,是最优结构。大家被迫要移除数字时,只能从末尾(低位)移除,对结果的影响最小。
操作:
while (k > 0) { s.pop(); k--; }
前导零怎么办?
比如
num = "10200",k = 1。栈会变成
['0', '2', '0', '0']。(1会被0弹掉)。最终结果是
"0200",但应该是"200"。操作:在从栈构建字符串后,需要一个循环来去除前导零。
结果为空怎么办?
比如
num = "10",k = 2。1被0弹掉。栈剩[0]。后处理第一步,
k=1,0被弹掉。栈空了。操作:如果最终结果字符串为空,应返回
"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) 的时间内,为我们完成了所有“高位大数”的“雕刻”工作。
从“找边界”到“算贡献”,再到今天的“贪心构造”,我们的单调栈工具箱已经日益强大。下一次,它又会给我们带来什么惊喜呢?
下期见!
浙公网安备 33010602011771号