#LC387. 字符串中的第一个唯一字符
字符串中的第一个唯一字符
你好!我是你的 NOI 竞赛教练。这道题虽然在 LeetCode 上被标记为简单,但它涵盖了信息学竞赛中最基础且重要的思想:桶计数(Frequency Array)与两次扫描法。在 NOI 普及组(CSP-J)中,这类时空复杂度平衡的技巧是基本功。
一、 预备知识点
- 计数排序/桶思想:利用数组下标映射字符(如
'a'映射为0),统计出现次数。 - ASCII 码偏移处理:理解
s[i] - 'a'的索引转换方法。 - 时间复杂度分析:区分 (暴力比对)与 (两次扫描)的性能差异。
- 空间复杂度分析:理解字符集大小(26)对空间开销的影响。
二、 题目描述 (NOI 竞赛风格)
题目名称:字符串中的第一个唯一字符 (First Unique Character in a String) 时间限制:1.0s 内存限制:256MB
【问题描述】 给定一个仅由小写英文字母组成的字符串 ,请你找出其中第一个不重复的字符,并返回它的索引。如果不存在,则返回 。 注意:索引从 开始计数。
【输入格式】 输入文件仅一行,包含一个长度为 的字符串 。
【输出格式】 输出一个整数,表示第一个唯一字符的索引。
【样例 1 输入】
leetcode
【样例 1 输出】
0
【样例 2 输入】
loveleetcode
【样例 2 输出】
2
【样例 3 输入】
aabb
【样例 3 输出】
-1
【数据范围说明】
- 对于 的数据,。
- 字符串 只包含小写字母。
三、 教学启发与草稿纸推演
1. 暴力思路:
如果你检查每一个字符 s[i],再扫一遍剩下的字符看它有没有出现过,总步数大约是 。在 1s 时限下(通常处理 次运算),这显然会 TLE。
2. 优化思路:
我们能不能只扫一遍就知道每个字母出现了几次?
- 第一步(统计):准备 26 个“小桶”,分别给 a-z。扫一遍 ,字母出现一次,对应的桶里数字加 1。
- 第二步(查询):再扫一遍 ,看哪个字母所在的桶里数字恰好是 1。第一个满足条件的索引就是答案。
3. 复杂度思考
- 时间:两次循环,共 步,复杂度 。
- 空间:固定 26 个整型空间,复杂度 。
四、 算法流程图 (伪代码逻辑)
为了防止 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 读题时,捕捉以下关键词:
- “第一个” (First):暗示你必须按照原字符串的原始顺序进行第二次扫描,而不是扫描桶数组。
- “不重复” (Unique):意味着桶计数值必须严格等于 1。
- “小写字母” (Lowercase letters):暗示桶的大小只需要固定为 26。这是空间优化的重要提示。
- “不存在返回 -1”:这是边界条件,意味着你的循环结束后需要有一个默认返回值。
优化建议:
在 C++14 竞赛实践中,字符串长度达到 时,建议使用 std::string 或 char 数组,并使用 s.length() 预存长度。对于更复杂的数据范围,如果字符集扩展到 Unicode,桶可能需要换成 std::map 或哈希表,但本题固定 26 个字母,**静态数组(桶)**是性能最优解。