题目描述

[!green]

牛客原题: ✅ 补充题 141. 字符流中第一个不重复的字符

字符流逐个插入 ASCII 字符。

每次查询返回目前只出现一次的字符中最早插入的那个;不存在时返回 #。

示例 1:

输入: 依次插入的字符 = "google",每次插入后查询一次
输出: ["g","g","g","#","l","l"]
解释: 插入第二个 g 后,g 和 o 都重复,因此返回 #;插入 l 后,它成为最早的唯一字符。

提示:

  • 每次插入一个 ASCII 字符。
  • 按目前整个输入前缀查询,不存在唯一字符时返回 #。

题意分析

查询需要同时知道“只出现一次”和“最早插入”两个条件。计数可以判断是否重复,队列则保留各字符首次出现顺序;字符一旦重复,在只插入的流中就永远不会重新唯一。

解法:三态计数与候选队列

核心思路

[!blue]

count[c] 只保存 0、1、2 三种状态,其中 2 表示至少两次。首次插入才把字符放到队尾,后续插入只把状态饱和到 2,避免长字符流中的计数溢出。

查询时,反复删除队首已重复的字符。只要队首仍唯一,它就是全部剩余候选中首次出现最早的一个;队内较后位置的失效字符暂时不影响答案,可以延后处理。

每个 ASCII 字符至多入队和出队一次,连续查询不会重复扫描已经淘汰的前缀。候选为空返回 #;Java 用队列删除队首,Go 只推进 head,避免搬移元素。

解题步骤

  1. 字符首次出现时入候选队列,计数饱和到 2 表示重复。
  2. 查询时不断移除队头已经重复的字符。
  3. 队列为空返回 #,否则返回队头唯一字符。

代码实现

class FirstUnique {
    private final int[] count = new int[128];
    private final ArrayDeque<Character> queue = new ArrayDeque<>();

    public void insert(char c) {
        if (count[c] == 0) {
            queue.addLast(c);
        }

        count[c] = Math.min(2, count[c] + 1);
    }

    public char first() {
        while (!queue.isEmpty() && count[queue.peekFirst()] != 1) {
            queue.removeFirst();
        }

        return queue.isEmpty() ? '#' : queue.peekFirst();
    }
}
type FirstUnique struct {
    count [128]int
    queue []byte
    head  int
}

func (s *FirstUnique) Insert(c byte) {
    if s.count[c] == 0 {
        s.queue = append(s.queue, c)
    }
    s.count[c] = min(2, s.count[c]+1)
}

func (s *FirstUnique) First() byte {
    for s.head < len(s.queue) && s.count[s.queue[s.head]] != 1 {
        s.head++
    }
    if s.head == len(s.queue) {
        return '#'
    }
    return s.queue[s.head]
}

复杂度分析

  • 时间复杂度:n次插入/查询总时间 $O(n)$。
  • 空间复杂度:固定ASCII字母表下空间 $O(1)$。

关键点总结

[!green]

一个字符变成重复后不会再次唯一,候选只需入队一次,后续失效可以等它到队首时再处理。

易错点总结

[!yellow]

重复后不能重新变成唯一;不是返回当前字符串中的最小字母;Java实例化FirstUnique,Go零值即可使用。

相似题目

题目 难度 关联与区别
387. 字符串中的第一个唯一字符 简单 从静态统计再扫描扩展到在线输入,队列保留第一次出现顺序,并延迟清除已重复候选。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/64917589
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!