news 2026/10/13 7:38:01

刷题复盘:五道简单题吃透前缀和、栈、双指针与链表哨兵

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
刷题复盘:五道简单题吃透前缀和、栈、双指针与链表哨兵

很多第一次打开算法题库的人,都会默认“简单题”等于“送分题”。我也曾经这么以为,直到在某道字符串题上连续踩了三十分钟的坑,才开始重新审视“简单题”这三个字。这篇刷题记录是我在“简单题”标签下完成到第5篇时的复盘,一共五道题,分别覆盖前缀和、栈、双指针、哈希表、链表哨兵这几个基础知识点。每道题我都会写清楚题目要求、我的思考过程、最终的AC代码,以及复盘时发现的坑。适合刚开始刷算法题的新手,也适合想快速找回手感的开发者参考。

1. 从“第五期”聊起:为什么还在刷简单题

1.1 简单题不等于送分题

可能有人会问:都第五期了,怎么还在刷简单题?说实话,我自己也在刷题中期突然冒出过这个念头,后来发现真正卡住我的不是难题的算法设计,而是一些简单题里藏着的边界条件。

比如后面会讲到的“相邻重复字符删除”,解法无非就是栈,但如果你没有想清楚“删除之后左右两边可能再次形成相邻重复字符”,写出来的第一版几乎必然有问题。简单题只是入门的门槛低,并不代表解题时不需要严谨。它们往往把一两个知识点讲透,让你在最小范围内体会“边界条件”的重要性。对新手来说,这是最好的起点;对有经验的人来说,这是最容易忽视的地基。

1.2 我挑选这五道题的标准

这五道题并不是随机抓来的,我刻意选了五个不同的考察点:前缀和、栈、双指针、哈希表、链表哨兵。前四道属于数组和字符串类,最后一道是链表。挑选标准有三个:

  • 第一,学了之后能用到很多其他题目里,复用价值高;
  • 第二,代码量通常不超过二十行,但边界条件值得研究;
  • 第三,每一题都能引申出一个通用套路,我会在复盘里把套路抽出来说清楚,而不是只贴一段代码。

这几道题的难度都控制在三四十分钟内能AC的范围,刚好适合作为“手感恢复剂”。如果你正在从简单题入门阶段过渡到中等题,这一期记录应该能在思路上给你一些参考。

2. 题一:区间和查询背后的前缀和思想

2.1 题目复述与暴力解

题目大概是这样的:给一个整数数组nums,然后有m次询问,每次询问给两个下标l和r,要求返回闭区间[l, r]内所有数字的和。比如nums = [1, 3, 2, 5, 4],询问(1, 3),结果就是3 + 2 + 5 = 10,数组下标从 0 开始。

看到这个题,大多数人第一反应就是循环累加:

def range_sum(nums, l, r): total = 0 for i in range(l, r + 1): total += nums[i] return total

这段代码本身没有逻辑错误,问题出在复杂度。如果nums长度是 10 万,询问也是 10 万,最坏情况下每次查询都要遍历整个数组,总的操作量会到 10^10 级别,想不超时都难。这也是为什么“简单题”也会强调算法复杂度——它不考高级数据结构,但要求你意识到重复计算可以被消除。

2.2 前缀和数组的构建逻辑

前缀和的核心思想是:在询问之前做一次预处理,把从 0 到当前位置的和全部算好,之后查询某一段的和,就只需要两次“减法”。

设prefix[i]表示nums[0]到nums[i-1]的和,注意这里是一个长度为n + 1的数组,这样prefix[0] = 0作为“空区间”的和,方便处理l = 0的情况。构建过程用递推:

prefix = [0] * (len(nums) + 1) for i in range(1, len(nums) + 1): prefix[i] = prefix[i - 1] + nums[i - 1]

查询闭区间[l, r]的和时,只需要:

def query(l, r): return prefix[r + 1] - prefix[l]

拿前面的例子验证:nums = [1, 3, 2, 5, 4],构建出来的前缀和是[0, 1, 4, 6, 11, 15]。查询(1, 3),prefix[4] = 11,prefix[1] = 1,差值是 10,正确。

2.3 为什么是“n+1”而不是“n”

这个n + 1是第一次写的时候最容易忽略的细节。如果直接建一个长度等于n的数组,查询[0, r]时就得单独判断l - 1 >= 0,或者写prefix[r] - (prefix[l - 1] if l > 0 else 0),代码会变丑,也容易踩边界。

用n + 1的写法,本质上是把“下标”和“前多少个元素”这两件事分开:prefix[i]表示“前 i 个元素的和”。这样[l, r]的和就是“前 r+1 个元素之和”减去“前 l 个元素之和”,语义非常干净,不需要任何特判。

