编程题-去除重复字母(中等)

题目:

给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

解法一(哈希表):

根据字典序最小的原理,应尽可能保证最小的字典序的字符出现在字符串前面(即字符串第一位尽可能小,字符串第二位在剩余字符串中也应尽可能小以满足题目要求),因此定义vector<char>保存出现的所有字符串并将其进行排序,定义unordered_map<char, int>保持出现所有字符的数量,用于确定满足题目要求的最小字典序的字符出现的位置。

在vector<char>排序字符中,先确定最小的字符是否满足题目要求(即在s中第一次出现时的位置,并保证vector<char>剩余包含的字符在后续中s中均可以查找到),通过哈希表计数,当s后续中不存在vector<char>剩余包含的字符时,该最小字典序不能作为最小字符,查找vector<char>第二小的字符,以此类推.....当最小字典序满足要求时,记录在s的索引位置(后续遍历查找均在该索引后进行),并在vector<char>删除满足要求的字符,进入下一次循环直至vector<char>中无剩余字符停止迭代。如下为笔者代码:

class Solution {
public:
    int getindex(string& s, unordered_map<char, int> hashTable1, char c, int index, vector<char> kyk){
        int length = s.size();
        int result=-1;
        int length_kyk = kyk.size();
        for(int i=0;i<index;i++){
            hashTable1[s[i]]--;
        }
        for(int i=index;i<length;i++){
            if(s[i]!=c){
                hashTable1[s[i]]--;
                if(hashTable1[s[i]]==0){     
                    return -1;
                }
            }
            else{
                return i;
            }
        }
        return result;
    }
    
    string removeDuplicateLetters(string s) {
        string result1="";
        vector<char> kyk;
        unordered_map<char, int> hashTable1;
        for(char c:s){
            auto it = hashTable1.find(c);
            hashTable1[c]++;
            if(it==hashTable1.end()){
                kyk.push_back(c);
            }
        }
        int length_kyk = kyk.size();
        int length = s.size();
        sort(kyk.begin(),kyk.end());
        int index=0;
        for(int i=0; i<length_kyk; i++){
            int a = getindex(s, hashTable1, kyk[i], index, kyk);
            if(a==-1){
                continue;
            }
            else{
                result1=result1+kyk[i];
                hashTable1.erase(kyk[i]);
                kyk.erase(kyk.begin()+i);
                length_kyk--;
                index = a;
                i=-1;
            }
        }
        return result1;
    }
};

解法二(贪心 + 单调栈):

首先考虑一个简单的问题:给定一个字符串s,如何去掉其中一个字符ch,使得得到的字符串字典序最小呢?答案是:找出最小的满足s[i]>s[i+1]的下表i,并去除字符s[i]。为了叙述方便,下面称这样的字符为【关键字符】。

我们从前向后扫描原字符串。每扫描到一个位置,我们就尽可能地处理所有的【关键字符】。假定在扫描位置s[i-1]之前的所有【关键字符】都已经被去除完毕,在扫描字符s[i]时,新出现的【关键字符】只可能出现在s[i]或者其后面的位置。

于是,我们使用单调栈来维护去除【关键字符】后得到的字符串,单调栈满足栈底到栈顶的字符递增。如果栈顶字符大于当前字符s[i],说明栈顶字符为【关键字符】,故应当被去除。去除后,新的栈顶字符就与s[i]相邻了,我们继续比较新的栈顶字符与s[i]的大小。重复上述操作,直到栈为空或者栈顶字符不大于s[i]。

我们还遗漏了一个要求:原字符串s中的每个字符都需要出现在新字符串中,且只能出现一次,为了让新字符串满足该要求,之前讨论的算法需要进行一下两点的更改:

1、在考虑字符s[i]时,如果它已经存在于栈中,则不能加入字符s[i]。为此,需要记录每个字符是否出现在栈中。

2、在弹出栈顶字符时,如果字符串在后面的位置上再也没有这一字符,则不能弹出栈顶字符。为此,需要记录每个字符的剩余数量,当这个值为0时,就不能弹出栈顶字符。

如下为利用贪心算法+单调栈数据结构实现的代码:

class Solution {
public:
    string removeDuplicateLetters(string s) {
        //num记录26个英文字符出现的次数
        //vis为记录每个字符是否出现在栈中(初始值为均0,1为在栈中,0为不在栈中)
        vector<int> vis(26), num(26);
        for (char ch : s) {
            num[ch - 'a']++;
        }
        //定义单调栈stk(string类型)
        string stk;
        for (char ch : s) {
            //判断vis站中是否存在相同的字符,如果栈中已出现,则不能加入字符ch
            if (!vis[ch - 'a']) {
                while (!stk.empty() && stk.back() > ch) {
                    if (num[stk.back() - 'a'] > 0) {
                        vis[stk.back() - 'a'] = 0;
                        stk.pop_back();
                    } else {
                        break;
                    }
                }
                vis[ch - 'a'] = 1;
                stk.push_back(ch);
            }
            num[ch - 'a'] -= 1;
        }
        return stk;
    }
};

时间复杂度:O(N),其中 N 为字符串长度。代码中虽然有双重循环,但是每个字符至多只会入栈、出栈各一次。空间复杂度:O(∣Σ∣),其中 Σ 为字符集合,本题中字符均为小写字母,所以 ∣Σ∣=26。由于栈中的字符不能重复,因此栈中最多只能有 ∣Σ∣ 个字符,另外需要维护两个数组,分别记录每个字符是否出现在栈中以及每个字符的剩余数量

笔者小记:

1、提示中s均为26个小写英文字符,则可以定义vector<int>数组用于计数和判断在某一(另一个数据结构)中存储出现情况。

2、相比利用哈希表,利用单调栈(string类型的)和vector<int>(技术和判断是否出现情况)可以更好的实现低时间复杂度的一次遍历。

3、vector<int> vis(26) 表示创建数组名为vis,数组大小为26的数组。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值