多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

数据结构与算法刷题攻略:剑指Offer到BAT真题的完整闭环

数据结构与算法刷题攻略:剑指Offer到BAT真题的完整闭环 简介这套《数据结构与算法刷题全攻略》项目资料面向备战大厂算法笔试与面试的开发者覆盖剑指Offer题解、程序员代码面试指南题解、九章算法讲解、牛客直通BAT课程及大公司笔试真题编程题并兼顾第一遍系统学习与两个月后复习回归两条主线适合从刷题入门到冲刺提分的完整链路使用。资源共969个文件以493个class与468个java源码为主class便于直接运行验证java便于阅读实现细节另辅以md、txt、docx说明文档与gitignore配置压缩包仅789KB目录按专题/来源拆分定位具体题目与对应代码十分方便。内容既保留第一遍学习逐步完成的代码也保留两个月后复习时全部重新实现的版本通过新旧两版对比可直观看出边界条件处理、递归转迭代等思路演进与优化过程。已有40人学习下载对希望扎实掌握数据结构与算法、提升笔试实战能力的读者具有较高参考价值。1. 数据结构与算法刷题全攻略从剑指Offer到BAT真题的完整闭环拿过这个资源包的人应该能感觉到它不是一个简单的题解合集而是一条完整的刷题链路剑指Offer题解打底程序员代码面试指南补漏九章算法讲解建立方法论牛客直通BAT课程对准大厂笔试最后还有lintcode和大公司笔试真题做实战验证。更难得的是里面保留了第一遍学习代码和两个月后复习重写代码这两份代码的对比价值比多数市面上的刷题笔记都高。这套资源适合正在准备校招的应届生、准备跳槽的社招开发也适合算法基础薄弱但想系统补课的自学者。接下来我按自己的拆解顺序把资源里的内容、用法和踩过的坑逐一展开。2. 资源选型逻辑为什么是剑指Offer、牛客直通BAT、九章、lintcode四件套2.1 剑指Offer在面试中的真实地位面试官直接从里面抽题剑指Offer里的67道题几乎覆盖了面试手撕代码的所有高频考点链表反转、二叉树遍历、动态规划、栈与队列、字符串处理。我面过的大厂技术面手撕环节至少有一半概率抽到原题或变体。这个资源里的剑指Offer题解每道题都给了多种解法比如反转链表就有迭代和递归两版这对面试现场和面试官聊优化空间很有用。第一遍刷的时候我建议按题号顺序来但不用一天刷太多。常见做法是每天4到5题每道题先自己写一遍再看题解对照。这里的题解代码不是那种只有一种解法的而是把暴力解、优化解和时间复杂度分析都列出来了。记住一个原则面试时想不起来最优解先把暴力解写对再一步步优化比卡在那里强。2.2 程序员代码面试指南和九章算法一个补边界一个建体系程序员代码面试指南这本书在资源里是以题解形式存在的它的特点是题目比剑指Offer更杂包含了很多看起来简单、写起来翻车的细节题比如链表相交、矩阵旋转、子数组最大累加和。这部分题解适合在剑指Offer刷完一轮之后做因为它考察的是你对边界条件的敏感度而不是对模板的背诵能力。九章算法讲解部分是整个资源的方法论核心。它不是按题目讲而是按算法范式讲比如二分法、双指针、动态规划、深度优先搜索。我一般会先把九章的视频或讲义过一遍把每个范式的套路框架记下来回头再刷剑指Offer的时候就能看到每道题对应的是哪个框架。比如看到求最长xxx子序列立刻想到动态规划看到有序数组中找目标值立刻想到二分看到子串/子数组先考虑滑动窗口。2.3 lintcode在整条链路里的定位从看懂到AC的必经路很多人刷题有个错觉题解看懂了就等于自己会了。实际上从看懂到独立AC之间还有一段距离。lintcode在这套资源里的作用就是给你一个可以立刻验证的在线评测环境。资源里的lintcode部分题目按难度和公司做了分类适合按专题刷。我习惯的做法是每学完一个算法范式就去lintcode上找5道同类型的题刷掉能自己AC的才算真正掌握了。这里有一个参数层面的建议lintcode刷题时重点关注通过率和耗时排名而不是只看有没有AC。同一道题你的运行时间如果排在倒数30%说明你的解虽然对了但复杂度不达标面试官很可能让你优化。2.4 两遍代码设计第一遍写对第二遍写优这个资源包里最有价值的部分我认为是第一遍学习代码和两个月后复习重新实现代码这两份代码。它们不是同一道题的两次提交而是同一个学习者在不同阶段对同一批题目给出的解法。对比来看第一遍代码通常更长、更啰嗦用了很多临时变量和多余的判断第二遍代码明显更简洁说明两个月后的理解已经内化了。这个思路本身就是一种学习策略我把它称为两遍刷题法。第一遍的目标只有一个AC不管代码丑不丑第二遍的目标是写出最优解、简洁的代码并且能讲清楚每一步为什么这么写。这个方法论我会在第5章展开讲这里先提一句刷题不做两遍等于白刷因为你第一遍记住的只是题目的答案而不是题目的规律。3. 分类刷题实战按数据结构和算法范式拆解剑指Offer与lintcode3.1 链表类题目反转、快慢指针、合并的代码模板链表是面试手撕的重灾区因为指针操作一旦遗漏边界就非常容易翻车。剑指Offer里的链表题不算多但每道都经典。以反转链表为例迭代写法是所有链表题的基础模板必须能盲写。看一下第一遍学习代码里这个题的最简写法def reverse_list(head): prev None cur head while cur: nxt cur.next # 先保存下一个节点 cur.next prev # 当前节点指向前一个 prev cur # prev 前移 cur nxt # cur 前移 return prev这段代码里prev是反转后的链表头cur是当前正在处理的节点nxt是cur的下一个节点。注释里已经列出了四步操作保存后继、反向指向前驱、移动prev、移动cur。这个模板需要练到闭着眼睛能写出来的程度因为它还衍生出了反转区间链表K个一组反转等变体。每次写完链表题都用空链表和单节点链表各跑一遍这是链表题的边界必测项。快慢指针是链表里另一个高频套路找链表中点、判断环形链表都用它。判断环形的代码短到让人容易轻视def has_cycle(head): slow head fast head while fast and fast.next: slow slow.next fast fast.next.next if slow fast: return True return False这里的slow每次走一步fast每次走两步如果链表里有环两个指针必然在环内相遇。注意循环条件里必须判fast和fast.next都不为空否则会抛出空指针异常。这两个模板直接吃透剑指Offer的链表题就解决了大半。3.2 树与递归遍历模板和重建二叉树树的题目在笔试中占比非常高牛客真题里几乎每套都有。二叉树的前序、中序、后序遍历递归版很简单但资源里的九章算法部分明确指出面试时递归写法只是及格线非递归写法才是加分项。以中序遍历为例用栈模拟递归的写法是def inorder_traversal(root): stack [] result [] cur root while cur or stack: while cur: stack.append(cur) # 先把左子树全部入栈 cur cur.left cur stack.pop() result.append(cur.val) # 左子树访问完访问根节点 cur cur.right # 再处理右子树 return result这个代码的核心思路是循环里第一个while cur负责把所有左子节点压栈弹栈后立即转向右子节点。stack的作用是记住回退路径cur是当前待处理节点。写非递归遍历时最常犯的错是忘记在弹栈后重置cur导致死循环这个问题我下章会专门再讲。重建二叉树也是剑指Offer里的经典题给出前序和中序遍历还原整棵树。这里的递归解法是利用前序的第一个元素确定根节点再在中序里找根节点位置左右递归。解决这道题的关键不是递归本身而是索引边界的计算建议在草稿纸上手动推导一次数组的划分过程再回来看代码思路会清楚很多。3.3 字符串与KMP暴力枚举打底KMP提速字符串匹配在笔试里出镜率不低lintcode上有一类题是判断一个字符串是否是另一个的子串。暴力枚举的写法是基于两层循环的逐位比较复杂度O(n*m)数据量小的时候完全能过。但遇到长度在10万级别的测试用例就会超时这时候要换成KMP算法。看资源里第二遍代码对KMP的实现def kmp_search(text, pattern): n, m len(text), len(pattern) if m 0: return 0 nxt [0] * m j 0 for i in range(1, m): # 构建next数组 while j 0 and pattern[i] ! pattern[j]: j nxt[j - 1] if pattern[i] pattern[j]: j 1 nxt[i] j j 0 for i in range(n): # 匹配阶段 while j 0 and text[i] ! pattern[j]: j nxt[j - 1] if text[i] pattern[j]: j 1 if j m: return i - m 1 return -1nxt数组是KMP的核心它记录了pattern的前缀函数也就是每个位置的最长公共前后缀长度。构建next时j指向前缀的下一个待比较位置一旦失配就回退到nxt[j-1]。匹配阶段和构建阶段结构几乎一样只是作用对象从pattern自身变成了text。KMP的复杂度是O(nm)比暴力的O(n*m)快一个量级。暴力写法用来保证正确性KMP写法用来应对大数据量两个都要会。3.4 动态规划与贪心从状态定义到边界值动态规划是面试题里的终极boss。资源里九章算法部分的DP专题讲得很细核心就三件事状态定义、状态转移方程、边界条件。以最常见的最长递增子序列为例第一遍代码里的DP写法是O(n²)的def length_of_lis(nums): n len(nums) if n 0: return 0 dp [1] * n # dp[i]表示以nums[i]结尾的最长递增子序列长度 for i in range(n): for j in range(i): if nums[j] nums[i]: dp[i] max(dp[i], dp[j] 1) return max(dp)dp[i]初始化为1因为每个元素自身就是一个长度为1的递增子序列。内层循环遍历所有j i如果nums[j] nums[i]说明nums[i]可以接到nums[j]后面更新dp[i]。边界条件是空数组返回0。这个解能处理n不超过几千的输入更大就要用贪心加二分把复杂度降到O(nlogn)。面试时先给出DP解再主动提出可以优化比直接上最优解更有说服力。贪心算法的题看起来简单实际非常容易翻车因为局部最优是否等于全局最优需要证明。剑指Offer里的典型贪心题不多牛客真题里倒是常见最大子数组和跳跃游戏这类。刷贪心题时建议多用几个反例验证自己的策略比如最大子数组和如果用固定窗口去做窗口大小一变就错了必须用当前和小于0就丢弃的贪心策略才对。4. 刷题避坑指南六个高频翻车现场与排查方法4.1 看题解时觉得自己都会合上书写不出来这是刷题人群里最常见的坑我把它称为假性掌握。现象是打开题解看得明明白白每个步骤都懂但关掉题解自己从头写卡在第一步或者写到一半逻辑就断了。原因在于看题解是被动输入大脑没有经历试错过程你记住的是翻译而不算法。解决的硬性办法是拿到一道题先独立写15分钟卡住再看题解看完了关掉凭记忆重写一遍。资源里的第一遍学习代码就是这样留下的如果你发现自己的第一遍代码有半成品、有注释掉的错误尝试说明这个学习过程是真实的。4.2 代码在本机跑得好好的提交lintcode就超时典型现象是本机测试数据少都秒过但提交后显示Time Limit Exceeded。原因几乎都是算法复杂度不合规。要养成一个习惯看到题目先看数据范围n如果是10⁵级别O(n²)的直接放弃如果是10⁵还上了双重循环必超时。解决方法是先看输入规模再选算法n ≤ 10³可以用O(n²)n ≤ 10⁵必须O(nlogn)n ≤ 10⁶只能O(n)n到10⁹就是O(logn)。这套对应关系自己在纸上多写几遍笔试时能救命。4.3 递归栈溢出或死循环这个坑在二叉树和回溯题里出现频率最高。现象有二一是报栈溢出错误二是程序运行时间超长不退出。栈溢出的原因是递归深度过深比如处理链表时用了递归而链表长度是10⁵必然爆栈这时候必须改成迭代写法。死循环的原因多半是没有明确的递归终止条件或者终止条件写错位置。排查方法很简单在递归函数开头打印当前参数跑一遍小数据看输出如果参数没有向终止条件逼近就是递归方向写错了。4.4 刷完一遍题两个星期后全忘了很多人刷题的路径是第一遍跟着题解把代码抄了一遍然后就结束了。这个资源里专门保留了两个月后重写的代码就是用来对抗遗忘的。如果你重写的时候发现第一遍能AC的题现在完全没思路这不代表你笨只说明第一遍的输入是浅层的。解决方法是做间隔重写而不是连续重复。第一遍刷完后隔4到6周再重写一遍重写时不看第一遍的代码写完后再对比两份代码的差异这个差异就是你的真实进步。4.5 剑指Offer刷完了笔试真题还是不会这是一道坎资源里的题解你都做透了但打开大公司笔试真题编程题还是懵。原因在于剑指Offer的题有明确的考点暗示——题目在链表那一章你自然知道用链表的方法而笔试题的考点是隐藏的甚至一道题可以同时用DP、贪心、二分三种解法。解决方法是刷完专题后立即转入混合真题。真题不要按题型刷要按套题刷每套题定时45分钟模拟真实笔试环境做完之后再按题型归类总结。第一遍刷真题分数低没关系这个过程本质是训练识别题型的能力比多刷20道专题题更管用。4.6 lintcode提交AC了但面试时讲不清楚思路这是资源的最后一个坑过分依赖刷题平台的AC反馈忽视了表达训练。面试手撕代码不仅要写对还要讲清楚为什么这么写。解决方法是每AC一题逼自己在注释里用三句话写出这题考的是什么我的解法是什么思路复杂度是多少。写不出来的说明理解不到位回头重做。我自己的习惯是AC后不立即看下一题而是先口头把解题思路讲一遍讲不出来就先不看下一题这个习惯帮我在面试中避免了很多代码对了但讲不明白的尴尬场面。5. 两遍刷题法执行细则第一遍学习代码与第二遍重写代码的正确用法5.1 第一遍刷题以AC为唯一目标允许代码丑陋第一遍刷题的正确姿势是不以写出完美代码为目标而是以理解题意并AC为目标。这意味着你的第一遍代码可以很长、很啰嗦可以有多余的变量甚至可以有注释掉的部分实现。资源里的第一遍学习代码就是这个状态它是真实的学习痕迹不是人工修过的标准答案。这一阶段的核心任务是建立题感看到题目能想到用什么算法边界条件是什么输入输出怎么处理。代码风格和效率优化留到第二遍再管。5.2 间隔期用三行笔记对抗遗忘第一遍和第二遍之间我建议间隔4到6周。这个间隔期不是什么都不做而是用三行笔记记录每道题的核心信息第一行写题目类型数组、树、DP、贪心第二行写解题思路的关键一句第三行写自己的薄弱点。这个做法的价值在于重写时先看笔记不看题解代码。如果笔记里的思路能帮你把代码写出来说明这道题的规律你已经内化了如果笔记完全没用说明这道题当初就是抄过去的必须回到题解重新消化。5.3 第二遍重写追求最优解与代码简洁度第二遍重写的要求比第一遍高很多不只是AC还要追求最低的复杂度、最简洁的表达、最优的变量命名。拿合并两个有序数组来说第一遍的代码可能是新建一个数组然后归并第二遍应该能写出从后往前填充、原地合并的版本省掉额外空间。第二遍重写还要求对第一遍的代码做diff分析找出哪里有重复逻辑、哪里可以提前返回、哪里不需要临时变量。这个环节是整个资源包的学习精华所在。5.4 两版代码对比从能跑到会讲的跃迁看一个具体的对比示例题目是删除排序数组中的重复项。第一遍学习代码可能是这样def remove_duplicates(nums): new_nums [] for i in range(len(nums)): if i 0 or nums[i] ! nums[i-1]: new_nums.append(nums[i]) for i in range(len(new_nums)): nums[i] new_nums[i] return len(new_nums)这版代码能AC但开了新数组空间复杂度是O(n)。第二遍重写时你会自然想到双指针原地覆盖def remove_duplicates(nums): if not nums: return 0 slow 1 for fast in range(1, len(nums)): if nums[fast] ! nums[slow - 1]: nums[slow] nums[fast] slow 1 return slow第二版里slow指向下一个不重复元素要填入的位置fast遍历整个数组遇到和slow-1位置不同的元素就填入。空间复杂度降到了O(1)而且代码更短。注意修改后的数组前slow个元素就是去重后的结果面试时还能顺手解释为什么能原地修改——因为有slow这个写指针保证不会覆盖未处理元素。能做出这种对比分析刷题才算到位。在资源使用顺序上我建议第一遍用剑指Offer题解打底同期看九章算法的框架讲解第二遍重写时进入lintcode验证最后用牛客直通BAT的真题做大考模拟。整套流程走下来核心知识点基本没有盲区。6. 进阶收尾技巧复杂度速查表与边界用例构造法最后分享两个让我在笔试中少翻车的具体技巧也是我拆完这套资源后一直在用的习惯。第一个是复杂度速查表。以下是我贴在显示器边上的对照表笔试时候选算法的第一标准就是它输入规模 n可接受复杂度代表算法n ≤ 10³O(n²)双重循环、DP朴素版n ≤ 10⁵O(nlogn)排序、分治、堆n ≤ 10⁶O(n)双指针、滑动窗口、哈希n ≤ 10⁹O(logn)二分、倍增拿到题目先看数据规模再定算法能省掉一多半的无效思考。尤其是DP题如果n到10⁵还写O(n²)的朴素DP基本必超时这时候要么优化状态定义要么上贪心。第二个是边界用例构造法。每次AC一道题后我都会额外构造5个用例再测一遍空输入、最小规模输入如单元素全重复值、逆序输入、最大规模极值。这5个用例能覆盖九成以上的边界错误。比如反转链表的代码普通测试用[1,2,3,4,5]能过但空链表、单节点链表就会暴露出问题快排和归并的代码逆序输入最容易测出递归栈溢出。从那以后我每刷一道题都会强制走一遍独立写代码 → AC → 构造边界用例 → 口头讲思路 → 记录三行笔记的流程这个流程就是从这个资源里的两遍代码对比中悟出来的。希望帮到你。本文还有配套的精品资源点击获取
返回列表