(#字符串处理)字符串中第一个不重复的字母

一、题目本质:这是什么问题?

这道题的核心是 "在有序序列中,按顺序找到满足特定统计条件的元素"

  • 有序序列:字符串的字符顺序是固定的。
  • 特定统计条件:字符出现的次数等于1。
  • 按顺序找:必须是从左往右第一个满足条件的。

关键矛盾 :统计频率需要遍历整个字符串,但找第一个又需要顺序。这就引出了经典的两遍遍历法

二、解题思路的诞生过程(像侦探破案)

想象你是侦探,要在人群中找到第一个只出现一次的人。

  1. 第一次调查(建立档案)

    • 你拿着笔记本(哈希表),从头到尾问每个人:"你叫什么?"
    • 每次听到一个名字,就在笔记本上给这个名字做标记(计数+1)。
    • 调查结束后,你知道每个名字出现了几次
  2. 第二次调查(按顺序指认)

    • 按照最初见面的顺序,再次一个个看这些人。
    • 每看一个人,就查笔记本上他的名字出现了几次。
    • 当查到第一个人"只出现一次"时,立刻抓住他(返回结果)。
    • 如果查到结尾都没找到,就报告"找不到"(返回-1)。

为什么必须两遍?

  • 如果只用一遍:当你第一次遇到"张三"时,你不知道后面还会不会出现第二个"张三"。只有等整个调查结束,你才知道每个人总共出现了几次。
  • 如果只用第二遍:你没有笔记本,无法知道每个人总共出现几次。

三、代码逐行解析(带面试话术)

python 复制代码
def firstUniqChar(s: str) -> int:
    # 步骤1:构建"档案本"(哈希表统计频率)
    freq = {}  # 创建一个空字典,键是字符,值是出现次数
    
    for char in s:  # 第一遍遍历:每个字符都登记
        # 如果char已经在字典里,get返回当前值;如果不在,返回0
        # 然后 +1,这样新字符就从1开始计数,旧字符就累加
        freq[char] = freq.get(char, 0) + 1
    
    # 此时,freq字典的样子示例:s="leetcode"
    # {'l':1, 'e':3, 't':1, 'c':1, 'o':1, 'd':1}
    
    # 步骤2:按顺序"指认"
    for i in range(len(s)):
        if freq[s[i]] == 1:  # 如果该字符只出现一次
            return i          # 返回其索引
    
    # 如果循环结束都没返回,说明没有符合条件的字符
    return -1

1、firstUniqChar(s: str) -> int

这是 Python 的类型注解(Type

Hints)。它不是代码执行逻辑的一部分,而是像"注释"或"标签"一样,用来告诉阅读代码的人这个函数期望接收什么类型的参数,以及返回什么类型的值。

参数注解s: str
s:这是函数的参数名。
: str:冒号后面的 str 是一个类型提示,意思是参数 s 期望是一个字符串类型。

返回值注解:-> int
->:这个箭头指示了函数的返回类型。
int:表示这个函数预期会返回一个整数。

返回什么类型通常是看题目要求,你可以这样判断返回类型:

2、 freq.get(char, 0) 的工作原理

  • 查找键:在字典 freq 中查找键 char
  • 如果存在:返回对应的值
  • 如果不存在:返回第二个参数指定的默认值(这里是 0)
bash 复制代码
freq = {}  # 空字典

# 第一次遇到字符 'a'
char = 'a'
current_value = freq.get('a', 0)  # 返回 0(因为'a'不在字典中)
freq['a'] = 0 + 1  # 现在 freq = {'a': 1}

# 第二次遇到字符 'a'
char = 'a'
current_value = freq.get('a', 0)  # 返回 1(因为'a'已在字典中,值为1)
freq['a'] = 1 + 1  # 现在 freq = {'a': 2}
  1. 为什么使用 .get() 而不是直接访问?
    如果你尝试直接访问不存在的键,会报错:
bash 复制代码
# 错误写法
freq = {}
freq['a'] = freq['a'] + 1  # KeyError: 'a'(第一次访问,'a'不存在)
  1. get() 方法的语法
bash 复制代码
字典.get(键, 默认值)
  • 如果键存在:返回该键对应的值
  • 如果键不存在:返回指定的默认值(这里是0)
  1. 键不存在时返回 0 是专门为 计数统计 场景设计的:
  • 如果字符已经在字典中(之前出现过),就返回它当前的计数(大于等于1)。
  • 如果字符不在字典中,就返回0,然后我们加1,表示这是第一次出现。

3、freq[s[i]]可以拆解为:

  • i:当前循环的索引(数字)
  • s[i]:字符串s中第i个位置的字符
  • freq[字符]:在字典freq中查找这个字符对应的出现次数

在 Python 的 for 循环中,循环变量 不需要提前定义。当你写:for i in range(len(s)):

Python 会自动:

创建变量 i

在每次循环时给 i 赋值(0, 1, 2, ...)

循环结束后,i 会保留最后一次的值(但通常我们不依赖这个)

4、为什么不用 for char in s

因为题目要求返回索引,而不是字符:

  • for char in s:只能得到字符,不知道位置
  • for index, char in enumerate(s):同时得到位置和字符

5、区分这些变量:

让我们想象一个场景:你拿着一张购物清单(这就是s),正在超市里一个个地拿商品(每次拿起的那个商品就是char)。

s=你的购物清单(一张纸条,上面写着一串文字)。

  • 例如:s="leo",你的清单上写着"leo"。
  • s是这个字符串整体的名字(变量名)。你一提到S,指的就是"leo"这整个字符 char=你当前正从清单上看着的、准备去拿的那一个商品。

char = 你当前正从清单上看着的、准备去拿的那一个商品。

  • 当你开始购物(开始循环),你的手指会从清单的第一个字母开始移动。
  • 第一次循环:char = 'l' (你看到了"苹果")
  • 第二次循环:char = 'e' (你看到了"牛奶")
  • 第三次循环:char = 'o' (你看到了"面包")
  • char 是你在循环过程中,用来临时存放当前单个字符的变量名。
  1. s 是完整的、不变的容器。循环不会改变 s 本身。
  2. char 是临时的、变化的代表。它依次成为 s 中的每一个元素。char 是字符本身('a', 'b', 'c', ...)
bash 复制代码
for char in s:  # 直接遍历字符串中的字符
    # char 是字符本身
    # 比如 s = "hello" 时:
    # 第一次循环:char = 'h'
    # 第二次循环:char = 'e'
    # ...
  1. i 是索引数字(0, 1, 2, ...)
bash 复制代码
for i in range(len(s)):  # 遍历索引数字
    # i 是索引位置(0, 1, 2, ...)
    # 比如 s = "hello" 时:
    # 第一次循环:i = 0, s[i] = 'h'
    # 第二次循环:i = 1, s[i] = 'e'
    # ...
  1. s[i] 是通过索引获取字符

四、复杂度分析(面试必说)

  • 时间复杂度:O(n)
    • 第一次遍历:O(n)
    • 第二次遍历:O(n)
    • 总时间:O(2n) = O(n) (常数系数可以忽略)
  • 空间复杂度:O(1) 或 O(k)
    • 字典最多存储字符集大小
    • 如果是英文字母:最多26*2=52个,是常数,所以是O(1)
    • 如果是Unicode:可能很多,但通常也视为O(1)

五、测试用例设计(展现测试思维)

面试时写完代码一定要说:"我考虑几个测试用例验证一下..."

测试用例 输入 预期输出 测试目的
正常情况 "leetcode" 0 (字符'l') 基本功能验证
唯一字符在中间 "loveleetcode" 2 (字符'v') 验证按顺序查找
没有唯一字符 "aabb" -1 边界情况处理
空字符串 "" -1 极端边界
全部重复 "zzzz" -1 全重复场景
单字符 "a" 0 最小输入
大小写敏感 "sTreSS" 取决于题目要求 验证是否区分大小写

六、力扣实战:第387题

  • 题目链接387. 字符串中的第一个唯一字符
  • 难度:简单 ⭐
  • 建议练习步骤
    1. 先理解 :读完题目,确保理解是找第一个不重复的字符。
    2. 手写思路:在白纸上画一下两遍遍历的过程。
    3. 尝试写代码:参考上面的代码,但尝试自己写。
    4. 提交测试:用上面的测试用例验证。
    5. 查看题解:如果卡住,看官方题解,但重点理解思路。

七、面试实战话术模板

当面试官出这道题时,你可以这样回答:

  1. 复述题目:"好的,这道题是要在字符串中找到第一个只出现一次的字符,并返回它的索引,对吧?"
  2. 举例说明 :"比如输入 "leetcode",字符 'l' 只出现一次且是第一个,应该返回索引0。"
  3. 阐述思路 :"我的思路是两次遍历。第一次用哈希表统计每个字符的频率,第二次按顺序遍历,找到第一个频率为1的字符。"
  4. 分析复杂度:"时间复杂度是O(n),空间复杂度是O(字符集大小),可以看作O(1)。"
  5. 写代码:边写边解释关键行。
  6. 主动测试 :"写完了,我用几个例子验证一下:"leetcode" 返回0,"aabb" 返回-1,空字符串返回-1,都符合预期。"

八、常见陷阱与改进

  1. 陷阱1 :用 list.count() 方法对每个字符计数 → 时间复杂度O(n²),太慢。
  2. 陷阱2:只遍历一次,用复杂的数据结构记录位置 → 可以但没必要,两遍遍历最简单清晰。
  3. 改进
bash 复制代码
def firstUniqChar(s: str) -> int: # 这里的s是函数参数名,代表传入的整个字符串
    freq = {}

    # 第一次遍历:统计频率
    for char in s: # 遍历整个字符串s,每次循环char代表一个字符
        # char 会依次是: 'l', 'e', 'e', 't', 'c', 'o', 'd', 'e'
        freq[char] = freq.get(char, 0) + 1

    # 第二次遍历:查找第一个唯一字符
    for index, char in enumerate(s): # 同样遍历s,char依然依次代表每个字符
        if freq[char] == 1: # 检查当前这个字符(char)的频率
            return index
    return -1

for index, char in enumerate(s):

是 Python 中一个非常常用的循环语法。 这行代码的意思是:遍历字符串 s,同时获取每个字符的索引(位置)和字符本身。

3.1 enumerate(s) 的作用

enumerate() 是 Python 内置构造函数,它会给可迭代对象(如字符串、列表)的每个元素添加一个计数器(索引)。

bash 复制代码
# 示例
s = "abc"
list(enumerate(s))  # 返回:[(0, 'a'), (1, 'b'), (2, 'c')]

3.2 for index, char in ... 的作用

这是元组解包(tuple unpacking):

  • enumerate(s) 每次产生一个元组,如 (0, 'a')
  • Python 自动将这个元组拆分成两部分:
    • index 接收第一个值(索引)
    • char 接收第二个值(字符)
  1. 改进 :如果字符集只有小写字母,可以用长度为26的数组代替字典,更高效:

    python 复制代码
    def firstUniqChar(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

九、举一反三

掌握这道题后,你可以解决一系列类似问题:

  • 变体1:最后一个不重复字符(倒序遍历)
  • 变体2:第一个重复字符(频率>1)
  • 变体3:所有不重复字符(遍历字典,按值过滤)
  • 相关题
    • 剑指 Offer 50:第一个只出现一次的字符
    • 其他涉及哈希表计数的题目

十、今日任务清单

  1. ✅ 理解两遍遍历的必要性
  2. ✅ 记住代码模板
  3. ✅ 设计测试用例
  4. ⬜ 去力扣387题提交代码(至少AC一次)
  5. ⬜ 口头练习面试话术3遍

记住 :这道题是哈希表最基础的应用,掌握它就打开了算法的一扇门。不要背代码,要理解为什么必须两遍遍历------这是面试官真正想听的。

相关推荐
lixzest2 小时前
PyTorch基础知识简述
人工智能·pytorch·python
Anastasiozzzz2 小时前
Redis的键过期是如何删除的?【面试高频】
java·数据库·redis·缓存·面试
飞Link2 小时前
深度学习里程碑:ResNet(残差网络)从理论到实战全解析
人工智能·python·深度学习
AlenTech3 小时前
197. 上升的温度 - 力扣(LeetCode)
算法·leetcode·职场和发展
3分钟秒懂大数据3 小时前
实时数仓实战篇一:长周期去重指标建设
大数据·数据仓库·面试·性能优化·flink
ASS-ASH3 小时前
霸王色霸气的本质概括分析
人工智能·python·机器学习·大脑·脑电波
ValidationExpression3 小时前
学习:词嵌入(Word Embedding / Text Embedding)技术
python·学习·ai
橘颂TA3 小时前
【Linux 网络】TCP 拥塞控制与异常处理:从原理到实践的深度剖析
linux·运维·网络·tcp/ip·算法·职场和发展·结构与算法
liliangcsdn3 小时前
如何使用lambda对python列表进行排序
开发语言·python