题目描述

✅ 387. 字符串中的第一个唯一字符

image-20260928235506120

题意分析

返回字符串中最靠前的、在整个字符串里只出现一次的字符下标;不存在则返回 -1。这里的“第一个”按原字符串位置判断,不是按字母顺序判断。题目只包含小写英文字母,可以用长度为 26 的数组计数。

解法:两次遍历计数

核心思路

[!blue]

判断一个字符是否唯一,必须知道它在整个字符串中的出现次数。第一次遇到它时只能确认“目前出现一次”,后面仍可能重复,因此先完整扫描字符串,将字符 c 的次数记录在 frequency[c - 'a'] 中。

计数完成后,再按下标从小到大扫描。若当前位置的字符频次为 1,它就是全局唯一字符;之前的位置都已检查过且不符合条件,所以第一次命中的下标必然是题目要求的最小下标,可以立即返回。

若第二次扫描结束仍未命中,说明所有字符都出现多次,返回 -1。两次遍历分别解决“是否唯一”和“是否最早”,不需要排序,也不用额外记录每个字符的位置。

解题步骤

  1. 创建长度为 26、初始全为 0 的计数数组 frequency。
  2. 第一次遍历字符串,用当前字符减去 'a' 得到计数下标,将对应次数加一。
  3. 第二次从下标 0 开始遍历;若当前字符的总频次为 1,返回当前字符串下标。
  4. 全部位置都不符合时返回 -1。

Java 的 charAt(i) 和 Go 的 s[i] 在本题小写英文字母范围内都能直接取得当前字符,返回的 i 就是所需位置。

代码实现

class Solution {
    public int firstUniqChar(String s) {
        int[] frequency = new int[26];

        // 先收集完整频次,避免过早判断唯一
        for (int i = 0; i < s.length(); i++) {
            frequency[s.charAt(i) - 'a']++;
        }

        // 再按原顺序返回首个全局唯一字符的下标
        for (int i = 0; i < s.length(); i++) {
            if (frequency[s.charAt(i) - 'a'] == 1) {
                return i;
            }
        }

        return -1;
    }
}
func firstUniqChar(s string) int {
    frequency := [26]int{}
    // 先收集完整频次,避免过早判断唯一
    for i := 0; i < len(s); i++ {
        frequency[s[i]-'a']++
    }

    // 再按原顺序返回首个全局唯一字符的下标
    for i := 0; i < len(s); i++ {
        if frequency[s[i]-'a'] == 1 {
            return i
        }
    }
    return -1
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是字符串长度,两次扫描最多各访问 $n$ 个字符。
  • 空间复杂度:$O(1)$,计数数组始终只有 26 项。

关键点总结

[!green]

  • 完整频次保证“只出现一次”,原串顺序保证“第一个”。
  • 字符减去 'a' 得到的是计数数组下标,最终返回的是它在字符串中的下标。

易错点总结

[!yellow]

  • 第一次遇到字符就判断唯一,会漏掉它在后面重复出现的情况。
  • 按 26 个字母的顺序检查计数,会找到字母序最小的唯一字符,不能保证位置最早。
  • 没有答案必须返回 -1,因为 0 本身也是合法答案。

相似题目

题目 难度 关联与区别
383. 赎金信 简单 同样先按字符累计频次,本题再结合原顺序定位首个频次为1的位置。
451. 根据字符出现频率排序 中等 原题按频次重新排列字符,本题保留原出现顺序并只找唯一字符。
补充题 201. 字符串中第一个不重复的字符 简单 区分大小写的首个唯一字符。
补充题 141. 字符流中第一个不重复的字符 中等 都维护每个字符的出现次数;补充题在每次插入后查询当前首个唯一字符。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/20972578
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!