【Leetcode】316. Remove Duplicate Letters(配数学证明)

题目地址:

https://leetcode.com/problems/remove-duplicate-letters/

给定一个长 n n n的字符串 s s s,题目保证只含小写字母,要求在其中删去重复字母,使得每个字母恰好只出现一次。问所能得到的字典序最小的字符串是什么。

思路是单调栈。遍历 s s s,例如遍历到 s [ i ] s[i] s[i],如果遇到栈内已经存在过的字符,则直接略过。否则逐个将字符加入栈中。当遇到栈顶比 s [ i ] s[i] s[i]大,并且 s [ i ] s[i] s[i]可以被删掉(也就是 s s s后面还有该字符),那么栈顶应该出栈。显然栈顶删掉后的字符串字典序与不删除相比会变小。此外,字符串遍历完后,从栈底到栈顶的字符就形成了最终答案。

算法正确性证明:
数学归纳法。如果 s s s长度是 1 1 1 2 2 2则结论显然。设对 s s s长度小于 n n n都成立,考虑 s s s长度等于 n n n的情况。主要考虑 s [ 0 ] s[0] s[0] s [ 1 ] s[1] s[1]是什么情况。如果 s [ 0 ] s[0] s[0]在整个 s s s里只出现了一次,那 s [ 0 ] s[0] s[0]是绝不可能出栈的,这时算法相当于是在求 s [ 1 : n − 1 ] s[1:n-1] s[1:n1]的最小字典序的、每个字母只包含一个的子序列是谁,由归纳假设,算法正确;如果 s [ 0 ] = s [ 1 ] s[0]=s[1] s[0]=s[1],那么 s [ 1 ] s[1] s[1]被略过了,问题就化为对长 n − 1 n-1 n1的字符串的问题,由归纳假设,算法正确;如果 s [ 0 ] s[0] s[0]出现多次,并且 s [ 0 ] > s [ 1 ] s[0]>s[1] s[0]>s[1],那去掉 s [ 0 ] s[0] s[0]之后,算法相当于在求 s [ 1 : n − 1 ] s[1:n-1] s[1:n1]的满足条件的子序列,该子序列一定不会以 s [ 0 ] s[0] s[0]开头(因为 s [ 1 ] < s [ 0 ] s[1]<s[0] s[1]<s[0],以 s [ 1 ] s[1] s[1]开头更优,而由归纳假设,算法求出的答案必然要以一个小于等于 s [ 1 ] s[1] s[1]的字母开头),所以 s [ 1 : n − 1 ] s[1:n-1] s[1:n1]的满足条件的子序列对于整个 s s s也是最优解,由归纳假设,算法求出了最优解,结论正确。如果 s [ 0 ] s[0] s[0]出现多次,并且 s [ 0 ] < s [ 1 ] s[0]<s[1] s[0]<s[1],此时要分两个情况考虑:
1、如果 s [ 2 : n − 1 ] s[2:n-1] s[2:n1]中不存在小于 s [ 0 ] s[0] s[0]的字母,那么算法从 s [ 1 ] s[1] s[1]开始就是在求 s [ 1 : n − 1 ] s[1:n-1] s[1:n1]的去掉所有 s [ 0 ] s[0] s[0]之后的满足条件的子序列(因为遇到 s [ 0 ] s[0] s[0]这个字母的时候会直接略过),这个子序列连同开头的 s [ 0 ] s[0] s[0]就是算法得出的解,可以证明这个解就是全局最优解,如果不然,则说明有另一个也以 s [ 0 ] s[0] s[0]开头的子序列是更优解(这里以 s [ 0 ] s[0] s[0]开头的意思是以这个字母开头,不是以在下标 0 0 0的那个 s [ 0 ] s[0] s[0]开头),比如以 s [ k ] = s [ 0 ] s[k]=s[0] s[k]=s[0]开头,那么说明算法求出了 s [ k + 1 : n − 1 ] s[k+1:n-1] s[k+1:n1]中的不含 s [ 0 ] s[0] s[0]这个字母的、且只包含所有字母(当然除了 s [ 0 ] s[0] s[0])一次的字典序最小的子序列,显然该子序列一定不会比 s [ 1 : n − 1 ] s[1:n-1] s[1:n1]的只包含所有字母(除了 s [ 0 ] s[0] s[0])一次的字典序最小的子序列更优,这就矛盾了,所以算法求出的就是最优解;
2、如果 s [ 2 : n − 1 ] s[2:n-1] s[2:n1]中存在小于 s [ 0 ] s[0] s[0]的字母,设第一个小于 s [ 0 ] s[0] s[0]的是 s [ k ] s[k] s[k],那么如果 s [ 1 : k − 1 ] s[1:k-1] s[1:k1]含至少一个”独苗“字母,则独苗不能删,所以最优解一定是 s [ 0 ] s[0] s[0]再拼上 s [ 1 : n − 1 ] s[1:n-1] s[1:n1]去掉字母 s [ 0 ] s[0] s[0]后里的最优解(论证和上面类似),由归纳假设,算法能算出规模更少的情况下的最优解,所以算法正确;如果 s [ 1 : k − 1 ] s[1:k-1] s[1:k1]不含”独苗“字母,那算法会把 s [ 0 : k ] s[0:k] s[0:k]全删掉,并以 s [ k ] s[k] s[k]为新的起点求解,由归纳假设算法可以求得 s [ k : n − 1 ] s[k:n-1] s[k:n1]里的最优解,而该最优解一定是全局最优解(因为全局最优解一定不能以 s [ 0 : k − 1 ] s[0:k-1] s[0:k1]里的字母开头),所以算法也正确。
综上,算法正确。

代码里可以直接用一个StringBuilder来当成栈来用。代码如下:

import java.util.HashMap;
import java.util.Map;

public class Solution {
    public String removeDuplicateLetters(String s) {
    	// 直接用sb作为栈
        StringBuilder sb = new StringBuilder();
        Map<Character, Integer> map = new HashMap<>();
        boolean[] used = new boolean[26];
        
        // 存每个字符出现的最后一个位置的下标
        for (int i = 0; i < s.length(); i++) {
            map.put(s.charAt(i), i);
        }
    
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
           	// 如果ch已经存在于sb中,则直接略过
            if (used[ch - 'a']) {
                continue;
            }
            
            char last = 0;
            // 栈顶大于新来的字符,则栈顶出栈
            while (sb.length() > 0 && (last = sb.charAt(sb.length() - 1)) > ch && map.get(last) > i) {
            	// 标记为false
                used[last - 'a'] = false;
                sb.setLength(sb.length() - 1);
            }
            
            sb.append(ch);
            used[ch - 'a'] = true;
        }
        
        return sb.toString();
    }
}

时空复杂度 O ( n ) O(n) O(n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值