2.4 复盘心得:先写暴力再优化

这道题我录完代码之后的体会是:简单题别上来就优化。先写一个暴力版本跑一遍,确认你理解了题意,然后再想“哪里重复计算”。大部分前缀和、双指针、哈希表的题,都是从暴力解出发自然推导出来的。如果你一开始就跳到优化解,很容易知其然不知其所以然,隔两天再见到同类题还是不会。

3. 题二:相邻重复字符删除的栈操作

3.1 题目复述与直觉

题目是:给定一个字符串s,不断删除相邻且相同的两个字符,直到字符串里没有相邻且相同的字符为止,返回最终字符串。例如s = "abbaca",过程是:

  1. 删除中间的bb,得到"aaca";
  2. 此时aa变成相邻重复字符,删除后得到"ca";
  3. 最终答案是"ca"。

第一眼看上去,很多人会想写一个while循环,在循环里replace('aa', '')或者手动删除连续字符。这个思路很危险:replace一次会替换所有匹配项,但删除后新增的相邻关系不会被二次处理;手动删除又会遇到下标动态变化的问题,调试起来非常难受。

3.2 用栈模拟删除过程

正确的姿势是维护一个栈。从左到右扫描字符串:

  • 如果当前字符和栈顶相同,说明它们相邻且重复,把栈顶弹出去;
  • 否则将当前字符压入栈。

由于每次弹出之后,新的栈顶又会和后续字符重新比较,所以“删除后产生新的相邻重复”这个性质被自动处理了。代码非常短:

def remove_duplicates(s): stack = [] for ch in s: if stack and stack[-1] == ch: stack.pop() else: stack.append(ch) return ''.join(stack)

用"abbaca"模拟一遍:

读 a -> 栈 [] 读 b -> 栈 [a] 读 b -> 栈顶是 b,相等,弹出 -> 栈 [a] 读 a -> 栈顶是 a,相等,弹出 -> 栈 [] 读 c -> 压入 -> 栈 [c] 读 a -> 压入 -> 栈 [c, a] 结果 "ca"

3.3 容易踩的坑:别用字符串替换

我第一次写这题时,没有用栈,而是想在while循环里s = s.replace('aa', '')。这看起来很像,但结果是错的。因为replace会一次性把所有“非重叠”的aa都删掉,比如"aaaa",执行一次replace('aa','')结果是"",但它已经跳过了“先删前两个,再删中间两个,变成空串”的完整过程。且如果下一次循环又生成新的aa,逻辑会变得更复杂。

所以这题的正确模型是“栈顶比较”,而不是“模式替换”。这让我意识到,哪怕是一道简单题,也要在动手前想清楚数据变化的状态:是“每次只处理最相邻的一组”,还是“一次处理所有匹配项”?这两个模型不同,对应的算法完全不同。

3.4 从“栈”到“状态机”

后来我复盘时把这题归类到了“状态机”思想:当前状态(栈顶字符)决定下一个字符该合并还是该保留。以后遇到很多括号匹配、相邻冲突、操作流水线类的题,都可以套这个模型。一道简单题能学到这种抽象,性价比非常高。

4. 题三、题四:双指针与哈希表的组合拳

4.1 合并两个有序数组:双指针的入门

题目:给定两个升序数组nums1和nums2,返回一个升序合并后的新数组。例如nums1 = [1, 3, 5],nums2 = [2, 4, 6, 8],结果是[1, 2, 3, 4, 5, 6, 8]。

看到“升序”两个字,第一反应应该是双指针。用两个指针分别指向两个数组的当前元素,比较它们的大小,把较小的放进结果数组,然后对应指针向后移动。当其中一个数组遍历完,把另一个数组剩余部分直接拼接:

def merge(nums1, nums2): i = j = 0 res = [] while i < len(nums1) and j < len(nums2): if nums1[i] <= nums2[j]: res.append(nums1[i]) i += 1 else: res.append(nums2[j]) j += 1 res.extend(nums1[i:]) res.extend(nums2[j:]) return res

很多同学可能会想:直接用列表的sort不就行了?sorted(nums1 + nums2)一行搞定。这么写当然也能过,但就失去了练习双指针的机会。双指针的复杂度是O(n + m),而排序是O((n + m)log(n + m)),在数据量大时差距很明显。更重要的是,双指针是后续归并排序、链表合并、区间合并等一堆题的基础。

4.2 两数之和:哈希表如何用空间换时间

