简介:面向备战大厂算法面试的求职者与在校生,这份压缩包是一份体系化的数据结构与算法刷题代码合集,覆盖剑指Offer题解、程序员代码面试指南、九章算法、牛客直通BAT课程及lintcode/大公司笔试真题编程题。资源同时收录第一遍学习代码和两个月后复习重新实现的全套代码,便于对照同一题目的多次解题思路;链表、二叉树、动态规划等经典考点均有涉及。包内共969个文件,以468个Java源文件与493个class编译文件为主,配合md/txt/docx说明文档及gitignore工程文件,可直接在IDE中运行、编译并对照验证。压缩包约789KB,轻量但模块划分清晰。已有40人学习下载,适合用来进行面试前集中刷题、专项复习与解题思路沉淀。
1. 这个“数据结构与算法刷题全攻略”到底值不值得啃:先看包里的三层材料
先说结论:这个项目最值钱的不是剑指Offer题解,也不是牛客直通BAT算法课的录播笔记,而是那个“第一遍学习代码”和“两个月后复习全部重新实现代码”的对照动作。数据结构与算法刷题全攻略这类资源网上很多,但大多数人只做了第一遍,看完题解就以为自己会了,结果笔试现场翻车。这个压缩包的价值在于它把“学”和“验”拆成了两个阶段,逼着你两个月后再写一遍,这一遍才是真正把题解变成自己能力的过程。适合三类人:准备大厂笔试的应届生、要跳槽但算法基础薄弱的在职开发、以及刷题刷到一半坚持不下来的半途选手。如果你是刚学完数据结构、连链表反转都要想半天的状态,这份材料也能用,但需要自己补一步基础语法。
2. 从刷题第一遍到两个月后重写:把剑指Offer和牛客课的题单串成一条线
2.1 剑指Offer题解:为什么它是第一遍的基准盘
剑指Offer这六十多道题,被大厂笔试反复引用了几轮,它的题面短、边界条件密集,几乎每一道都能延伸出一个笔试变种。我一般建议第一遍不要按书里的章节顺序刷,而是按题型重排:数组、链表、树、字符串、动态规划五类。原因是剑指Offer的题目难度不是线性递增的,按书刷容易在树和DP阶段劝退。
以“树的遍历”为例,剑指Offer里有重建二叉树、树的子结构、镜像二叉树、层次打印等六七道题。放在一起刷,你会在三天内把前序中序后序、递归和非递归写法全部过一遍,这种集中轰炸的效果远好于隔几天碰一道。
题解的价值在于它给的是“最小可复现”的解法,不是最优解。很多人读完觉得“就这?太简单了”,其实漏了考点。比如反转链表这道题,题解给的往往是三指针迭代法,但这道题真正的考察点不只是迭代,还有递归写法能不能在两分钟内写对。刷第一遍的时候,每道题都要做两遍:先按题解思路默写一遍,再试着自己推递归版本。这个习惯比刷题数量重要得多。
2.2 牛客直通BAT课与九章算法讲解:课和题怎么配着用
牛客直通BAT这套课,本质上是把大厂笔试题按公司维度做了归类,而九章算法讲解是按算法范式做的归类。两套课的维度不同,很多人只看一套,这是个漏。正确的用法是第一遍以九章的算法分类为主线,配合剑指Offer题解验证;第二遍以牛客的公司真题为主线,用来摸底自己离目标公司的差距。
我见过不少人是这样翻车的:先看九章的DP讲义,觉得听懂了,然后直接去做牛客的高频题,结果做不出来,回头又去看讲义,陷入“听课—看不懂—再听课”的循环。问题不出在课,出在中间少了一层剑指Offer的过渡。对应关系大概是:
第一周:数组、链表、字符串基础,配合剑指Offer对应章节 第二周:树、栈、队列、双端队列,配合牛客BAT课的树章节 第三周:排序与二分,这里的归并排序算法和快排必须手写过关 第四周:动态规划、贪心、回溯,这一周要放慢节奏,不可贪多
这样安排下来,九章讲的抽象模型落在了剑指Offer的具体题目上,牛客真题又验证了掌握程度,三层材料各司其职。
2.3 两个月后重新实现:把“看懂的代码”变成“自己的代码”
标题里“两个月后复习全部重新实现代码”这个动作,很多刷题的人都不会做,但它恰恰是检验真懂的唯一标准。人类记忆的遗忘曲线很残酷:第一遍刷完的题,两周后能独立写出来的不到四成。两个月后如果还能保持七八成以上的复现率,说明你确实建立了自己的解题框架,而不是在背答案。
我自己的做法是:第一遍刷的时候,在每道题的代码文件头部写一个两行的思路注释。两个月后复习时,先把注释盖住,自己推一遍思路,再写代码。如果推不出来,才允许看注释。如果注释也看不懂,说明第一遍就没真懂,这道题需要标记为“重刷”,而不是“复习”。
这个“重写代码”的过程会暴露一个特别常见的问题:你当时能照着题解写出来,是因为题解把中间的空跳过了。比如快速排序的partition边界条件,第一遍照着写不会出错,但自己重写的时候,递归退化的边界、等于pivot的元素放左边还是右边,这些细节全都会冒出来。所以复习时重写,类似于工作里的code review,不是抄一遍,而是找出自己当时的理解盲区。
3. 用最小环境跑通刷题代码:目录结构、对拍脚本与两段必手写算法
3.1 建立按题型分组的目录结构:别把题号当文件夹
刷题代码的目录管理是个被严重低估的环节。很多人把代码放在“一行一个文件”的文件夹里,文件名是offer_03.py、offer_04.py,两个月后想找一道题得翻半天。我见过比较实用的目录组织方式是按算法范式分组,再在里面按题目编号命名。
algorithm-daily/ ├── array/ │ ├── offer_03_duplicate.py │ ├── offer_04_matrix_search.py │ └── ... ├── linked_list/ │ ├── offer_24_reverse.py │ └── ... ├── tree/ │ ├── offer_07_rebuild.py │ └── ... ├── dp/ │ ├── offer_10_fib.py │ └── ... ├── string/ │ ├── offer_05_replace_space.py │ └── ... └── cases/ ├── 001.in ├── 001.out └── ...这样做的逻辑是:复习的时候你不会记得题号,但你会记得“我当时刷过一道树的层次遍历题”,按题型目录去找,效率会高很多。cases 目录存放的是自己造的测试用例,配合对拍脚本使用,不要手动验证输出。
3.2 一个最小测试脚本:用命令行把答案和期望值对拍
刷题不能“看完了觉得对就算对”,必须跑起来。大部分OJ平台提交后只告诉你“通过/不通过”,但本地调试时的信息远远不够。我写了一个很小的对拍脚本,放在项目根目录下,用来做本地批量验证。
# judge.py 用法: python judge.py solution.py case_id import importlib.util import sys import time from pathlib import Path def load_solution(path): # 从指定路径动态加载题解文件 spec = importlib.util.spec_from_file_location("sol", path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def run_case(sol, case_id, cases_dir="cases"): # 约定: case_id 对应的输入和期望输出都在 cases 目录下 in_path = Path(cases_dir) / f"{case_id}.in" out_path = Path(cases_dir) / f"{case_id}.out" data = in_path.read_text().strip().splitlines() expect = out_path.read_text().strip() t0 = time.time() got = sol.solve(data) # 约定题解文件必须暴露 solve(data) 入口 cost_ms = (time.time() - t0) * 1000 if str(got).strip() == expect: print(f"case {case_id}: PASS ({cost_ms:.1f}ms)") else: print(f"case {case_id}: FAIL, got={got}, expect={expect}") if __name__ == "__main__": sol_path, cid = sys.argv[1], sys.argv[2] run_case(load_solution(sol_path), cid)这个脚本的核心约定是:每题解文件必须暴露一个solve(data)函数,data是从.in文件按行读取的列表,返回值会被和.out文件逐字符比较。逻辑上很简单,但它解决了两个问题:一是用例可以反复跑,二是能统计耗时,让你直观地感受到暴力枚举和优化算法之间的时间差。
这里的参数设置需要注意:cases目录下的文件名必须是001.in和001.out这种严格对应关系,否则脚本会读不到文件。题解入口统一暴露solve,这样不同题目的代码格式一致,批处理时不需要改脚本。如果你习惯让入口接受参数而不是读文件,也可以改成sol.solve(rows),但整套项目的约定要保持一致。
3.3 归并排序与KMP:两段必须手写的对照代码
排序和字符串匹配是笔试里出现频率最高的两块内容。归并排序是“分治思想”的典型代表,它的递归写法和非递归写法(也就是自底向上的归并)至少要有一版能直接手写。KMP则是字符串题的分水岭,会写KMP的人和不写KMP的人,在笔试中的表现会差一个档次。
归并排序的递归实现:
def merge_sort(arr): # 递归终止条件:数组只剩一个元素时天然有序 if len(arr) <= 1: return arr mid = len(arr) // 2 # 分治:先排左半,再排右半 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) # 合并两个有序数组是核心操作 return merge(left, right) def merge(a, b): res = [] i = j = 0 while i < len(a) and j < len(b): if a[i] <= b[j]: # 等于时取左边,保持排序稳定性 res.append(a[i]) i += 1 else: res.append(b[j]) j += 1 # 剩余部分直接接在末尾 res.extend(a[i:]) res.extend(b[j:]) return res逻辑说明:merge_sort把问题拆成两半,各自递归解决后合并。merge是稳定合并,比较时用<=而不是<,这样相等元素的相对顺序不会被打乱。笔试中常见的要求是“排序后保持原顺序”,这个细节会被考到。
参数说明:arr[:mid]这种切片写法简单直观,但每层递归都会产生新数组,空间复杂度是O(n log n)。如果你想在真实笔试环境中写空间复杂度更优的版本,应该用索引下标加辅助数组的方式,也就是原地归并。笔试如果明确说了内存受限,用切片版会超空间,这是踩坑点。
KMP的主函数和 next 数组构建:
def get_next(p): n = len(p) nxt = [-1] * n i, j = 0, -1 while i < n - 1: if j == -1 or p[i] == p[j]: i += 1 j += 1 # KMP优化的一步:跳过必然失配的回退位置 if p[i] != p[j]: nxt[i] = j else: nxt[i] = nxt[j] else: j = nxt[j] return nxt def kmp_match(s, p): nxt = get_next(p) i = j = 0 while i < len(s) and j < len(p): if j == -1 or s[i] == p[j]: i += 1 j += 1 else: j = nxt[j] if j == len(p): return i - j # 返回匹配起始下标 return -1逻辑说明:get_next里j代表前缀匹配长度,nxt[i]记录的是p[:i]串的最长相等前后缀长度。注意第一行nxt = [-1] * n,这里-1是一个哨兵值,表示“回到了开头”,能避免在循环里写额外的边界判断。KMP匹配阶段,失配时模式串指针通过nxt[j]回退,主串指针i不回退,所以整体复杂度是O(m + n)。
参数说明:当模式串长度为n时,nxt数组的长度是n,第一个元素固定为-1。如果你在OJ上遇到“KMP匹配超时”,九成问题出在get_next的实现上——没有做p[i] != p[j]这个优化,会导致失配时回退过多。另一个常见问题是返回的下标从0开始还是从1开始,这在笔试题目里经常不明确说,建议刷题时统一按“下标从0开始”来假设。
4. 刷题节奏的三个关键参数:21天周期、难度梯度、复盘粒度怎么定
4.1 第一遍以21天为周期:没有周期的刷题坚持不下去
三个月刷完六百题的规划对大多数人来说是不现实的,因为缺少短期里程碑。我见过相对靠谱的节奏是把第一遍压缩到21天:每天六到八道题,周末只做题解回顾不做新题。21天正好对应一个习惯养成周期,也对应大多数公司笔试通知到正式笔试之间的间隔。
第一遍的21天里,目标不是“每道题都能独立写对”,而是“每种算法范式都见过,能说出它的典型标志”。比如看到“求最长回文子串”能想到中心扩展或DP,看到“求区间最大值”能想到单调队列,这种“知道该用什么方法”的能力,比“能把代码完全写对”更优先。
一个可行的21天分配表是这样:
| 时间段 | 内容 | 每日题量 | 备注 |
|---|---|---|---|
| 第1-3天 | 数组、字符串基础 | 6-8题 | 双指针、前缀和、滑动窗口 |
| 第4-7天 | 链表、栈、队列 | 6-8题 | 双端队列在滑动窗口中的用法 |
| 第8-11天 | 树、递归、回溯 | 5-6题 | 二叉树遍历手写递归与非递归 |
| 第12-14天 | 排序、二分 | 5-6题 | 归并排序、快排必手写 |
| 第15-18天 | 动态规划、贪心 | 4-5题 | 背包问题、区间DP重点过 |
| 第19-21天 | 综合回顾 | 3-4题 | 只重写之前做错的题 |
这个节奏的关键参数是“每天新题数不超过旧题回顾数”。很多人刷题失败不是题难,而是旧题忘得比新题学得快,导致挫败感。每天开头的三十分钟先重写前一天的代码,这一条能救回一半的遗忘问题。
4.2 难度梯度按题型而不是按平台热度分
牛客的题目列表是按热度排序的,热度高的题往往集中在某些固定题型上,比如链表反转、快排、斐波那契。如果你只看热度刷,很容易陷入“简单题刷了几百道,难题一道没碰”的状态。难点在于如何给自己的每个题型定难度级别,而不是跟着平台走。
更靠谱的做法是给每个题型建一个三级梯度:入门题、核心题、拔高题。以字符串为例,入门题是判断回文串、找最长公共前缀;核心题是KMP、滑动窗口找最小覆盖子串;拔高题是编辑距离、正则匹配这类DP与字符串的结合题。刷的时候只允许按照“入门→核心”的顺序走,核心题没有全过之前不要碰拔高题。
这个梯度不需要自己从零划分,剑指Offer的题目天然天然就是入门到核心之间的过渡,牛客BAT真题就是核心与拔高的混合体。所以你只要把自己要刷的题库按上述三类打标签,就能得到一个梯度清晰的题单。打标签本身也是复盘的一种,标签会提醒你哪些题型一直停留在“拔高题没碰”的状态。
4.3 复盘粒度细化到“卡点”:别写“这道题难”
刷题笔记最大的误区是写“这道题用了DP,有点难”。这句话三个月后没有任何信息量。有效的复盘要记录的不是难度,而是卡点——你在哪一行代码上停住了,为什么停在那一行。
举个例子,写“剑指Offer第46题把数字翻译成字符串”的复盘时,不要写“DP好难”,要写:“卡在状态转移方程上,没想清楚 dp[i] 依赖 dp[i-1] 和 dp[i-2] 的两种情况分别由什么触发。原因是对‘当前数字能否和前一个数字组成两位数’这个条件翻译不到位。”这样的记录两个月后复习时,你看到它就会立刻回想起当时的思维阻塞点,而不需要重新推导一遍。
关于复盘频率,我一般会定成“每次复习结束时写下三道题的最短卡点描述”,并放在代码文件头部的注释里。就像这样:
# 题意简述: 把数字翻译成字符串,有多少种翻译方式 # 卡点: 忘记判断 10 <= 当前两位 <= 25 的边界 # 解法: dp[i] = dp[i-1] + (可组合时?) dp[i-2] def solve(data): ...这个注释在两个月后重写代码时特别有用——它是逼着自己先回想卡点的起点,而不是直接看完整题解。重写代码时由于注释只记卡点和思路,不写完整代码,所以不会变成“抄答案但看不下去”的情况。
5. 刷题避坑指南:五个最常见的翻车现场与对应解法
5.1 现象:题解看得懂,自己写就卡壳
这个问题不是智力的锅,而是“阅读代码”和“生产代码”用的不是同一套脑回路。看题解时,你的注意力是顺着别人写好的逻辑走的,大脑会默认“这里自然递进过去了”;但自己写的时候,你要自己决定每一步的边界条件、空值处理、循环终止条件。所以我始终建议在看完题解后立刻关上答案,自己从头默写一遍,即使磕磕绊绊也要写完再比对。解决步骤:先看题解,合上,立刻手写,写不出来就再看一遍,但每次重看必须把没写出的那一行标注出来。
5.2 现象:刷了三百题,笔试还是没做出来
原因往往不是刷题量不够,而是题目分布不均。三百题里如果有两百道是数组和链表,树和DP只有三十道不到,那笔试碰上高频的DP题基本靠蒙,这是结构性问题,不是态度问题。解法是查一下自己在每类题型上的完成数,低于总量的15%的题型要补上来。牛客、剑指Offer、九章这三套材料覆盖的题型侧重点不同,跨材料对照就能发现自己偏科。别只刷自己擅长的题型,这是一种舒适的逃避。
5.3 现象:两个月后复习,发现全忘了
忘了是正常的,忘了才是符合客观规律的。但“全忘了”只有一种可能:第一遍刷的时候没有做任何结构化整理,代码文件是一堆题号的散装堆叠。解决方法是回到目录结构那一步,把代码按题型重排,并给每道题加上卡点注释。复习时不是按做题时的顺序看,而是按题型把同一类的题一起重写一次。这样做最大的好处是:同类题重写时,框架是相似的,你只要改核心逻辑,大脑的负担大幅降低。
5.4 现象:本地跑得通,OJ提交超时或超内存
这背后是对空间复杂度理解不足。本地数据规模小,时间复杂度差两倍根本感知不到;但OJ的用例是按题目给出的数据范围设计的,暴力枚举算法在本地不超时,线上就会直接跑满。解决方法是养成分析数据范围的习惯,凡是看到n <= 10^5的参数,必须从一开始就确认算法复杂度不能是O(n^2)。刷题时先想复杂度再动键盘,而不是先写完再说。
5.5 现象:环境配置把刷题兴趣耗光了
这是个隐藏很深的问题。不同版本的Python在切片、内存占用、sys.setrecursionlimit默认值上表现差异很大。比如递归解法在n=1000时直接超递归深度限制,这在本地跑不出来,你会误以为是自己算法写错了,结果是Python解释器在默认限制下不跑深层递归。解决办法是提前在项目根目录放一个requirements.txt,并且在每个题的代码头部注明“需要设置sys.setrecursionlimit吗”。用虚拟环境固定Python版本,是刷题项目的第一笔基建投资。
6. 用两小时盲写验证两个月后的复习成果:一套可复现的收尾动作
复习完一轮、代码全部重写一遍之后,最后一个动作是“盲写两小时”。流程很简单:从每类题型里随机抽一道题,共抽六道左右,不看任何题解和注释,限时两小时独立完成,然后拿第一遍的代码和这次的新代码做对比,看差异在哪里。
对比的重点不是“新代码比旧代码短了多少”这种虚荣指标,而是关注三件事:第一,边界条件处理比上次稳还是不如上次;第二,有没有发现上次写了一个隐藏的bug但当时因为是照着题解抄的所以没暴露;第三,新代码是否更简洁,比如交换变量不再用临时变量,循环边界从<=调整到了<配合+1的写法。这三项观察才是复习最大的收获。
如果盲写两小时后,六道题有五道以上能独立完成,那数据结构与算法刷题全攻略这个项目的核心目标就已经达到了。如果你发现某类题卡壳明显,不用慌,这恰恰说明两个月前的第一遍就没有在这个题型上形成肌肉记忆,补刷的时候把同类题再集中来一轮即可。
这套盲写验证法我在自己带过的几次小小学习小组里试过,“第一遍看懂的题到第二遍盲写时面目全非”是常态,但坚持用这种复盘节奏的人,通常两三周后就能很快能认出题目背后的算法原型。盲写没过的题重新排序,往往比赶着刷新题更重要。祝刷题路上的你早日不再闻算法变色,希望帮到你。
本文还有配套的精品资源,点击获取