题目描述

:::fold-green 相关原题

LeetCode 原题: ✅ 316. 去除重复字母

:::

给定小写英文字母字符串 s,删除若干字符,使结果包含原串中的每种字母且恰好一次。

在所有满足条件的子序列中,返回字典序最大的一个。

示例 1:

输入: s = "abacb"
输出: "bac"
解释: 保留第二、三、四个字符,包含 a、b、c 各一次,且不能在保留顺序的条件下得到 c 开头的合法答案。

示例 2:

输入: s = "bcabc"
输出: "cab"
解释: 选择中间的 c 和末尾的 a、b。

提示:

  • 只允许删除,不能打乱剩余字符顺序。
  • 空串返回空串。

题意分析

要让字典序最大,应尽量把较大的字母放在前面;但每种字母必须保留一次,不能直接排序,也不能删掉以后无法补回的字母。需要同时掌握当前结果和后续可用字符。

解法:可恢复字符的贪心单调栈

核心思路

[!blue]

remaining 表示当前位置之后各字母的剩余次数,used 表示字母是否已在结果栈中。读取字符先减少剩余次数,若已在栈中就跳过,保证结果不重复。

新字符 c 大于栈顶时,若栈顶在后面还会出现,就可弹出它并清除 used,让更大的 c 提前,旧字母以后再补。这在第一个变化位置严格改善字典序,同时不破坏所有字母最终可选的条件。

如果栈顶不小于 c,弹出不会变优;如果剩余次数为 0,弹出会永久漏字母,两种情况都必须停止。栈按扫描顺序接收字符,保留子序列顺序,每个输入位置最多入栈、出栈一次。

解题步骤

  1. 先统计所有字母的剩余次数。
  2. 读取字符时减少其剩余次数;已在栈中则跳过。
  3. 新字符更大且栈顶以后还能出现时弹栈,再把新字符入栈并标记。

代码实现

class Solution {
    public String largestDistinct(String s) {
        int[] remaining = new int[26];
        boolean[] used = new boolean[26];

        for (char c : s.toCharArray()) {
            remaining[c - 'a']++;
        }

        StringBuilder stack = new StringBuilder();

        for (char c : s.toCharArray()) {
            int index = c - 'a';

            remaining[index]--;

            if (used[index]) {
                continue;
            }

            while (stack.length() > 0) {
                char top = stack.charAt(stack.length() - 1);

                if (top >= c || remaining[top - 'a'] == 0) {
                    break;
                }

                used[top - 'a'] = false;
                stack.setLength(stack.length() - 1);
            }

            stack.append(c);
            used[index] = true;
        }

        return stack.toString();
    }
}
func largestDistinct(s string) string {
    remaining := [26]int{}
    used := [26]bool{}
    for _, c := range []byte(s) {
        remaining[c-'a']++
    }
    stack := []byte{}
    for _, c := range []byte(s) {
        index := c - 'a'
        remaining[index]--
        if used[index] {
            continue
        }
        for len(stack) > 0 {
            top := stack[len(stack)-1]
            if top >= c || remaining[top-'a'] == 0 {
                break
            }
            used[top-'a'] = false
            stack = stack[:len(stack)-1]
        }
        stack = append(stack, c)
        used[index] = true
    }
    return string(stack)
}

复杂度分析

  • 时间复杂度:$O(n)$。
  • 空间复杂度:字符集合固定为 26 个时,辅助空间 $O(1)$;Java 转字符数组的实现另占 $O(n)$。

关键点总结

[!green]

只有被弹出的字母还能在后面补回时,才可以让更大字母提前;used 与剩余次数分别保证不重复和不遗漏。

易错点总结

[!yellow]

只有栈顶字符在后面还会出现时才能弹出,否则会漏掉必须保留的字母。

相似题目

题目 难度 关联与区别
316. 去除重复字母 中等 同样利用剩余次数决定能否弹栈,把弹栈比较方向反转即可从最小字典序改为最大字典序。
402. 移掉 K 位数字 中等 同样贪心弹出不利的栈顶,原题按删除次数限制,本题按每种字母至少保留一次限制。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/1790168853
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!