题目:给定整数数组nums和一个目标值target,找出和为target的两个不同下标,返回它们的下标。例如nums = [2, 7, 11, 15],target = 9,返回[0, 1]。

暴力是两层循环。进阶思路是:遍历数组时,对于当前数x,我们想知道target - x是否在之前出现过。哈希表可以做到 O(1) 的查找,于是一次遍历就够:

def two_sum(nums, target): seen = {} for i, x in enumerate(nums): y = target - x if y in seen: return [seen[y], i] seen[x] = i return []

这里有个关键细节:一定要先检查y是否存在,再把当前x放入哈希表。如果先放,后面遇到x + target - x时,会误把当前元素本身当成答案,比如nums = [3, 2, 4],target = 6,如果先放,会错报[0, 0]。

4.3 双指针和哈希表怎么选

这两道题放在一起复盘,是因为它们很有代表性:

  • 双指针适合输入本身有序,或者可以排序的场景,空间复杂度 O(1);
  • 哈希表适合需要快速查找“之前看过什么”的场景,时间复杂度 O(n),但空间是 O(n)。

如果同一道题,数组无序且要求返回下标,通常用哈希表更稳;如果数组有序,双指针往往更优雅。刷题多了之后你会发现,简单题里反复出现的不是某个算法,而是这种选择逻辑。

5. 题五:链表删除目标值的哨兵节点

5.1 题目与链表删除的经典困扰

题目:给定单链表头节点head和一个整数val,删除链表中所有值等于val的节点,返回新的头节点。

链表删除的通用思路是找到目标节点的前一个节点prev,然后把prev.next指向目标节点的下一个节点。但这里有一个天生的麻烦:如果要删除的是头节点,它没有前驱节点。很多人会写一堆if head.val == val: head = head.next的分支,代码很容易漏。

5.2 哨兵节点解法

更优雅的做法是在真正头节点前面再加一个“哨兵节点”,也叫哑节点dummy。dummy.next = head,然后统一从dummy开始遍历。这样无论删除的是头节点还是中间节点,都有统一的前驱处理方式:

def remove_elements(head, val): dummy = ListNode(-1) dummy.next = head prev = dummy cur = head while cur: if cur.val == val: prev.next = cur.next else: prev = cur cur = cur.next return dummy.next

注意一个细节:当删除节点时,prev不要移动。因为删掉当前节点后,prev.next会指向下一个节点,而下一个节点可能仍然是目标值,需要继续判断。只有当当前节点不是目标值时,prev才向前移动。这个“删除时不移动 prev,保留后续指针”的习惯,能避免大批链表题里的 bug。

5.3 简单题里的链表边界训练

做了这道题之后,我才发现简单题里的链表题练的其实是“对空指针和头节点的敏感度”。比如:

  • 空链表:dummy.next是None,不会出错;
  • 所有节点都是目标值:最后返回dummy.next,也是None;
  • 尾节点是目标值:删除后要手动把prev.next = None,代码里已经通过cur.next天然实现。

如果不用哨兵节点,这三种情况每一种都得单独写特判。所以我个人强烈建议:新手从第一道链表题开始就养成用哨兵节点的习惯,不要觉得“多加一个节点很奇怪”。它最常见的现实例子,就是给队列加一个空的队头哨兵,从而简化边界判断。

6. 三处翻车现场与我的复习SOP

6.1 坑一:前缀和查询的下标偏移

第一道题我翻过车:建前缀和数组时长度用了n,然后查询(l, r)写成了prefix[r] - prefix[l - 1]。当l = 0时,prefix[-1]取到了数组最后一个元素,结果完全错误。后来改成n + 1和prefix[r + 1] - prefix[l]才彻底解决。

这个坑告诉我们:数组下标不是从语义上理解,而是从“索引位置”和“元素数量”两个角度理解。前缀和里prefix[i]的语义是“前 i 个元素的和”,只有当i可以等于 0 时,才能优雅表达“前 0 个元素的和”。这提醒我以后设计辅助数组时,先问一句:边界下标语义是什么?

6.2 坑二:栈不等于字符串替换

第二道题的翻车我前面已经提过,用replace处理连续删除会得到错误结果。更深层的教训是:看到“相邻”“连续”“删除后重新比较”这几个关键词,要自动联想到栈。栈不只是一种数据结构,也是一种“最近的状态最优先被处理”的模型。

还有一次,我写栈解法时把判断条件写成了if ch in stack,变成了“只要字符在栈里就弹出”,而不是“栈顶等于当前字符”。这导致输入稍微复杂一点就输出错得离谱。所以一定要记住:栈的动态位置只关心栈顶,不关心栈里的其他元素。

6.3 坑三:链表题里过早判断空头

