#LC387. 字符串中的第一个唯一字符

字符串中的第一个唯一字符

你好!我是你的 NOI 竞赛教练。这道题虽然在 LeetCode 上被标记为简单,但它涵盖了信息学竞赛中最基础且重要的思想:桶计数(Frequency Array)两次扫描法。在 NOI 普及组(CSP-J)中,这类时空复杂度平衡的技巧是基本功。


一、 预备知识点

  1. 计数排序/桶思想:利用数组下标映射字符(如 'a' 映射为 0),统计出现次数。
  2. ASCII 码偏移处理:理解 s[i] - 'a' 的索引转换方法。
  3. 时间复杂度分析:区分 O(n2)O(n^2)(暴力比对)与 O(n)O(n)(两次扫描)的性能差异。
  4. 空间复杂度分析:理解字符集大小(26)对空间开销的影响。

二、 题目描述 (NOI 竞赛风格)

题目名称:字符串中的第一个唯一字符 (First Unique Character in a String) 时间限制:1.0s 内存限制:256MB

【问题描述】 给定一个仅由小写英文字母组成的字符串 ss,请你找出其中第一个不重复的字符,并返回它的索引。如果不存在,则返回 1-1。 注意:索引从 00 开始计数。

【输入格式】 输入文件仅一行,包含一个长度为 nn 的字符串 ss

【输出格式】 输出一个整数,表示第一个唯一字符的索引。

【样例 1 输入】

leetcode

【样例 1 输出】

0

【样例 2 输入】

loveleetcode

【样例 2 输出】

2

【样例 3 输入】

aabb

【样例 3 输出】

-1

【数据范围说明】

  • 对于 100%100\% 的数据,1s1051 \le |s| \le 10^5
  • 字符串 ss 只包含小写字母。

三、 教学启发与草稿纸推演

1. 暴力思路:O(n2)O(n^2)

如果你检查每一个字符 s[i],再扫一遍剩下的字符看它有没有出现过,总步数大约是 105×105=101010^5 \times 10^5 = 10^{10}。在 1s 时限下(通常处理 10810^8 次运算),这显然会 TLE

2. 优化思路:O(n)O(n)

我们能不能只扫一遍就知道每个字母出现了几次?

  • 第一步(统计):准备 26 个“小桶”,分别给 a-z。扫一遍 ss,字母出现一次,对应的桶里数字加 1。
  • 第二步(查询):再扫一遍 ss,看哪个字母所在的桶里数字恰好是 1。第一个满足条件的索引就是答案。

3. 复杂度思考

  • 时间:两次循环,共 2n2n 步,复杂度 O(n)O(n)
  • 空间:固定 26 个整型空间,复杂度 O(1)O(1)

四、 算法流程图 (伪代码逻辑)

为了防止 Mermaid 语法冲突,我们使用圆括号表示节点,并避免使用大于号和方括号。

graph TD
    A(开始读取字符串 s) --> B(初始化长度为 26 的整数数组 cnt 为 0)
    B --> C(循环变量 i 从 0 到 字符串末尾)
    C --> D(字符对应的 cnt 下标递增 1)
    D --> E(所有字符统计完成)
    E --> F(循环变量 j 重新从 0 到 字符串末尾)
    F --> G{查询 s_j 对应的 cnt 是否等于 1}
    G -- "是" --> H(返回当前索引 j 并退出)
    G -- "否" --> I(继续循环 j)
    I --> J{遍历完所有字符了吗}
    J -- "否" --> F
    J -- "是" --> K(返回 负1)
    H --> L(结束)
    K --> L

五、 关键词理解 (读题技巧)

在 NOI 读题时,捕捉以下关键词:

  1. “第一个” (First):暗示你必须按照原字符串的原始顺序进行第二次扫描,而不是扫描桶数组。
  2. “不重复” (Unique):意味着桶计数值必须严格等于 1
  3. “小写字母” (Lowercase letters):暗示桶的大小只需要固定为 26。这是空间优化的重要提示。
  4. “不存在返回 -1”:这是边界条件,意味着你的循环结束后需要有一个默认返回值。

优化建议: 在 C++14 竞赛实践中,字符串长度达到 10510^5 时,建议使用 std::stringchar 数组,并使用 s.length() 预存长度。对于更复杂的数据范围,如果字符集扩展到 Unicode,桶可能需要换成 std::map 或哈希表,但本题固定 26 个字母,**静态数组(桶)**是性能最优解。