ARTICLE DETAIL

资讯详情

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

第8天编程自学:从“能跑”到“写得好”的思维进阶

第8天编程自学:从“能跑”到“写得好”的思维进阶 1. 第8天第一次真正意识到“会写代码”和“会编程”的差距学编程第八天到底是一个什么状态如果你正好处在“前一周热情还在、语法刚记住、但一动手就卡壳”的节点上那你大概率会在这天经历一次特殊的心理波动昨天还觉得循环和函数不过如此今天看别人写的代码却突然觉得自己像个文盲。我的第八天就是在这种波动中度过的。白天逛程序员社区时看到一个很常见的练习人家用五行代码就解决了。我自己昨晚写了二十多行还带一个临时变量来倒腾数据。对比之下除了羞愧感更让我难受的是我根本说不清自己的代码是哪里不好——它就是“能跑”但怎么看怎么笨。这其实就是初级程序员开始分化的时候。前七天大家做的事情都差不多装环境、写第一行输出、搞懂变量和分支、把循环背下来。第八天开始真正拉开差距的不是谁更勤奋而是谁开始注意到“写出来”和“写好”之间的距离。这篇文章就记录一下我在这一天里做的事、想清楚的问题以及踩进去就爬了很久的三个坑。适合正在自学编程一周左右、准备进入算法和数据结构初阶的人参考。1.1 第七天的结束与第八天的“平台期”先说心态。第七天结束的时候我自我感觉良好因为能把一个猜数字游戏完整写出来还加了重玩功能。但第八天起床后重新翻那份代码发现问题一堆变量命名随意、逻辑嵌套太深、一个函数的代码块里有三处重复逻辑。为什么偏偏是第八天开始觉得不对劲我个人理解是前七天你所有的注意力都放在“怎么把语法用对”等到第八天语法不再是最主要的阻碍时大脑才有余力去做“审视代码”这件事。这个过程非常像学开车刚拿到驾照的头几天只关注油门和刹车开了一周之后才有心思感受方向盘回正的角度。所以如果你也在第八天前后突然发现自己“不会写代码”了别慌。这是一种可贵的不适感说明你正在从“记住语法”过渡到“思考解法”。这个阶段要做的第一件事不是去背更多API而是回头整理之前写的所有练习代码——把它们重写一遍按现在的理解拆得更干净。哪怕只是改十几个变量名收获也比新看三章教程大。1.2 算法题、社区、接单这些热门词的正确打开方式打开搜索引擎随便看一眼程序员相关的话题很容易让人焦虑算法五十例、接单平台报价、大厂年薪结构……对于一个第八天的初学者来说这些内容可以作为了解行业的信息但一定不要当成当下要完成的任务。我的处理方式是给自己设定一个“三不原则”不比较工资不囤资料不着急接单。程序员社区里高手很多看到别人一天刷五道题你也跟着开始刷题那就是在给第八天的自己挖坑。我见过太多朋友在第一周就下载了算法书和视频课结果它们躺在收藏夹里再也没有打开过。正确的操作是只看和当天学习内容相关的话题——比如字符串处理、简单的数组循环凡是涉及动态规划和二叉树的内容一律跳过。这不是逃避而是保证自己在初期建立“我能学会”的正反馈循环。学习的节奏上我还建议把每天写代码时的报错信息记下来用文档工具单独建一个复盘库。第八天这个时候你可能已经积累了十几个报错它们比任何教程都值钱。我后来能快速排查问题靠的正是这些原始记录。2. 数据处理三板斧数组、循环、输入输出怎么配合进入第八天的实际内容。这一天我没有急着学新语法而是把一块过去一周偷懒跳过的内容补上了怎么用数组和循环配合起来处理一批数据。说它是“三板斧”是因为数据处理在编程初期的本质就是读进来、存起来、算出来。2.1 一个成绩统计题引发的代码审美问题我开始练习的题目是输入五个学生的成绩输出最高分和平均分。如果只用刚学的知识很容易写出这样的代码s1 int(input(请输入第一个成绩: )) s2 int(input(请输入第二个成绩: )) s3 int(input(请输入第三个成绩: )) s4 int(input(请输入第四个成绩: )) s5 int(input(请输入第五个成绩: )) total s1 s2 s3 s4 s5 avg total / 5 max_score max(s1, s2, s3, s4, s5) print(平均分:, avg, 最高分:, max_score)这段代码能跑而且结果也对。但它有个严重问题假如把五个学生改成一百个或者把“成绩”换成“商品销量”你就得复制一百行吗这不是写法丑不丑的问题而是同一个思路根本没有扩展性。初级程序员和中级程序员的第一个分水岭就在这里是否意识到存储结构比计算步骤更早决定代码的复杂度。正确做法是用列表把成绩存起来再用循环统一处理。循环允许你用同一个处理逻辑面对不同数据量列表则允许你把“数据是什么”和“数据有多少”解耦。scores [] for i in range(5): score int(input(请输入成绩: )) scores.append(score) total 0 for score in scores: total score avg total / len(scores) max_score scores[0] # 先假设第一个最大 for score in scores[1:]: if score max_score: max_score score这里我特意亲手写了一版找最大值的过程而不是直接用max函数。原因很简单我需要通过“手动找最大值”来理解比较逻辑这是第八天值得花时间的地方。等你想清楚怎么找最大值再看max函数就会觉得它只是一个替你干活的工具而不是一个让你绕开原理的魔法。2.2 三个新手最常写错的循环模式循环和数组配合时有许多暗坑。第一个坑也是最常见的是数组越界。Python的列表索引从0开始长度为n的列表最后一个元素索引是n-1。很多人一激动就写了 for i in range(len(scores))然后在循环体内访问 scores[i 1]结果在最后一轮直接崩掉。这类错误非常典型遇到报错先别怀疑人生停下来数一数边界索引就好。第二个坑是空列表。如果你真的用程序去处理一批长度不确定的数据——比如从文件里读取一堆数字——那么列表有可能是空的。空列表里取scores[0]会直接报错。所以我在上面代码中给 max_score 赋初值时用了 scores[0]但更稳妥的做法是先判断列表非空再操作或者用默认值兜底。第三个坑是循环变量被误用。有的同学会在循环体里给循环变量重新赋值比如在Python里用 for i in range(n) 时循环体内部又写了 i 5结果循环行为变得完全不可预测。这个在部分语言里后果会更严重Python倒是不会让你死循环但会让代码逻辑混乱得让你怀疑人生。2.3 “读数据-处理-输出”的三段式思维第八天我还悟到一个小框架任何程序都可以拆成三段——读取输入、处理逻辑、输出结果。这个框架看起来简单但极其有用。拿成绩统计题目来说读取输入就是那一段循环输入和收集处理逻辑就是求平均数和最大值输出结果就是打印。你写任何程序都可以像切蛋糕一样先切成三段每一段单独聚焦。接下来真正上难度的是一个关键意识很多人会把读取和处理混在一起边读边算导致程序只能完成“恰好这一堆输入”的任务一旦格式变化就得重写。而我用第一阶段把数据全部装进列表第二阶段再遍历处理逻辑就能独立成一块。这个“先把数据装进容器再从容器里拿数据计算”的习惯是你日后接触文件读写、接口请求、数据库查询这些更复杂场景的地基。第八天如果不把这三段式的习惯立住后面写任何超过五十行的程序都会自然而然地变成一锅粥。3. 字符串处理的几个必练题第八天的第二个主题我把时间花在字符串处理上。为什么这个阶段一定要练字符串因为字符串几乎出现在所有程序员日常工作中日志分析、用户输入校验、爬虫数据处理、搜索功能……而且字符串也是各种面试里出题频率极高的素材。对初级程序员来说回文判断、字符统计、单词反转这些题既能练到循环和条件分支又不会难到劝退。3.1 从最笨的方法开始写回文判断“回文”这个概念很简单一个字符串正着读和反着读是一样的比如 level、abba。判断一个字符串是不是回文第一反应就是把字符串反转过来再比较。在Python里可以用切片s level if s s[::-1]: print(不是回文 if False else 是回文) print(s[::-1]) # 看一眼反转结果直接在条件判断里写反转版本虽然简洁但对初学者来说会掩盖一个重要思维过程为什么不把字符串拆成两半让第一个字符和最后一个字符比较第二个字符和倒数第二个字符比较这背后有三种方案反转比较、双指针夹逼、递归判断。双指针方案最优因为它的空间复杂度是O(1)不需要额外创建一份反转后的字符串。但如果上来就讲双指针新手很容易机械地记住“两根指针来回走”却不知道为什么能判断回文。所以我练习时是从最笨的“取每个字符存到新字符串里”开始的def is_palindrome_slow(s): reversed_s for ch in s: reversed_s ch reversed_s # 把新字符放前面相当于反转 return s reversed_s这版代码虽然慢但它直接展示了反转的本质把字符一个个从原字符串里取出来头尾颠倒地拼在一起。理解了这一步再看切片和双指针就不会一头雾水。3.2 双指针思路与代码优化理解了笨办法之后我尝试自己写出双指针版本。做法是两头各放一个索引一个从前往后走一个从后往前走每次比较两个索引位置的字符是否相同只要有不一样就立刻返回不是回文。def is_palindrome(s): left, right 0, len(s) - 1 while left right: if s[left] ! s[right]: return False left 1 right - 1 return True双指针有两个“为什么”值得搞清楚。第一为什么条件是 left right 而不是 left right因为当字符串长度为奇数时中间那个字符不需要跟自己比较小于号刚好能避开多余判断。第二为什么每次循环要 left 1 和 right - 1 一起做因为它们就是在模拟“从两端向中间靠近”这个过程少了任何一边的移动程序都会陷入死循环。3.3 变体练习反转、去空格、统计频次做完基础回文后我给自己加了三个变体第一个是“忽略大小写的回文判断”。很简单判断前先统一转小写s.lower()。但别小看这一步很多教程直接把转换写死而不告诉你真实场景里用户输入英文时经常大小写混杂。第二个是“忽略非字母数字字符”。比如句子 “A man, a plan, a canal: Panama” 是经典回文题需要把标点、空格全部过滤掉只保留字母和数字再比较。Python里可以用字符串方法配合条件判断去掉无用的字符。第三个是“统计字符串中每个字符出现次数”。这其实是综合题的前菜def count_chars(s): result {} for ch in s: result[ch] result.get(ch, 0) 1 return result这里用了字典也用了 get 方法它比先判断 ch 在不在字典里再翻倍赋值要简洁得多。如果你恰好看到这里建议亲手敲一遍这版体会 dict.get 的用法。字符串处理这块如果你是准备考基础类的证书比如软考初级程序员其实这些题目大概率会出现在基础知识的卷子里。不要因为题目简单就跳着过基础扎实不是看你记住了多难的算法而是看你能不能把最简单的方法写对、讲清楚。4. 递归第八天能接受的抽象思维训练第八天前我还很怕递归。看到“递归”两个字第一反应是“完了程序要爆栈了”。但真正坐下来学习后我发现递归并没有想象中那么难它只是把一个规模较大的问题拆成几个同样思路但规模更小的问题。4.1 用阶乘把递归讲清楚阶乘是理解递归的经典入门。n的阶乘等于 n * (n-1) * (n-2) * ... * 1数学定义可以写成当 n 0 时0! 1当 n 0 时n! n * (n-1)!第二个公式本身就包含递归的含义计算5! 要先算4!算4! 要先算3!一直到计算0! 才遇到一个确定的基准值。用代码写就是这样def factorial(n): if n 0: return 1 return n * factorial(n - 1)这个函数只有两处关键内容终止条件 n 0以及递归调用 factorial(n - 1)。缺少了终止条件它会无限调用自己一直到程序崩溃。4.2 递归的运行过程与“调用栈”递归能跑起来依赖的是程序底层的“调用栈”机制。每次调用函数系统会把当前函数的参数、局部变量和返回地址打包压入栈内存当函数 return 时再把栈顶恢复。这句话看起来很深奥但用人话讲就是你向别人传话每传一层上一层的“待办事项”先放到抽屉里等最底层的人传回来了你再一层一层打开抽屉接着办。我在电脑上单步调试了 factorial(3) 的执行过程看到的顺序是factorial(3)先调用factorial(2)factorial(2)调用factorial(1)factorial(1)调用factorial(0)并得到1然后返回给factorial(1)得到111再返回给factorial(2)得到212再返回给factorial(3)得到3*26。这个“先往下递再逐层归”的执行路径比任何解释都有说服力。4.3 递归和循环怎么选以及一个必考的坑有了递归基础后有几个问题是第八天值得想的。既然递归能写的循环也能写那递归存在的意义是什么答案是对某些天然具有层次结构的问题目录树遍历、括号匹配、树的深度递归的思维建模成本比循环低得多。而递归比循环“贵”在哪也是需要直面的每次递归调用都有函数栈的开销递归过深会触发栈溢出循环则没有这个问题。下面这个对照表可以帮助初学者做选择对比维度递归循环思路建模抽象、分治、接近数学定义强调迭代、顺序推进代码长度通常更短通常更长可读性对熟悉递归的人友好对新手更直观性能开销每次调用有栈帧开销更轻量危险点栈溢出、兜底条件缺失死循环、循环变量失控典型场景树形结构、分治算法数组遍历、累加求和第八天阶段我的建议是递归看懂、会写但不要强行递归一切。你完全可以先用循环写阶乘和斐波那契然后用递归重写一遍再把两个版本对照起来看。很多程序员必会算法书里都会提到递归思想但初学时不必急着啃完先把“自己用递归实现阶乘”这个动作做熟就算过关。此外有一个经典的坑必须提在递归函数中使用可变默认参数。比如你写 def func(cur, result[])然后在递归里不断往 result 追加元素第二次调用时 result 还保留着上一轮的数据。这是我见过初学者最容易踩的“非显性错误”代码不报错但结果永远不对。5. 综合实战统计文本中出现次数最多的字符知识学了一堆如果不拼起来用一次第二天就会忘。第八天的下午我给自己出了一道综合题统计一段文本中出现次数最多的英文字母及次数。这道题需要用到字符串遍历、字典存储、条件判断、最大值查找恰好把前面学的知识点串成一条线。5.1 题目与需求分析题目本身不难但它很贴近真实场景。比如你想统计一篇文章里哪个单词最常出现、一份日志中哪个错误码出现频率最高本质上都是同一个模式把数据分成一类一类的然后盘点每类出现的次数。我把需求拆成了三个小步骤遍历文本中的每个字符把每个字母的出现次数记录到字典里找出字典中值最大的键值对前两道题不区分大小写第四道题可以按字符频次排序。做这个的目的很单纯同样的字典计数套路换一层要求就多一层对数据结构的使用练习。完成之后我反而对字符串天然处理头大的人感同身受——这是“工具先会用理论先弄懂”的胜利。5.2 第一版能跑的代码字典计数法我一开始先写了一个最直观的版本每一步都拆得很开有借鉴意义。如下text Hello World, welcome to Python Programming! text_lower text.lower() counts {} for ch in text_lower: if ch.isalpha(): if ch in counts: counts[ch] counts[ch] 1 else: counts[ch] 1 max_char None max_count 0 for ch, cnt in counts.items(): if cnt max_count: max_count cnt max_char ch print(出现最多的字母:, max_char, 次数:, max_count)这个版本里有两个细节值得说。第一我用了 if ch.isalpha() 过滤掉空格、标点以及数字否则统计结果会被逗号和句号带偏。第二我找最大值用的是最朴素的“假设一个最大然后遍历比较”方式和前面成绩统计里找最高分的思想完全一致。你会发现很多“新题”背后的旧思想并没有变变的只是数据形态。前两版代码能跑但第一个版本里 if ch in counts 和 counts[ch] counts[ch] 1 写起来有些重复。更新后我用 dict.get 简化了代码for ch in text_lower: if ch.isalpha(): counts[ch] counts.get(ch, 0) 1一行代码就完成了“如果不存在则从0开始加如果存在则取原值加1”的过程。初学阶段很多人都愿意把所有条件展开写清楚但会觉得 dict.get 是黑魔法。其实它的原理非常简单get 的第一个参数是键第二个参数是这个键不存在时返回的默认值。理解之后就能放心使用。统计完字典我再用 max(counts, keycounts.get) 直接取出值最大的键。max 函数的 key 参数以前没接触过查了一下才知道它允许你指定“用什么标准来比较大小”这里就是告诉 max 函数按字典的值来比较最后返回对应的键。这个知识点属于“工具类”的补充不理解也不影响你写对结果但理解了会让代码更干净。5.3 反例和效率意识别用 count() 逐字符扫掌握字典解法后我故意写了一个反面版本对文本中的每个字符都调用一次 count 方法来计算它在整个文本里出现多少次然后选出最高者。best_char best_count 0 seen [] for ch in text_lower: if ch.isalpha() and ch not in seen: seen.append(ch) cnt text_lower.count(ch) if cnt best_count: best_count cnt best_char ch这个版本对于“Hello World”这样的小文本完全没问题。但如果在很长的文章上运行它的性能就会迅速暴露问题每遇到一个新字符count 方法都要从头到尾把整个字符串扫描一遍。本来只需要扫描一趟就能统计完所有字符结果变成了“有多少个不同字符就扫描多少趟”复杂度接近O(n²)。这里我第一次感觉到“能跑”和“跑得好”的差距。初级程序员容易把性能当成后期才用管的优化但真正的经验是从一开始就养成“尽量看一遍数据就完成统计”的意识。字典版本把所有统计都积累在一趟遍历里这才是可持续使用的方法。5.4 边界条件与验证代码写完之后我花了一小段时间测试各种边界情况。空文本时 counts 是空字典代码不会报错max函数会因为空字典抛 ValueError所以正式的程序里要加个判断。全部是非字母字符时统计结果也应该给出“没有字母可统计”的提示。大小写混合时只要统一转小写就不会出现重复计数。这些边界条件如果不在练习时主动想以后写到真实项目里就会莫名其妙出现问题。我总结的一条心得是每完成一段代码都问自己“输入为空怎么样”“输入全部都是异常格式怎么样”“输入最大数据量会怎么样”这三个问题能兜住绝大多数初级容易出的问题。6. 第8天踩过的三个坑全记录这一天里最耗时间的部分不是我学知识而是排错。本来只是练习字符串和数组结果前前后后报错花去一个多小时。把这三个坑完整记录下来我觉得比单纯记录“今天学了什么”更有价值。如果你也在自学看完应该能少走一些弯路。6.1 坑一数组越界报错信息我都背下来了第一个大坑出现在我写一个“找所有比前一个元素大的数”练习时。我的第一版代码把遍历终止条件写成 range(len(arr))内部访问 arr[i 1]结果运行到最后一个元素时直接抛 IndexError。arr [3, 1, 4, 1, 5] for i in range(len(arr)): if arr[i 1] arr[i]: # 到 i4 时 arr[5] 越界 print(arr[i 1])原因一句话一个长度为5的列表索引范围是0到4不存在索引5。这类问题都是边界判断失误。修法很简单把循环范围改成 range(len(arr) - 1) 即可。for i in range(len(arr) - 1): if arr[i 1] arr[i]: print(arr[i 1])从此我养成一个习惯在涉及索引加一减一的代码里写之前先在心里数一遍边界。这个习惯可以省下无数次要到深夜才能找到的排错时间。6.2 坑二边遍历边删列表元素悄悄失踪第二个坑发生在做去重练习时。我的想法是遍历列表如果发现当前元素等于前一个位置的元素就把它从列表里删除。听起来合理实际运行结果却完全不对。nums [1, 1, 2, 2, 3, 4] for num in nums: if num 2: nums.remove(num)问题在于Python 的 for 循环在遍历列表时是按索引顺序从前往后读的而用 remove 删除元素后列表的所有元素位置会往前挪一位。于是循环可能跳过刚刚补齐到当前位置的那个元素最终结果里残留某些未删除项。面对这种坑最好奇的解决办法是“不边遍历边改容器”。要么先复制一份原始列表要么从后往前遍历要么先用列表推导式过滤出结果。nums [num for num in nums if num ! 2]列表推导式生成新列表完全不改动原遍历对象。这段经历让我意识到更换容器内容时最好先想清楚改动会不会影响循环的顺序不要盲目相信“遍历时修改是安全的”这种直觉。6.3 坑三循环变量被意外修改程序“死”了第三个坑了一段极其隐蔽的逻辑错误我在循环内部给当前遍历的字符或者索引变量重新赋值导致程序进入不正常的执行路径。我做的是读取一串数字统计最大值但在循环体重用了一个变量名去接收中间结果恰好把循环变量覆盖掉。在Python中如果覆盖的是range里的循环索引范围不会变所以表现一样不会死循环。但如果是while循环就会很危险。i 0 while i 10: if i % 2 0: i 5 # 误操作重置了循环变量 i 1这段代码当然能结束但已经失去了本来的轨迹检查逻辑时非常难排查。如果误写成 i 0就是妥妥的死循环程序会一直打印或一直空转直到卡死。从这次之后我给自己打电话了一个规矩循环体内部尽量不要给循环变量赋值。如果确实需要保存中间值用另外一个名字明显的新变量。循环变量的职责是“掌控次数”你一旦让它兼职做其他事它就会罢工给难看。6.4 复盘方法这一天的学习如何转化为下一天的起点一天的代码练习结束之后我用了约二十分钟做复盘。操作很简单打开当天的doc文档列出“今天掌握了什么”“今天哪些报错出现过”“今天的代码哪一段最丑”分别写入三栏。不要小看这个复盘动作。初学者经常陷入“看完教程马上忘”的循环原因就是缺少一个把知识编码进大脑的整理环节。我观察过身边学习进度快的朋友他们都有定期整理笔记或者输出内容的习惯比如把当天学到的代码要点贴到社区里打卡或者是把一段卡了很久的问题描述清楚发给别人。程序员社区本身也经常有这样的学习记录帖子我发现一个规律那些愿意公开整理问题的人进步速度远高于闭门造车的人。第八天结束后我把这段经历总结成一句话成为初级程序员不是靠记住多少语法而是靠认清楚自己写的代码为什么能跑、为什么不能跑。今天的三个坑里第一个是边界意识第二个是容器改动与循环的关系第三个是变量职责边界——每一个都是编程思维的重要拼图。回顾整个第八天给我最大的感受就是“意识到差距”比“学会抢跑”更重要。看到别人的精简解法时我一度焦虑自己是不是太笨但把代码拆解并重写一遍之后焦虑变成了实实在在的进步。如果你也在第8天、第9天或者第15天感到自己写的东西很粗糙那是好兆头。静下心把一个简单题目写出一百种变体远比囫囵吞枣地往后赶进度有意义。最后再分享一个小技巧每天哪怕只学明白一个知识点坚持写一行“今天的代码哪里不对劲”一个月后再回头看你会发现自己根本不是第八天那个害怕报错的新手了。
返回列表