ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Python刷LeetCode:模板、调试与题解仓库实战指南

用Python刷LeetCode:模板、调试与题解仓库实战指南 简介这是一份以 Python 语言实现的 LeetCode 全套题解面向正在准备技术面试、希望系统提升算法能力的程序员。资源围绕题目理解、方案设计、代码编写与优化展开覆盖数组、链表、树等基础数据结构以及动态规划、回溯搜索等核心算法场景能帮助读者建立完整的刷题知识框架。压缩包共 1160 个文件其中 580 个 Markdown 文档用于记录题目解析和思路要点579 个 Python 脚本提供可直接运行的题解代码整体仅 544KB内容紧凑、便于离线查阅和按目录检索。目前已有 586 人学习使用适合从基础巩固到面试冲刺的多个阶段。通过学习这些解答读者既能熟悉 Python 内置数据结构和常用库的高效用法也能在反复练习中提升逻辑思维与实际问题拆解能力为求职和职场发展积累扎实的算法功底。1. 为什么“leetcode全套解答python版本”值得你亲手整理而不是直接背答案你搜“leetcode全套解答python版本”跳出来的结果大多是两类搬运来的代码仓库或者只讲思路的题解帖。真正缺的是一套能落到自己手里的东西——按什么顺序刷、同一题型用什么模板、提交前怎么验证。这个方向不是某份现成源码能替代的它需要你维护一个自己的题解仓库每道题用Python写一遍配好测试用例和性能记录刷完还能回头讲得清。它能解决三个具体问题抄了答案看不懂、看懂了下次还不会、本地能跑但提交超时。适合正准备用Python系统性刷题的新手也适合刷过一轮却没留下可复现笔记的从业者。2. 拿到题目先做什么限制、复杂度与三个高频模板上网找leetcode刷题指南大部分默认你从C或者Java转过来讲数据结构时也偏底层。对Python选手来说拿到一道题的第一步不是打开编辑器而是读题目底部的Constraints。数据范围决定复杂度预算复杂度预算直接决定解题方向。热门100题里的大多数靠这套判断就能确定用哈希、二分还是BFS根本不用猜。2.1 先看数据范围O(n^2)能不能活下来从这里判断LeetCode每道题都会在描述末尾给出Constraints这是整道题里最被忽视的信息。我见过太多人拿到题就写双层for循环也不管n是不是10万。先把输入规模拆出来对照下面这张表复杂度预算就清楚了。n 的规模可接受的复杂度常见解法方向n ≤ 20O(2^n) / O(n!)回溯、状态压缩n ≤ 1000O(n^2)双重循环、简单DPn ≤ 10^5O(n log n)排序、二分、堆n ≤ 10^6 或更大O(n) 以下哈希、滑动窗口、数学推导对刚起步的刷题新手这表的用法是先数一遍输入里有几个数、每个数多大再决定要不要写那层嵌套循环。如果n是10^5第一版就写O(n^2)那么不管代码多漂亮提交时大概率是TLE这不是Python的语法问题是量级选错了。还有一条我个人的习惯第一版永远先写暴力解。很多人一上来就追求最优解思路没理清就卡在边界处理上。暴力版答案一定对它给你一个可靠的对照基准优化版写完和暴力版对拍不一致就知道是哪里改出了问题。暴力解不是白写它是调试时的参照物。2.2 BFS模板从994腐烂的橘子吃透网格遍历腐烂的橘子是理解BFS最合适的题目腐烂从多个起点同时向外扩散每分钟只感染相邻的新鲜橘子。这个过程天然是逐层的正好是BFS的语义。如果用DFS写就得为每个橘子记录被感染的时间再取最小值代码量立刻翻倍。from collections import deque def oranges_rotting(grid): rows, cols len(grid), len(grid[0]) q deque() fresh 0 for r in range(rows): for c in range(cols): if grid[r][c] 2: q.append((r, c)) elif grid[r][c] 1: fresh 1 minutes 0 while q and fresh 0: minutes 1 for _ in range(len(q)): r, c q.popleft() for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)): nr, nc r dr, c dc if 0 nr rows and 0 nc cols and grid[nr][nc] 1: grid[nr][nc] 2 fresh - 1 q.append((nr, nc)) return minutes if fresh 0 else -1这段代码里有几个关键点。q里存的是当前这一分钟要处理的腐烂橘子popleft()是O(1)操作换成list.pop(0)就是O(n)在网格题里很容易成为隐藏的性能瓶颈。for _ in range(len(q))是BFS按层处理的固定写法等于先把这一轮要扩散的节点数量快照下来处理完再进入下一分钟。fresh记录剩余新鲜橘子数量扩散结束后如果还大于0说明有橘子永远接触不到腐烂源返回-1。这个模板对网格类题目通用性很强。把四方向换成八方向或者把坐标换成带步数的状态结构不用变。复杂度是O(R*C)每个格子至多入队一次这也是BFS类题目的标准复杂度。2.3 二分答案模板爱吃香蕉的狒狒怎么调上下界爱吃香蕉的狒狒是典型的二分答案题求最小吃香蕉速度使得能在H小时内全部吃完。这类题有个共同特征——答案是单调的。速度越快越有可能在时限内完成所以可以在这个单调区间里做二分逼近最小可行值。def min_eating_speed(piles, h): left, right 1, max(piles) def can_finish(speed): return sum((p speed - 1) // speed for p in piles) h while left right: mid (left right) // 2 if can_finish(mid): right mid else: left mid 1 return leftleft从1开始而不是0因为速度为0没有意义除数为0更是直接报错。right取最大堆的香蕉数因为速度超过这个值不会让结果更好。判断函数里(p speed - 1) // speed是向上取整的写法意思是一堆香蕉要分几轮吃完剩几根也算一轮。二分循环用的是左闭右开风格当can_finish(mid)成立时收缩右边界、保留mid否则加大左边界最后left和right相遇的位置就是答案。这个模板的适用范围比想象中广。求最小最大、最接近、第K小这类问题只要判断条件满足单调性都能套先写判断函数再套二分框架剩下的事情就是调边界。不需要每次重新推导循环不变量。2.4 栈与递归模板227基本计算器为什么总在边界翻车表达式求值是经典高频考点基本计算器这一族题考的是栈的挂起能力。遇到乘除必须先结算遇到加减可以暂时压栈因为一个数符不确定只有看到它后面的运算符才知道前面的运算能不能马上算完。def calculate(s: str) - int: stack [] num 0 op for i, ch in enumerate(s): if ch.isdigit(): num num * 10 int(ch) if ch in -*/ or i len(s) - 1: if op : stack.append(num) elif op -: stack.append(-num) elif op *: stack.append(stack.pop() * num) elif op /: stack.append(int(stack.pop() / num)) op ch num 0 return sum(stack)op记录的是上一个遇到的运算符遇到新的运算符才结算上一个数这就是延迟结算。乘除结算时先弹出栈顶再压入结果加减直接入栈减号存成负数最后把栈里所有数相加。这里有个Python特有的坑int(a / b)而不是a // b。Python对负数整除是向下取整-3 // 2的结果是-2而题目要求向零截断int(-3 / 2)才是-1。这处不留意隐藏用例一测就翻车。这类题还能扩展到带括号的版本思路是用递归或另一个栈处理括号段把括号内的结果当成一个普通操作数。理解延迟结算以后括号只是多了一层递归深度处理逻辑完全一致。3. 搭建一套可持续维护的python题解仓库上一章讲的是单道题怎么做这一章解决另一个问题刷到第50题、第200题的时候你之前写过的代码还在吗还能找到吗还能跑吗一套可持续维护的python题解仓库至少要有三样东西清晰的目录、统一的调试入口、一致的运行环境。下面的结构我用了很久也是我认为最省事的组织方式。3.1 按题型建目录按编号留命名两套并用的组织方式常见做法是把所有文件堆在一个目录里命名001.py、002.py。刷到30题以后找一道题靠猜复习一个专题要翻十几个文件。推荐的做法是第一层按题型分目录文件名带题号和英文简称这样专题复习和定位题目两不误。leetcode_py/ ├── array/ │ ├── _0001_two_sum.py │ └── _0015_three_sum.py ├── binary_search/ │ ├── _0033_search_in_rotated.py │ └── _0875_koko_eating_bananas.py ├── dp/ ├── graph/ │ └── _0994_rotting_oranges.py ├── stack/ └── tree/目录按题型分是因为面试前按专题过一遍效率最高文件名带编号是因为别人问起“two sum你怎么写”时你能三秒定位到文件。文件最前面加下划线是个小技巧Python模块名不能以数字开头0994_rotting_oranges.py没法直接import加个下划线就合法了。网上那些免费python源码大全下载包最大的问题就是没有统一入口更不会为你的记忆方式定制结构。刷题是长期工程仓库是你自己的目录得按你的复习习惯来。我习惯每个文件顶部用一行注释写题目的一句话描述和核心思路三个月后回来看一眼就知道这题考什么不用重新读一遍代码。3.2 写一个统一调试入口自动跑样例、报耗时、对比输出每道题都在文件底部写if __name__ __main__然后手动敲输入输出前20题没问题刷到后面就变成重复劳动。更好的做法是写一个统一的调试入口自动加载题解函数、批量跑用例、报告耗时。import importlib.util import time def load_solution(path): spec importlib.util.spec_from_file_location(solution, path) mod importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod.Solution def run_cases(handler, cases): for idx, (args, expected) in enumerate(cases): start time.perf_counter() result handler(*args) cost time.perf_counter() - start mark OK if result expected else FAIL print(f[{mark}] case {idx}: got{result}, expected{expected}, {cost:.4f}s) if mark FAIL: return False return Trueload_solution用importlib加载指定路径的文件不依赖整个目录成为Python包。run_cases接收一个函数和用例列表用例格式是((参数1, 参数2), 期望值)调用时用handler(*args)按位置解包。每个题解文件底部只要维护一个cases列表就行。if __name__ __main__: cases [ (([[2, 1, 1], [1, 1, 0], [0, 1, 1]],), 4), (([[0, 2]],), 0), ] run_cases(Solution().oranges_rotting, cases)参数包成元组这层不能省否则*args解不出来。time.perf_counter比time.time精度高适合测毫秒级差距真正超时的代码往往在10毫秒和1秒之间普通计时器测不出区别。3.3 最小vscode python环境配置装解释器、配pytest、记住三个快捷键从python官网下载安装包装完在vscode里能跑代码这只是起点。我建议再花十分钟把测试框架配上收益会持续到整个刷题周期结束。pytest能自动发现test_开头的文件断言失败时打印详细对比比print调试强得多。# test_0994.py import sys sys.path.insert(0, graph) from _0994_rotting_oranges import Solution def test_case1(): grid [[2, 1, 1], [1, 1, 0], [0, 1, 1]] assert Solution().oranges_rotting(grid) 4 def test_case2(): grid [[0, 2]] assert Solution().oranges_rotting(grid) 0这段代码里的sys.path.insert把graph目录加进模块搜索路径这样pytest跑起来才能import到题解文件。在vscode里按CtrlShiftP打开命令面板搜“Python: Configure Tests”选pytest左侧会出现测试列表点一下就能跑单题。提示vscode的调试快捷键先记三个——F9打断点、F5启动调试、F10单步。递归和链表题用断点调试比print直观得多。环境版本也要统一一个原则本地的Python版本和刷题平台判题环境尽量保持同一个大版本。3.10和3.12的语法差异不大但有些新特性在旧环境上就是SyntaxError这一条能避免大量无意义的排查。4. 本地调试与超时排查把“样例过了但提交TLE”拦在上传之前刷到中期最打击人的不是不会做而是本地跑自测用例秒过、提交上去TLE。这种问题的根源基本都出在复杂度量级或者某个隐藏O(n)操作上。排查顺序应该是先量化耗时和内存再按固定顺序找问题最后针对三个高频性能陷阱做优化。4.1 耗时统计与内存快照先量化再优化不量化就优化全凭感觉那是玄学。我给题解方法套一个统一的profile装饰器跑性能测试时直接看数字。import time import tracemalloc def profile(fn): def wrapper(*args, **kwargs): tracemalloc.start() start time.perf_counter() result fn(*args, **kwargs) elapsed time.perf_counter() - start _, peak tracemalloc.get_traced_memory() tracemalloc.stop() print(felapsed{elapsed:.4f}s peak{peak // 1024}KB) return result return wrappertracemalloc.start()开启内存跟踪get_traced_memory()返回当前和峰值两个值这里只取峰值。注意要在stop()之前调用否则取到的数据不准。装饰器直接叠在题解方法上拿一个接近边界的大样例去测耗时和内存都出来了。优化完再跑一次数字对比比感觉可靠得多。4.2 超时排查顺序循环嵌套、容器选择、输入规模一个都不能漏出现TLE后别急着改代码先按顺序排查。第一步回头确认Constraints的n如果n是10^5还写了双层for那问题就是量级错了优化局部代码救不回来。第二步数循环层数确认复杂度预算。第三步看容器操作这是Python特有的坑。症状优先怀疑对象n 10^5 还写了双层for换哈希、排序、双指针循环里用了 in list、list.remove、list.pop(0)换成set/dict/deque递归深度很大改迭代栈或BFS每次循环都重新搜索一遍把重复计算挪到循环外看一个最典型的对比两数之和的暴力版和哈希版def two_sum_n2(nums, target): for i in range(len(nums)): for j in range(i 1, len(nums)): if nums[i] nums[j] target: return [i, j] return [] def two_sum(nums, target): seen {} for i, n in enumerate(nums): if target - n in seen: return [seen[target - n], i] seen[n] i return []暴力版O(n^2)n10^4时大概要跑一亿次判断本地也要几秒。哈希版只遍历一次dict的in操作是平均O(1)。如果复杂度已经降到O(n)还超时就去查常数是不是每次循环都在做无谓的列表拷贝是不是在热点路径里调了过于复杂的库函数。4.3 三个python性能陷阱列表拼接、字典键类型、字符串格式化第一个陷阱是列表拼接。循环里写a a [x]每次都会创建新列表并复制旧元素整体变成O(n^2)。正确做法是a.append(x)原地修改。第二个陷阱是字典键的类型。热点循环里反复用新构造的元组或字符串做键会把一次O(1)哈希查找拖成O(len(key))的构造开销。能复用键对象就别在循环里重新创建。刷题时常见写法cache[(x, y)]没问题但如果循环里每次都new一个元组就要考虑把元组改成两个参数嵌套的dict或者用整数编码。第三个陷阱是字符串拼接。s str(x)在CPython里因为有引用计数优化有时能原地扩展但这是实现细节大样本下一旦触发复制就是灾难。# 慢 s for x in nums: s str(x) # 快 s .join(str(x) for x in nums)join把拼接交给C层一次完成生成器表达式不会额外构造出中间列表。刷题时这三条改完很多TLE就消掉了。注意join里用生成器还是列表推导式差别不大内存上生成器略省但可读性上列表推导式更直观。5. 避坑用python刷leetcode绕不开的5个坑这一章是血泪经验汇总。下面5个坑每一个都能让一道本来写对的题白耗半小时而且大多不是算法问题是Python语言特性在作怪。5.1 全局变量泄漏class里藏状态重跑时上一轮结果串场现象同一道题本地跑第一个用例没问题跑第二个用例时结果莫名多了1或者偶尔差一个固定值。原因LeetCode的判定器会实例化同一个Solution对象连续跑多个测试用例。如果状态存在self上上一轮的计数或缓存没清零就会串到下一轮。解决给self属性在方法入口处显式重置或者干脆全部用局部变量。class Solution: def tree_depth(self, root): self.ans 0 def dfs(node, depth): if not node: return self.ans max(self.ans, depth) dfs(node.left, depth 1) dfs(node.right, depth 1) dfs(root, 1) return self.ansself.ans在方法开头重置多次调用是安全的如果把这行放到方法外面第二个用例一进来就挂。这个坑在带缓存的递归题里尤其隐蔽因为lru_cache本身是函数级的不会串但你自己写在self上的memo会串。5.2 遍历时修改list删元素导致跳项结果错得莫名其妙现象要删除所有满足条件的元素写完发现有一半没删干净而且不是每个用例都这样时好时坏。原因for x in nums在底层按索引推进列表删除后元素整体前移游标跳过了一个位置。用nums[1,2,4,3]删偶数举例x2删掉后列表变成[1,4,3]下一轮取到的是原索引2位置的34被跳过没删。解决换列表推导式过滤或者倒序遍历。nums [1, 2, 4, 3] # 对生成新列表 nums [x for x in nums if x % 2 1]遍历时只读不写这个习惯要养成。如果题目要求原地删除用while循环手动控制索引删完不加1不删才加1。5.3 二维列表引用共享DP矩阵一改整列跟着变现象DP题里初始化二维数组dp [[0] * n] * m写完状态转移一调试发现dp[0][0]改成1dp[1][0]也变成1整列全变。原因乘号复制的是外层列表的引用m个子列表指向的是同一个底层list对象。改一个是改所有。解决用列表推导式。# 错 dp [[0] * n] * m # 对 dp [[0] * n for _ in range(m)]这个坑在二维DP、网格类题目里几乎必踩尤其是从C转过来的选手最容易中招因为C的vector行为不一样。初始化时多看一眼能省掉半小时的“玄学调试”。5.4 递归深度1000DFS题目直接RecursionError现象递归写的树题本地小样例通过提交报maximum recursion depth exceeded。原因Python默认递归上限是1000。二叉树退化成链或者网格DFS路径超过1000层就会爆栈。解决sys.setrecursionlimit(10000)可以临时抬高但要明白Python递归本身偏慢抬太高可能导致进程直接段错误。最稳的做法是把DFS改成显式栈。def preorder(root): if not root: return [] stack, out [root], [] while stack: node stack.pop() out.append(node.val) if node.right: stack.append(node.right) if node.left: stack.append(node.left) return out注意setrecursionlimit调得过高会直接压爆C栈导致进程崩溃不是无限使用的后悔药。迭代栈先序遍历把递归的系统调用栈换成显式数据结构深度不受1000限制。写完以后你会发现很多递归改迭代的题性能反而更好因为没有函数调用的额外开销。5.5 本地算得对、提交就错注意判定环境与Python版本现象本地怎么跑都对复制进提交框就WA有时连语法都报SyntaxError。原因本地的Python是3.12线上判定环境可能更早。dict保持插入顺序是3.7的特性f-string是3.6match语句3.10才有某些类型注解写法3.9才合法。用了新语法本地能跑线上直接编译失败。解决刷题代码只写3.8以前就存在的基础语法。Python入门阶段学到的那些标准写法在判题环境一定兼容不要为了展示新特性去用type hint里的list[str]这种3.9才支持的类型。# 兼容写法 def two_sum(nums, target): seen {} for i, n in enumerate(nums): if target - n in seen: return [seen[target - n], i] seen[n] i return []这条习惯比想象中重要。刷题是在构建一套可迁移的肌肉记忆写出的代码不仅要能过判题还得能在一个干净环境里随手跑起来。追求版本兼容性本身就是工程素养。6. 从“刷完答案”到“真会题”验证方法、复习节奏与周赛定位能照着题解写出正确答案和能独立做出来中间隔着一整个复习周期。把题解仓库建好之后真正让能力往上涨的是怎么对待错题以及怎么检验自己是不是真会。6.1 错题重测表三天、七天、十四天的三遍复习我维护一张简单的错题重测表每道错题按时间节点重试。重试的要求很严格不看题解从空文件开始写能AC才算过。节点做什么当天独立写一遍卡住不超过30分钟第3天不看题解重做能AC才算过第7天做同一类型的另一道题不重复原题第14天用自己的话把题解讲给另一个人听第3天和第7天之间的差别是核心第3天重做原题检验记忆第7天做同类新题检验能不能迁移。很多时候原题第3天能过第7天做同类题还是卡说明当时只是记住了代码没真正理解模板的边界。第14天“讲出来”是最高强度的验证讲不清的地方就是没懂的地方回到仓库里标注一下再约一轮重试。6.2 周赛是最好的体检leetcode周赛430暴露的问题定期参加周赛是性价比最高的自测方式。比如周赛430这种场次四道题难度从简单到困难递进限时环境下能暴露两类问题一类是思路慢看完题想不出方向另一类是语法卡顿知道用什么模板但写不出来或者在细节上反复改。我自己的习惯是每场周赛从头到尾做完赛后立刻补完没AC的题并把四道题全部并入题解仓库在文件顶部标注当时卡住的点。每周看一次自己的AI Rating曲线上涨还是停滞比刷完多少道题更能反映真实水平。周赛里犯过的错比刷题时犯过的错印象深得多因为它们是在时间压力下犯的。真到了面试手撕算法时那种压力场景和周赛最接近——用这套方法把周赛当训练场考场上翻车的概率会小很多希望帮到你。本文还有配套的精品资源点击获取
返回列表