题目来源
题目描述
class Solution {
public:
string removeKdigits(string num, int k){
}
};
题目解析
思路
如果n是 num 的长度,我们要去除k个,那么需要剩下 n-k 个,怎么判断哪些数字应该去掉呢?
- 首先来考虑,若数字是递增的话,比如 1234,那么肯定是要从最后面移除最大的数字。
- 若是乱序的时候,比如 1324,若只移除一个数字,移除谁呢?这个例子比较简单,我们一眼可以看出是移除3,变成 124 是最小。
所以:
- 维护一个递增栈,只要发现当前数字 < 栈顶元素,那么就移除栈顶元素。为什么呢?
- 因为要数字最小,所以我们要让高位尽可能变小
- 所以如果这个字符串是递增的,没什么好说的,擦除当前大的这个数,让小的到高位去,比如[9],1,2,3,一定移除9,让栈顶边1
- 如果后面可能出现比栈顶元素高的元素,因为此时栈顶元素在高位上,就算后面的数字再大,也是在低位上,我们只有将高位上的数字尽可能的变小,才能使整个剩下的数字尽可能的小。比如 [3], 1 9 7 ,一定移除3,让栈顶变1
思路:
- 从左至右扫描,当前扫描的数还不确定要不要删,入栈暂存。
- 123531这样「高位递增」的数,肯定不会想删高位,会尽量删低位。
- 432135这样「高位递减」的数,会想干掉高位,直接让高位变小,效果好。
- 所以,如果当前遍历的数比栈顶大,符合递增,是满意的,让它入栈。
- 如果当前遍历的数比栈顶小,栈顶立刻出栈,不管后面有没有更大的,为什么?
因为栈顶的数属于高位,删掉它,小的顶上,高位变小,效果好于低位变小。
"1432219" k = 3
bottom[1 ]top 1入
bottom[1 4 ]top 4入
bottom[1 3 ]top 4出 3入
bottom[1 2 ]top 3出 2入
bottom[1 2 2 ]top 2入
bottom[1 2 1 ]top 2出 1入 出栈满3个,停止出栈
bottom[1 2 1 9 ]top 9入
照这么设计,如果是"0432219",0 遇不到比它更小的,最后肯定被留在栈中,变成 0219,还得再去掉前导0。
"0432219" k = 3
bottom[0 ]top 0入
bottom[0 4 ]top 4入
bottom[0 3 ]top 4出 3入
bottom[0 2 ]top 3出 2入
bottom[0 2 2 ]top 2入
bottom[0 2 1 ]top 2出 1入 出栈满3个,停止出栈
bottom[0 2 1 9 ]top 9入
能不能直接不让前导 0 入栈?——可以。
加一个判断:栈为空且当前字符为 “0” 时,不入栈。取反,就是入栈的条件:
if c != '0' || len(stack) != 0 {
stack = append(stack, c) // 入栈
}
这避免了 0 在栈底垫底。
注意到,遍历结束时,有可能还没删够 k 个字符,继续循环出栈,删低位。
删够了,但如果栈变空了,什么也不剩,则返回 “0”。
否则,将栈中剩下的字符,转成字符串返回。
class Solution {
public:
string removeKdigits(string num, int k){
std::string ans;
int n = num.size(), keep = n - k;
for(char c : num){
while (k && !ans.empty() && ans.back() > c){
ans.pop_back();
k--;
}
if(!ans.empty() || c != '0'){
ans.push_back(c);
}
}
while (!ans.empty() && k-- ){
ans.pop_back();
}
return ans.empty() ? "0" : ans;
}
};
思路
要求从一个字符串数字中删除k个数字,使得剩下的数最小。也就是说,我们要保存原来的数字的相对位置不变
以题目中的 num = 1432219, k = 3 为例,我们需要返回一个长度为4的字符串。问题是:我们怎么才能求出这四个位置依次是什么呢?
暴力法的话,我们需要枚举 C n ( n − k ) C_n^{(n - k)} Cn(n−k) 种序列(其中 n 为数字长度),并逐个比较最大。这个时间复杂度是指数级别的,必须进行优化。
一个思路是:
- 从左到右遍历
- 对于每一个遍历到的元素,我们决定是丢弃还是保留
问题是:我们怎么知道,一个元素是应该丢弃还是保留呢?
前置知识:
- 对于两个数 123a456 和 123b456
- 如果a > b,那么数字 123a456 > 123b456;
- 如果a < b,那么数字 123a456 < 123b456;
- 也就是说,两个相同位数的数字大小关系取决于第一个不同的数的大小
因此我们的思路就是:
- 从左到右遍历
- 对于遍历到的元素,我们选择保留。 但是我们可以选择性丢弃前面相邻的元素。丢弃与否的依据如上面的前置知识中阐述中的方法。
- 也就是说,对于遍历到的元素(从第二个元素开始),与前面的元素进行比较
- 如果比前一个元素小,就把前一个元素[替换成当前小的元素]
- 如果大于等于前一个元素,则该元素直接拼接到最后面
以题目中的 num = 1432219, k = 3 为例的图解过程如下:
-
由于没有左侧相邻元素,因此没办法丢弃。
-
由于 4 比左侧相邻的 1 大。如果选择丢弃左侧的 1,那么会使得剩下的数字更大(开头的数从 1 变成了 4)。因此我们仍然选择不丢弃(因为字典序要求最小)
-
由于 3 比左侧相邻的 4 小。 如果选择丢弃左侧的 4,那么会使得剩下的数字更小(开头的数从 4 变成了 3)。因此我们选择丢弃。
-
…
然而需要注意的是,如果给定的数字是一个单调递增的数字,那么我们的算法会永远选择不丢弃。这个题目中要求的,我们要永远确保丢弃 k 个矛盾。
一个简单的思路是:
- 每次丢弃一次,k减去1。当k减到0时,我们提前终止遍历
- 而当遍历完成,如果k仍然大于0。不妨假设最终还剩下x个需要丢弃,那么我们需要选择删除末尾 x 个元素。
上面的思路可行,但是稍显复杂。
我们需要把思路逆转过来。刚才我的关注点一直是丢弃,题目要求我们丢弃 k 个。反过来说,不就是让我们保留 n - kn−k 个元素么?其中 n 为数字长度。 那么我们只需要按照上面的方法遍历完成之后,再截取前n - k个元素即可。
按照上面的思路,我们来选择数据结构。由于我们需要保留和丢弃相邻的元素,因此使用栈这种在一端进行添加和删除的数据结构是再合适不过了
什么时候用单调栈?
- 需要给当前的元素,找右边/左边第一个比它大/小的位置。
- 单调递增栈,利用波谷剔除栈中的波峰,留下波谷;
- 单调递减栈,利用波峰剔除栈中的波谷,留下波峰。
- 本题想维护高位递增,即,元素想找右边第一个比它小的数,即右侧第一个波谷。
- 单调递增栈遇到波谷,用它来剔除栈中的波峰,维持单增。
- 留下波谷保持了单增,而剔除掉的栈中字符,就是删掉的字符。