简介:该资源收录LeetCode题库的Python完整解答,覆盖数组、链表、树、动态规划、回溯、图论等核心算法专题,适合正在备战技术面试、希望系统梳理算法知识体系的中级及以上Python开发者。包内共1160个文件,主体为579个.py源码与580个.md题解笔记,另有gitignore等工程辅助文件,压缩包仅544KB,轻量便于离线检索。目前已有586人次学习使用。每道题按题目理解、方案设计、代码实现、测试验证与复杂度优化等环节拆解,代码注释与思路说明并重,便于对照复盘;md文档便于快速查看题目要点与解法脉络。读者可借此熟悉常见数据结构的Python写法,掌握heapq、itertools、collections等标准库在算法题中的典型用法,并在反复练习中强化逻辑推理与复杂度分析能力,为求职笔试和日常工程编码打下扎实基础。
1. LeetCode Python 题解的打开方式:别把题库当成收藏夹
刷 LeetCode 的人分两种:一种把题库当成收藏夹,收藏了就等于刷过了;另一种把它当成训练场,每一道题都抠到能讲清楚为止。这套 Python 版本的全套解答,明显属于后者——它不只是一堆答案,而是把每道题的题干、思路、AC 代码和复杂度揉进了一个个question.md里,配合README.md做索引,拿到手就能按图索骥。如果你正在准备算法面试,或者想系统补一遍 Python 的数据结构与算法功底,这套题解能省下大量从零摸黑的时间。下面我从仓库结构、环境配置讲起,再落到高频题型的实现套路和踩坑记录,把怎么用、怎么改、哪里会翻车一次说清。
2. 这套 Python 题解的组织结构:从 question.md 看作者的刷题思路
2.1 仓库结构与 question.md 的定位
仓库顶层是.gitignore、README.md和一批question.md。.gitignore负责忽略本地临时文件,正常情况下你不需要去动它,它保护的是本地的虚拟环境目录和缓存文件。README.md是入口,第一次打开仓库先读这个文件,里面通常写着题号覆盖范围、目录约定和刷题顺序建议。我习惯的做法是先把 README 里的题号列表导出,对照自己的薄弱题型做标记,再决定从哪一块切入。
question.md是这套资源的核心资产,每个题目对应一个文档。打开任意一个question.md,你大概率会看到四块内容:题目描述与输入输出格式、解题思路、Python 实现代码、时间与空间复杂度分析。这四块对应的正是刷题的完整闭环——先读懂问题,再设计解法,然后写代码,最后评估复杂度。很多题解只给代码不给思路,导致读者复现时一知半解,这套文档把「为什么这么写」也写进去了,这是它最值钱的地方。
| 文件/目录 | 作用 | 我的使用建议 |
|---|---|---|
README.md | 仓库说明与刷题索引 | 先通读一遍,确认题号范围 |
question.md | 每题的题干、思路、AC 代码、复杂度 | 按题号对照使用,先盖住代码自己想 |
.gitignore | 忽略本地环境与缓存文件 | 不需要改动,保持仓库干净即可 |
这里有一个容易被忽略的细节:既然每个题目都有独立的question.md,你在使用时要克制住「一道题反复打开关闭」的冲动。我的习惯是每天固定挑 3 道题,把对应的question.md一次性读透,读的过程中记录自己的思路,再和文档里的解法对比,而不是漫无目的地挨个翻。
2.2 为什么选择 Python:内置结构、标准库与可读性
这套题解选 Python 不是偶然。算法面试里,写代码的速度和正确性往往比语言本身的性能更关键。Python 的内置数据结构直接对应算法题的常见操作:list当数组和栈用,dict当哈希表和计数器用,set当去重集合用,省掉了 C++ 里std::vector、std::unordered_map的声明步骤。我在面试时用 Python 写题,平均每道题的代码量比用 Java 少 30% 左右,这意味着你能把更多时间花在思考解法而不是打字上。
标准库在这套题解里出镜率很高。collections.deque是 BFS 队列的标配,collections.Counter处理频率统计一行搞定,heapq直接实现堆结构,itertools提供排列组合和累加等现成工具。functools.lru_cache更是把带记忆化的递归题从「手动开数组」解放成「加一个装饰器」。这些库让题目解法的核心逻辑更突出,不会被底层实现细节淹没。
不过要提醒一句:Python 的简洁是把双刃剑。列表推导式和内置函数写得飞起,确实能缩短代码,但面试官往往会追问你「这段推导式的等价展开是什么」。所以我建议在使用这套题解时,先看作者的简洁写法,再自己在草稿纸上展开成基础循环版本,两版都过一遍,才算真正掌握。
2.3 一道题从理解到 AC 的五步流程
这套题解里反复出现的解题流程,可以归纳成五步,顺序固定。第一步是理解题目,重点确认输入输出格式、数据范围、以及边界条件——比如空数组、只有一个元素、数值为负等特殊情况。第二步是设计解决方案,这一步要选定数据结构和算法框架,决定是用递归还是迭代,用 BFS 还是 DFS,用贪心还是动态规划。
第三步是编写代码,按照设计好的逻辑用 Python 实现。第四步是测试用例验证,我一般会针对边界条件构造至少三组用例:正常输入、空输入、极端值输入。第五步是优化,如果提交后时间超限或空间超限,回到第二步重新调整算法。这套流程看起来朴素,但它保证了每一步都有明确产出,不会出现「代码写完了不知道对不对」的情况。
我个人的经验是把第五步单独拎出来重点对待。LeetCode 的判题系统对复杂度很敏感,同样一道题,O(n^2)的暴力解法可能在小数据集能过,但到大数据集就超时。这时候不要急着微调代码细节,先回到算法层面想清楚:当前的时间复杂度瓶颈在哪,能不能换一种数据结构把某个操作从O(n)降到O(1)。这套题解的文档里每一步都标注了复杂度,你在复现时可以刻意对比自己的写法和作者的写法在复杂度上差多少。
3. 本地复现与刷题闭环:环境、调试与跑通一道 994
3.1 Python 安装与 VS Code 调试配置
拿到这套题解后,第一件事不是刷题,而是把本地环境跑通。Python 版本建议 3.8 以上,太老的版本对类型注解和functools.lru_cache的支持不完整。如果你机器上还没有 Python,直接去官网下载对应系统的安装包,安装时勾选「Add Python to PATH」,装完在终端里执行python --version确认。Mac 和 Linux 用户通常自带 Python 3,但版本可能偏旧,我一般会用pyenv管理版本,避免系统环境被搞乱。
python --version pip --version pip install pytest执行上面三条命令依次确认解释器版本、包管理工具版本、以及测试框架是否装好。我一般用pytest来跑题解附带的测试用例,它是目前最主流的 Python 测试框架,断言写起来直观,失败信息也清晰。pip install pytest装的是最新稳定版,你如果本地已有旧版本,pip install -U pytest升级一下就行。
调试配置上,我用 VS Code 加 Python 扩展。打开仓库根目录后,创建.vscode/launch.json,写入下面的调试配置,之后就可以在question.md对应的代码文件里打断点,一行行观察变量变化。
{ "version": "0.2.0", "configurations": [ { "name": "LeetCode Debug", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal" } ] }这个配置的关键是program设为${file},这样无论你当前打开哪个题解文件,按 F5 都调试那一个文件。console设为integratedTerminal可以让你在调试过程中直接输入自定义测试数据。配置好后,我在question.md里建议你先建一个main入口,把题目示例作为参数传进去,打上断点走一遍,亲眼看着程序执行路径再开始改代码。
3.2 以 994 腐烂的橘子为例跑通 BFS
环境就绪后,选一道有代表性的题上手最有效。994 腐烂的橘子是 LeetCode 上 BFS 的经典题目,几乎每份热门前一百题清单里都有它,你的热搜里也高频出现。题目大意是:一个网格里,2代表腐烂的橘子,1代表新鲜的橘子,0代表空格,每分钟腐烂橘子会污染上下左右四个方向的新鲜橘子,问多久后所有橘子都腐烂,如果有橘子始终新鲜则返回 -1。
from collections import deque def orangesRotting(grid): rows, cols = len(grid), len(grid[0]) queue = deque() fresh = 0 for i in range(rows): for j in range(cols): if grid[i][j] == 2: queue.append((i, j)) elif grid[i][j] == 1: fresh += 1 if fresh == 0: return 0 directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] minutes = 0 while queue and fresh: for _ in range(len(queue)): x, y = queue.popleft() for dx, dy in directions: nx, ny = x + dx, y + dy if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == 1: grid[nx][ny] = 2 fresh -= 1 queue.append((nx, ny)) minutes += 1 return minutes if fresh == 0 else -1代码里的queue保存当前这一分钟所有腐烂橘子的坐标,fresh统计新鲜橘子的数量,directions定义了四个邻接方向。每次外层while循环代表一分钟,内部的for _ in range(len(queue))是关键——它保证这一分钟只处理当前队列里的节点,而不是新加入的节点,从而实现「分层」效果。你如果把for那层去掉,改成直接while queue,结果会变成按节点顺序传播,分钟数就会算错。
跑通这道题后,我建议你在本地加三组测试:空网格、全是空格、橘子被空格隔开永远无法全腐烂。第一组验证fresh == 0提前返回,第二组验证循环不会越界,第三组验证返回 -1 的分支。这三组用例能覆盖 BFS 题绝大多数边界坑,值得养成习惯。
3.3 复杂度自查与提交对照
每道题跑通之后,我会强制自己做一次复杂度自查,不急着看下一题。时间复杂度分析不能只看代码里嵌了几层循环,要看数据规模随输入怎么增长。还是以 994 为例,每个橘子最多入队一次、出队一次,所以整体是O(rows * cols),空间复杂度也是O(rows * cols),因为最坏情况下队列里可能装下整个网格。
if __name__ == "__main__": print(orangesRotting([[2, 1, 1], [1, 1, 0], [0, 1, 1]])) print(orangesRotting([[0, 2]]))上面这段直接跑就能看到两个结果:第一个是 4,第二个是 0。第一个用例对应题目自带的示例,第二个覆盖了没有新鲜橘子的情形。对比题解里标注的复杂度,如果自己的实现多了个不必要的排序或者反复index()查找,就要警惕超时风险。这套自查习惯,是把你从「能跑」推向「能过」的关键一步。
4. 高频题型的 Python 实现套路:热门 100 题背后的核心模型
4.1 二叉树与回溯:递归模板与传参边界
二叉树是 LeetCode 热门 100 题里的常驻题型,套路高度固定。绝大多数树的题都能套同一个递归模板:处理当前节点,递归处理左子树,递归处理右子树。区别只在返回值的设计上——是返回高度、返回节点、还是返回布尔值。以最大深度为例,代码可以短到几乎只有一行逻辑。
def maxDepth(root): if not root: return 0 return max(maxDepth(root.left), maxDepth(root.right)) + 1这段代码的边界条件看root是否为空,空树深度为 0,非空树的深度等于左右子树最大深度加 1。很多人写递归容易漏掉not root这个出口,导致无限递归最后爆栈。我通常会在纸上画一棵三层的小树,手动推一遍递归展开路径,确认每一层返回什么、上一层拿什么做计算。
递归代码的另一个坑是可变参数在回溯中的维护。比如求路径和时,常见的错误做法是把路径列表直接 append 进结果数组,一不注意就把引用存进去了,后面的回溯会修改已保存的结果。正确做法是传入副本,result.append(path[:])。这套题解里涉及回溯的题目,你可以重点看作者是怎么处理「进入递归前修改状态、退出递归后还原状态」的,这个前后对称的结构是回溯题不出 bug 的保障。
4.2 动态规划:状态定义与转移方程的书写习惯
动态规划在面试里的出镜率最高,也是最容易翻车的题型。我的经验是两个步骤能筛掉大部分错误:第一步把状态定义写清楚,是dp[i]表示前 i 个元素的最优值,还是dp[i][j]表示从 i 到 j 的某个属性;第二步把转移方程写成一行的数学表达式,再翻译成代码。如果状态定义含糊,代码写到一半一定会卡壳。
def climbStairs(n): if n <= 2: return n dp0, dp1 = 1, 2 for _ in range(3, n + 1): dp0, dp1 = dp1, dp0 + dp1 return dp1这个爬楼梯的例子展示了经典的滚动变量压缩技巧。状态转移是dp[i] = dp[i-1] + dp[i-2],因为dp[i]只依赖前两个值,所以不需要开整个数组,两个变量滚动覆盖即可。dp0, dp1 = dp1, dp0 + dp1这一行同时完成两件事,Python 的元组赋值保证右侧先算完再统一赋值,不会出现交替污染的经典 bug。使用这套题解时,我会先读作者完整数组版本的代码,理解转移过程,再看压缩版本的代码,理解空间优化,两个版本都写一遍才算过。
背包类、区间类 DP 的代码会更长,但核心都是「状态定义 — 初始化 — 转移 — 取答案」四步。我习惯把每一步用注释标出来,调试时按步骤排查错误来源。如果结果不对,先看初始化是否覆盖了边界状态,再看转移方程的索引是否越界,最后看答案取的是哪个位置,这个排查顺序能省大量时间。
4.3 二分与堆:073 爱吃香蕉的狒狒与 heapq
二分查找在 Python 里有着最经典的运用场景。073 爱吃香蕉的狒狒是 LeetCode 上二分答案的入门必刷题,题目问的是:给定香蕉堆piles和守卫离开的时间h,求狒狒能在h小时内吃完所有香蕉的最小速度。这个题的思路是对「速度」做二分,而不是对数组做二分,掌握这道题的二分框架可以解决一大类「最小化最大值」问题。
def minEatingSpeed(piles, h): def can_finish(k): return sum((p + k - 1) // k for p in piles) <= h left, right = 1, max(piles) while left < right: mid = (left + right) // 2 if can_finish(mid): right = mid else: left = mid + 1 return leftcan_finish(k)判断用速度k是否能吃完,(p + k - 1) // k是向上取整的写法,比math.ceil更直接也不依赖浮点运算。二分区间是[1, max(piles)],最小速度不可能小于 1,也不可能大于最大堆的香蕉数。每次缩小区间时,right = mid和left = mid + 1是这套模板的固定动作,两个边界处理方式不同,写反了就会死循环。
堆的典型场景是求 Top K、合并有序链表、以及调度类问题。heapq库是 Python 的神器,heapq.heappush和heapq.heappop操作都是O(log n),比每次排序的O(n log n)高效得多。热门 100 题里的合并 K 个升序链表,最佳解法就是维护一个小顶堆,每次弹出当前最小的节点,再把它的后继节点入堆。这套题解里凡是涉及 K 路归并的题,几乎都能套这个堆模板。
5. 刷题避坑清单:我在这套题解上翻过车的五个地方
5.1 递归爆栈:RuntimeError 不是算法问题
现象:本地运行小用例没问题,一提交就报RecursionError: maximum recursion depth exceeded,我还以为是 LeetCode 判题系统的问题。
原因:Python 默认递归深度限制是 1000 层,二叉树在极端情况下退化成链表,递归深度就会超过这个限制。算法上没错,但解释器不买账。
解决:两种方案,第一种是在代码开头加sys.setrecursionlimit(10000)临时提高限制,能缓解但治标不治本;第二种是把递归写法改成显式栈迭代,或者用尾递归优化思路重构。我现在遇到深度可能超过 1000 的题,直接默认用迭代或lru_cache配合递归,不赌判题系统的栈空间。这道题在本地测试时,我也会故意构造一个深度 1 万层的输入验证不会爆。
5.2 可变默认参数:测试用例之间互相污染
现象:同一道题连续跑多个测试用例,第三次开始结果突然不对,而且错误很诡异——像是上一个用例的数据残留到了下一个用例里。
原因:Python 函数的默认参数在定义时只评估一次。如果写成def dfs(node, visited=[]),这个空列表是全局共享的,第一个用例往里面塞了数据,第二个用例拿到的就不是空列表。
解决:所有可变默认参数一律改成None,函数内部再初始化。def dfs(node, visited=None),进函数第一行写if visited is None: visited = []。从那以后我每次写递归函数,看到默认参数里有列表、字典、集合,都强制自己停下来改掉。这套题解里的代码如果出现这种写法,说明作者也有过同样的坑,你可以对照着看有没有踩到。
5.3 切片当常数时间用:大数据集超时的元凶
现象:代码逻辑和题解完全一样,提交就是超时,但小用例本地跑得飞快,百思不得其解。
原因:Python 的列表切片arr[1:]会创建一个新列表,时间复杂度是O(n)。在递归或循环里高频使用切片,会把总复杂度悄悄抬升一个数量级。比如在递归里每层都切片,整体就变成了O(n^2)。
解决:改成传索引下标而不是传切片。递归函数增加start和end参数,需要子数组时直接操作原数组的区间,避免复制。如果确实需要拷贝,优先用arr[start:end]明确切片范围,而不是无脑arr[1:]。查这类问题我一般先在本地用timeit跑大数据集,看耗时变化趋势,再定位到具体是哪个操作拖慢了速度。
5.4 边界条件漏判:空输入与极端值
现象:提交后只挂了某一个用例,报错信息是IndexError或者返回了完全错误的结果,而这个用例往往是空输入或者包含极大数值的输入。
原因:写代码时盯着主流程,忽略了边界分支。比如链表题没处理头节点为空,数组题没考虑长度为 0 或者只有 1 个元素,数值题没考虑负数或 0 的除法。
解决:我给自己定了一条规矩:写完主逻辑立刻写边界守卫。函数入口先处理最小输入,if not nums、if not head、if n <= 1这些守卫语句永远放在最前面。LeetCode 的题目通常在描述里会标注数据范围,读题时圈出范围,代码里对范围内的极值都要过一遍。我把这套题的边界用例集合做成了一个固定清单,每道题跑三遍:空输入、单元素、超大值,全部通过才提交。
5.5 照搬题解不复盘:看懂了不等于会写
现象:照着question.md的代码敲了一遍,感觉全懂了,过了三天重新遇到同类型的题,还是写不出来,甚至无从下手。
原因:复制代码是零成本获得反馈的过程,但大脑没有参与构建思路。刷题的本质是建立「问题特征 → 算法选择」的条件反射,这个反射只能通过自己从空白文件开始写来训练。
解决:这套题解的每个question.md我都先盖住代码部分,只读题目描述和思路分析,然后自己动手写。写完之后再对照作者的代码,找出差异点——可能他是用字典我用了两层列表,可能他用迭代我用了递归。有差异才有收获。我给自己定的目标是每题至少独立写两遍,第二次在一周后重刷时进行,能顺畅写出来才算真正吸收。
6. 验证进阶:用 pytest 给题解建回归测试,再按主题做两轮重刷
6.1 把题解变成可回归的测试资产
光有代码没有测试,刷过的题很快就会变成黑匣子——你知道它当时通过了,但不知道它现在是否还通过。我的做法是给每道题在本地建一个对应的测试文件,用pytest管理,把题目附带的示例和自己的边界用例全部固化下来。以 994 为例,测试文件长这样:
import pytest from solutions.p0994 import orangesRotting def test_normal_case(): grid = [[2, 1, 1], [1, 1, 0], [0, 1, 1]] assert orangesRotting(grid) == 4 def test_no_fresh(): grid = [[0, 2]] assert orangesRotting(grid) == 0 def test_impossible(): grid = [[2, 1, 1], [0, 1, 1], [1, 0, 1]] assert orangesRotting(grid) == -1test_开头的函数会被pytest自动收集,每个函数里的assert是断言语句,条件为假时测试就失败并输出当时的实际值。运行pytest p0994_test.py -v可以看到每个用例的通过状态。这样做的价值在于,未来你换了一种思路重写这道题,或者 Python 版本升级带来行为变化,运行一次测试就能立刻发现回归问题。
6.2 按主题建刷题索引:两周一轮的复习节奏
题目刷太多之后,按题号顺序重刷效率很低,我按「数据结构 + 算法范式」给这套题解建了一个索引文件,把每道题的编号标记为主题标签,比如二叉树、动态规划_背包、二分答案、BFS_网格。复习时按标签批量重做,每次一个主题。第一次刷是学解法,第二次刷是练速度,第三次刷是口头讲清思路。从我的血泪经验看,能不看代码把一道题的思路完整讲给别人听,才是真的过关。
我给自己定的节奏是两周一轮重刷,每次只重做「当时卡壳超过半小时」的题和一个随机主题,总共控制在 20 道左右。重刷时不用本地测试文件,直接在 LeetCode 或本地空文件里写,写完再跑测试对比优化。从那以后,我每次拿到新题解都先建测试、再开刷,不给自己的记忆留黑匣子。希望今天的这些实践经验,能让你把这份 Python 版题解刷出比原仓库更扎实的效果。
本文还有配套的精品资源,点击获取