Day3打卡继续。今天这第7到第9题我特意放在同一天做不是随手翻到哪页刷哪页而是按“字符串处理—链表操作—栈结构应用”这条线排的。三道题恰好覆盖了三类面试高频考点题目本身不算难但想在三十分钟内一次写对、还能把思路讲清楚其实很考验基本功。这篇文章记录一下我当天的完整过程为什么这么安排、每道题怎么拆解、踩了哪些坑以及最后怎么把代码从“能AC”练到“能讲给面试官听”。1. 先聊聊这三道题为什么要组队刷1.1 题型之间的隐藏关联很多初学者喜欢按题库序号顺序往下刷今天1到5明天6到10看起来很有节奏感但实际效率不高。按序号刷的最大问题是题目考察点比较跳跃还没形成肌肉记忆就切换去了另一个方向第二天再看到同类题照样蒙。我这次把7、8、9三道题归为一组原因是它们的内部有一条清晰的逻辑链哈希表解决“查找唯一性”指针操作解决“结构反转”栈解决“顺序匹配”。这三类操作正好覆盖了笔试里最常出现的三种数据结构基础能力容器类数据结构、链式结构、受限线性表。一次打卡把这三样都过一遍比单纯刷三道风马牛不相及的题要划算得多。1.2 从“会做”到“会讲”才是有效打卡单纯把代码提交通过只是第一步。我给自己定的打卡标准是多做一步每道题写完对着空白编辑器口述一遍解题思路模拟被面试官追问的场景。三道题口述下来大概多花十五分钟但收获完全不同。第9题“有效的括号”我一开始只是会用栈被追问“为什么非要用栈数组不行吗”时我愣了几秒才发现自己并没有真正理解栈在此处的不可替代性。打卡刷题如果只是刷数量那第七天你就会发现前面的全忘了。组队刷、讲出来、记笔记这三件事缺一不可。1.3 时间分配的节奏感三道题我给自己定的总预算是九十分钟单题不超过半小时。实际执行下来第7题用了二十分钟第8题用了二十八分钟第9题只用了十二分钟。前两道题因为涉及细节边界多花点时间值得第9题因为思路太清晰反而要提醒自己别大意。这种节奏感很重要刷题不是为了感动自己而是为了在有限时间里覆盖最多的考点。2. 题7字符串中的第一个唯一字符2.1 题目与暴力解法的直觉第7题的内容很简单给定一个字符串找到第一个不重复的字符返回它的下标如果不存在返回-1。例如aabb应返回 -1leetcode应返回 0。拿到这题的第一反应大概率是两层循环外层枚举每个字符内层扫描整个字符串统计它出现了多少次遇到第一个计数为1的直接返回。这种暴力解法思路自然但时间复杂度是O(n²)在字符串长度达到十万级时基本不可接受。我第一遍做的时候也写了这个版本原因很简单它最不容易写错。但写完提交后我盯着复杂度想了半分钟决定无论如何都要换成哈希表方案因为刷题不能只满足于“能过”还要考虑数据规模变大后的表现。2.2 哈希计数解法与代码实现正确的思路是用哈希表Python里叫Counter先遍历一次字符串记录每个字符出现的次数再遍历第二次从左到右检查第一个计数为1的字符。这个方案的时间复杂度是O(n)空间复杂度是O(1)因为字符集大小有限通常就26个小写字母。from collections import Counter def firstUniqChar(s: str) - int: count Counter(s) for i, ch in enumerate(s): if count[ch] 1: return i return -1代码只有五行但有几个细节值得注意。第一Counter(s)是对整个字符串做一次完整遍历所以后面count[ch]的查询都是O(1)。第二第二次遍历用的是enumerate同时拿到下标和字符比先index()再赋值高效得多。第三这里返回的是第一个满足条件的字符下标不是字符本身所以循环里直接return i。注意如果题目改成“返回第一个不重复的字符本身”那只需要去掉下标逻辑返回ch即可。面试时一定要听清楚问的是下标还是字符。2.3 边界条件与追问场景这道题的边界条件其实不多但要全部覆盖空字符串返回 -1字符串全是重复字符返回 -1只有一个字符返回 0大小写敏感问题比如aA中a和A算不算同一个字符题目没说明就默认算不同面试时要主动确认。我把这段代码在本地跑了五组用例leetcode返回 0、loveleetcode返回 2、aabb返回 -1、返回 -1、a返回 0。全部通过后我追问了自己一个扩展场景如果是流式数据也就是字符不是一次性给出而是源源不断到来怎么处理答案是维护一个哈希表记录每个字符的状态未出现、出现一次、出现多次同时用一个队列保存候选字符。这个扩展点让一道简单题的含金量直接翻倍。3. 题8反转链表3.1 迭代法的核心过程第8题是链表题里最经典的一道反转一个单链表。输入1 - 2 - 3 - 4 - 5输出5 - 4 - 3 - 2 - 1。我推荐先从迭代法入手因为它不涉及递归栈溢出的风险且每一步的状态变化都看得见。核心操作是准备三个指针prev前驱节点、cur当前节点、nxt后继节点。每次循环做四件事先保存cur.next到nxt防止丢失再把cur.next指向prev完成反转接着把prev移到cur最后把cur移到nxt。def reverseList(head): prev None cur head while cur: nxt cur.next # 先保存下一个节点 cur.next prev # 反转指针方向 prev cur # prev 前进到 cur cur nxt # cur 前进到 nxt return prev这里容易出错的地方是用完cur.next之后一定要先存nxt再改指针。我第一天练习时在这行栽过跟头——先改了cur.next prev然后想用cur.next去拿后面的节点结果拿回来的是prev链表直接断成两截。这个“先保存后修改”的顺序是链表题里最高频的翻车点。3.2 递归解法与脉络对比迭代会写之后我建议再花十分钟看递归写法。递归的出口是链表为空或只剩一个节点直接返回该节点递归的核心在于假设后面的链表已经反转好只需要把当前节点接到末尾。def reverseList_recursive(head): if not head or not head.next: return head new_head reverseList_recursive(head.next) head.next.next head head.next None return new_head这段代码初看很绕但拆开就清晰了head.next.next head是让当前节点的下一个节点反过来指向自己head.next None是切断原来的正向连接防止成环。递归写法代码短但额外占用O(n)的调用栈空间如果链表特别长比如十万节点以上可能会栈溢出。面试时最好先说迭代法再补充“我也可以用递归实现”展示自己两种思路都掌握。3.3 边界检查与常见追问方向边界条件我列了个小清单输入None返回None只有一个节点返回它本身两个节点反转正常多节点反转。每次写完代码先空想一遍这三条再切换到编译器验证。面试官很喜欢在这题后面追加进阶问题比如“每K个节点一组反转”“反转链表的一部分”。第8题练稳之后这些变体其实就是加了分组和定位逻辑的迭代法。我的建议是打卡当天先把基础反转写三遍第一遍照抄理解第二遍关掉代码自己写第三遍在白纸上画指针变化图。三遍下来链表指针的流转基本不会再用混。4. 题9有效的括号4.1 为什么栈是唯一合理的结构第9题给定一个只包含()[]{}的字符串判断括号是否有效。有效条件是左右括号必须成对出现且顺序正确。我最初尝试过用一个普通数组模拟每次遇到左括号就记下来遇到右括号就往前找有没有对应的左括号。这样在嵌套层级很深时会出现问题比如([{}])最右边的右括号)需要匹配的是最左边的(如果靠遍历往前找复杂度会很高而且逻辑混乱。栈的出现正是为了解决这个问题因为栈天然就是“后进先出”这正好对应了括号匹配的规则最近出现的左括号一定优先匹配当前的右括号。用生活化的比喻这就像一摞盘子你总是先拿最上面那个而最先放进柜子的盘子反而最后才能被碰到。括号嵌套时左括号是“放进去的盘子”右括号就是要“拿出来的盘子”顺序完全吻合。这就是为什么我说遇到嵌套匹配类问题首先要想到栈。4.2 实现细节与容易踩的坑def isValid(s: str) - bool: stack [] mapping {): (, ]: [, }: {} for ch in s: if ch in mapping: # 遇到右括号栈顶必须是对应的左括号 if not stack or stack[-1] ! mapping[ch]: return False stack.pop() else: # 遇到左括号直接入栈 stack.append(ch) return not stack这段代码里最妙的细节是使用一个反向映射mapping让右括号能O(1)地找到它对应的左括号。判断条件if not stack很关键它处理了“第一个字符就是右括号”的情况比如 字符串)(此时栈是空的stack[-1]会直接抛异常所以必须先检查栈是否为空。最后return not stack既能处理合法情况也能处理“有多余左括号”的情况。我在本地测试时专门准备了一组刁钻用例()返回 True、()[]{}返回 True、(]返回 False、([)]返回 False、{[]}返回 True、返回 True。其中([)]这个用例很多人会弄错因为它左右括号数量是配对的但交叉排列不合法只有逐层匹配的栈能做到正确判断。4.3 变体与应用价值这道题看起来简单但应用面远不止字符串。编辑器里代码缩进的配对检测编译器语法解析中的作用域判定甚至可以扩展到HTML标签的嵌套闭合校验底层思路都是同一个栈模型。刷完第9题后我又顺手做了个变体字符串里同时包含小括号、中括号、大括号和尖括号还允许中间混入普通字符判断嵌套是否合法。只需要在判断前先过滤掉非括号字符再走同样的栈逻辑。这个变体其实就是很多系统里“括号语法高亮”的简化版本实战价值很高。5. 打卡当天的实操流程复盘5.1 一个多小时的时间怎么分的当天我给自己列了排程前十五分钟快速浏览三道题不急着写代码先在白纸上画出每道题的数据流中间四十五分钟分别实现三道题并跑测试最后二十分钟做复盘把三道题的思路用语音口述一遍同时记录踩坑点。第7题是最顺利的哈希表的直觉一上来就有写完就跑通。第8题反而是三道题里最耗时间的因为我在迭代法和递归法之间切换着写了两遍还额外画了三张指针图用来确认反转过程中每个节点的next指向。第9题代码最短但我在测试用例([)]上多盯了两分钟确认栈出栈顺序完全正确才放心。整个流程下来大约八十分钟比预算的九十分钟还省了一点。5.2 用测试用例把边界条件逼出来我习惯在写业务代码之前先写测试用例刷题时同样如此。三道题我分别整理了一份最小测试集第7题四个用例第8题四个用例第9题六个用例。运行完一轮后我把每个用例的结果和预期逐一比对再把报错的用例单独记到错误日志里。比如第7题如果不加enumerate而是直接s.index(ch)代码逻辑也能过但在反复调用index时会引入额外的O(n)开销让整体复杂度退化成O(n²)。我特意对比了两种实现的耗时在十万字符的随机串上差距非常明显。这就是“测试用例倒逼优化”的价值不是先想着优化而是先用用例跑出问题再让你有动力去改。5.3 复盘笔记怎么记才有用我的复盘笔记不是简单抄代码而是记“一类题的钥匙”。第7题我写的是唯一性问题优先想哈希表第8题写的是链表改指针先保存再修改第9题写的是嵌套匹配栈加反向映射。每把“钥匙”都配合一个错误案例方便下次复习时快速唤醒记忆。这套笔记方法在打卡到第15天左右时会明显比单纯累积提交记录更有价值因为你不需要从头回忆每道题只看钥匙卡片就能迅速捡起关联知识点。6. 常见问题与排查技巧实录6.1 运行报错案例空栈引发的异常第9题最容易出现的运行时错误就是if not stack漏写。字符串)进来时栈为空直接执行stack[-1]会抛出IndexError。这种问题在本地调试时一眼就能看到但在笔试平台上传代码时很难定位因为报错行号往往指向的是调用函数那一层被包装过的代码。我的排查经验是所有涉及stack[-1]的地方先问自己“栈可能为空吗”然后在纸上模拟一遍最极端的输入。6.2 复杂度误判自以为O(n)其实不是第8题有个典型误判有人用“把链表所有值放进数组反转数组再重建链表”的办法最后得出结论说这是O(n)。从时间复杂度角度确实也是O(n)但空间复杂度从O(1)退化成了O(n)而且多了一次遍历和数组构造。面试官如果问“你能否用常数空间完成”这种解法直接出局。我吃过一次亏所以现在每写完一道题都会额外问自己空间上是否存在更优解。6.3 面试现场“演”代码的细节刷题最终要落到面试场景。我总结了一套三分钟展示模板先复述题目确认理解再说暴力解法然后说优化后的解法最后手动跑一个用例。第8题在口述时重点强调“三个指针的节奏感”和“先保存nxt再改next”第9题在口述时强调“反向映射让代码更简洁”和“栈为空时要提前拦截”。这些都是可以提前排练的表达细节练到脱口而出比临时组织要自然得多。我个人在打卡中最深的体会是代码题写对只是及格稳定写对、还能边写边解释才是真正的高手状态。第7到第9题这三道让我把哈希计数、链表反转、栈匹配这三类基础动作练成了肌肉记忆。如果你也刚开始这个系列建议别贪多一天三道题走完做题、测试、复盘、口述这四步坚持一周你会明显感觉自己写代码时脑子里有了一张“武器清单”看到题目就知道该抽哪把武器出来。