写链表删除时,一开始我在函数开头写了if head is None: return head,然后在循环里处理。这个写法本身没错,但它让我在实现时总想着“头节点特殊处理”。用了哨兵之后,连这个开头判断都可以省掉,代码反而更统一。

我复盘时意识到,很多边界代码不是“必需”的,而是“思路没到位时的补丁”。如果你发现自己写了一大堆if处理头、尾、空,大概率是缺少一个统一结构。链表题的哨兵、数组题的前缀占位、树题里的虚拟根节点,都是同一个道理。

6.4 可复用的复习SOP

这几期简单题刷下来,我总结了一套自己的复习流程,分享出来:

  • 每次 AC 之后,不急着看下一题,先在记录表里写三行内容:题目关键词、核心解法、风险点;
  • 一周之后,只根据“题目关键词”重建代码,要求自己三分钟内写完整;
  • 如果三分钟写不出来,说明这道题只是“看过答案”而不是“会了”,把它放回待复习队列;
  • 等累计到 20 道题后,不按题目顺序,而是按“解法类型”重新分组,把区间和、栈、双指针这类题放到一起看,归纳模板。

这套流程让我把“简单题”刷出了“中等题”的效果。很多你以为已经会的东西,一旦脱离题解默写,就会原形毕露。而简单题恰好是练习默写的最小成本场景。

如果你也卡在简单题反复 AC 又反复忘的状态,不妨试试这个复习 SOP。等基础扎实了,再往中等题推进时,你会发现真正难的不是“没见过这个算法”,而是“不知道什么时候该用它”。而这件事,往往就是在那些不起眼的简单题里慢慢养成的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 7:36:50

链表绝对值配对抵消:栈/递归/快慢指针三解法

1. 这道题不是考“删除”&#xff0c;而是考“如何不删却让链表逻辑上消失” 刚看到“删除链表中绝对值相等的结点”这个标题时&#xff0c;我下意识点了右上角的关闭按钮——又一道披着链表外衣的哈希表模拟题&#xff1f;但真正动手写了几版代码后才发现&#xff0c;这题的陷…

作者头像 李华
网站建设 2026/10/13 7:36:26

JavaWeb前台后台网页模板:从Servlet部署到项目改造的完整指南

简介&#xff1a;面向个人开发者与Java Web初学者的前后台网页模板&#xff0c;以Java技术为核心&#xff0c;融合前端展示与后端处理逻辑。这套模板将后台管理界面与前台页面集成于一体&#xff0c;适合作为课程设计、毕业设计或个人网站的起始框架&#xff1b;应用者最好具备…

作者头像 李华
网站建设 2026/10/13 7:36:07

基于Qt与MuPDF的PDF阅读器开发:从渲染到交互实战

简介&#xff1a;PDFReader是一套基于Qt5.4与MuPDF的PDF阅读器源码&#xff0c;面向需要在桌面端实现PDF解析、渲染与交互的C开发者&#xff0c;尤其适合课程设计或项目移植参考。压缩包共104个文件&#xff0c;以87个头文件和6个C源文件为主&#xff0c;同时包含Qt工程配置、M…

作者头像 李华
网站建设 2026/10/13 7:36:05

MSI文件打不开?从服务到注册表的Windows Installer故障排查指南

msi文件无法正常打开这个问题&#xff0c;最早是从某位同事那里转到我这来的&#xff1a;双击一个msi安装包&#xff0c;要么毫无反应&#xff0c;要么弹一个“Windows Installer服务不可用”的提示。网上查了一圈&#xff0c;一半人让重装系统&#xff0c;另一半人贴了一堆看不…

作者头像 李华
网站建设 2026/10/13 7:35:13

ETL不是数据搬运,而是数据变资产的核心引擎

先说结论&#xff1a;业务数据从混乱走向有序&#xff0c;靠的不是更强的报表工具&#xff0c;而是ETL——Extract&#xff08;抽取&#xff09;、Transform&#xff08;转换&#xff09;、Load&#xff08;加载&#xff09;&#xff0c;一套把原始数据打磨成可用资产的处理流水…

作者头像 李华
网站建设 2026/10/13 7:35:13

鸢尾花分类实战:从零跑通机器学习全流程

1. 为什么鸢尾花分类是机器学习最好的起点 如果你刚开始接触机器学习&#xff0c;或者想找一个能快速跑通全流程的练手项目&#xff0c;鸢尾花分类几乎是绕不开的经典。这个数据集小到可以塞进一张Excel表&#xff0c;但麻雀虽小五脏俱全——它包含了特征工程、数据划分、模型训…

作者头像 李华