资讯动态

Python初学总结:从语法到拆解问题的算法与数据结构实践

发布时间:2026/10/13 12:03:09 来源:尧图企业网站定制
1. 阶段复盘第五次总结我终于开始“写明白”了照例先交代背景。这个系列走到第五篇我的学习主线其实变过好几次第一篇在消化变量、分支、循环这些最基础的东西第二篇开始碰函数与模块化第三篇和第四篇主要围着文件读写、异常处理、简单命令行交互打转。到了这篇“初学总结5”我给自己设定的目标不是再背多少新语法而是把前面学过的东西串起来真正能独立拆解一个小需求并且把它落成一段不太难看的代码。说白了前面的阶段是“看得懂”这个阶段我想追求“写得顺”。如果你现在也处于类似的状态——语言基础基本过完一遍但一到自己写东西就不知道从哪下手看到稍微复杂一点的问题脑子就乱——那这篇总结应该能给你一些参考。我会把我这阶段学到的核心知识点、做过的两个小项目、踩过的几个坑以及整理出来的排查思路完整过一遍。没有花哨的内容全是自己手动敲过、改过、又改回来过的真实记录。2. 这一阶段的核心突破从“会语法”到“会拆解”2.1 复杂度意识下手写代码之前先算算会不会跑不动以前写代码真的就是“能跑就行”完全不考虑数据量大了会怎样。这阶段第一个让我开窍的概念是时间复杂度特别是大O表示法。我举个例子你就明白我在说什么了。假设你要在一个列表里找某个元素最简单的是从头到尾遍历这在Python里就是一句def find_index(items, target): for i, item in enumerate(items): if item target: return i return -1这个写法没问题但如果列表里有十万个元素而目标恰好在最后你就得老老实实比较十万次。写成大O表示法就是O(n)n代表数据规模。同样的问题如果列表是有序的用二分查找可以把复杂度降到O(log n)十万个元素的情况下大概只需要比较十七次左右。可能你会说现在电脑性能这么强多点几次循环怕什么。但等你真正面对真实场景比如处理一份几万行的数据文件或者在一个查询结果集里反复筛选的时候O(n^2)和O(n)的差距会从“几秒”变成“几分钟到几小时”。这阶段我把常见复杂度列了一个对照表方便自己平时估算复杂度名称数据量从100翻到10000时的大致表现常见场景O(1)常数时间时间几乎不变直接按下标访问列表、字典按键取值O(log n)对数时间增长极慢二分查找、平衡树操作O(n)线性时间时间同比例增长单次遍历、线性查找O(n log n)线性对数增长稍快快速排序、归并排序O(n^2)平方时间时间爆炸式增长双层嵌套循环、朴素排序记住一套核心判断方法看代码里循环的嵌套层数一层循环通常是O(n)两层就是O(n^2)循环里每次又在循环内部做了列表切片之类的操作就要特别注意实际开销可能比表面复杂度更高。这段学习让我养成了一个习惯任何数据处理逻辑动手之前先问自己一句“最坏情况下它要执行多少次”。2.2 栈、队列、链表这三个结构在解决同类问题以前总觉得“数据结构”这四个字很吓人实际学完才发现栈、队列、链表做的事用生活场景类比一下就非常好懂。栈就像一摞盘子。后放上去的盘子总是最先拿走专业说法叫“后进先出”LIFO。典型应用就是我每次逛网页时的浏览器后退按钮每访问一个新页面就压入栈顶点后退就弹出栈顶的页面。Python里用列表就可以模拟append入栈pop出栈。队列就是排队打饭。先到的人先得到服务专业说法叫“先进先出”FIFO。典型应用包括打印机任务排队、消息队列。Python里可以用collections.deque来实现两端的插入删除都是O(1)比直接用列表在头部插入要快得多。链表则是我这阶段觉得最需要转变思维的一个结构。它不像列表那样是一整块连续空间而是一个节点指向下一个节点。好处是中间插入和删除节点不需要大规模移动其他元素只需要改一下相邻节点的指针关系。我第一次在纸上画链表的插入过程时画了快十遍才彻底搞懂“先把新节点.next指向旧节点.next再把旧节点.next改成指向新节点”这两步顺序不能反。这三个结构真正常见的困惑是“我到底什么时候该用哪一个”。我的简单判断标准是如果数据只在一端进出优先考虑栈如果两端都需要进出且有序处理考虑队列如果数据需要频繁在中间位置增删而且量又大链表值得考虑。这些判断标准在真实项目里还会结合内存、缓存等因素变复杂但作为初级阶段的决策参考已经很够用。2.3 递归和二叉树初次见面互相折磨二叉树这阶段只能算刚刚入门。为什么一定要学它因为很多真实问题最后都能抽象成树形结构最常见的例子就是文件夹目录、组织架构、表达式的解析。二叉树遍历有三种顺序前序、中序、后序区别只在于访问根节点的时机。配合递归写起来非常短。class TreeNode: def __init__(self, val0, leftNone, rightNone): self.val val self.left left self.right right def inorder_traversal(root): result [] if root is None: return result result.extend(inorder_traversal(root.left)) result.append(root.val) result.extend(inorder_traversal(root.right)) return result代码看着简单理解递归过程才是关键。我自己的经验是把递归展开成“先处理左子树、再处理自己、最后处理右子树”的步骤在纸上画一棵三层的小树一步一步写下函数调用和返回的顺序慢慢就能体会到“递进去、归回来”的感觉。递归容易犯的问题也很经典忘记终止条件或者终止条件写错。比如求树的深度如果写成下面这样就永远等不到返回def max_depth(root): # 漏掉了 if root is None: return 0 return 1 max(max_depth(root.left), max_depth(root.right))这个问题我在本地跑了好几次都卡住后来才知道是因为root.left或者root.right为None时又在继续往里递归。这种错误去逻辑上想半天不如直接打印每一步的节点值来得直观。这阶段处理递归问题我给自己规定了几个必做动作明确终止条件、明确每层递归干什么、明确返回值给谁用。三个问题都能回答说明这个递归函数想清楚了。3. 动手环节两个小项目把知识点真正用起来3.1 命令行记账小工具第一次感受到状态管理看书和写代码之间隔着一条叫“需求”的河。这阶段第一个完整项目是给别人做的一个记账命令行工具我称之为模拟项目X。需求并不复杂支持添加收支记录、查看当前余额、列出所有记录、退出。就是这样一个看起来很“玩具”的东西真写起来比我想象中更考验细节。先说数据结构上的选择。记账记录天然可以用栈来组织最新的记在最上面查看历史时最先看到最近的一笔。我用列表模拟了栈的行为每条记录用一个字典保存类型、金额、备注、时间四个字段。添加记录用append删除最近一条用pop。records [] def add_record(record_type, amount, note): records.append({ type: record_type, amount: float(amount), note: note, }) print(添加成功) def show_recent(limit5): for r in records[-limit:][::-1]: print(f[{r[type]}] {r[amount]:.2f}元 - {r[note]}) def total_balance(): return sum(r[amount] if r[type] 收入 else -r[amount] for r in records)别看代码简单真正写的时候问题全出在“状态管理”上。比如用户输入负数金额要不要拦截输入格式不对时是提示重新输入还是直接退出“收入”和“支出”用字符串表示还是用布尔值这些决策背后都是实际使用体验的问题。我最后选择了统一把金额按正数接收类型字段单独控制这样计算余额时逻辑清晰可读性也更好。这个项目让我第一次意识到写代码之前把规则想清楚比多写几行代码重要得多。还给这个工具加了一个简单的文本持久化数据保存到本地文件里启动时自动加载。这就把之前学的文件读写、异常处理全串起来了每次运行完数据都能保留终于有了一点“真软件”的感觉。3.2 文件批量重命名脚本在动手前先想好“怎么反悔”第二个项目是一个批量处理文件夹内文件的脚本。起因很朴素我电脑某个文件夹里全是截图文件名带时间戳乱七八糟想统一改成“项目A_序号.扩展名”的格式。这个需求拆解下来主要是三步遍历目标目录、按规则生成新文件名、批量重命名。但真操作的时候我并没有一上来就写完整代码而是先在草稿纸上列了一个问题列表这一步帮我避免了一次大事故文件名冲突怎么办比如两个文件都要改成同一个名字。重命名过程中途出错已经改过的文件要不要恢复扩展名要不要保留是否要跳过那些已经符合目标格式的文件最终我选择了先预览再执行的两阶段方案。第一阶段只打印“旧名字 - 新名字”的映射表不真正改动等人工确认无误后再进入真正的重命名阶段。这个方案让我觉得特别踏实因为批量操作最怕的不是不会写而是写出来的程序在特定文件上翻车然后一堆文件的状态变得不可控。import pathlib folder pathlib.Path(./screenshots) prefix 项目A counter 1 plan [] for path in sorted(folder.iterdir()): if path.suffix.lower() not in {.png, .jpg, .jpeg}: continue new_name f{prefix}_{counter:03d}{path.suffix.lower()} new_path path.with_name(new_name) plan.append((path, new_path)) counter 1 for old, new in plan: print(f{old.name} - {new.name})这段代码先列出所有计划我检查完不会有重名或者误改的问题之后才把最后一行换成old.rename(new)真正执行。这个过程让我养成了一个重要的职业习惯需要批量修改数据的操作一定要提供“预演模式”和“恢复方案”哪怕程序是自己一个人用也不例外。3.3 两个项目做完之后的复盘心得做完这两个项目我有几个很直接的体会第一代码短不等于问题少。几十行的小程序调试时间和代码行数往往不成正比。问题是隐藏在对细节规则的思考里的。第二学会用“最小可行步骤”推进。不要想着一次把所有功能全实现而是先让一个核心功能跑通再逐步加东西。做记账工具时我第一版只支持添加和查看余额后面才补上类型筛选和记录删除。第三把测试变成习惯。这里说的测试不是最后写完再测而是每写完一个小函数就立刻用几条典型数据跑一遍。写add_record函数的时候我当场就打印了几条不同的输入确认异常输入能被正确处理再继续下一部分。4. 踩坑记录与排查思路这些坑我替你走过了4.1 Python可变默认参数看着没问题运行起来全是问题这个坑我是在一次数据处理练习里踩进去的。当时我写了一个函数想把新记录追加到一个历史记录列表里默认参数用了一个空列表结果多次调用后数据越攒越多明明每次都是“新会话”历史记录里却残留着上一次的任务数据。问题代码长这样def add_task(task, history[]): history.append(task) return history第一次调用返回[写总结]第二次调用返回[写总结, 调试代码]看起来像是正常累积但如果你希望每次不传history时都从头开始那就完全错了。原因在于Python默认参数只在函数定义时计算一次之后每次都复用同一个列表对象。修复方式很简单改为默认None函数内部再初始化def add_task(task, historyNone): if history is None: history [] history.append(task) return history这个坑对我的警示作用非常大。它让我养成了一个习惯所有可变类型列表、字典、集合一律不要直接作为默认参数。4.2 金额计算的浮点陷阱0.1加0.2不等于0.3第二个坑是在记账工具里遇到的。当时我计算两笔金额相加得到的结果是0.30000000000000004。这是浮点数在二进制表示下的固有误差不是bug是计算机表示实数的方式决定的。解决方式有三种层次展示层用格式化控制两位小数计算层用decimal.Decimal自己规定所有金额都用整数“分”为单位存储。我做账目时选择了第三种全部按分存整数比如12.34元就存成1234只在展示时除以100。这种方法直观、可靠也不用关心浮点误差问题。from decimal import Decimal amount Decimal(12.34) Decimal(0.66) print(amount) # 13.00这个经历让我在以后写任何与钱、度量、时间相关的程序时都会格外小心凡是涉及连续计算的场景优先考虑整数或者十进制类型。4.3 Git操作分支切换前不提交差点丢了半天工作量这阶段我还遇到过一次工作区文件被覆盖的险情。当时我在一个分支上改了文件还没提交临时要切到另一个分支去查一个旧版本直接执行了git switch结果文件内容瞬间变成另一套。好在后来用git stash恢复了修改虚惊一场。这件事的教训可以浓缩成一句话切分支之前要么提交、要么暂存不要带着未处理的修改乱跑。我现在每次准备切换之前都会先看一下git status确认工作区是干净的再操作。这不是什么高深技巧但真的能省掉很多不必要的恐慌。4.4 排查问题的方法论别靠感觉靠打印和分段确认初学阶段最怕的是出了问题就胡乱改代码改完还是错心情爆炸。这阶段我总结出一套简单的排查流程效果意外地好步骤动作目的1复现问题明确触发条件能不能稳定复现2缩小范围把代码拆成几段注释掉疑似有问题的部分3加打印语句在关键节点输出变量值和执行顺序4对比预期每个输出和应该看到的结果对比找到第一个偏差5修改并回归修好后再跑一遍完整流程确认不会引入新问题这个方法本质上是把一个大问题切成小问题逐个击破。尤其是“找第一个偏差”这个思路让我少走了很多弯路。以前出了错总是从头到尾通读代码试图凭眼睛找bug效率极低现在我会先定位到出错的行号再看这条语句之前的输入到底符不符合预期往往几分钟就能定位问题。5. 下一阶段的目标和一点个人体会这篇总结写到这里该说说接下来的方向了。下一阶段我准备花时间把查找和排序的几种常见算法真正吃透尤其是快速排序、归并排序的思想和实现同时开始看一些基础的数据结构在实际工程中的应用案例比如数据库索引和内存缓存把抽象概念和具体场景搭上桥。另外一个重点是锻炼用伪代码或者流程图表达思路的能力这个习惯在以后面对复杂需求时会非常有用。按我自己的学习节奏接下来的三到四周大概会保持每天一小时左右的练习量重点不再是学新语法而是刷少量有代表性的小题目总结套路。以前总觉得“总结”没用现在回头看每次写总结时都会发现一些以为自己懂了、仔细一讲却说不清楚的东西。这正是最值得补的地方。最后分享两个我亲身实践下来特别有用的经验第一每学一个新知识点立刻用一个最简例子跑通不要只停留在“看懂了”的层面第二保留一份“错误代码笔记”把自己踩过的坑按症状、原因、解决方案记下来以后遇到类似现象可以快速翻查。这份笔记现在已经成为我学习效率最高的材料之一。初学阶段最大的好处是每天都能感受到明显的进步。希望下次写“初学总结6”的时候能带着更多实际跑通的例子继续把这条路走扎实。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