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,之后无论移掉什么数字,都不会得到最小数。

leetcode — 402. 移掉 K 位数字

基于上述分析,可以得出「删除一个数字」的贪心策略:

  • 给定一个长度为 n 的数字序列leetcode — 402. 移掉 K 位数字,从左往右找到第一个位置 i(i>0)使得 leetcode — 402. 移掉 K 位数字 ,并删去 leetcode — 402. 移掉 K 位数字;如果不存在,说明整个数字序列单调不降,删去最后一个数字即可。
  • 基于此,可以每次对整个数字序列执行一次这个策略;删去一个字符后,剩下的 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)。栈存储数字需要线性的空间。

leetcode — 402. 移掉 K 位数字


最原始思路实现:降低最高位的数值。 

  • 使用双层循环,外层循环次数为 要删除的数字个数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();
	}

}

上一篇:402. 移掉 K 位数字 c++


下一篇:402,位1的个数系列(三)