LeetCode-Go 题解:387. First Unique Character in a String —— 字符串中第一个唯一字符的两种实现
2026/9/11 2:44:08 网站建设 项目流程

LeetCode-Go 题解:387. First Unique Character in a String —— 字符串中第一个唯一字符的两种实现

【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go

导读

本文基于 LeetCode-Go 仓库中leetcode/0387.First-Unique-Character-in-a-String目录下的官方题解展开,深入讲解 LeetCode 第 387 题「字符串中的第一个唯一字符」的题目背景、解题思路与两种 Go 实现方案。读完本文,你将掌握如何用计数数组位置记录法两种思路在线性时间内定位第一个只出现一次的字符,并通过仓库自带的单元测试验证实现正确性。

题目理解

原题描述

Given a string, find the first non-repeating character in it and return it's index. If it doesn't exist, return -1.

给定一个字符串s,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。

题目还给出了两个示例:

s = "leetcode" return 0 s = "loveleetcode" return 2

对于"leetcode",字符l是第一个只出现一次的字符,其索引为 0;对于"loveleetcode",字符v是第一个只出现一次的字符,其索引为 2。

关键约束

题目明确给出了一个前提(Note):

You may assume the string contain only lowercase letters.

可以假设字符串只包含小写字母。

这一约束是本题得以用固定大小数组实现 O(1) 空间复杂度的基础:小写字母只有 26 个,因此可以用一个长度为 26 的数组直接完成字符频率统计,无需使用哈希表。

解题思路

本题是一道经典的「计数 + 顺序扫描」问题,解题思路分两个阶段:

  1. 统计阶段:遍历一次字符串,记录每个字符出现的频次(或首次/再次出现的位置)。
  2. 查找阶段:再次从头遍历字符串(或遍历 26 个字母),找到第一个频次为 1 的字符并返回其索引;若所有字符都重复出现,则返回 -1。

仓库在 387. First Unique Character in a String.go 中给出了两种解法,下面分别展开。

解法一:计数数组(两次遍历)

实现代码

// 解法一 func firstUniqChar(s string) int { result := make([]int, 26) for i := 0; i < len(s); i++ { result[s[i]-'a']++ } for i := 0; i < len(s); i++ { if result[s[i]-'a'] == 1 { return i } } return -1 }

原理剖析

解法一的核心是利用字符与索引的映射关系s[i] - 'a'将字符'a'~'z'映射为整数 0 ~ 25,从而把「字符频率统计」转化为「数组下标累加」。

  • 第一趟遍历(第 6~8 行):统计每个字符在整个字符串中出现的次数,存入result数组;
  • 第二趟遍历(第 9~13 行):按原字符串的顺序从头扫描,一旦遇到某个字符的计数恰好为 1,立即返回其下标i

这里的关键在于第二趟遍历必须保持字符串的原始顺序,而不是遍历result数组,否则只能得到「字典序最小的唯一字符」而非「第一个出现的唯一字符」。

复杂度分析

  • 时间复杂度:O(n),两趟遍历均为线性扫描,其中 n 为字符串长度;
  • 空间复杂度:O(1)result数组长度恒为 26,与输入规模无关。

解法二:位置记录法(首末出现位置)

实现代码

// 解法二 // 执行用时: 8 ms // 内存消耗: 5.2 MB func firstUniqChar1(s string) int { charMap := make([][2]int, 26) for i := 0; i < 26; i++ { charMap[i][0] = -1 charMap[i][1] = -1 } for i := 0; i < len(s); i++ { if charMap[s[i]-'a'][0] == -1 { charMap[s[i]-'a'][0] = i } else { //已经出现过 charMap[s[i]-'a'][1] = i } } res := len(s) for i := 0; i < 26; i++ { //只出现了一次 if charMap[i][0] >= 0 && charMap[i][1] == -1 { if charMap[i][0] < res { res = charMap[i][0] } } } if res == len(s) { return -1 } return res }

原理剖析

解法二不再记录「出现次数」,而是为每个字符记录首次出现位置再次出现位置,数据结构为[26][2]int

  • charMap[i][0]:字符首次出现的位置(初始化为 -1);
  • charMap[i][1]:字符再次出现的位置(初始化为 -1)。

扫描阶段(第 26~32 行):遍历字符串,若某字符的[0]仍为 -1,说明是首次出现,记录当前位置;否则说明已经出现过,将当前位置写入[1]。注意:即使字符出现三次及以上,[1]也只会被持续覆盖为最后一次出现的位置,但这不影响判断——因为只要出现过一次以上,[1]就必然不再是 -1。

筛选阶段(第 33~41 行):遍历 26 个字母,找出所有「首次出现且仅出现一次」的字符,即满足charMap[i][0] >= 0 && charMap[i][1] == -1的项,并取其中首次出现位置最小的那个作为答案。

兜底判断res初始化为len(s),若最终仍为len(s),说明不存在只出现一次的字符,返回 -1。res == len(s)这个条件天然不会与任何合法下标冲突,因为字符串有效下标最大为len(s)-1

源码注释中记录了该解法在 LeetCode 在线评测环境中的实测表现为「执行用时 8 ms,内存消耗 5.2 MB」,可作为参考。

复杂度分析

  • 时间复杂度:O(n),一次字符串扫描 + 一次固定 26 项的字母扫描;
  • 空间复杂度:O(1),固定为 26 个[2]int元素。

测试验证

仓库为本题配套了完整测试 387. First Unique Character in a String_test.go,采用「参数 + 期望答案」的结构化用例组织方式:

qs := []question387{ { para387{"leetcode"}, ans387{0}, }, { para387{"loveleetcode"}, ans387{2}, }, { para387{"aabb"}, ans387{-1}, }, }

测试覆盖了三种典型场景:

输入期望输出覆盖场景
"leetcode"0首个字符即为唯一字符(l
"loveleetcode"2唯一字符位于中间(v
"aabb"-1所有字符均重复,不存在唯一字符

测试函数Test_Problem387firstUniqCharfirstUniqChar1两种实现逐一断言,任一实现返回结果与期望不符都会通过t.Fatalf立即终止测试并输出具体差异:

if got := firstUniqChar(p.n); got != a.one { t.Fatalf("firstUniqChar(%q) = %d, want %d", p.n, got, a.one) } if got := firstUniqChar1(p.n); got != a.one { t.Fatalf("firstUniqChar1(%q) = %d, want %d", p.n, got, a.one) }

在仓库根目录执行go test ./leetcode/0387.First-Unique-Character-in-a-String/即可运行上述用例(也可参考仓库根目录的 gotest.sh 批量执行测试脚本)。

两种解法对比与小结

维度解法一:计数数组解法二:位置记录法
数据结构[]int长度 26[26][2]int
统计信息每个字符出现次数首次与再次出现位置
遍历次数字符串 2 趟字符串 1 趟 + 字母表 1 趟
时间复杂O(n)O(n)
空间复杂O(1)O(1)
代码量更简洁略长,但少一次字符串扫描

两种解法本质上是同一思路的两种载体:解法一以「计数」为判断依据,逻辑最直观、代码最精简,是面试中最推荐的写法;解法二以「位置」为判断依据,虽然代码略长,但将字符的首次出现位置直接保留下来,省去了一次对字符串的完整回扫(用固定 26 项的字母表遍历替代),在特定场景下能减少一次内存访问开销。

无论采用哪种实现,都必须牢牢抓住两个核心点:一是在统计阶段利用s[i]-'a'完成字符到下标的映射;二是在查找阶段严格保证「第一个」的语义(按字符串顺序或取最小首次位置),并在无唯一字符时正确返回 -1。

【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询