leetcode — 402. 移掉 K 位数字
作者:互联网
给你一个以字符串表示的非负整数 num
和一个整数 k
,移除这个数中的 k
位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。
示例 1 :
输入:num = "1432219", k = 3 输出:"1219" 解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。
示例 2 :
输入:num = "10200", k = 1 输出:"200" 解释:移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。
示例 3 :
输入:num = "10", k = 2 输出:"0" 解释:从原数字移除所有的数字,剩余为空就是 0 。
提示:
1 <= k <= num.length <= 10^5
num
仅由若干位数字(0 - 9)组成- 除了 0 本身之外,
num
不含任何前导零
题意:给出一个整数,从该整数中去掉 k 个数字,要求剩下的数字形成新的整数尽可能小。应该如何选取被去掉的数字?
其中整数的长度 大于 或 等于 k,给出的整数的大小可以超过 long 类型的数字范围。
- 问题简化:如果只删除其中的一个数字,即 k = 1,如何让新整数值最小?
- 不仅要考虑数字本身的大小,也要考虑 位置 的影响。—— 一个整数的最高位减少1,对数值的影响也非常大。
- 以 541 270 936 为例,去掉一个数之后的结果为 从9位整数变为8位整数。—— 既然同样是8位整数,显然应该优先把高位的数字降低,这样对新数值的影响最大。
- 如何把高位的数字降低?
- 把原整数的所有数字从左向右进行比较。如果发现某一位数字大于它右面的数字,那么在删除该数字后,必然会使该数位的值降低,因为右方的比它小的数字顶替了它的位置。
- 依次求得局部最优解,最终得到全局最优阶的思想,即 贪心算法。
对于两个相同长度的数字序列,最左边不同的数字决定了这两个数字的大小。
- 例如,对于 A = 1axxx,B = 1bxxx,如果 a > b则 A > B。
- 基于此,若要使得剩下的数字最小,需要保证靠前的数字尽可能小。
e.g. 给定一个数字序列,例如 425,如果要求只删除一个数字,那么从左到右,有 4、2 和 5 三个选择。将每一个数字和它的左邻居进行比较。从 2 开始,2 小于它的左邻居 4。假设保留数字 4,那么所有可能的组合都是以数字 4(即 42,45)开头的。相反,如果移掉 4,留下 2,得到的是以 2 开头的组合(即 25),明显小于任何留下数字 4 的组合。因此应该移掉数字 4。如果不移掉数字 4,之后无论移掉什么数字,都不会得到最小数。
基于上述分析,可以得出「删除一个数字」的贪心策略:
- 给定一个长度为 n 的数字序列,从左往右找到第一个位置 i(i>0)使得 ,并删去 ;如果不存在,说明整个数字序列单调不降,删去最后一个数字即可。
- 基于此,可以每次对整个数字序列执行一次这个策略;删去一个字符后,剩下的 n−1 长度的数字序列就形成了新的子问题,可以继续使用同样的策略,直至删除 k 次。
- 暴力的实现复杂度最差会达到 O(nk)(考虑整个数字序列是单调不降的),需要加速这个过程。
考虑从左往右增量的构造最后的答案。可以用一个栈维护当前的答案序列,栈中的元素代表截止到当前位置,删除不超过 k 次个数字后所能得到的最小整数。根据之前的讨论:在使用 k 个删除次数之前,栈中的序列从栈底到栈顶单调不降。
对于每个数字,如果该数字小于栈顶元素,就不断地弹出栈顶元素,直到
栈为空
或者新的栈顶元素不大于当前数字
或者已经删除了 k 位数字
- 上述步骤结束后还需要针对一些情况做额外的处理:
- 如果删除了 m 个数字且 m<k,这种情况需要从序列尾部删除额外的 k−m 个数字。
- 如果最终的数字序列存在前导零,要删去前导零。
- 如果最终数字序列为空,应该返回 0。
- 最终从栈底到栈顶的答案序列即为最小数。
- 考虑到栈的特点是后进先出,如果通过栈实现,则需要将栈内元素依次弹出然后进行翻转才能得到最小数。为了避免翻转操作,可以使用双端队列代替栈的实现。
时间复杂度:O(n),其中 n 为字符串的长度。尽管存在嵌套循环,但内部循环最多运行 k 次。由于 0<k≤n,主循环的时间复杂度被限制在 2n 以内。对于主循环之外的逻辑,时间复杂度是 O(n),因此总时间复杂度为 O(n)。
空间复杂度:O(n)。栈存储数字需要线性的空间。
最原始思路实现:降低最高位的数值。
- 使用双层循环,外层循环次数为 要删除的数字个数k,内层循环从左向右比哪里所有数字。当遍历到需要删除的数字时,利用字符串的substring方法把对用的数字删除,并重新拼接字符串。— O(n*k)
- 性能问题
- 每一次内层循环都需要从头开始遍历所有数字。— 停留在上一次删除的位置继续进行比较;
- subString() 方法本身性能不高。—— 底层实现设计新字符串的创建,以及逐个字符的复制,该方法的自身时间复杂度为 O(n)。—— 应避免每删除一个数字后就调用subString()。
import java.util.*;
// 移除K位数字,使得剩下的数字形成的新整数尽可能小
public class DeleteKNumberMin {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc = new Scanner(System.in);
while(sc.hasNext()) {
String nums = sc.next();
int k = sc.nextInt();
System.out.println(removeKDigits(nums, k));
}
sc.close();
}
private static String removeKDigits(String nums, int k) {
// TODO Auto-generated method stub
// 删除k个数字,比较相邻元素的大小
// 从左向右遍历,找到比自己右侧数字大的数字并删除
for(int i = 0; i < k; i ++) {
boolean hasDelete = false;
for(int j = 0; j < nums.length() - 1; j ++){
if(nums.charAt(j) > nums.charAt(j+1)) {
nums = nums.substring(0,j) + nums.substring(j+1,nums.length());
hasDelete = true;
break;
}
}
// 如果没有找到要删除的数字,则删除最后一个数字
if(hasDelete == false) {
nums = nums.substring(0, nums.length()-1);
}
}
// 清除整数左侧的数字 0
int start = 0;
for(int j = 0; j < nums.length() - 1; j ++) {
if(nums.charAt(j) != '0') {
break;
}
start ++;
}
nums = nums.substring(start,nums.length());
// 如果整数的所有数字都被删除了,直接返回0
if(nums.length() == 0) {
return "0";
}
return nums;
}
}
方案优化:贪心 + 单调栈 —— 以遍历数字作为外循环,以 k 作为内循环
import java.util.*;
// 使用单调栈存放最终的结果,进行贪心寻找
public class DeleteKNumberMinSecond {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while(sc.hasNext()) {
String nums = sc.next();
int k = sc.nextInt();
System.out.println(removeKDigits(nums, k));
}
sc.close();
}
private static String removeKDigits(String nums, int k) {
// 贪心 + 单调栈(使用双端队列模拟,方便结果的输出)
Deque<Character> deque = new LinkedList<Character>();
for(int i = 0; i < nums.length(); i ++) {
char digit = nums.charAt(i);
// 出栈的条件:双端栈不为空、k>0(还未删除完需要删除的个数)、栈顶的元素大于当前的数字
// 当栈顶数字大于遍历到的当前数字时,栈顶数字出栈(相当于删除数字)
while(deque.isEmpty() == false && k > 0 && deque.peekLast() > digit) {
deque.pollLast();
k --;
}
// 遍历到的当前数字入栈
deque.offerLast(digit);
}
/* 对一些额外情况进行考虑
1、删除 m 个数字且 m<k,需要从序列尾部删除额外的 k-m 个数字。
2、如果最终的数字序列存在前导零,删去前导零。
3、如果最终数字序列为空,返回 0。
*/
for(int i = 0; i < k; i ++) {
deque.pollLast();
}
StringBuilder result = new StringBuilder();
boolean isBeginningZero = true;
while(!deque.isEmpty()) {
char digit = deque.pollFirst();
if(isBeginningZero && digit != '0') {
isBeginningZero = false;
}
if(isBeginningZero == false) {
result.append(digit);
}
}
if(result.length() == 0) {
return "0";
}
return result.toString();
}
}
标签:数字,删除,nums,int,整数,402,sc,移掉,leetcode 来源: https://blog.csdn.net/qq_34176797/article/details/119316329