简介:这套《数据结构与算法刷题全攻略》项目资料,面向备战大厂算法笔试与面试的开发者,覆盖剑指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与lintcode
3.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(n+m),比暴力的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 → 构造边界用例 → 口头讲思路 → 记录三行笔记"的流程,这个流程就是从这个资源里的两遍代码对比中悟出来的。希望帮到你。
本文还有配套的精品资源,点击获取