316. Remove Duplicate Letters

練習日期:2026-03-28

難度:Medium

類型:String、Stack、Greedy、Monotonic Stack

📘 題目敘述

給定一個字串 s,請移除重複字母,讓每個字母都只出現一次。

同時還要保證最後得到的結果,在所有可能答案中,字典序最小。

條件限制

  • 1 <= s.length <= 10^4
  • s 只包含小寫英文字母

🧠 解題思路

我這題的想法是用 greedy 搭配 monotonic stack。

因為題目不只是要把重複字母刪掉,還要求最後答案的字典序最小,所以我在掃字串時,會盡量讓前面的字元小一點。

如果目前要放進答案的字元 c 比 stack 頂端更小,而且 stack 頂端那個字元後面還會再出現,那我就可以先把頂端彈掉,等之後再放回來。 這樣可以讓比較小的字元更早出現在答案裡,字典序就會更小。

所以整體做法是:

  • count 記錄每個字元後面還剩幾次
  • check 記錄某個字元目前是不是已經在 stack 裡了
  • stack 維護目前建出來的答案

每次處理新字元時:

  • 先更新它剩餘次數
  • 如果它已經在 stack 裡,就跳過
  • 否則就檢查能不能把 stack 頂端較大的字元彈掉
  • 最後再把目前字元放進 stack

所有變數

  • count:記錄每個字母目前還剩幾次還沒處理到
  • check:記錄每個字母目前是否已經在 stack 裡
  • st:monotonic stack,維護目前答案的字元順序
  • ans:最後從 stack 還原出來的答案字串

🪜 主要流程步驟

1. 先統計每個字元總共出現幾次

我先用 count 把整個字串裡每個字母的出現次數都統計起來:

vector<int> count(26, 0);
for (char c : s) {
    count[c - 'a']++;
}

這一步很重要,因為後面我在決定要不要把 stack 頂端字元彈掉時, 要知道那個字元未來還會不會再出現。

如果後面不會再出現,那就不能亂彈,不然答案就會少掉那個字母。


2. 用 check 記錄哪些字元已經在答案裡

我另外開一個 check 陣列:

vector<bool> check(26, false);

它的用途是記錄某個字母目前是不是已經放進 stack 裡了。

因為題目要求每個字母最後只能出現一次, 所以如果某個字元已經在 stack 裡,後面再遇到同樣字元時,就可以直接跳過,不需要重複放進去。


3. 逐個掃描字串,決定每個字元要不要放進 stack

接著我開始從左到右掃字串中的每個字元 c

每次先做:

count[c - 'a']--;

這表示目前這個字元已經被我處理到了,所以它「後面還剩幾個」的數量要先扣掉一。

接著如果這個字元已經在 stack 裡,也就是:

if (check[c - 'a']) continue;

那我就直接跳過。 因為它已經存在答案中了,不需要再放一次。


4. 如果 stack 頂端比較大,而且之後還會再出現,就把它彈掉

這題最核心的地方就在這個 while

while (!st.empty() && st.top() > c && count[st.top() - 'a'] > 0) {
    check[st.top() - 'a'] = false;
    st.pop();
}

這段意思是:

  • !st.empty():stack 不能是空的
  • st.top() > c:目前 stack 頂端字元比現在這個字元大
  • count[st.top() - 'a'] > 0:而且那個頂端字元後面還會再出現

如果這三個條件都成立,我就可以放心把頂端字元先彈掉。

原因是:

  • 目前有一個更小的字元 c 可以提早放進答案
  • 被彈掉的字元之後還找得到,不會真的消失

所以這樣做可以讓答案的字典序變得更小,而且又不會漏掉任何必須保留的字母。

另外在彈掉之前,我也要先把:

check[st.top() - 'a'] = false;

設回去,表示這個字元現在已經不在 stack 裡了,之後如果再遇到它,還可以重新放回來。


5. 把目前字元放進 stack,並標記已出現

當前面的彈出動作都做完之後,就代表目前這個字元 c 已經找到它現在最適合放的位置了。

所以我就把它 push 進 stack:

st.push(c);
check[c - 'a'] = true;

這代表:

  • 目前答案尾端加入了這個字元
  • 而且它已經存在於 stack 中,之後再遇到相同字元時就不能重複加入

6. 最後把 stack 還原成字串答案

當整個字串都掃完之後,stack 裡就放著最後答案的字元,只是順序是由底到頂。

所以我最後用一個字串 ans 把它倒回來

因為 stack 是後進先出,所以我每次把頂端字元接到 ans 前面, 這樣最後組出來的字串順序才會正確。

最後回傳 ans 就可以了。

⏱️ 複雜度

n = s.length

  • 時間複雜度:O(n) 每個字元最多只會被 push 進 stack 一次、pop 出來一次,所以整體還是線性時間。
  • 空間複雜度:O(n) stack 最多會放進 n 個字元;另外 countcheck 是固定大小。

💻 程式碼實作

class Solution {
public:
    string removeDuplicateLetters(string s) {
        vector<int> count(26, 0);
        vector<bool> check(26, false);
        for (char c : s) {
            count[c - 'a']++;
        }

        stack<char> st;
        for (char c : s) {
            count[c - 'a']--;
            if (check[c - 'a']) continue;
            while (!st.empty() && st.top() > c && count[st.top() - 'a'] > 0) {
                check[st.top() - 'a'] = false;
                st.pop();
            }
            st.push(c);
            check[c - 'a'] = true;
        }

        string ans = "";
        while (!st.empty()) {
            ans = st.top() + ans;
            st.pop();
        }
        return ans;
    }
};

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

作者: scottnick
撰寫日期: 2026-03-28
類別:
原文連結: https://scottnick.github.io/cpp-notes/all%20problems/316-remove-duplicate-letters.html