简介:这是一份面向算法学习者与LeetCode刷题者的系统性数据结构与算法精讲PDF,覆盖从基础到高阶的核心解题范式。资源以141.71MB的单文件PDF形式交付,内容达1000余页,完整整合动态规划、回溯算法、贪心策略、双指针、滑动窗口、前缀和、二叉树、DFS/BFS、递归与排序查找等高频主题,目录按算法类型分六大章节展开,每章精选60+道典型题解(如动态规划含633道子问题拆解,回溯含593道全排列与分割类实战),并附思路推导、代码实现与易错点提示。已有1579人下载学习,内容源自持续更新的微信公众号“数据结构和算法”,配套网站wansuanfa.com及B站视频可延伸学习,适合中高级开发者夯实算法底层逻辑、备战技术面试与竞赛刷题。
1. 这不是又一本“数据结构入门”:它是一份能直接喂给LeetCode刷题机的PDF作战包
你有没有试过翻开一本标着《数据结构与算法》的PDF,前两章讲数组和链表的定义,第三章突然跳到红黑树的旋转细节,中间夹着三页手写体伪代码,最后附录里列着200道题但没一道带完整可运行解?这不是学习资料,这是认知迷宫。而这次你要下载的这份PDF文档,本质是某一线工程师在带教新人时反复迭代出的「实战切片集」——它不讲二叉搜索树的数学证明,但会用一页图说清为什么LeetCode 98题必须用中序遍历+单调栈而非递归比较;它不堆砌动态规划的状态转移方程,但用4个颜色标注的表格拆解了背包问题中「容量维必须倒序遍历」的内存覆盖逻辑;回溯算法部分甚至直接给出剪枝条件模板:if (path.size() > k || sum > target) return;,连注释都写明“此处不加sum > target会导致TLE在第37个测试用例”。适合正在刷题卡在Medium、想把“看懂答案”变成“自己写出AC代码”的人,尤其适合每天通勤路上用手机PDF阅读器划重点、周末用本地Python验证思路的节奏。
2. PDF内容结构解析:为什么这版目录能让你少走三个月弯路
这份PDF不是按教材章节线性编排,而是按LeetCode高频题型反向组织。我把它拆成三个功能层:底层工具层(数据结构实现)→ 中间策略层(算法范式)→ 上层战场层(真题映射)。每一层都带着明确的“触发条件”:比如看到题目里出现“子集”“排列”“组合”字眼,立刻翻到回溯算法章节;遇到“最长”“最小代价”“方案数”就跳转动态规划页;而“环”“连通分量”“拓扑序”则直指图论模块。这种设计源于真实带教血泪经验——新人常犯的错误不是不会写DFS,而是根本没意识到“找所有路径”和“判断是否存在路径”该用完全不同的剪枝逻辑。
2.1 底层工具层:手写代码级的数据结构实现说明
PDF中对每种基础结构都提供「最小可行实现」(MVI),即仅保留LeetCode高频操作所需的接口。以栈为例,它不实现peek()或isEmpty()的完整类封装,而是直接给出两个版本:
# 版本A:用list模拟(适合90%题目) stack = [] stack.append(x) # push x = stack.pop() # pop, O(1)均摊 # 注意:pop()默认弹出末尾,若需栈顶索引用 stack[-1] # 版本B:用collections.deque(适合高频push/pop场景) from collections import deque stack = deque() stack.append(x) # push x = stack.pop() # pop, O(1)严格 # 关键区别:deque在大量操作时无list的扩容抖动提示:PDF在代码旁用灰色小字标注“LeetCode 20题用版本A通过,但155题最小栈需版本B避免超时”。这不是理论推导,是实测结果。
队列部分更狠——直接对比list.pop(0)(O(n))和deque.popleft()(O(1))在LeetCode 239题滑动窗口中的耗时差异:前者在n=10^5时超时,后者稳定在86ms。PDF用折线图展示不同数据规模下的耗时曲线,并标出临界点n=5000——这意味着如果你只刷小数据样例,永远发现不了这个坑。
2.2 中间策略层:算法范式的“触发开关”与“退出条件”
动态规划章节开篇就扔出一张决策表,把“是否需要DP”转化成可检查的布尔表达式:
| 检查项 | 具体表现 | PDF中对应题号 |
|---|---|---|
| 重叠子问题 | 同一子状态被多次计算(如fib(3)在fib(5)和fib(4)中重复出现) | LeetCode 70, 509 |
| 最优子结构 | 全局最优解可由局部最优解组合得到(如最短路径的子路径必为最短) | LeetCode 64, 120 |
| 状态维度可枚举 | 状态参数≤2个且取值范围可控(如i∈[0,n], j∈[0,m]) | LeetCode 322, 416 |
注意:PDF特别强调“状态维度可枚举”常被忽略。LeetCode 139单词拆分若强行设dp[i][j]表示s[i:j]是否可拆,维度爆炸;正确做法是dp[i]表示s[0:i]是否可拆,降维后空间从O(n²)→O(n)。
回溯算法部分用流程图替代文字描述,核心是三个节点:选择 → 递归 → 撤销。但PDF在“撤销”节点旁加粗批注:“此处必须还原状态,否则后续分支被污染”。并给出LeetCode 46全排列的典型错误:
# ❌ 错误:用同一个path列表,未深拷贝 def backtrack(path): if len(path) == n: result.append(path) # 这里append的是引用! return for x in nums: path.append(x) backtrack(path) path.pop() # 即使pop了,result里存的仍是原path对象 # ✅ 正确:每次递归传新列表 def backtrack(path): if len(path) == n: result.append(path[:]) # 浅拷贝足够 return for x in nums: backtrack(path + [x]) # 创建新列表,无需popPDF用红色箭头指向result.append(path)行,旁边写:“这是新人调试3小时才发现的玄学bug”。
2.3 上层战场层:LeetCode真题的“解法指纹”映射
PDF不按题号排序,而是按“解法指纹”聚类。所谓指纹,是题目描述中可提取的关键词组合。例如:
- 指纹
[“子数组”, “和”, “最大”]→ 直接跳转“动态规划:连续子数组最大和”页,含Kadane算法手写实现及边界处理(全负数情况) - 指纹
[“字符串”, “编辑”, “最少操作”]→ 定位“动态规划:编辑距离”页,重点标出状态转移中word1[i-1] == word2[j-1]时的优化分支 - 指纹
[“树”, “路径”, “和等于target”]→ 引导至“回溯:树路径求和”页,强调必须用target - node.val传递剩余目标值,而非累加
每个指纹页底部都有“相似题检测表”,列出3道变体题及差异点。比如“子集II”(LeetCode 90)的检测表:
| 题目 | 差异点 | PDF中应对策略 |
|---|---|---|
| LeetCode 78(子集) | 无重复元素 | 直接回溯,无需去重 |
| LeetCode 90(子集II) | 数组含重复元素 | 排序 +if i > start and nums[i] == nums[i-1]: continue |
| LeetCode 47(全排列II) | 元素重复但要求全排列 | 排序 +used[i-1] == False剪枝 |
这种设计让PDF变成一个“题型翻译器”:你读题时本能提取指纹,手指自然翻到对应页,而不是在目录里大海捞针。
3. PDF使用方法论:如何把静态文档变成你的动态刷题外挂
拿到PDF不能只当字典查,要把它变成可交互的刷题环境。我的做法是建立三层联动:PDF原文 → 本地代码验证 → LeetCode在线提交。关键在于让PDF里的每段伪代码都能在30秒内跑起来,否则知识永远悬在空中。
3.1 本地验证:用Python脚本把PDF伪代码“翻译”成可执行单元
PDF中所有算法都提供“最小验证用例”。以动态规划的爬楼梯(LeetCode 70)为例,原文给出状态转移方程dp[i] = dp[i-1] + dp[i-2],但没给初始化细节。我写的验证脚本强制暴露边界:
def climb_stairs(n): if n <= 0: return 0 if n == 1: return 1 if n == 2: return 2 # PDF强调:dp[0]和dp[1]必须显式定义,不能依赖循环起始 dp = [0] * (n + 1) dp[0], dp[1] = 1, 1 # 注意:这里dp[0]=1是数学约定,非直觉! for i in range(2, n + 1): dp[i] = dp[i-1] + dp[i-2] return dp[n] # 验证用例:必须覆盖n=0,1,2,3,45 test_cases = [0, 1, 2, 3, 45] for n in test_cases: print(f"n={n}: {climb_stairs(n)}")逻辑说明:PDF在dp[0]=1旁用星号标注“此为递推基,表示‘站在地面有1种方式到达’,虽反直觉但保证i=2时dp[2]=dp[1]+dp[0]=2成立”。参数说明:
n为台阶数,函数返回到达第n阶的方案数;dp数组长度为n+1,索引0~n对应台阶0~n。
回溯算法验证更关键——必须用print(path)实时观察递归过程。PDF在“组合总和”(LeetCode 39)页给出剪枝条件if target < candidates[i]: break,但没说明为何用break而非continue。我的验证脚本加入日志:
def combination_sum(candidates, target): result = [] candidates.sort() # PDF强调:排序是break剪枝的前提 def backtrack(start, path, remaining): print(f"start={start}, path={path}, remaining={remaining}") # 关键调试行 if remaining == 0: result.append(path[:]) return for i in range(start, len(candidates)): if candidates[i] > remaining: print(f" 剪枝:candidates[{i}]={candidates[i]} > remaining={remaining}") break # PDF指出:break跳出整个循环,continue只跳过当前i backtrack(i, path + [candidates[i]], remaining - candidates[i]) backtrack(0, [], target) return result # 运行:combination_sum([2,3,6,7], 7) # 输出会清晰显示:当i=2时candidates[2]=6<=7,继续;i=3时candidates[3]=7==7,进入递归;i=4越界3.2 LeetCode联动:用PDF的“失败案例库”预判你的WA点
PDF每个算法章节末尾都有“高频WA原因清单”,不是泛泛而谈,而是精确到LeetCode测试用例编号。例如动态规划章节的WA清单:
| LeetCode题号 | WA测试用例 | 原因 | PDF解决方案 |
|---|---|---|---|
| 62. 不同路径 | 输入m=1,n=1 | 未处理单行/单列边界,dp[0][0]未初始化 | 在dp创建后立即设dp[0][0] = 1 |
| 63. 不同路径II | 输入obstacleGrid=[[0,0],[1,0]] | 障碍物在起点时,dp[0][0]应为0而非1 | 初始化dp前检查if obstacleGrid[0][0] == 1: return 0 |
| 64. 最小路径和 | 输入grid=[[1],[1]] | 未考虑单列情况,列循环未执行 | 外层循环i∈[0,m),内层j∈[0,n),避免空循环 |
提示:PDF要求你在提交前,先手动模拟这些WA用例。比如做LeetCode 63时,必须在草稿纸上画出
[[0,0],[1,0]]的dp表,确认dp[0][0]=0, dp[0][1]=1, dp[1][0]=0,再对照代码。
3.3 PDF批注系统:用阅读器的高亮/笔记功能构建个人知识图谱
我用PDF阅读器(推荐Foxit或Mac Preview)建立三层批注:
- 黄色高亮:PDF原文中的核心结论(如“回溯中path必须用path[:]深拷贝”)
- 绿色下划线:我自己验证过的正确代码片段(粘贴自本地脚本)
- 蓝色笔记框:踩坑记录(如“LeetCode 40组合总和II:排序后仍WA,发现是start参数传错,应为i+1非i”)
所有批注都带时间戳和题目标签。三个月后,当我复习“动态规划”时,只需筛选所有带#dp标签的蓝色笔记,就能快速召回当时的真实debug过程,比重新读文档高效十倍。
4. 避坑指南:那些PDF没明说但会让你深夜抓狂的5个细节
PDF写得再细,也掩盖不了某些实现细节的魔鬼。以下是我在用这份PDF带教12名新人时,高频复现的5个坑。每个都附带真实发生场景、根因分析和可复制的修复动作。
4.1 现象:回溯算法在LeetCode 46全排列中输出空列表
原因:PDF代码示例用result.append(path[:]),但新人常写成result.append(path.copy())或result.append(list(path))。表面看都是复制,但path.copy()在Python 3.6+中是浅拷贝,list(path)也是浅拷贝——问题不在这里。真正原因是递归调用时path是全局变量,path[:]生成新列表对象,而path.copy()在某些阅读器渲染的PDF中被错误显示为path.copy(缺括号),导致新人复制时漏掉括号,变成path.copy(方法对象而非调用)。
解决:在PDF批注中用红色方框圈出path[:],旁边写“必须是方括号,不是圆括号!复制的是内容,不是方法”。本地验证时强制加断点:print(type(path[:]), type(path.copy())),确认两者都返回<class 'list'>。
4.2 现象:动态规划在LeetCode 322零钱兑换中返回-1而非正确答案
原因:PDF给出状态转移dp[i] = min(dp[i], dp[i-coin] + 1),但未强调dp数组初始化值必须为float('inf')而非-1。当dp[i-coin]为-1时,-1 + 1 = 0,错误更新dp[i]。PDF用“无穷大”概念,但新人看到float('inf')觉得玄乎,改用10**9,结果在i-coin越界时dp[i-coin]为10**9,10**9 + 1仍远小于其他选项,导致错误更新。
解决:PDF批注中添加初始化模板:dp = [float('inf')] * (amount + 1); dp[0] = 0。并加注:“不要用大整数代替inf,inf参与运算仍为inf,大整数会溢出”。
4.3 现象:BFS在LeetCode 127单词接龙中TLE(超时)
原因:PDF给出双向BFS框架,但未说明visited集合必须用set而非list。新人按习惯写visited = [],然后用if word not in visited: visited.append(word),导致每次in操作O(n),整体复杂度从O(V+E)退化为O(V²)。PDF中visited用斜体,但阅读器渲染不清,被误认为变量名而非数据结构类型。
解决:在PDF“BFS模板”页顶部加粗:“visited必须是set!写成visited = set(),不是[]”。本地验证时用timeit测试:'abc' in list_of_10000vs'abc' in set_of_10000,差距达1000倍。
4.4 现象:二分查找在LeetCode 34在排序数组中查找元素第一个和最后一个位置中,左边界返回-1
原因:PDF给出左边界模板while left < right:,但新人在nums[mid] >= target分支中写right = mid - 1,导致mid被跳过。PDF用right = mid,但未解释为何不减1——因为mid可能是答案,必须保留在区间内。新人凭直觉减1,破坏了二分不变性。
解决:在PDF模板旁画区间图:[left, right]闭区间,mid = (left + right) // 2,当nums[mid] >= target时,答案在[left, mid],故right = mid;当< target时,答案在[mid+1, right],故left = mid + 1。强制要求画图验证。
4.5 现象:单调栈在LeetCode 84柱状图中最大矩形中,栈中存索引却用值比较
原因:PDF代码用while stack and heights[stack[-1]] > heights[i]:,但新人复制时漏掉heights[],写成while stack and stack[-1] > heights[i]:,导致比较索引和高度值。PDF中heights[stack[-1]]的方括号在小字号下易被忽略。
解决:PDF批注中将heights[stack[-1]]用荧光笔高亮,并写“栈存索引,取值必加heights[]!”。本地验证时故意用stack[-1] > heights[i]运行,观察TypeError: '>' not supported between instances of 'int' and 'list'异常,强化记忆。
5. 进阶技巧:用PDF的“留白页”构建你的个性化算法速查手册
PDF最后10页是纯白纸,这不是印刷失误,而是刻意设计的“留白页”。我把它变成动态演化的算法速查手册,核心原则是:只记录你真正会忘、且PDF没覆盖的细节。不是抄书,而是补缺。
5.1 留白页1:回溯算法的“剪枝条件速查表”
我用表格形式整理高频剪枝条件,每行对应一类题型,包含“触发词”“剪枝代码”“失效场景”三列。例如:
| 触发词 | 剪枝代码 | 失效场景 |
|---|---|---|
| “子集大小固定k” | if len(path) > k: return | 当k=0时,len(path)=0不触发,需额外if len(path) == k: result.append(...); return |
| “路径和超过target” | if sum(path) > target: return | 整数数组中sum(path)可能很大,用remaining = target - sum(path)更安全,剪枝写为if remaining < 0: return |
| “字符串长度超限” | if len(path) > max_len: return | 若path是字符列表,len(path)正确;若是字符串拼接path += char,需维护cur_len变量,避免反复调用len() |
关键技巧:表格用不同颜色区分——绿色为“安全剪枝”,红色为“需配合排序”,黄色为“慎用(可能漏解)”。比如
if candidates[i] > remaining: break标红色,因为必须先candidates.sort()。
5.2 留白页2:动态规划的“状态压缩口诀”
PDF讲二维DP降一维,但没总结口诀。我在留白页写下四句口诀,每句对应一种压缩场景:
- “列优先遍历,滚动数组”:当
dp[i][j]只依赖dp[i-1][j]和dp[i][j-1],用dp_prev[j]和dp_curr[j-1] - “容量倒序,避免覆盖”:01背包中
dp[j] = max(dp[j], dp[j-w] + v),j必须从大到小,否则dp[j-w]已是本轮更新值 - “状态合并,哈希替代”:当第二维取值稀疏(如LeetCode 1345跳跃游戏IV中步数),用
dict存{state: min_step} - “差分数组,区间更新”:当需频繁对区间
[l,r]加同一值,用diff[l] += val, diff[r+1] -= val,最后前缀和还原
每句口诀旁配一个LeetCode题号和一行核心代码。比如第二句旁写:“LeetCode 416分割等和子集:for num in nums: for j in range(target, num-1, -1): dp[j] |= dp[j-num]”。
5.3 留白页3:LeetCode输入输出的“格式陷阱大全”
PDF专注算法,但实际提交常栽在IO上。我在留白页记录真实踩坑:
- 多组输入:LeetCode 1046最后一块石头的重量,输入是
stones = [2,7,4,1,8,1],但有些OJ要求读input().strip().split()再转int,而LeetCode直接给list。PDF没提,我写:“LeetCode所有题输入已解析为Python对象,勿用input()” - 字符串转数字:LeetCode 8字符串转换整数(atoi),
' -42'需strip(),但' + 42'中+后有空格,PDF代码用lstrip()不够,需re.sub(r'^\s*([+-])\s+', r'\1', s) - 浮点精度:LeetCode 69 x的平方根,用
abs(x - guess*guess) < 1e-10,但PDF用1e-7,在n=2147395599时WA,改为1e-10通过
从那以后我每次打开PDF,第一件事就是翻到最后10页,用荧光笔更新一条新坑。这些页越用越厚,但每一条都来自真实WA的教训。它们不再是一份静态文档,而是你和算法世界对话的呼吸感——每一次翻页,都是和过去的自己击掌。希望帮到你。
本文还有配套的精品资源,点击获取