力扣刷题实战:从零搭建Python环境到双指针算法精解

发布时间:2026/8/25 12:35:47
力扣刷题实战:从零搭建Python环境到双指针算法精解 这类刷题工具最值得先看的不是它有多少题解而是能不能帮你把“看懂了”变成“能写对”。很多新手卡在“思路有代码写不出来”或者“本地能跑提交就错”的阶段根本原因不是算法不懂而是对题目边界、输入输出格式、以及不同语言环境下的细节处理不熟。“小登带你刷力扣”这个主题核心就是解决从“知道”到“做到”的最后一公里问题。它适合两类人一是刚开始刷题对着题目描述和示例输入输出不知道怎么下手的纯新手二是刷了一些题但提交通过率总是不稳定想系统提升代码一次写对能力的中阶选手。最关键的价值在于它应该提供一种“手把手”的拆解逻辑把一道题从理解题意到最终AC的完整思考链路和编码细节都呈现出来而不仅仅是给一个最终答案。下面我会以最常见的“双指针”类问题为例结合Python环境拆解一个完整的刷题实战流程。你会看到如何从零开始搭建一个可靠的刷题环境如何一步步分析题目、写出代码、并通过测试以及在这个过程中最容易踩坑的几个地方。1. 刷题第一步别急着写代码先搞定可复现的本地环境很多人刷题效率低是因为环境太乱。一会儿用在线编辑器一会儿用本地IDE依赖库版本不一致或者连基本的输入输出函数都记不住。我建议无论你用什么工具第一步必须是建立一个稳定、可复现的本地调试环境。1.1 Python环境安装与核心工具选择如果你还没安装Python直接去官网下载安装包。这里有个关键点不要安装最新版本。力扣LeetCode的判题环境通常比较稳定Python版本一般在3.8到3.10之间。为了最大兼容性我建议安装Python 3.9.x。安装时务必勾选“Add Python to PATH”这样才能在命令行直接调用。安装完成后打开命令行Windows是CMD或PowerShellmacOS/Linux是Terminal输入python --version确认版本。接下来是编辑器的选择。VSCode是主流配置也简单。安装VSCode。在VSCode扩展商店搜索并安装“Python”扩展由Microsoft发布。打开一个空文件夹作为你的刷题项目目录。在这个目录下新建一个Python文件比如test.py写一句print(“Hello LeetCode”)按F5运行。如果能看到输出基础环境就通了。注意很多“运行报错”问题根源是VSCode没有正确选择Python解释器。你可以点击VSCode左下角的状态栏那里会显示当前使用的Python版本点击它可以切换。确保它指向你刚安装的Python 3.9。1.2 力扣本地调试脚手架搭建在力扣上刷题你只需要写核心函数。但在本地你需要自己模拟输入输出。建立一个“调试脚手架”能极大提升效率。在你的刷题项目目录下创建一个utils.py文件里面可以放一些常用函数# utils.py from typing import List, Optional import json def build_list(arr: List[int]) - Optional[ListNode]: 根据数组构建链表用于链表类题目调试 if not arr: return None head ListNode(arr[0]) cur head for val in arr[1:]: cur.next ListNode(val) cur cur.next return head def print_list(head: Optional[ListNode]) - None: 打印链表 res [] while head: res.append(head.val) head head.next print(-.join(map(str, res))) # 二叉树节点定义很多题目会用到 class TreeNode: def __init__(self, val0, leftNone, rightNone): self.val val self.left left self.right right def build_tree(arr: List[Optional[int]]) - Optional[TreeNode]: 根据层序遍历数组构建二叉树力扣常用格式 if not arr or arr[0] is None: return None root TreeNode(arr[0]) from collections import deque queue deque([root]) i 1 while queue and i len(arr): node queue.popleft() if arr[i] is not None: node.left TreeNode(arr[i]) queue.append(node.left) i 1 if i len(arr) and arr[i] is not None: node.right TreeNode(arr[i]) queue.append(node.right) i 1 return root然后针对每一道具体题目你的主文件结构应该是这样的# solution_xxx.py from utils import build_list, print_list, TreeNode, build_tree # 导入工具 # 力扣题目给出的函数签名 class Solution: def myFunction(self, input_param): # 你的解题代码 pass if __name__ __main__: sol Solution() # 1. 准备测试用例完全模仿力扣示例 test_input ... # 2. 调用函数 result sol.myFunction(test_input) # 3. 打印结果与预期对比 print(f输入: {test_input}) print(f输出: {result}) print(f预期: ...) # 手动写上预期结果 # 4. 可以快速测试多个用例 test_cases [ (输入1, 预期1), (输入2, 预期2), ] for i, (inp, expected) in enumerate(test_cases): actual sol.myFunction(inp) if actual expected: print(f用例{i1}通过) else: print(f用例{i1}失败: 输出{actual}, 预期{expected})这个流程的好处是你可以无限次、无成本地在本地运行和调试直到逻辑完全正确再复制核心函数到力扣提交。这比在力扣网页上反复“提交-报错-修改”要高效得多。2. 以“双指针”为例拆解一道题的完整攻克流程我们选一道经典的、热搜上也出现的题目作为例子“力扣热题100”中的“移动零”LeetCode 283. Move Zeroes。这道题是双指针思想的绝佳入门题。2.1 第一步彻底理解题意与约束而不是只看示例题目要求给定一个数组nums编写一个函数将所有0移动到数组的末尾同时保持非零元素的相对顺序。请注意必须在原数组上操作不能拷贝额外的数组。尽量减少操作次数。很多人只看示例 输入:[0,1,0,3,12]输出:[1,3,12,0,0]然后就开始写代码。这很容易漏掉关键约束。你需要自己推导出更多信息操作对象是数组Python中是list并且是可变对象。原地修改这意味着你不能return [非零部分] [零部分]这样返回一个新列表。你必须修改传入的nums。保持顺序所有非零元素原来的前后关系不能变。最小操作暗示我们可能不需要每次遇到零都执行复杂的数组插入删除那会导致大量元素移动。把这些想清楚你才能选择正确的算法。双指针就是为这种“原地重新排列”而生的。2.2 第二步构思算法与手动模拟双指针顾名思义用两个“指针”在Python里就是下标索引来遍历数组。对于这道题常见的思路是指针i用于遍历整个数组。指针j指向下一个非零元素应该放置的位置。手动模拟一遍 初始状态nums [0, 1, 0, 3, 12],j 0i0,nums[0]0是零跳过。j不动。j0i1,nums[1]1非零。将nums[1]的值赋给nums[j]即nums[0]。然后j向后移动一位。操作后nums [1, 1, 0, 3, 12],j1注意此时原nums[0]的0被覆盖了但我们在后续会把零补上。i2,nums[2]0是零跳过。j不动。j1i3,nums[3]3非零。将nums[3]赋给nums[j]即nums[1]。j后移。操作后nums [1, 3, 0, 3, 12],j2i4,nums[4]12非零。将nums[4]赋给nums[j]即nums[2]。j后移。操作后nums [1, 3, 12, 3, 12],j3遍历结束。此时下标0到j-1即0,1,2的位置已经按顺序存放了所有非零元素[1,3,12]。最后一步将下标从j到数组末尾的所有元素置为0。即nums[3]0,nums[4]0。得到最终结果[1,3,12,0,0]。这个过程的关键是指针j始终指向“下一个非零元素该放的位置”而i负责寻找非零元素。所有非零元素都被顺序地“搬运”到了数组的前部。2.3 第三步将思路转化为代码并注意边界根据上面的模拟我们可以直接写出代码class Solution: def moveZeroes(self, nums: List[int]) - None: Do not return anything, modify nums in-place instead. j 0 # 指向下一个非零元素该放的位置 # 第一遍遍历把非零元素往前挪 for i in range(len(nums)): if nums[i] ! 0: nums[j] nums[i] j 1 # 第二遍遍历将剩余位置补零 for i in range(j, len(nums)): nums[i] 0写完后不要马上运行。先做边界检查空数组[]len(nums)0第一个循环不执行j0第二个循环range(0,0)也不执行。没问题。没有零的数组[1,2,3]第一个循环后j3第二个循环range(3,3)不执行。数组保持不变正确。全是零的数组[0,0,0]第一个循环不执行任何赋值j0第二个循环将nums[0], nums[1], nums[2]都置零。数组不变正确。2.4 第四步本地测试与调试现在在本地用我们搭建的脚手架进行测试。创建一个leetcode_283.py文件from typing import List class Solution: def moveZeroes(self, nums: List[int]) - None: j 0 for i in range(len(nums)): if nums[i] ! 0: nums[j] nums[i] j 1 for i in range(j, len(nums)): nums[i] 0 if __name__ __main__: sol Solution() # 测试用例集 test_cases [ ([0,1,0,3,12], [1,3,12,0,0]), ([], []), ([1,2,3], [1,2,3]), ([0,0,0], [0,0,0]), ([1,0,1], [1,1,0]), # 自己加的边界用例 ] for i, (input_nums, expected) in enumerate(test_cases): # 重要因为函数修改原数组我们需要用副本测试 nums_copy input_nums.copy() sol.moveZeroes(nums_copy) if nums_copy expected: print(f测试用例 {i1} 通过: {input_nums} - {nums_copy}) else: print(f测试用例 {i1} 失败!) print(f 输入: {input_nums}) print(f 输出: {nums_copy}) print(f 预期: {expected})运行这个文件如果所有测试用例都通过恭喜你这道题的核心逻辑你已经掌握了。你可以把Solution类里的moveZeroes函数复制到力扣的代码编辑器里提交大概率会直接通过。3. 从“通过”到“精通”代码优化与思维扩展一道题AC了并不意味着结束。这才是深度学习的开始。针对“移动零”我们可以从几个角度进行优化和思考。3.1 代码优化减少一次循环上面的代码进行了两次遍历。我们能否在一次遍历内完成可以这就是更经典的“快慢指针”交换法。思路指针j依然指向下一个非零元素该放的位置。指针i遍历时如果遇到非零元素就将其与nums[j]交换然后j后移。这样遍历结束后j之前的都是非零元素之后的都是零因为零被交换到后面去了。class Solution: def moveZeroes(self, nums: List[int]) - None: j 0 for i in range(len(nums)): if nums[i] ! 0: nums[i], nums[j] nums[j], nums[i] # 交换 j 1这个版本只需要一次遍历并且操作次数更少只有在遇到非零时才交换。但是请注意当i和j相等时即开头部分是非零元素交换是自身和自身交换没有实际作用但也没有错误。这是一个可以接受的微小开销。3.2 思维扩展同类双指针问题举一反三掌握了“移动零”的模式你可以去解决一系列类似问题这才是刷题的意义——掌握模式而非单题。力扣 27. 移除元素几乎一模一样只是把判断条件nums[i] ! 0改成nums[i] ! val。力扣 26. 删除有序数组中的重复项指针j指向下一个不重复元素该放的位置。判断条件变为nums[i] ! nums[j]因为数组有序。力扣 80. 删除有序数组中的重复项 II允许最多重复两次判断条件需要增加一个计数的逻辑。力扣 75. 颜色分类荷兰国旗问题这是双指针的进阶需要三个指针或两个指针加一个遍历指针来对三种颜色进行原地排序。你可以用我们搭建的本地环境把这几道题都练习一遍。你会发现核心框架都是初始化一个或多个指针。遍历数组。根据条件决定是否移动指针、交换元素或赋值。遍历结束后数组的前半部分就是满足条件的元素。3.3 复杂度分析与为什么这么做在面试或自我总结时你需要能说清楚为什么这么做。时间复杂度O(n)。无论是两次遍历还是一次遍历交换我们都只完整地扫描了数组一或两遍与数组长度 n 成线性关系。空间复杂度O(1)。我们只使用了常数个额外变量i,j没有使用与 n 相关的额外数组空间符合“原地修改”的要求。这解释了题目要求“尽量减少操作次数”和“原地操作”是如何被满足的。如果你用了类似non_zeros [x for x in nums if x ! 0]然后再拼接零列表的方法空间复杂度就是 O(n)不符合要求。4. 建立你的刷题系统超越单题的方法论刷几百道题如果不成体系效果会大打折扣。根据“小登带你刷力扣”这个主题我建议你建立自己的刷题管理系统。4.1 题目分类与进度跟踪不要随机刷题。按照算法专题来刷比如数组与字符串链表双指针栈与队列哈希表二叉树回溯算法动态规划 ...你可以创建一个Excel表格或Notion页面记录每道题的题号与链接题目名称难度核心算法标签如双指针、哈希表首次通过日期代码存放路径指向你本地的solution_xxx.py文件复习状态如一次通过/需复习/不熟个人总结与易错点定期比如每周回顾“需复习”的题目重新写一遍代码。这才是对抗遗忘的关键。4.2 本地代码库管理在你的刷题项目目录下建立清晰的文件夹结构/LeetCode ├── /utils (存放公共工具类如链表、二叉树构建函数) ├── /by_topic (按专题分类) │ ├── /two_pointers │ │ ├── 283_move_zeroes.py │ │ ├── 26_remove_duplicates.py │ │ └── ... │ ├── /linked_list │ └── ... ├── /by_number (按题号分类方便查找) │ ├── 1_two_sum.py │ ├── 283_move_zeroes.py │ └── ... └── README.md (记录个人刷题计划与心得)每个解题文件都像我们之前写的那样包含完整的Solution类、本地测试用例和if __name__ “__main__“部分。这样任何时候你都可以单独运行这个文件来验证或复习。4.3 高效刷题工作流读题与思考在力扣上阅读题目先自己思考5-10分钟尝试在纸上或脑子里勾勒算法。不要直接看题解。本地编码打开本地对应的文件编写解题函数。务必先写测试用例包括常规用例和边界用例再写函数体。本地调试运行文件确保所有测试用例通过。如果失败利用本地IDE的调试功能如VSCode的断点、逐行执行排查问题。这是在线编辑器无法比拟的优势。复杂度分析代码通过后分析时间、空间复杂度并思考是否有优化空间。提交与对比将核心函数复制到力扣提交。通过后一定要去看官方题解和评论区的高赞解答。对比自己的解法学习更优的代码写法、更巧妙的思路。把收获记在代码文件的注释里。归类与复习将题目归入你的分类系统并标记复习状态。4.4 应对“刷了忘”和“遇到新题没思路”这是最正常的现象。应对方法是针对“刷了忘”依靠你的进度跟踪系统定期复习标记为“不熟”的题目。重写代码时先别看之前的答案逼自己回忆。针对“没思路”第一步暴力解法。先别管时间空间复杂度想一个最直接、能解决问题的办法。这能帮你理清题意。第二步寻找冗余。分析暴力解法中哪些计算是重复的哪些信息可以被复用。第三步匹配模式。思考这个问题可能属于哪个算法专题排序、查找、双指针、滑动窗口、动态规划等。尝试把你分析出的“冗余”和“复用”点套入已知的算法模式。第四步画图模拟。在纸上画出小规模样例手动模拟你的算法思路。这是将抽象思路具体化的关键一步很多bug在模拟阶段就能发现。如果超过20-30分钟仍无头绪果断看题解。但看题解不是背代码而是理解“为什么用这种方法”以及“是怎么想到的”。把这种“联想”过程记下来丰富你自己的解题模式库。刷题就像健身是个长期积累的过程。“小登带你刷力扣”的核心不是给你鱼而是教你如何造渔具、如何选钓点、如何分析鱼情。建立起稳定的本地环境、科学的刷题流程和个人的知识管理系统你才能从被动刷题变为主动学习真正把算法和数据结构内化成解决问题的能力。