
Python常见算法1-50题这组题是我比较推荐新手用来建立算法感觉的入门套餐。它不追求偏难怪而是把数组、链表、栈、哈希表、双指针、排序、二分、回溯、动态规划这些最常考的知识点密集地串了一遍。我前后刷了三轮也带过几个零基础的实习生最大的感受是很多人不是不会写代码而是不会把题目翻译成数据结构操作。这篇文章会以我整理出的50题清单为主线拆开讲怎么给题目分类、每类题的核心套路、关键代码怎么落地、以及我实际踩过的坑。文章里给的解法都是Python尽量少写花哨技巧多写能直接跑的方案。无论你是为了准备面试还是想提升日常写脚本的逻辑能力这套题都值得慢慢过一遍。1. 50题怎么分类刷题顺序怎么排1.1 为什么我把50题拆成10个专题很多人拿到题目就开始硬写写不出来就翻答案翻完还是记不住。我自己第一轮也是这个状态后来痛定思痛把前50道常见题按底层逻辑拆成了10个专题效果立刻不一样。我这套分类不按题目难度排而是按数据结构和算法思维排编号区间专题代表题核心考点1-5数组与字符串两数之和、三数之和、反转字符串索引操作、双指针的开始6-10哈希表字母异位词分组、最长连续序列空间换时间、去重11-15滑动窗口/双指针无重复字符的最长子串、接雨水区间收缩、窗口维护16-20链表反转链表、合并有序链表、环检测指针修改、虚拟头节点21-25栈与队列有效括号、最小栈、滑动窗口最大值单调性、先进后出/先进先出26-30排序与二分手写快排、旋转数组最小值分治、有序性查找31-35回溯全排列、子集、括号生成决策树、状态撤销36-40递归与树二叉树遍历、最大深度递归出口、遍历框架41-45动态规划爬楼梯、零钱兑换、最长递增子序列状态转移、最优子结构46-50贪心/位运算跳跃游戏、只出现一次的数字局部最优、按位处理这样分完以后你会发现题目之间是有血缘关系的。比如第3题无重复字符的最长子串本质是滑动窗口第15题接雨水虽然看起来像数组题但最优雅的解法也是双指针或者单调栈。分类的价值就在这里你不是在记题而是在总结一类题的解题模式。1.2 推荐的三轮刷题节奏我把这套50题按三轮计划走每一轮目标完全不同。第一轮是熟悉套路不要求每道题都独立做出来。拿到题先想五分钟想不出来直接看题解看完后用Python抄一遍抄完关掉答案再自己写一遍。这一轮大约需要三到四周每天两到三题重点是“见过题型”。第二轮是限时训练。每道题给自己二十分钟写不出来的标记为红色写出来的标记为绿色。第二轮结束后重点复盘红色题目看是卡在数据结构上还是卡在状态转移上。第三轮是默写和变式。我会把每类题的模板代码整理成笔记比如二分模板、滑动窗口模板、回溯模板然后只看题目编号不看答案在编辑器里把核心代码默写出来。这轮最痛苦但效果也最扎实。如果你只有两周时间那就压缩成一轮半把所有题过一遍然后只把二叉树、回溯、动态规划、滑动窗口这四个模块单独拎出来再刷一遍。面试和实际工作中这几个模块出现频率最高。1.3 为什么用Python刷算法而不是其他语言Python在算法题里最大的优势是语法糖和内置容器。list可以当栈、当队列、当动态数组dict和set帮你省去手写哈希表的步骤切片处理数组边界极其方便。同样一道反转字符串C要写swap循环Python一行[::-1]就结束。但这不代表Python没有坑。Python的/是浮点除法//才是整数除法二分查找里写错一个符号就会死循环。Python的默认递归深度是1000层DFS深度超过这个数就会报错。这些坑我会在后面单独拿出来讲刷题前心里有数能省很多排查时间。还有一个现实原因是生态。很多公司面试允许用Python写算法即使公司主力栈是Java或Go面试官也更关心你的思路清不清楚、复杂度对不对而不是纠结语言细节。用Python快速验证思路再补一版工程实现是我觉得效率最高的方式。2. 环境准备与调试工具不要被配置拖后腿2.1 装好Python之后先做三件事刷题前不需要折腾太复杂的环境但基础配置如果能一次性到位后面会舒服很多。我个人推荐用Python 3.10以上版本VSCode作为主力编辑器Python插件一定要装。装完后第一件事不是在编辑器里写Hello World而是先确认终端里能直接运行python -V。很多人在Windows上会遇到python was not found; run without arguments to install from the Microsoft Store这种报错原因就是安装时没有勾选Add Python to PATH。解决办法也很简单重新运行安装包选择Modify把“Add Python to environment variables”勾上或者手动把Python目录和Scripts目录加进系统Path。第二件事是把pip镜像源换成国内地址。默认源在国外装numpy、pandas这种包时速度可能很慢甚至超时。临时指定源可以用pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple长期使用建议直接修改配置文件。在用户目录下创建pip.ini或pip.conf写入镜像地址之后所有安装都走国内源速度会有质的提升。第三件事是在VSCode里选中正确的Python解释器。按CtrlShiftP输入Python: Select Interpreter选择你刚安装的那个Python。这一步不做很可能出现终端运行正常、但编辑器里跑代码却提示找不到模块的情况。2.2 用Python调试算法题的三个技巧算法题调试最忌讳只靠眼睛看代码。我见过太多人盯着一个错误看了十分钟最后发现是变量名拼错。这里分享三个我常用的调试技巧。第一个是用assert做边界断言。比如写二分查找可以在开头加上assert nums sorted(nums)来验证输入有序写动态规划时在状态转移后断言一下结果不为负数。断言失败会直接报错比等到最终结果错误再排查要快得多。第二个是善用条件断点。VSCode的断点可以设置表达式比如循环里只想在right - left 5时停下来直接在断点上右键添加条件。这样不用手动在代码里写一堆if加print。第三个是打印关键变量。如果题目涉及双指针或滑动窗口我会在每次循环结束前打印左指针、右指针、当前窗口内的状态。看起来原始但能快速定位指针移动逻辑有没有错。2.3 算法题常用的数据结构和内置模块Python刷题最常碰到的内置模块我列一下你不用全部背但至少要知道它们的存在。collections提供deque双端队列、defaultdict带默认值的字典、Counter计数器。滑动窗口最大值这类题标准做法就是维护一个单调双端队列。heapq堆操作模块提供heappush、heappop、heapify。TopK问题、合并K个有序链表都会用到。itertools提供排列、组合、笛卡尔积等生成器。做全排列时如果只是想快速验证结果可以直接用itertools.permutations但面试时最好还是能手写回溯。functoolslru_cache装饰器递归剪枝特别好用。很多深度优先搜索加上它直接从指数级复杂度变成线性复杂度。这些模块不是银弹但能帮你把精力集中在算法本身上。特别是deque很多人习惯直接用list模拟队列头部删除是O(n)而deque的左右两端操作都是O(1)在滑动窗口题里差距非常明显。3. 数组、字符串、双指针基础题的四个经典解法3.1 两数之和哈希表空间换时间两数之和几乎是所有人算法题生涯的第一道题。题目很简单给定一个数组和一个目标值返回两个下标使这两个数之和等于目标值。暴力解法是两层循环时间复杂度O(n^2)。我在第一次刷这道题时也写的暴力解法后来才意识到哈希表可以把查找补数的时间从O(n)降到O(1)。def two_sum(nums, target): seen {} for i, v in enumerate(nums): if target - v in seen: return [seen[target - v], i] seen[v] i return []这里最关键的一点是“先查再存”不是“先存再查”。如果先存后查当两个数相等时第二次遇到的元素会先被放进字典再查补数时就会查到它自己导致结果错误。比如数组[3, 3]目标值6先存后查会得到[0, 0]这明显不对。先查再存则保证字典里的数据永远来自当前元素之前的位置。复杂度是O(n)时间和O(n)空间。这个“空间换时间”的思路后面会在很多题目里反复出现。3.2 最长无重复子串滑动窗口的入门题这道题有个很经典的描述给定一个字符串找出其中不含有重复字符的最长子串的长度。比如abcabcbb答案是abc长度3。我第一反应是用两层循环枚举所有子串再用set判断是否重复复杂度O(n^2)在字符串很长的时候会超时。正确解法是滑动窗口维护一个window集合右指针不停往右走遇到重复字符就把左指针右移直到窗口内没有重复字符为止。def length_of_longest_substring(s): window set() left 0 ans 0 for right, ch in enumerate(s): while ch in window: window.remove(s[left]) left 1 window.add(ch) ans max(ans, right - left 1) return ans需要注意的点是窗口每次加入新字符之前要先通过while把重复的字符清出去。这个while看起来不起眼但它保证了窗口的“合法性”。整个过程每个字符最多被加入和移出各一次因此复杂度是O(n)。这道题也是后面很多“最长子串”“最小覆盖子串”类题目的原型。我会在第6节给你一个更通用的滑动窗口模板刷多了你就知道模板背熟真的很省事。3.3 合并区间排序后一次遍历合并区间属于数组题里的常青树。给定一堆区间有重叠的就要合并返回合并后的新区间。比如[[1,3],[2,6],[8,10],[15,18]]合并后是[[1,6],[8,10],[15,18]]。我的处理思路是先按区间起点排序然后遍历每个区间。如果当前区间的起点比结果里最后一个区间的终点大说明没有重叠直接加入结果否则就更新最后一个区间的终点为两者终点较大值。def merge(intervals): if not intervals: return [] intervals.sort(keylambda x: x[0]) res [] for a, b in intervals: if not res or a res[-1][1]: res.append([a, b]) else: res[-1][1] max(res[-1][1], b) return res排序的时间复杂度是O(nlogn)一次遍历合并是O(n)。这道题的关键在于把“区间合并”这个看似复杂的问题转换成“比较相邻区间”的简单逻辑。很多数组题都是这样先排序再遍历整个思路就顺了。3.4 反转字符串双指针的起点反转字符串看起来太简单了Python里一行return s[::-1]就能解决。但算法题的意义不在“实现结果”而在“理解过程”。用双指针从两端往中间走每次交换左右指针的字符才是这道题真正想考的能力。def reverse_string(s): left, right 0, len(s) - 1 while left right: s[left], s[right] s[right], s[left] left 1 right - 1 return s注意这里的s如果是Python的字符串是不可变对象无法原地修改。所以实际操作时通常先把字符串转成list交换完再转回字符串。这种“可变性”的坑在Python刷题里特别容易踩尤其是从C或Java转过来的人。4. 链表、栈、堆数据结构题的常见套路4.1 反转链表三指针还是递归反转链表是链表题里最基础也最重要的一道。题目要求把单链表反转比如1-2-3-NULL变成3-2-1-NULL。迭代解法维护三个指针prev表示前一个节点head表示当前节点nxt保存当前节点的下一个节点。每次循环把当前节点的next指向prev然后三个指针整体后移。def reverse_list(head): prev None while head: nxt head.next head.next prev prev head head nxt return prev这里最容易出错的是在修改head.next之前没有先保存nxt。链表题就是指针的游戏你丢了下一个节点的引用整个链表就断了。很多刚刷题的人在这里卡很久其实就是没养成“先保存再修改”的习惯。递归版反转链表更短但理解成本更高。我第一次看递归版时花了很久才明白每一步返回的其实是新的头节点。建议先吃透迭代版递归版可以等有了一定题量再看。4.2 有效括号栈的经典应用有效括号这道题常见的版本是判断只包含()[]{}的字符串是否有效。有效条件有三个左括号必须用相同类型右括号匹配左括号必须以正确顺序关闭每个右括号都有对应的左括号。我第一反应是数括号数量后来发现([)]这种字符串左右括号数量对得上但顺序不对所以必须用栈。遇到左括号就压栈遇到右括号就弹出栈顶检查是否匹配。def is_valid(s): stack [] pairs {): (, ]: [, }: {} for ch in s: if ch in pairs: if not stack or stack.pop() ! pairs[ch]: return False else: stack.append(ch) return not stack这个解法里用pairs字典统一处理右括号对应的左括号代码就很干净。如果一开始就把三种括号分开写if代码会很啰嗦也更容易漏掉边界。栈在这里的作用是保存“最近的期待”非常契合后进先出的特性。4.3 用堆解决TopK问题TopK问题有很多变体比如找第K大的数、前K个高频元素。最直接的想法是排序完取前K个复杂度O(nlogn)。但如果K远小于n用堆更好维护一个大小为K的小顶堆时间复杂度是O(nlogK)。import heapq def find_kth_largest(nums, k): heap [] for x in nums: if len(heap) k: heapq.heappush(heap, x) elif x heap[0]: heapq.heapreplace(heap, x) return heap[0]这里的细节是维护“小顶堆”而不是“大顶堆”。当堆的大小等于K时堆顶是堆中最小的元素也就是当前前K大元素里的“门槛”。新元素只有比门槛大才替换堆顶。遍历完成后堆顶就是第K大的元素。如果题目要求前K个高频元素思路也类似只不过先要用Counter统计频率再把频率当作比较对象。堆操作的复杂度比全排序低但代码可读性稍差面试时最好把堆的规模和作用讲清楚。5. 排序、二分、回溯、动态规划算法思维类题目解析5.1 手写快排分治思想的最小体现很多面试题会让你手写排序算法快排是最常考的。快排的核心是选定一个pivot把数组分成小于pivot、等于pivot、大于pivot三部分然后递归排序左右两部分。Python最简单易懂的快排写法def quick_sort(nums): if len(nums) 1: return nums pivot nums[len(nums) // 2] left [x for x in nums if x pivot] mid [x for x in nums if x pivot] right [x for x in nums if x pivot] return quick_sort(left) mid quick_sort(right)这种写法很简洁但每次递归都创建新列表空间复杂度高。实际面试时最好能写出原地快排版本虽然代码长一些但能展示你对分区的理解。原地快排的关键是partition函数。我常用的方法是双指针一个从左边找比pivot大的一个从右边找比pivot小的找到后交换。需要注意pivot的选择取中间位置比取第一个元素更不容易退化成O(n^2)尤其对于已经有序的输入。5.2 二分查找边界条件的三个坑二分查找看起来简单但每次写都会有人出bug。最经典的模板是左闭右闭区间def binary_search(nums, target): left, right 0, len(nums) - 1 while left right: mid (left right) // 2 if nums[mid] target: return mid elif nums[mid] target: left mid 1 else: right mid - 1 return -1初学者最容易犯三个错误。第一个是循环条件写成left right这样会漏掉区间里只剩一个元素的情况。第二个是更新边界时写成left mid或right mid如果mid本身不等于目标值会导致死循环。第三个是用/而不是//在Python里mid会变成浮点数下标直接报错。二分查找的变体很多比如查找第一个等于target的位置、最后一个小于target的位置。我建议把标准模板背熟再推导演变版本不要每次现想。5.3 全排列与回溯模板回溯算法在Python面试里出现频率很高常见题目包括全排列、子集、组合总和、括号生成。回溯的本质是在一棵隐式的决策树上做深度优先搜索走到头就回退换另一条路。以全排列为例def permute(nums): res [] n len(nums) used [False] * n def dfs(path): if len(path) n: res.append(path[:]) return for i, x in enumerate(nums): if not used[i]: used[i] True path.append(x) dfs(path) path.pop() used[i] False dfs([]) return res这里有两个特别重要的细节。第一追加结果时必须写path[:]而不是path因为path是同一个列表对象后续会继续修改直接追加会把引用存进去最后所有结果都变成同一个列表。第二递归之后要撤销两个状态把元素从path弹出把used标记改回False。忘记撤销是回溯题最常见的bug。回溯题的复杂度往往是指数级或阶乘级比如全排列是O(n!)。面试时如果能说出剪枝思路比如元素重复时先排序然后跳过相同值会加分不少。5.4 动态规划爬楼梯到零钱兑换动态规划的核心是状态转移方程。以最简单的爬楼梯为例每次可以爬1级或2级问到第n级有多少种方法。因为到达第n级只能从第n-1级迈一步或者从第n-2级迈两步所以状态转移方程是dp[n] dp[n-1] dp[n-2]。这个形式和斐波那契数列一样可以只用两个变量滚动计算省掉整个数组。def climb_stairs(n): if n 2: return n dp1, dp2 1, 2 for _ in range(3, n 1): dp1, dp2 dp2, dp1 dp2 return dp2再来看零钱兑换给定不同面额的硬币和一个总金额求凑成总金额所需的最少硬币个数。这道题的状态转移方程是dp[i] min(dp[i], dp[i-c] 1)其中c是硬币面额。def coin_change(coins, amount): dp [amount 1] * (amount 1) dp[0] 0 for i in range(1, amount 1): for c in coins: if i c: dp[i] min(dp[i], dp[i - c] 1) return dp[amount] if dp[amount] amount else -1动态规划题的难点不在写代码而在推导状态定义和转移方程。我自己的经验是如果题目是“求最值”“求方案数”“判断可行性”大概率是动态规划优先往状态转移上想。初学者可以先从爬楼梯、打家劫舍、零钱兑换这三道题入手建立感觉后再刷更难的最长递增子序列和编辑距离。6. 高频模板整理三类代码可以直接抄6.1 滑动窗口通用框架滑动窗口不是某一道题而是一类题的模板。无论是求最长无重复子串还是最小覆盖子串都可以套这个框架left 0 for right in range(len(s)): # 把 s[right] 加入窗口更新窗口状态 while 窗口不合法: # 从窗口中移除 s[left] left 1 # 更新答案窗口此时合法右指针负责扩展窗口左指针负责收缩窗口。窗口是否合法根据题目定义来判断。比如无重复字符时用set判断是否包含右指针指向的字符最小覆盖子串时用Counter判断窗口内是否覆盖了目标字符的所有数量。我刷题时特别喜欢这个模板因为它把思路和代码结构解耦了。你只需要搞清楚“窗口不合法”这个条件剩下的就是套框架。6.2 二分查找通用框架二分查找的变体太多我最终整理出这样一个可复用的模板用来查找左边界def binary_search_left(nums, target): left, right 0, len(nums) while left right: mid (left right) // 2 if nums[mid] target: left mid 1 else: right mid return left这种左闭右开的写法返回的是第一个不小于target的下标。如果想找右边界调整比较符号和收敛方向即可。关键是要记住left mid 1和right mid是一对不要一会儿right mid - 1一会儿left mid很容易混。我不建议死记所有变体而是记住一个范式先写一个能跑通的基础版然后通过几个测试用例推演边界。写完后用[1, 2, 2, 2, 3]这种重复元素数组自测能快速暴露边界问题。6.3 DFS/BFS的遍历模板树的DFS和BFS是递归与队列的典型应用。二叉树前序遍历的递归写法很简单但非递归版本能看出你对栈和迭代的理解。def preorder_traversal(root): if not root: return [] stack [root] res [] while stack: node stack.pop() res.append(node.val) if node.right: stack.append(node.right) if node.left: stack.append(node.left) return resBFS层序遍历则用队列每次处理当前层所有节点def level_order(root): if not root: return [] from collections import deque q deque([root]) res [] while q: level [] for _ in range(len(q)): node q.popleft() level.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) res.append(level) return res树的题目只要把这两种遍历框架吃透后面遇到最大深度、验证二叉搜索树、路径总和都能往框架里套。BFS还有一个好处是天然适合求最短路径比如二叉树的最小深度BFS第一次遇到叶子节点时深度就是最小深度。6.4 复杂度估算与自测建议拿到一道题先别急着写代码问自己三个问题。最朴素的做法是什么复杂度是多少能不能用哈希表把某个O(n)操作降到O(1)能不能用双指针把两层循环降为一层循环我刷完一道题后会在题解旁边顺手写一行复杂度比如“O(n)时间O(n)空间瓶颈在哈希表”。这个习惯看起来很小但对提升算法敏感度很有帮助。面试考算法最终一定会落到复杂度和优化方案上。自测时也不要只测题目给的示例。我常用的测试集合包括空输入、只有一个元素、所有元素相等、目标值小于最小值、目标值大于最大值、数组长度大但元素范围小。这些边界情况能帮你揪出大部分隐藏bug。7. 刷题踩坑记录与排查技巧实录7.1 列表引用res.append(path)为什么全是同一个值回溯题里这个坑我踩了好几次。你明明看到path每一步都不同但最后结果列表里的元素全长得一样问题往往出在res.append(path)没有用切片复制。Python列表是可变对象path这个变量指向的是同一个列表对象。递归过程中每次path.append和path.pop都在原地修改它所以当你最后打印res时看到的是所有引用共同指向的最终状态。解决办法就是我在5.3代码里写的res.append(path[:])生成一个新的列表对象。这个坑不仅出现在回溯里凡是把列表变量存到另一个列表或字典时都要警惕。7.2 递归深度超限与运行超时Python默认递归深度是1000层遇到树退化成链表或者DFS路径很长时会直接抛RecursionError。很多人第一反应是把递归改成循环但有些题目递归写起来更直观。如果只是临时应付深度大的测试数据可以在代码开头加import sys sys.setrecursionlimit(10000)但这不是长久之计递归深度设置过大会有栈溢出的风险。我一般建议树和回溯题能用递归就用递归但如果能改成迭代尽量在面试时展示两版。另外如果DFS里重复计算很多记得用functools.lru_cache做记忆化否则很容易超时。7.3 除法运算符和整数边界Python3里/是浮点除法//是整除。二分查找里写mid (left right) // 2没有问题但如果你写成了mid (left right) / 2mid就变成float后面nums[mid]直接报TypeError。还有一道常见题求两个数的平均值有人写(left right) // 2在left和right都是很大的正整数时没问题但如果是负数且leftright为奇数整除是向下取整可能导致向左偏。处理这种问题统一用left (right - left) // 2更稳妥能避免整数溢出也能处理一些边界情况。7.4 环境相关的三个高频报错刷题时环境报错也很影响心情我汇总了三个最常见的方便你排查。第一个是ModuleNotFoundError: No module named numpy原因是没有安装包或者没有选择正确的解释器。先在终端用pip list看包是否安装再检查VSCode当前选择的解释器是不是你装包的那个环境。第二个是python was not found; run without arguments to install from the Microsoft Store这个我在2.1节已经说过本质是PATH没配置好。第三个是代码能跑但中文输出乱码。Windows终端上有时会出现编码问题建议在脚本开头加一行# -*- coding: utf-8 -*-或者把终端编码改成UTF-8。虽然算法题一般不涉及中文输出但注释里如果有中文偶尔也会触发编码问题。7.5 刷完50题之后做什么这套50题只是个起点。你会发现很多所谓“新题”其实是这些基础题型的组合变式。比如一道看起来复杂的“最小区间”拆开看就是滑动窗口加堆。一道“编辑距离”本质是二维动态规划模板。我个人体会是刷题最重要的不是数量而是复盘。每道题做完后我都会在题解开头写一句话总结这题考的是什么数据结构用了什么算法思想最容易错的地方是哪。等到50题刷完回头翻这些总结你会发现自己已经能形成一套解题直觉。最后再分享一个小技巧遇到不会的题不要立刻看答案先把它和之前做过的题做类比。你可能会发现“这不就是两数之和换了个壳”或者“这不就是二叉树遍历加一个计数器”。算法题大多如此模板记住了剩下的就是识别题目的模式。这套50题就是帮你建立模式识别能力的最短路径。