字符串中查找第一个不重复字符的问题,本质上是一个"在有序序列中按特定条件筛选元素"的典型场景。这个看似简单的题目背后隐藏着几个关键特征:
这种问题的核心矛盾在于:统计频率需要完整遍历整个字符串,而查找第一个符合条件的元素又要求我们按顺序检查。这就引出了经典的"两遍遍历"解法范式——第一遍收集全局信息,第二遍按需筛选。
实际工程中类似场景比比皆是。比如日志分析时查找第一个异常事件,或者用户行为序列中定位首次违规操作,都需要这种"先统计后筛选"的思路。
Python中使用字典作为哈希表实现是最直观的方案。以下是带详细注释的实现:
python复制def first_uniq_char(s: str) -> int:
"""查找字符串中第一个不重复字符的索引
Args:
s: 输入字符串
Returns:
第一个唯一字符的索引,若无则返回-1
"""
freq = {}
# 第一遍遍历:构建字符频率字典
for char in s:
# 使用get方法安全访问,避免KeyError
freq[char] = freq.get(char, 0) + 1
# 第二遍遍历:按顺序查找第一个唯一字符
for idx, char in enumerate(s):
if freq[char] == 1:
return idx
return -1 # 没有找到唯一字符
字典的get方法是这个实现中的关键技巧:
python复制freq[char] = freq.get(char, 0) + 1
这行代码的精妙之处在于:
get返回默认值0,+1后该字符计数变为1get返回当前计数值,+1实现计数递增枚举遍历是另一个重要细节:
python复制for idx, char in enumerate(s):
相比传统的range(len(s))方案,enumerate能同时获取索引和字符,使代码更Pythonic且不易出错。
当问题明确字符集范围时(如仅小写字母),可以使用数组替代字典:
python复制def first_uniq_char(s: str) -> int:
count = [0] * 26 # 26个小写字母的计数器
# 第一遍遍历:统计字符出现次数
for c in s:
count[ord(c) - ord('a')] += 1
# 第二遍遍历:查找第一个唯一字符
for i, c in enumerate(s):
if count[ord(c) - ord('a')] == 1:
return i
return -1
这种方案的优势:
理论上可以通过单次遍历实现,但需要更复杂的数据结构:
python复制def first_uniq_char(s: str) -> int:
position = {} # 存储字符首次出现位置
count = {} # 字符出现次数
for idx, char in enumerate(s):
if char not in count:
count[char] = 1
position[char] = idx
else:
count[char] += 1
# 如果字符已重复,可以从position中移除
if char in position:
position.pop(char)
# 返回position中值最小的索引
return min(position.values()) if position else -1
这种方案虽然只需一次遍历,但:
在面试中,建议优先选择清晰易懂的两遍遍历方案,除非面试官明确要求优化。
全面的测试用例应该覆盖各种边界情况:
| 测试类型 | 示例输入 | 预期输出 | 验证目的 |
|---|---|---|---|
| 常规情况 | "leetcode" | 0 ('l') | 基本功能验证 |
| 唯一字符在中间 | "loveleetcode" | 2 ('v') | 顺序查找验证 |
| 无唯一字符 | "aabbcc" | -1 | 无结果处理 |
| 空字符串 | "" | -1 | 极端边界 |
| 全重复字符 | "zzzzzz" | -1 | 全重复场景 |
| 单字符 | "x" | 0 | 最小输入 |
| 大小写混合 | "sTreSS" | 0 ('s') | 大小写敏感度 |
| Unicode字符 | "😊😊😂" | 2 ('😂') | 非ASCII支持 |
在面试中,主动提及这些测试用例能展现全面的思维。实际编码时可以编写单元测试:
python复制import unittest
class TestFirstUniqChar(unittest.TestCase):
def test_examples(self):
self.assertEqual(first_uniq_char("leetcode"), 0)
self.assertEqual(first_uniq_char("loveleetcode"), 2)
self.assertEqual(first_uniq_char("aabbcc"), -1)
self.assertEqual(first_uniq_char(""), -1)
self.assertEqual(first_uniq_char("z"), 0)
self.assertEqual(first_uniq_char("sTreSS"), 0)
生产环境中的实现需要考虑更多边界情况:
python复制def first_uniq_char(s: str) -> int:
if not isinstance(s, str):
raise TypeError("Input must be a string")
if len(s) == 0:
return -1
# 剩余实现...
对于超长字符串的优化策略:
当内存受限时:
"让我先理解题目要求:我们需要在一个字符串中找到第一个只出现一次的字符,并返回它的索引。如果没有这样的字符,则返回-1。关键点在于'第一个'和'只出现一次'这两个条件的结合。"
"我的初步想法是需要两遍遍历:第一遍统计每个字符的出现频率,第二遍按原始顺序查找第一个频率为1的字符。这是因为判断字符是否唯一需要全局信息,而查找第一个又需要保持原始顺序。"
"这个算法的时间复杂度是O(n),因为我们需要遍历字符串两次,但常数系数可以忽略。空间复杂度取决于字符集大小,对于ASCII可以视为O(1),对于Unicode可能需要更多空间。"
边写代码边解释关键点:
"这里使用字典来统计频率,get方法可以安全地处理未出现的字符..."
"第二次遍历使用enumerate同时获取索引和字符,这样可以直接返回位置..."
"让我们验证几个例子:'leetcode'应该返回0,'loveleetcode'返回2,'aabb'返回-1,空字符串也返回-1。这些例子覆盖了正常情况、中间位置、无结果和边界条件。"
错误1:使用list.count方法
python复制# 低效实现:时间复杂度O(n^2)
def first_uniq_char(s: str) -> int:
for i in range(len(s)):
if s.count(s[i]) == 1:
return i
return -1
问题:count方法每次都要遍历整个字符串,导致O(n^2)时间复杂度。
错误2:错误处理大小写
python复制def first_uniq_char(s: str) -> int:
freq = {}
for c in s.lower(): # 错误:改变了原始字符串
freq[c] = freq.get(c, 0) + 1
for i, c in enumerate(s):
if freq[c.lower()] == 1: # 混乱的大小写处理
return i
return -1
问题:大小写处理不一致导致逻辑错误。
掌握这个算法模式可以解决许多变体问题:
这个看似简单的题目包含了算法设计的核心思想:空间换时间、预处理与查询分离、问题分解等。掌握这些基础模式是解决更复杂问题的关键。