• 单调栈题目:移掉 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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    复杂度分析

    • 时间复杂度: 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

  • 相关阅读:
    C#程序采用AOT发布,真的可以避免被反编译?
    性能测试理论1 | 性能测试难点问题梳理
    【科普知识】什么是模拟量控制?
    tensorflow cuda gpu 安装
    grid布局
    UE 智能指针的介绍
    Java三大特性篇之——多态篇(千字详解)
    【JavaScript】语法与对象以及案例验证码切换
    【入门到精通】「Java工程师全栈知识路线」
    【Kubernetes】初识k8s--扫盲阶段
  • 原文地址:https://blog.csdn.net/stormsunshine/article/details/121088626