单调栈题目:移掉 K 位数字

题目

标题和出处

标题:移掉 K 位数字

出处:402. 移掉 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} 1knum.length105
  • num \texttt{num} num 只包含数字
  • 除了 0 \texttt{0} 0 本身之外, num \texttt{num} num 不含任何前导零

解法

思路和算法

首先考虑一个简化的问题:从 num \textit{num} num 中移除一位数字,使得剩下的数字最小。由于只移除一位数字,因此剩下的数字的长度是固定的,为了使得剩下的数字最小,应该使高位的数字尽可能小。可能有两种情况,以下是两种情况和对应的策略:

  1. 存在下标 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 处的数字移除之后,剩下的数字最小;

  2. 不存在下标 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 n1 位数和移除一位数字之后的 n − 1 n - 1 n1 位数。

对于第 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,对于每个数字,进行如下操作:

  1. 如果栈不为空、栈顶数字大于当前数字且已经移除的数字个数小于 k k k,则将栈顶数字出栈,重复该操作直到上述条件不满足(三个条件之一不满足即停止);

  2. 将当前数字入栈。

遍历结束之后,如果已经移除的数字个数小于 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

  1. 下标 0 0 0 处的数字是 1 1 1,将 1 1 1 入栈, sb = “1" \textit{sb} = \text{``1"} sb=“1"

  2. 下标 1 1 1 处的数字是 4 4 4,将 4 4 4 入栈, sb = “14" \textit{sb} = \text{``14"} sb=“14"

  3. 下标 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 个数字。

  4. 下标 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 个数字。

  5. 下标 4 4 4 处的数字是 2 2 2,将 2 2 2 入栈, sb = “122" \textit{sb} = \text{``122"} sb=“122",共移除 2 2 2 个数字。

  6. 下标 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 个数字。

  7. 由于已经移除 3 3 3 个数字,因此不移除更多的数字,将剩下的数字入栈, sb = “1219" \textit{sb} = \text{``1219"} sb=“1219"

  8. 由于 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 kn,因此时间复杂度也可以表示成 O ( n ) O(n) O(n)

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是字符串 num \textit{num} num 的长度。空间复杂度主要取决于栈空间,栈内元素个数不会超过 n n n

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

伟大的车尔尼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值