题目
标题和出处
标题:移掉 K 位数字
难度
6 级
题目描述
要求
给你一个以字符串表示的非负整数 num \texttt{num} num 和一个整数 k \texttt{k} k,移除这个数中的 k \texttt{k} k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。
示例
示例 1:
输入:
num
=
"1432219",
k
=
3
\texttt{num = "1432219", k = 3}
num = "1432219", k = 3
输出:
"1219"
\texttt{"1219"}
"1219"
解释:移除三个数字
4
\texttt{4}
4、
3
\texttt{3}
3 和
2
\texttt{2}
2 形成一个新的最小的数字
1219
\texttt{1219}
1219。
示例 2:
输入:
num
=
"10200",
k
=
1
\texttt{num = "10200", k = 1}
num = "10200", k = 1
输出:
"200"
\texttt{"200"}
"200"
解释:移除首位的
1
\texttt{1}
1 剩下的数字为
200
\texttt{200}
200。注意输出不能有任何前导零。
示例 3:
输入:
num
=
"10",
k
=
2
\texttt{num = "10", k = 2}
num = "10", k = 2
输出:
"0"
\texttt{"0"}
"0"
解释:从原数字移除所有的数字,剩余为空就是
0
\texttt{0}
0。
数据范围
- 1 ≤ k ≤ num.length ≤ 10 5 \texttt{1} \le \texttt{k} \le \texttt{num.length} \le \texttt{10}^\texttt{5} 1≤k≤num.length≤105
- num \texttt{num} num 只包含数字
- 除了 0 \texttt{0} 0 本身之外, num \texttt{num} num 不含任何前导零
解法
思路和算法
首先考虑一个简化的问题:从 num \textit{num} num 中移除一位数字,使得剩下的数字最小。由于只移除一位数字,因此剩下的数字的长度是固定的,为了使得剩下的数字最小,应该使高位的数字尽可能小。可能有两种情况,以下是两种情况和对应的策略:
-
存在下标 i i i 使得 num [ i ] > num [ i + 1 ] \textit{num}[i] > \textit{num}[i + 1] num[i]>num[i+1],则找到满足该要求的最小的下标 i i i,将下标 i i i 处的数字移除之后,剩下的数字最小;
-
不存在下标 i i i 使得 num [ i ] > num [ i + 1 ] \textit{num}[i] > \textit{num}[i + 1] num[i]>num[i+1],则将 num \textit{num} num 的最后一个数字移除,剩下的数字最小。
对于上述两种情况的策略的正确性说明如下。假设原始 num \textit{num} num 的长度是 n n n,则比较数字大小都是比较原始 num \textit{num} num 的前 n − 1 n - 1 n−1 位数和移除一位数字之后的 n − 1 n - 1 n−1 位数。
对于第 1 种情况,满足 num [ i ] > num [ i + 1 ] \textit{num}[i] > \textit{num}[i + 1] num[i]>num[i+1] 的任意一个下标 i i i 处的数字被移除之后,剩下的数字都会变小,移除的数字所在的位越高,则剩下的数字比原始 num \textit{num} num 减少得越多。
对于第 2 种情况,如果移除的数字不是最后一个,都会导致剩下的数字变大,只有在移除最后一个数字的情况下,剩下的数字才会保持不变。
对于这道题,从 num \textit{num} num 中移除 k k k 位数字使得剩下的数字最小,可以基于上述策略得到一个解法:每次从 num \textit{num} num 中移除一位数字,使得移除一位数字之后剩下的数字最小,执行 k k k 次。该解法的时间复杂度在最坏情况下是 O ( n k ) O(nk) O(nk),会超出时间限制,因此需要优化。
由于上述策略优先考虑移除左边的数字,因此可以从左到右遍历 num \textit{num} num,在遍历过程中进行移除操作,只能移除 k k k 位数字。为了使得移除操作符合上述策略,可以使用单调栈,单调栈存储数字,满足从栈底到栈顶的数字单调递增。
从左到右遍历 num \textit{num} num,对于每个数字,进行如下操作:
-
如果栈不为空、栈顶数字大于当前数字且已经移除的数字个数小于 k k k,则将栈顶数字出栈,重复该操作直到上述条件不满足(三个条件之一不满足即停止);
-
将当前数字入栈。
遍历结束之后,如果已经移除的数字个数小于 k k k,则需要继续移除数字,移除数字的方法是将栈顶数字出栈,直到移除的数字个数达到 k k k。此时,栈内数字按照从栈底到栈顶的顺序拼接得到的字符串为从 num \textit{num} num 中移除 k k k 个数字之后的最小的数字,将该字符串称为「结果字符串」。
由于除了 0 0 0 以外的任何数字都不能有前导零,因此还需要移除结果字符串中的前导零。移除前导零的做法是,在结果字符串中寻找第一个不是 0 0 0 的数字,并将该数字前面的数字全部删除,如果结果字符串中的数字都是 0 0 0,则在移除前导零之后,结果字符串变为空。
在移除前导零之后,如果结果字符串为空,则返回 “0" \text{``0"} “0",否则返回结果字符串。
具体实现方面,可以用 Java 的 StringBuffer \texttt{StringBuffer} StringBuffer 类或 StringBuilder \texttt{StringBuilder} StringBuilder 类的对象 sb \textit{sb} sb 模拟栈操作,在末尾添加和删除字符等价于入栈和出栈操作,移除前导零时找到 sb \textit{sb} sb 的第一个不是 0 0 0 的数字所在下标,从该下标开始的部分为结果字符串,如果 sb \textit{sb} sb 的所有数字都是 0 0 0,则结果字符串为空,返回 “0" \text{``0"} “0"。
以下是示例 1 的计算过程,其中 num = “1432219" \textit{num} = \text{``1432219"} num=“1432219", k = 3 k = 3 k=3。
-
下标 0 0 0 处的数字是 1 1 1,将 1 1 1 入栈, sb = “1" \textit{sb} = \text{``1"} sb=“1"。
-
下标 1 1 1 处的数字是 4 4 4,将 4 4 4 入栈, sb = “14" \textit{sb} = \text{``14"} sb=“14"。
-
下标 2 2 2 处的数字是 3 3 3,由于 4 > 3 4 > 3 4>3,因此将 4 4 4 出栈,将 3 3 3 入栈, sb = “13" \textit{sb} = \text{``13"} sb=“13",共移除 1 1 1 个数字。
-
下标 3 3 3 处的数字是 2 2 2,由于 3 > 2 3 > 2 3>2,因此将 3 3 3 出栈,将 2 2 2 入栈, sb = “12" \textit{sb} = \text{``12"} sb=“12",共移除 2 2 2 个数字。
-
下标 4 4 4 处的数字是 2 2 2,将 2 2 2 入栈, sb = “122" \textit{sb} = \text{``122"} sb=“122",共移除 2 2 2 个数字。
-
下标 5 5 5 处的数字是 1 1 1,由于 2 > 1 2 > 1 2>1,因此将 2 2 2 出栈,将 1 1 1 入栈, sb = “121" \textit{sb} = \text{``121"} sb=“121",共移除 3 3 3 个数字。
-
由于已经移除 3 3 3 个数字,因此不移除更多的数字,将剩下的数字入栈, sb = “1219" \textit{sb} = \text{``1219"} sb=“1219"。
-
由于 sb \textit{sb} sb 没有前导零,结果字符串就是 “1219" \text{``1219"} “1219"。
代码
class Solution {
public String removeKdigits(String num, int k) {
int length = num.length();
StringBuffer sb = new StringBuffer();
int top = -1;
for (int i = 0; i < length; i++) {
char c = num.charAt(i);
while (sb.length() > 0 && sb.charAt(top) > c && k > 0) {
sb.deleteCharAt(top);
top--;
k--;
}
sb.append(c);
top++;
}
while (k > 0) {
sb.deleteCharAt(top);
top--;
k--;
}
int remainLength = sb.length();
int startIndex = 0;
while (startIndex < remainLength && sb.charAt(startIndex) == '0') {
startIndex++;
}
return startIndex == remainLength ? "0" : sb.substring(startIndex).toString();
}
}
复杂度分析
-
时间复杂度: O ( n + k ) O(n + k) O(n+k),其中 n n n 是字符串 num \textit{num} num 的长度。需要遍历字符串 num \textit{num} num 进行移除 k k k 位数字的操作,由于每个数字最多入栈和出栈各一次,其中最多有 k k k 个数字出栈,因此时间复杂度是 O ( n + k ) O(n + k) O(n+k)。由于 k ≤ n k \le n k≤n,因此时间复杂度也可以表示成 O ( n ) O(n) O(n)。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是字符串 num \textit{num} num 的长度。空间复杂度主要取决于栈空间,栈内元素个数不会超过 n n n。