欢迎光临
我们一直在努力

Python 编程思维训练指南 from NWAFU

🐍 Python 编程思维训练指南

—— 从「背语法」到「会编程」,你只差一种思维方式的转变


序言:你不是学不会,你是用错了方法

你好,很高兴你能打开这份教程。

先问你一个问题:你是不是这样的?

  • 看书的时候觉得「哦,这很简单嘛,我懂了」
  • 看视频教程的时候跟着敲一遍,能跑起来,觉得自己会了
  • 但关掉教程,面对一道新的编程题,大脑一片空白
  • 或者跑去刷题网站,看到题目,感觉"这个知识点我学过",但就是不知道从哪下手

如果以上有任何一条戳中了你,别担心——你不是一个人。这是几乎所有 Python 初学者都会遇到的困境。

但我想告诉你一个可能会让你有点意外的真相:

你学不会编程,不是因为你笨,也不是因为你不够努力——而是因为你在用「学文科」的方式学「工科」的东西。

什么意思呢?

从小到大,我们的学习方式大多是 「输入 → 记忆 → 复现」 的模式:

  • 学历史:记住了某年某月发生了什么事 → 考试写出来
  • 学政治:背下了某个概念的定义 → 选择题里选出来
  • 学英语:记住了单词和语法 → 阅读理解中认出来

这种模式在应试教育中非常有效。但问题是——编程不吃这一套。

编程不是「记住知识点然后复现」的技能,而是 「面对问题,现场创造解决方案」 的技能。

这就像什么呢?

就像你学游泳。你看再多的游泳教学视频、背再多的游泳要领口诀,不下水,你永远学不会。而且就算下了水,你呛几口水、扑腾几下,也是学习的一部分。

编程也是这样。

这本教程的目的,不是教你又多背几个 Python 语法——市面上这样的教程已经太多了——而是要帮你建立一种「计算机语言思维」。一旦你拥有了这种思维,Python 的语法对你来说就不再是需要死记硬背的知识点,而是你表达想法时自然而然会使用的工具。

准备好了吗?那我们开始吧。


第一章:认识「计算机语言思维」

1.1 计算机不是人——它听不懂「大概」

这是整个教程中最重要的一个认知转变,请一定认真看完。

人类之间的沟通,有一个巨大的特点:我们能用「大概」来交流。

比如我对你说:「帮我把桌子收拾一下。」

你会自动理解:

  • 「桌子」指的是那张有东西的桌子,不是所有桌子
  • 「收拾一下」是把东西摆整齐、把垃圾扔掉
  • 你不会问我:「桌子的定义是什么?收拾的标准是什么?垃圾的定义是什么?」

因为你有常识,你会推断。

但计算机不一样。计算机是世界上最"笨"的东西——它没有任何常识,不会猜测你的意图,不会理解"大概"是什么意思。

你对计算机说:「帮我把这个列表里的数字排序。」

计算机的反应是:

  • 「列表」是什么?
  • 「数字」在哪里?
  • 「排序」是什么意思?从小到大?从大到小?
  • 「怎么排」?用什么方法排?

你必须把每一步都精确地告诉它,不能跳过任何细节,不能用模糊的语言。


这就是编程思维的第一课:精确到变态的思维方式。

举个例子,现在有一个非常简单的任务:找出一个列表里最大的数。

你作为人类,一眼扫过去就知道了。但计算机需要你告诉它:

1. 先假设第一个数是最大的
2. 然后看第二个数,如果它比当前最大的大,就把它记作新的最大
3. 再看第三个数,同样比较一下
4. 重复这个动作,直到看完所有数
5. 最后告诉我你记下的那个最大的数

你看,这么简单的一件事,需要拆成这么多步。

这种「把一件事拆成计算机能理解的、一步一步的精确指令」的能力,就是编程思维的核心。

1.2 编程的本质:把问题翻译给计算机听

很多人把编程想得太复杂了。

其实编程的本质非常简单,就两件事:

  • 你搞清楚问题怎么解决(这在计算机领域叫做「算法」)
  • 你用计算机能听懂的语言,把解决步骤写下来(这在计算机领域叫做「编码」)
  • 第一步的核心是 「人脑思考」——你需要自己想清楚怎么做。
    第二步的核心是 「翻译」——你把想清楚的步骤,翻译成 Python 代码。

    很多新手犯的错误是:他们跳过了第一步,直接开始做第二步。

    看到一个题目,立刻开始想「这个用 Python 的什么语法?这个用哪个函数?」而不是先想「这个问题要解决,需要做哪几个步骤?」

    这就像你要写一篇文章,但你还没想好要写什么内容,就开始纠结「我该用哪个成语?这个句子要用什么修辞手法?」——你不卡壳才怪呢。


    所以,请记住这个最重要的心法:

    先想清楚「怎么做」,再想「用什么语法写」。

    先有思路,再有代码。

    先解决人的问题,再解决计算机的问题。

    这个顺序一旦搞反了,你就会被语法绑架,永远学不会编程。

    1.3 语法 vs 思维:为什么两者缺一不可

    到这里你可能会想:「那语法就不重要了?」

    不是的。语法也很重要。

    一个好的类比是这样的:

    • 编程思维 = 你脑子里想好的菜谱(先放油、再放葱姜蒜、再放菜……)
    • 语法 = 你的厨具和调料(锅、铲、油、盐、酱油……)

    只有菜谱没有厨具,你做不了菜。只有厨具没有菜谱,你不知道做什么菜。

    但问题是:厨具可以随时学、随时查。菜谱的思维方式,才是真正需要训练的。

    你不需要背下 Python 所有的函数和方法——你完全可以在写代码的时候去查文档、去 Google、去问 AI。所有专业的程序员都是这样做的。

    但如果你没有编程思维,即使你背下了整本 Python 手册,面对一个新问题,你还是不知道从哪下手。


    第二章:编程思维的五大核心能力

    编程思维不是一种玄乎的东西,它由五个可以训练的核心能力组成。我们一个个来看。

    2.1 分解问题(Decomposition)—— 把大象装进冰箱

    核心思想:把一个复杂的大问题,拆成一个个简单的小问题。

    还记得那个经典笑话吗?

    问:把大象装进冰箱需要几步?
    答:三步。第一步,打开冰箱门。第二步,把大象放进去。第三步,关上冰箱门。

    这就是分解问题——把一个看起来很夸张、不可能的任务,拆成可执行的步骤。

    在实际编程中,几乎所有的复杂问题,都可以通过分解来搞定。

    举个例子:

    假设你要写一个程序,让电脑帮你从网上下载100张猫咪图片。

    如果你直接想「我要写一个下载猫咪图片的程序」,这听起来很难,你不知道从哪开始。

    但如果你把它分解一下:

  • 先搞清楚:从哪里下载猫咪图片?(找到图片来源网站,或者知道有哪些提供图片的 API)
  • 再搞清楚:怎么下载一张图片?(发一个网络请求,把返回的内容存成文件)
  • 再搞清楚:怎么下载100张?(把第2步重复100次)
  • 再搞清楚:怎么保存图片?(给每张图片取不同的名字,存到某个文件夹)
  • 你看,分解之后,每个小问题看起来就简单多了。单独来看,「发一个网络请求」— 这个你可以学;「把内容存成文件」— 这个你也可以学;「重复100次」— 用个循环就行。

    分解问题的训练方法:
    拿到任何任务,先不要想代码怎么写。拿出纸和笔,用自然语言(中文)把步骤写下来。写得越细越好。等到你写出来的步骤细到「每一步计算机都能直接执行」的程度,再开始写代码。

    2.2 识别模式(Pattern Recognition)—— 发现重复

    核心思想:很多看似不同的问题,底层有相似的解决模式。

    人类的大脑天生就是模式识别机器。你看到一张猫的照片,即使这只猫你从没见过,你也知道它是猫,因为它有猫的特征(耳朵尖尖的、有胡须、会喵喵叫……)。

    编程中也是这样——

    当你做过的题目足够多,你会发现:很多看似不同的问题,它们的解决思路是相似的。

    比如:

    • 「找出一个列表里最大的数」
    • 「找出一个班级里最高的学生」
    • 「找出一个公司里工资最高的员工」
    • 「找出一个数组里最长的字符串」

    这些问题看起来完全不同,但它们底层的解决模式是一样的:

  • 先假设第一个是最大的
  • 遍历剩下的,每次遇到更大的就替换
  • 识别模式的训练方法:
    每做完一道题,问自己一个问题:「这道题和我之前做过的哪道题很像?它们的共同点是什么?」时间久了,你就会建立起自己的「模式库」。

    2.3 抽象化(Abstraction)—— 忽略不重要的细节

    核心思想:只关注「是什么」,不关注「具体是谁」。

    抽象化可能是编程思维中最难理解、但也是最有力量的一个能力。

    简单来说,抽象化就是 「把具体的东西,归纳成通用的概念」。

    举个例子:

    • 具体的:我家的那只橘猫「胖橘」,3岁,5公斤重,喜欢吃鱼
    • 抽象的:「猫」这个概念——有毛、四条腿、会喵喵叫、是宠物

    在编程中,抽象化让我们能够写一次代码,解决一类问题,而不是一个问题。

    举例说明:

    没有抽象化的写法:

    # 找出列表 [3, 7, 2, 9, 5] 里最大的数
    numbers = [3, 7, 2, 9, 5]
    max_num = numbers[0]
    for num in numbers:
    if num > max_num:
    max_num = num
    print(max_num) # 输出 9

    有抽象化的写法(写成函数):

    def find_max(numbers):
    """找出任意数字列表中最大的数"""
    max_num = numbers[0]
    for num in numbers:
    if num > max_num:
    max_num = num
    return max_num

    # 现在这个函数可以用于任何数字列表
    scores = [88, 92, 75, 100, 83]
    print(find_max(scores)) # 输出 100

    ages = [25, 30, 18, 42, 35]
    print(find_max(ages)) # 输出 42

    你看,我们用 def find_max(numbers): 把"找最大"这个逻辑抽象成了一个通用的方法。从此不管面对什么样的数字列表,我们都能复用这个函数。

    这就是抽象化的力量——把你解决问题的思路,包装成可以反复使用的"思维积木"。

    抽象化的训练方法:
    每当你发现自己在重复写相似的代码,停下来想一想:能不能把这个逻辑抽出来,变成一个通用的函数/工具?

    2.4 算法思维(Algorithmic Thinking)—— 设计步骤

    核心思想:为问题设计一个明确、无歧义、有效率的解决步骤。

    算法思维其实就是我们前面一直在说的:「想清楚怎么做」。

    但这里有一个很多新手忽略的关键点:好的算法,不仅要"能做",还要"做得好"。

    比如,给你一个已经排好序的列表 [1, 3, 5, 7, 9, 11, 13, 15],要你找出里面有没有数字 7。

    方法一(线性查找):

    从头到尾一个一个看 → 需要看4次才能找到7

    方法二(二分查找):

    1. 先看中间的数 —— 是9
    2. 7比9小,所以只看左半边 [1, 3, 5, 7]
    3. 再看中间的 —— 是3
    4. 7比3大,看右半边 [5, 7]
    5. 看中间的 —— 是5
    6. 7比5大,看右半边 [7]
    7. 找到了!只用了3次

    如果列表有100万个数字,线性查找平均需要50万次,而二分查找最多只需要20次。这就是算法思维的威力——同样的结果,不同的效率。

    但对于新手来说,先不用太纠结效率问题。你先做到「能做」,再慢慢追求「做得好」。

    算法思维的训练方法:
    遇到问题,先用中文/流程图把步骤写清楚。然后问自己:这个方法在所有情况下都有效吗?有没有更简单的方法?

    2.5 调试思维(Debugging Mindset)—— 与错误做朋友

    核心思想:程序出错不是失败,而是告诉你「这里需要改进」。

    这是很多新手最需要转变的一个心态。

    在传统的应试教育中,犯错 = 扣分 = 不好的事情。所以我们本能地害怕犯错、逃避错误。

    但在编程中,错误是最好的老师。

    我甚至可以这么说:编程这项技能,90% 是通过犯错学来的。

    你写的每一个程序,第一次运行时几乎不可能完全正确。你会遇到:

    • 语法错误(SyntaxError)—— 少了个冒号、多了个括号
    • 运行时错误(TypeError、IndexError……)—— 类型不对、下标越界
    • 逻辑错误——代码能跑,但结果不对

    每一个错误,都在告诉你一件事:你的理解和计算机的理解之间,存在差距。 每修复一个错误,你的理解就加深了一层。

    我在带新人的时候,经常说一句话:

    不要怕报错。报错是计算机在帮你。

    真正的可怕是什么都不报错,但结果全错——那叫逻辑错误,最难找。

    调试思维的训练方法:

  • 看到报错,先冷静,仔细读报错信息(很多新手一看报错就慌了,根本不去读它在说什么)
  • 找到报错指向的行号,看那一行代码
  • 理解报错类型(上网查这个错误是什么意思)
  • 用 print() 打印中间变量,看看程序的执行过程是否符合你的预期
  • 修复,再试

  • 第三章:从「看得懂」到「写得出来」

    3.1 「看懂了」—— 这是最大的陷阱

    几乎所有编程新手,在学习过程中都会遇到这个"陷阱":

    你看了一段代码,逐行读过去,觉得「嗯,这行是把值赋给变量,这行是判断条件,这行是循环……我都懂」。

    于是你觉得自己「会了」。

    但真相是:你只是「认得出这些语法」,距离「会写」还有十万八千里。

    这就像什么?就像你学英语,看一篇文章,每个单词你都认识,每句语法你都能分析——但让你自己用英语写一篇同样水平的文章,你就写不出来。

    因为 「读懂」是输入能力,「写出来」是输出能力。两者是完全不同的能力。

    而且还有一个更关键的问题:教程里的代码是别人写好的,思路是别人的。 你看懂了别人的思路,不代表你自己能产生同样的思路。

    这就好比看别人下象棋: 你看得懂他走的一步棋,但不代表你自己下的时候也能想到这一步。

    3.2 从模仿到真正的理解

    那么,怎么才能真正「学会」而不是「感觉学会」呢?

    这里有一个学习编程的核心方法论——四步进阶法:

    第一步:抄(Copy)

    看到好的代码示例,逐字逐句地敲一遍(不是复制粘贴!)。在敲的过程中,你会注意到很多看的时候注意不到的细节——这里有个逗号、那里有个冒号、缩进是4个空格……

    这一步的目的:让你的手指记住语法。

    第二步:改(Modify)

    在抄完的代码基础上,做一些小改动:

    • 改改变量名
    • 改改参数值
    • 加个 print() 看看中间结果
    • 把循环改成不同的范围

    看看改动之后发生了什么。理解「改这里 → 导致那里变化」的因果关系。

    这一步的目的:建立「代码 → 行为」的因果理解。

    第三步:写(Write)

    关掉教程,只保留题目的描述。自己从头开始写一遍。

    写不出来是正常的。卡住了就回去看一眼教程——但只看你能卡住的那个点的提示,然后继续自己写。

    这一步的目的:训练从零开始的输出能力。

    第四步:讲(Explain)

    最难的一步:用中文向另一个人(或者甚至就向你自己)解释这段代码做了什么、为什么这么做。

    如果你能清晰地讲出来,说明你真的懂了。如果你讲着讲着发现自己卡住了——那个卡住的地方,就是你没真懂的地方。

    这一步的目的:检验和巩固真正的理解。

    把这四步应用到你的每一个学习单元中。你每学一个新的知识点(比如列表推导式、字典、函数参数……),都走完这四步。

    3.3 「由外而内」的学习法

    很多新手的另一个问题是:学得太「线性」了。

    他们觉得必须「从头到尾」学完一本教材,把所有语法都学完了,才能开始写东西。

    这是错的。

    编程学习应该是 **「由外而内」**的——先有一个你想解决的问题,然后去学解决这个问题需要的知识。

    • 你想做一个计算器? → 学输入输出、基本运算
    • 你想分析一份数据? → 学文件读写、列表操作
    • 你想爬取网页? → 学网络请求、HTML解析

    带着问题去学,比你盲目地通读教材,效率高十倍。

    为什么呢?因为有问题的时候,你知道「学这个是为了什么」,你的大脑会把新知识和你的问题关联起来,记忆和理解都会深刻得多。


    第四章:Python 学习路径与思维训练

    这一章,我们结合 Python 的具体知识点,来看每个阶段应该重点训练什么思维方式。每个阶段我会给你三个东西:要学什么、思维要点、具体的练习方法。请一定动手做练习,光看是不够的。


    4.1 第一阶段:建立直觉——变量、数据类型、基本操作

    学习的知识点:

    • 变量与赋值
    • 数字(int / float)、字符串(str)、布尔值(bool)
    • 基本的输入输出(input() / print())
    • 基本的算术运算与字符串运算

    这个阶段的核心任务:建立「代码 = 操作数据」的直觉。

    很多新手在这一阶段犯的错误是:死记硬背「变量是什么」的定义。 别背!你要做的是理解「变量就是给数据贴个标签,方便以后用它」。

    来看一个非常直观的例子:

    # 把数据存到变量里
    name = "小明"
    age = 18
    height = 1.75

    # 用变量来操作数据
    print(name, "今年", age, "岁")
    # 输出:小明 今年 18 岁

    # 对数据做运算
    next_year_age = age + 1
    print("明年", name, "就", next_year_age, "岁了")
    # 输出:明年 小明 就 19 岁了

    思维要点 1:赋值不是数学里的"等于"

    这是新手最容易混淆的地方。在数学里,= 表示「左右两边相等」;但在编程里,= 表示 「把右边的值,贴到左边的标签上」。

    来看一个对比:

    # 数学思维:x = x + 1 怎么可能成立?
    # 编程思维:把 x 当前的值取出来,加上 1,然后重新贴回 x 这个标签

    x = 5
    print(x) # 5
    x = x + 1 # 先算右边 x + 1 -> 6,然后把 6 赋给 x
    print(x) # 6

    你可以把变量想象成一个带标签的盒子:

    • x = 5 -> 拿一个盒子,贴上标签"x",里面放 5
    • x = x + 1 -> 打开盒子看,里面是 5,算出 5+1=6,清空盒子,放进 6
    • 现在盒子里的值变成了 6
    思维要点 2:程序 = 数据 + 对数据的操作

    每一行代码要么是在定义数据(创建变量),要么是在操作数据(运算、拼接、转换等)。初学者如果能时刻意识到「我现在是在定义数据还是在操作数据」,思维就会清晰很多。

    # 定义数据
    name = "小红"
    age = 20

    # 操作数据
    greeting = "你好," + name + "!" # 字符串拼接
    age_in_months = age * 12 # 算术运算
    is_adult = age >= 18 # 比较运算(结果是布尔值)

    print(greeting) # 你好,小红!
    print(age_in_months) # 240
    print(is_adult) # True

    思维要点 3:变量在被赋值之前不能被使用

    # 错误写法
    print(total) # NameError: name 'total' is not defined
    total = 100

    # 正确写法
    total = 100
    print(total) # 100

    这看起来很简单,但很多新手写循环的时候会犯这个错——在循环里累加一个变量,却忘了先把它初始化为 0。

    这个阶段最有效的练习

    练习 1:纸笔模拟(变量追踪)
    拿一张纸,画几个方框代表变量。逐行执行下面的代码,每执行一行,更新方框里的值:

    a = 10
    b = a + 5
    a = a + 1
    c = a + b

    你的纸上应该出现这样的变化过程:

    行号代码a 的值b 的值c 的值
    1 a = 10 10
    2 b = a + 5 10 15
    3 a = a + 1 11 15
    4 c = a + b 11 15 26

    练习 2:温度转换器
    自己写一个程序:输入摄氏温度,输出华氏温度。公式:F = C * 9/5 + 32

    celsius = float(input("请输入摄氏温度:"))
    fahrenheit = celsius * 9 / 5 + 32
    print(f"{celsius}C = {fahrenheit}F")

    写完之后,修改它:改成从华氏转摄氏。这个练习帮你建立「输入 -> 处理 -> 输出」的基本思维模型。

    练习 3:变量交换
    你有两个变量 a = 5 和 b = 10,请交换它们的值,让 a 变成 10,b 变成 5。

    提示:你需要一个临时变量。这个简单的问题,能帮你深刻理解赋值语句的执行顺序。

    # 解法
    a = 5
    b = 10
    temp = a # 先把 a 的值存到 temp
    a = b # 把 b 的值赋给 a
    b = temp # 把 temp 里存的旧 a 值赋给 b
    print(a, b) # 10 5


    4.2 第二阶段:让计算机做决定——控制流

    学习的知识点:

    • if / elif / else 条件判断
    • for 循环(遍历)
    • while 循环(条件循环)
    • break / continue(循环控制)
    • range()(生成数字序列)

    这个阶段的核心任务:理解「计算机程序不是一条直线,它会根据情况选择不同的路」。

    这是新手理解的第一个分水岭。程序不再是「从上到下执行完就结束」——它开始有了「选择」和「重复」,这才是计算机真正的威力所在。

    条件判断:你是在给计算机画决策树

    思维训练:当你写条件判断时,想象你在给计算机画「决策树」——每个分岔路口,计算机要根据条件决定往哪走。

    score = 85

    if score >= 90:
    print("优秀")
    elif score >= 80:
    print("良好")
    elif score >= 60:
    print("及格")
    else:
    print("不及格")

    执行过程:

    分数是 85
    -> 是 >= 90 吗? -> 不是 -> 跳过
    -> 是 >= 80 吗? -> 是 -> 输出"良好"
    -> 后面的 elif 和 else 全部跳过

    一个非常重要的认知:if-elif-else 只会执行其中一条分支。 一旦某个条件匹配,剩下的就不再检查了。这和多个独立的 if 不一样:

    # if-elif-else:排他性的,只走一条路
    x = 5
    if x > 0:
    print("正数") # 这个会执行
    elif x > 2:
    print("大于2") # 这个不会执行(因为上一个已经匹配了)
    else:
    print("其他") # 不会执行

    # 多个独立的 if:每个都会检查
    x = 5
    if x > 0:
    print("正数") # 这个会执行
    if x > 2:
    print("大于2") # 这个也会执行

    条件判断中常见的思维陷阱

    陷阱 1:把 = 和 == 搞混

    • = 是赋值(把值放到变量里)
    • == 是比较(判断两个值是否相等)

    x = 10
    if x = 5: # 语法错误!应该用 ==
    print("x是5")

    if x == 5: # 正确
    print("x是5")
    else:
    print("x不是5") # 输出这行

    陷阱 2:条件顺序错了导致逻辑不对

    # 错误的顺序
    score = 85
    if score >= 60:
    print("及格") # 这个先匹配了,永远不会走到 elif
    elif score >= 80:
    print("良好") # 这行永远不会执行!
    elif score >= 90:
    print("优秀")

    # 正确的顺序:从高到低
    if score >= 90:
    print("优秀")
    elif score >= 80:
    print("良好") # 85 >= 80
    elif score >= 60:
    print("及格")

    循环:让计算机做重复劳动

    计算机最擅长的就是重复。人做重复的事会累、会出错,计算机不会。循环就是利用这个优势的核心工具。

    思维训练:当你写循环时,想象你在对计算机说「把这个事情重复 N 遍」或者「一直做,直到某个条件满足」。

    for 循环 vs while 循环:什么时候用哪个?

    这是一个很容易混淆的问题。这里有一个简单的判断标准:

    • for 循环:当你知道要循环多少次的时候用

      • “遍历列表中的每个元素”
      • “重复做 10 次”
      • “处理字符串中的每个字符”
    • while 循环:当你不知道要循环多少次,只知道什么时候停下来的时候用

      • “一直问用户输入,直到用户输入 quit”
      • “不断地把数字除以 2,直到它变成 0”
      • “猜数字游戏,没猜对就继续”

    # for:已知次数
    for i in range(5):
    print(f"第{i+1}次") # 明确知道要做 5 次

    # while:未知次数,只知道条件
    import random
    target = random.randint(1, 100)
    guess = -1
    while guess != target:
    guess = int(input("猜一个数(1-100):"))
    if guess < target:
    print("猜小了")
    elif guess > target:
    print("猜大了")
    print("猜对了!") # 不知道要循环多少次,直到猜对为止

    循环的三要素

    任何一个循环都可以用三个要素来描述:

    # 用 while 循环从 1 数到 5
    count = 1 # 1. 从哪里开始(初始状态)
    while count <= 5: # 2. 什么时候结束(继续条件)
    print(count)
    count += 1 # 3. 每次做什么变化(更新)

    新手最常见的错误:忘记写第 3 步。 这会导致死循环——计算机永远停不下来。

    # 死循环!
    count = 1
    while count <= 5:
    print(count) # 忘记写 count += 1
    # count 永远都是 1,条件永远为 True

    常见的循环模式

    模式一:累加(求和)

    # 计算 1 到 100 的和
    total = 0 # 累加器从 0 开始
    for i in range(1, 101):
    total += i # 等价于 total = total + i
    print(f"1+2+…+100 = {total}") # 5050

    模式二:计数(数出符合条件的个数)

    # 统计列表中偶数的个数
    numbers = [3, 8, 2, 7, 10, 5, 6]
    even_count = 0
    for num in numbers:
    if num % 2 == 0: # 判断是否为偶数
    even_count += 1
    print(f"偶数有 {even_count} 个") # 4 个(8, 2, 10, 6)

    模式三:查找(找出符合条件的第一个)

    # 在列表中找第一个大于 10 的数
    numbers = [3, 8, 12, 7, 15, 5]
    found = None # None 表示"还没找到"
    for num in numbers:
    if num > 10:
    found = num
    break # 找到了,立即退出循环
    print(f"第一个大于10的数是:{found}") # 12

    模式四:过滤(收集符合条件的元素)

    # 从一个列表中筛选出所有的偶数
    numbers = [3, 8, 2, 7, 10, 5, 6]
    evens = [] # 创建一个空列表来存放结果
    for num in numbers:
    if num % 2 == 0:
    evens.append(num)
    print(evens) # [8, 2, 10, 6]

    这个阶段最有效的练习

    练习 1:纸笔模拟循环
    拿张纸,画一个表格。逐行执行下面的代码,记录每次循环中所有变量的变化:

    total = 0
    for i in range(1, 5):
    total = total + i
    print(total)

    你应该画出如下表格:

    循环次数i 的值total 的变化(计算过程)total 的最新值
    第1次 1 total = 0 + 1 1
    第2次 2 total = 1 + 2 3
    第3次 3 total = 3 + 3 6
    第4次 4 total = 6 + 4 10

    练习 2:乘法表打印
    用嵌套循环打印 9×9 乘法表:

    for i in range(1, 10):
    for j in range(1, i + 1):
    print(f"{j}x{i}={i*j}", end="\\t")
    print() # 换行

    运行看看输出结果。重点理解: 外层循环控制行,内层循环控制列。每次外层循环走一步,内层循环都要完整地走完一轮。

    练习 3:猜数字游戏
    自己写一个猜数字游戏:程序随机生成一个 1-100 的数,用户猜,程序提示「猜大了」或「猜小了」,直到猜中为止。最后输出猜了多少次。

    这道题综合了循环、条件判断、输入输出、随机数——是控制流阶段的经典练习题。


    4.3 第三阶段:封装思维——函数

    学习的知识点:

    • 定义函数(def)
    • 参数与返回值
    • 作用域(全局 vs 局部)
    • 默认参数与关键字参数

    这个阶段的核心任务:学会「把一段逻辑包起来,给它起个名字,以后反复用」。

    如果你只能从这份教程里带走一个概念,那么函数可能就是最重要的那一个。

    为什么函数如此重要?

    没有函数的时候,你的代码是一大坨——所有逻辑摊平了写,像一盘散沙。

    # 没有函数:代码散乱,重复,难以理解
    # 计算三个矩形的面积
    area1 = 5 * 3
    print("第一个矩形的面积是", area1)

    area2 = 8 * 4
    print("第二个矩形的面积是", area2)

    area3 = 6 * 2
    print("第三个矩形的面积是", area3)

    有了函数之后,你把逻辑封装成一个个"积木块",代码变得整洁、可复用、可理解:

    # 有函数:逻辑清晰,代码可复用
    def rectangle_area(width, height):
    return width * height

    # 像搭积木一样使用函数
    print("第一个矩形的面积是", rectangle_area(5, 3))
    print("第二个矩形的面积是", rectangle_area(8, 4))
    print("第三个矩形的面积是", rectangle_area(6, 2))

    函数的本质:输入 -> 处理 -> 输出

    一个函数本质上就是一个**「加工厂」**:

    • 参数是原材料(输入)
    • 函数体是加工程序(处理)
    • 返回值是成品(输出)

    def double(x): # x 是输入
    result = x * 2 # 处理
    return result # result 是输出

    # 使用函数
    print(double(5)) # 10
    print(double(100)) # 200

    那些没有返回值的函数(只有 print 没有 return)就像没有成品的加工厂——它做了事,但没给你留下任何结果。

    理解参数传递:形参 vs 实参

    def greet(name): # name 是形参(参数占位符)
    print(f"你好,{name}!")

    greet("小明") # "小明" 是实参(实际传入的值)
    greet("小红") # "小红" 是实参

    形参就像函数定义里的"槽位",实参就是实际放进去的值。每次调用函数,实参的值都会复制给形参。

    理解 return:函数的结果

    return 有两个作用:

  • 给出函数的计算结果
  • 立即结束函数的执行
  • # return 之后的代码不会执行
    def early_return(x):
    if x < 0:
    return "负数,不处理" # 直接结束函数
    # 上面的 return 执行了,下面的代码不会执行
    return f"正数:{x}"

    print(early_return(-5)) # 负数,不处理
    print(early_return(10)) # 正数:10

    如果一个函数没有 return,它会默认返回 None:

    def just_print(x):
    print(x)

    result = just_print(5) # 输出 5
    print(result) # None(没有 return)

    作用域:变量在哪里"活着"?

    这是函数里最容易让人困惑的概念,但理解它至关重要。

    简单的规则是:函数内部定义的变量,只能在函数内部使用。函数外面的变量,函数内部可以使用但不能修改(除非用 global)。

    # 全局作用域
    message = "你好" # 全局变量

    def say_hello():
    # 局部作用域
    name = "小明" # 局部变量,只能在函数内部使用
    print(message) # 可以访问全局变量
    print(name) # 可以访问局部变量

    say_hello()
    print(message) # 可以访问全局变量
    print(name) # NameError: 局部变量在函数外不可见

    变量屏蔽:当局部变量和全局变量同名时,在函数内部,局部变量"屏蔽"全局变量:

    x = 10 # 全局 x

    def change_x():
    x = 20 # 这是局部 x,和全局 x 是两回事
    print("函数内部:", x) # 20

    change_x()
    print("函数外部:", x) # 10(全局 x 没有被改变)

    函数组合:用函数搭建更复杂的功能

    函数的真正威力在于组合——用小的函数搭建大的功能:

    def is_even(num):
    """判断一个数是否为偶数"""
    return num % 2 == 0

    def square(num):
    """计算一个数的平方"""
    return num * num

    def sum_of_even_squares(numbers):
    """计算列表中所有偶数的平方和"""
    total = 0
    for n in numbers:
    if is_even(n):
    total += square(n)
    return total

    nums = [1, 2, 3, 4, 5, 6]
    print(sum_of_even_squares(nums)) # 2^2 + 4^2 + 6^2 = 56

    你看,我们用 is_even、square、sum_of_even_squares 三个函数,层层组合,完成了"求列表中所有偶数的平方和"这个功能。每个函数只做一件事,但组合起来就能做复杂的事。

    函数训练的终极目标

    你看到一个需求,脑子里的第一反应不是「我先写几行代码」,而是:

  • 这个需求可以拆成哪几个功能?
  • 每个功能用一个什么函数来实现?
  • 这些函数之间怎么调用、怎么组合?
  • 这就是函数式的思维方式——以「功能」为基本单位来思考问题。

    这个阶段最有效的练习

    练习 1:把之前写的代码改造成函数
    把你之前写的所有代码翻出来,找一个大段的、没有用函数的代码,把它改写成使用函数的版本。比如把温度转换器改成一个函数:

    def celsius_to_fahrenheit(c):
    return c * 9 / 5 + 32

    def fahrenheit_to_celsius(f):
    return (f – 32) * 5 / 9

    # 测试
    print(celsius_to_fahrenheit(0)) # 32.0
    print(celsius_to_fahrenheit(100)) # 212.0
    print(fahrenheit_to_celsius(32)) # 0.0

    练习 2:写一个计算器函数
    写一个函数 calculator(a, b, op),接受两个数字和一个操作符(+、-、*、/),返回计算结果。如果操作符无效,返回提示信息。

    def calculator(a, b, op):
    if op == "+":
    return a + b
    elif op == "-":
    return a – b
    elif op == "*":
    return a * b
    elif op == "/":
    if b == 0:
    return "除数不能为0"
    return a / b
    else:
    return "不支持的操作符"

    print(calculator(10, 5, "+")) # 15
    print(calculator(10, 5, "/")) # 2.0
    print(calculator(10, 0, "/")) # 除数不能为0


    4.4 第四阶段:组织信息——数据结构

    学习的知识点:

    • 列表(List)—— 有序、可变
    • 字典(Dict)—— 键值对映射、可变
    • 元组(Tuple)—— 有序、不可变
    • 集合(Set)—— 无序、不重复

    这个阶段的核心任务:理解「不同的数据应该用不同的方式组织」。

    很多人学数据结构的时候,只记住了各种结构的语法——「列表用方括号、字典用花括号、元组用圆括号」——但却不知道什么时候该用哪个。

    永远记住:数据结构的选择决定了你代码的复杂度和效率。选对数据结构,代码就写好了一半。

    列表(List):有序的一串东西

    当你需要 「按顺序存储一组东西」 并且经常需要遍历、追加、修改时,用列表。

    # 创建列表
    names = ["小明", "小红", "小刚"]
    mixed = [1, "hello", True, 3.14] # 可以混装不同类型

    # 常用操作
    names.append("小丽") # 追加:["小明", "小红", "小刚", "小丽"]
    names.insert(1, "小强") # 插入到指定位置
    names.remove("小红") # 删除指定元素
    popped = names.pop() # 弹出最后一个元素
    first = names[0] # 通过下标访问:小明
    last = names[-1] # 负数下标从末尾开始:-1 是倒数第一个

    # 切片:一次取出一段
    numbers = [0, 1, 2, 3, 4, 5]
    print(numbers[1:4]) # [1, 2, 3](从1到3,左闭右开)
    print(numbers[:3]) # [0, 1, 2](从头到2)
    print(numbers[::2]) # [0, 2, 4](步长为2)

    # 遍历列表
    for name in names:
    print(name)

    # 同时获取下标和值(高频用法)
    for i, name in enumerate(names):
    print(f"第{i+1}个:{name}")

    列表的常见误区:

    # 误区:直接复制列表
    a = [1, 2, 3]
    b = a # b 和 a 指向同一个列表!
    b.append(4)
    print(a) # [1, 2, 3, 4] — a 也被改了!

    # 正确做法:创建副本
    b = a.copy() # 或者 b = a[:]
    b.append(4)
    print(a) # [1, 2, 3] — a 没变
    print(b) # [1, 2, 3, 4]

    字典(Dict):一一对应的映射关系

    当你需要 「通过一个东西快速找到另一个东西」 时用字典。字典的核心是键值对——每个键(key)对应一个值(value)。

    # 创建字典
    student = {
    "name": "小明",
    "age": 18,
    "score": 92
    }

    # 访问和修改
    print(student["name"]) # 小明
    student["score"] = 95 # 修改
    student["gender"] = "男" # 新增键值对

    # 安全的访问方式(不会报错)
    print(student.get("name")) # 小明
    print(student.get("grade")) # None(不存在,不会报错)
    print(student.get("grade", "未知")) # "未知"(指定默认值)

    # 遍历字典
    for key in student:
    print(key, student[key])

    for key, value in student.items():
    print(key, value)

    # 检查键是否存在
    if "name" in student:
    print("有 name 这个键")

    一个经典的选择题:列表 vs 字典

    假设你要存一个班级的成绩,需要通过学生名字找到他的分数:

    # 用列表来实现
    names = ["小明", "小红", "小刚"]
    scores = [92, 88, 75]

    # 要找到"小刚"的分数:得先找到小刚在 names 里的下标
    index = names.index("小刚") # 2
    print(scores[index]) # 75 麻烦,而且容易出错

    # 用字典来实现
    scores_dict = {"小明": 92, "小红": 88, "小刚": 75}
    print(scores_dict["小刚"]) # 75 直接!清晰!

    什么时候用列表,什么时候用字典?一个简单判断标准:

    • 你的数据是"一排东西"吗?-> 用 列表
    • 你的数据是"一个东西对应另一个东西"吗?-> 用 字典
    • 你需要按顺序访问吗?-> 用 列表
    • 你需要快速查找吗?-> 用 字典
    元组(Tuple):不能改的列表

    元组和列表几乎一样,除了一个关键区别:元组一旦创建就不能修改。

    # 创建元组
    point = (3, 5)
    rgb = (255, 0, 0)

    # 访问和列表一样
    print(point[0]) # 3
    print(point[1]) # 5

    # 但不能修改!
    point[0] = 10 # TypeError: 'tuple' object does not support item assignment

    元组什么时候用?

    • 你希望数据不能被意外修改(比如坐标、颜色值)
    • 你想让数据作为字典的键(列表不能做键,元组可以)
    • 函数返回多个值时(实际上返回的是一个元组)

    # 函数返回多个值(实际上返回的是元组)
    def divide(a, b):
    quotient = a // b
    remainder = a % b
    return quotient, remainder # 返回元组 (quotient, remainder)

    result = divide(17, 5)
    print(result) # (3, 2)
    q, r = divide(17, 5) # 解包(unpacking)
    print(q, r) # 3 2

    集合(Set):唯一性至上

    当你需要 「去重」 或者 「判断是否存在」 时,用集合。

    # 创建集合
    unique_numbers = {1, 2, 3, 3, 2, 1}
    print(unique_numbers) # {1, 2, 3} — 重复的被自动去掉了

    # 从列表去重
    names = ["小明", "小红", "小明", "小刚", "小红"]
    unique_names = set(names)
    print(unique_names) # {"小明", "小红", "小刚"}

    # 常用操作
    numbers = {1, 2, 3, 4, 5}
    print(3 in numbers) # True — 判断是否存在非常快!
    numbers.add(6) # 添加
    numbers.remove(2) # 删除

    # 集合运算
    a = {1, 2, 3, 4}
    b = {3, 4, 5, 6}
    print(a & b) # 交集:{3, 4}
    print(a | b) # 并集:{1, 2, 3, 4, 5, 6}
    print(a – b) # 差集:{1, 2}

    嵌套数据结构:组合使用

    实际编程中,很少只用一种数据结构。更常见的情况是多种数据结构嵌套使用:

    # 列表里套字典:每个学生是一条记录
    students = [
    {"name": "小明", "scores": {"math": 95, "english": 88}},
    {"name": "小红", "scores": {"math": 78, "english": 92}},
    {"name": "小刚", "scores": {"math": 88, "english": 76}},
    ]

    # 找出数学成绩最高的学生
    best = max(students, key=lambda s: s["scores"]["math"])
    print(best["name"]) # 小明

    # 计算所有人的英语平均分
    total_english = sum(s["scores"]["english"] for s in students)
    average = total_english / len(students)
    print(f"英语平均分:{average:.1f}") # 85.3

    这个阶段最有效的练习

    练习 1:选对数据结构
    对于以下场景,你会选择哪种数据结构?为什么?

  • 存储一周七天的名称
  • 通过学号查找学生姓名
  • 存储多个点的坐标
  • 从一篇文章中统计出现了哪些不同的单词
  • 答案: 1.列表(有序可遍历);2.字典(学号->姓名映射);3.元组列表(每个坐标不可变);4.集合(自动去重)

    练习 2:单词统计器
    写一个程序,输入一段英文文本,统计每个单词出现的次数,按出现次数从高到低排序输出。

    text = "the cat and the dog and the bird"
    words = text.split() # 拆分成单词列表
    word_count = {} # 字典:单词 -> 次数

    for word in words:
    if word in word_count:
    word_count[word] += 1 # 已存在,次数 +1
    else:
    word_count[word] = 1 # 第一次出现,设为 1

    # 按次数排序输出
    sorted_words = sorted(word_count.items(), key=lambda x: x[1], reverse=True)
    for word, count in sorted_words:
    print(f"{word}: {count}")


    4.5 第五阶段:模拟现实——面向对象

    学习的知识点:

    • 类(class)与对象(instance)
    • 属性(数据)与方法(行为)
    • __init__ 构造方法
    • self 的含义
    • 继承(简单了解)

    这个阶段的核心任务:学会用代码「模拟现实世界中的事物和关系」。

    面向对象编程(OOP)的本质其实很简单:你把现实世界中的某个「东西」用代码来描述,然后创建多个具体的「实例」来使用。

    从函数到类的思维升级

    在没有面向对象之前,你的代码是这样组织的:

    程序 = 数据 + 函数
    数据在这里,函数在那里,数据通过参数传给函数

    有了面向对象之后:

    程序 = 对象与对象之间的互动
    数据(属性)和行为(方法)绑定在一起,成为一个"东西"

    一个逐渐深入的理解过程

    第一步:用类来描述一个现实中的事物

    class Dog:
    def __init__(self, name, breed, age):
    self.name = name # 名字
    self.breed = breed # 品种
    self.age = age # 年龄

    def bark(self):
    print(f"{self.name} 说:汪汪!")

    def run(self):
    print(f"{self.name} 正在奔跑")

    # 创建具体的狗(实例化)
    dog1 = Dog("旺财", "金毛", 3)
    dog2 = Dog("小黑", "拉布拉多", 2)

    dog1.bark() # 旺财 说:汪汪!
    dog2.run() # 小黑 正在奔跑

    重点理解 __init__: 这是"构造方法",在创建对象时自动调用。它的作用就是初始化这个对象的属性——给每个新对象"装上"它应该有的数据。

    重点理解 self: self 代表当前正在操作的这个对象本身。当你调用 dog1.bark() 时,Python 自动把 dog1 作为 self 传入。所以 self.name 访问的是 dog1 的 name。

    为什么需要面向对象?一个对比

    没有面向对象(用函数和字典):

    # 每个狗是一个字典
    dog1 = {"name": "旺财", "breed": "金毛", "age": 3}
    dog2 = {"name": "小黑", "breed": "拉布拉多", "age": 2}

    # 操作狗的函数
    def bark(dog):
    print(f"{dog['name']} 说:汪汪!")

    def run(dog):
    print(f"{dog['name']} 正在奔跑")

    bark(dog1) # 旺财 说:汪汪!

    有面向对象:

    class Dog:
    def __init__(self, name, breed, age):
    self.name = name
    self.breed = breed
    self.age = age

    def bark(self):
    print(f"{self.name} 说:汪汪!")

    def run(self):
    print(f"{self.name} 正在奔跑")

    dog1 = Dog("旺财", "金毛", 3)
    dog1.bark() # 清晰:这个狗自己在叫

    面向对象的优势:

  • 数据和操作捆绑在一起——你不需要把字典传来传去
  • 代码更自然——dog1.bark() 读起来就像"让狗叫"
  • 便于扩展——要给所有狗增加一个新功能,只需在类中添加一个方法
  • 继承——可以创建"更具体的种类"
  • 一个更复杂的例子:模拟银行账户

    class BankAccount:
    def __init__(self, owner, initial_balance=0):
    self.owner = owner
    self.balance = initial_balance
    self.transactions = [] # 交易记录

    def deposit(self, amount):
    """存钱"""
    if amount <= 0:
    print("存款金额必须大于0")
    return
    self.balance += amount
    self.transactions.append(f"存入 +{amount}")
    print(f"存入 {amount} 元,余额:{self.balance}")

    def withdraw(self, amount):
    """取钱"""
    if amount <= 0:
    print("取款金额必须大于0")
    return
    if amount > self.balance:
    print(f"余额不足!当前余额:{self.balance}")
    return
    self.balance -= amount
    self.transactions.append(f"取出 -{amount}")
    print(f"取出 {amount} 元,余额:{self.balance}")

    def show_transactions(self):
    """显示交易记录"""
    print(f"\\n=== {self.owner} 的交易记录 ===")
    for t in self.transactions:
    print(t)
    print(f"当前余额:{self.balance}")

    # 使用
    acc = BankAccount("小明", 1000)
    acc.deposit(500) # 存入 500 元,余额:1500
    acc.withdraw(200) # 取出 200 元,余额:1300
    acc.withdraw(2000) # 余额不足!当前余额:1300
    acc.show_transactions()

    这个例子展示了面向对象的真正威力——一个 BankAccount 对象同时包含了数据(owner, balance, transactions)和行为(deposit, withdraw, show_transactions),它们是一个整体。

    继承:从一个类派生出更具体的类

    class Animal:
    """动物的基类"""
    def __init__(self, name):
    self.name = name

    def make_sound(self):
    pass # 暂时不做任何事(由子类具体实现)

    class Dog(Animal): # Dog 继承自 Animal
    def make_sound(self):
    print(f"{self.name} 说:汪汪!")

    class Cat(Animal): # Cat 继承自 Animal
    def make_sound(self):
    print(f"{self.name} 说:喵喵!")

    # 多态:同样的接口,不同的行为
    animals = [Dog("旺财"), Cat("咪咪"), Dog("小黑")]
    for animal in animals:
    animal.make_sound()
    # 输出:
    # 旺财 说:汪汪!
    # 咪咪 说:喵喵!
    # 小黑 说:汪汪!

    继承的核心思想是:子类"是"父类的一种,它继承了父类的所有特性,同时可以有自己的额外特性。

    对于新手的建议

    面向对象是 Python 学习中最后、也是最难的一个阶段。如果你感觉不太理解,不要着急。 你可以继续用函数写代码,等到你积累足够的经验后,自然会遇到"这里用类会更清晰"的场景。

    关键在于:先能用函数写出好代码,再考虑用类让代码更好。

    这个阶段最有效的练习

    练习 1:定义一个 Student 类
    包含属性:name, student_id, scores(字典,科目->分数)
    包含方法:add_score(subject, score), average(), print_report()

    class Student:
    def __init__(self, name, student_id):
    self.name = name
    self.student_id = student_id
    self.scores = {}

    def add_score(self, subject, score):
    self.scores[subject] = score

    def average(self):
    if not self.scores:
    return 0
    return sum(self.scores.values()) / len(self.scores)

    def print_report(self):
    print(f"学生:{self.name}({self.student_id})")
    for subject, score in self.scores.items():
    print(f" {subject}:{score}")
    print(f" 平均分:{self.average():.1f}")

    # 使用
    s = Student("小明", "2024001")
    s.add_score("数学", 95)
    s.add_score("英语", 88)
    s.add_score("语文", 92)
    s.print_report()

    练习 2:想想现实中的对象
    观察你周围的事物——手机、书本、咖啡杯……试着用类的思维来描述它们。手机有什么属性(品牌、颜色、电量……)?有什么方法(打电话、发消息、充电……)?这种练习能帮你建立起"万物皆对象"的编程思维。


    通过这五个阶段的学习,你从最基础的数据操作开始,经历控制流、封装思维、数据结构选择,再到面向对象——你已经建立起了一套完整的编程思维体系。接下来,让我们进入第五章,看看如何用这套体系来解题。

    第五章:遇到编程题,该怎么想?

    好了,前四章我们一直在讲「思维」,这一章我们来点实际的——拿到一道编程题,到底应该按什么步骤来思考?

    这一章我会给你两样东西:

  • 一套通用的解题五步法(任何时候都可以套用)
  • 多个实战案例(跟着我的思路一步步走,注意看我每一步在想什么)

  • 5.1 解题思维流程(五步法)

    这套五步法是我自己总结的,每次做题,都严格按照这五步来走。不要跳步,尤其是前两步。

    题目 -> 1.理解 -> 2.设计思路 -> 3.写代码 -> 4.测试 -> 5.优化
    └─ 先动脑,再动手 ─┘


    第一步:理解题目(5分钟)

    你的任务: 确保你完全搞清楚题目在问什么。

    为什么这一步如此重要? 很多新手答错题,不是因为不会写代码,而是因为没看懂题。你花 5 分钟理解清楚,能省下后面半小时的瞎折腾。

    具体怎么做:

  • 用自己的话复述一遍题目。 复述时强迫自己说出:输入是什么?输出是什么?
  • 划出关键词。 在题目上圈出"递增"、“非空”、“所有可能”、“至少一个”……这些词往往决定了思路。
  • 找一个最简单的例子,手动推演一遍。 用笔在纸上算一遍,验证你对题目的理解。
  • 检查标准: 如果你能用中文把题目讲给另一个人听,并且对方能听懂,那你就算理解了。

    常见错误:

    • 题目还没看完就开始写代码(最常见!)
    • 忽略边界条件暗示("非负整数"意味着要考虑 0)
    • 自己给题目加条件(题目没说排好序,你默认排好序了)

    第二步:设计思路(10分钟)

    你的任务: 想清楚解决思路,不要写代码,绝对不能写代码!

    为什么? 因为一旦开始写代码,你的注意力就会从"想清楚"变成"怎么写对",你的思维会被语法细节绑架,反而想不清楚整体逻辑了。

    具体怎么做:

  • 用中文(或伪代码)写出解决步骤

    • 就像在教一个完全不懂编程的人怎么做
    • 写得越细越好:先做 A,再做 B,如果 C 成立就做 D……
  • 或者画流程图

    • 方框代表操作,菱形代表判断,箭头代表流向
    • 一张图胜过千言万语
  • 或者先写注释大纲

    • 在代码编辑器里,先用中文注释把框架写出来
    • 然后在每个注释下面填充代码
  • 检查标准: 你的步骤详细到「可以把这些步骤交给另一个人(他完全不懂编程),他按步骤做就能得出答案」。

    一种有效的工具:伪代码

    伪代码就是用半中文半编程语言的格式来写思路,不需要遵守任何语法规则:

    // 伪代码示例:找最大数
    1. 假设第一个数是 max
    2. 遍历剩下的每个数:
    3. 如果当前数比 max 大:
    4. 更新 max 为当前数
    5. 输出 max


    第三步:转化为代码(时间不定)

    你的任务: 把第二步的步骤,逐条翻译成 Python 代码。

    怎么做:

  • 一行思路,翻译成几行代码。 把第二步的每一条伪代码,翻译成对应的 Python 语句。
  • 写一小段,就跑一下测试一下。 不要一口气写 50 行再运行——那样出错了你根本找不到是哪里的问题。
  • 先写能跑的代码,再写好代码。 第一版不用追求优雅、不用追求效率、不用追求代码复用——能跑就行。
  • 关键心态:

    "能跑"比"跑得快"更重要。

    先用最简单、最笨的方法做出来,再考虑优化。

    很多新手倒在这一步,是因为他们想一次性写出"完美"的代码——结果写不出来,卡住了,挫败感爆棚。


    5.2 如何把伪代码翻译成 Python 代码?

    这一步是很多新手最卡壳的地方。你明明思路清楚了,但一打开编辑器就大脑空白——不知道"遍历"怎么写、"如果"怎么写、"返回"怎么写。

    这一节,我会给你一个伪代码 → Python 的"翻译词典",以及多个逐步拆解的案例。以后你写代码,就像查字典一样简单。

    核心翻译规则:把中文概念映射到 Python 语法

    下面这张表,建议你抄下来或者截图保存。每次写代码时,对照着用:

    伪代码中的中文概念对应的 Python 语法示例
    定义一个函数 def 函数名(参数): def add(a, b):
    定义一个变量 变量名 = 值 count = 0
    输出/打印 print(内容) print("Hello")
    获取输入 input(提示文字) name = input("你的名字:")
    如果…那么… if 条件: if x > 0:
    否则如果… elif 条件: elif x == 0:
    否则… else: else:
    重复 N 次 for i in range(N): for i in range(5):
    遍历列表每个元素 for item in 列表: for num in nums:
    同时获取下标和值 for i, item in enumerate(列表): for i, num in enumerate(nums):
    当…条件满足时重复 while 条件: while count < 5:
    结束循环 break break
    跳过本次循环 continue continue
    返回结果 return 值 return result
    返回多个值 return a, b return min_val, max_val
    创建一个空列表 列表名 = [] result = []
    往列表末尾加元素 列表.append(元素) result.append(num)
    获取列表长度 len(列表) n = len(nums)
    获取列表第 i 个元素 列表[i] first = nums[0]
    创建一个空字典 字典名 = {} count_map = {}
    字典中查找/设置值 字典[键] 或 字典.get(键) count_map["a"] = 1
    检查键是否在字典中 if 键 in 字典: if key in seen:
    翻译技巧:逐行对照,不要跳步

    核心原则:伪代码的每一行,对应 Python 代码的一到三行。 不要试图把三行伪代码压缩成一行 Python——那样容易出错。

    来看一个完整的翻译过程:

    案例:找出列表中的最大数

    伪代码:

    1. 假设第一个数是 max
    2. 遍历剩下的每个数:
    3. 如果当前数比 max 大:
    4. 更新 max 为当前数
    5. 返回 max

    逐行翻译:

    伪代码行翻译思路Python 代码
    1. 假设第一个数是 max 把列表第一个元素赋给变量 max max_val = nums[0]
    2. 遍历剩下的每个数 从第1个下标(即第二个元素)开始遍历 for i in range(1, len(nums)):
    3. 如果当前数比 max 大 用 if 判断 if nums[i] > max_val:
    4. 更新 max 为当前数 重新赋值 max_val = nums[i]
    5. 返回 max return return max_val

    组合起来:

    def find_max(nums):
    max_val = nums[0] # 1. 假设第一个数是 max
    for i in range(1, len(nums)): # 2. 遍历剩下的每个数
    if nums[i] > max_val: # 3. 如果当前数比 max 大
    max_val = nums[i] # 4. 更新 max 为当前数
    return max_val # 5. 返回 max

    更多翻译案例

    案例 1:计算列表中所有偶数的和

    伪代码:

    1. 初始化 sum = 0
    2. 遍历列表中的每个数:
    3. 如果这个数是偶数:
    4. sum = sum + 这个数
    5. 返回 sum

    翻译过程:

    • “初始化 sum = 0” -> total = 0
    • “遍历列表中的每个数” -> for num in numbers:
    • “如果这个数是偶数” -> if num % 2 == 0:(偶数判断:能被2整除)
    • “sum = sum + 这个数” -> total += num
    • “返回 sum” -> return total

    def sum_of_evens(numbers):
    total = 0 # 1. 初始化
    for num in numbers: # 2. 遍历
    if num % 2 == 0: # 3. 判断偶数
    total += num # 4. 累加
    return total # 5. 返回

    案例 2:统计字符串中每个字符出现的次数

    伪代码:

    1. 创建一个空字典 count_map
    2. 遍历字符串中的每个字符:
    3. 如果这个字符已经在 count_map 中:
    4. count_map[这个字符] = count_map[这个字符] + 1
    5. 否则:
    6. count_map[这个字符] = 1
    7. 返回 count_map

    翻译过程:

    • “创建一个空字典” -> count_map = {}
    • “遍历字符串中的每个字符” -> for char in s:
    • “如果字符已经在字典中” -> if char in count_map:
    • “否则” -> else:

    def count_chars(s):
    count_map = {} # 1. 空字典
    for char in s: # 2. 遍历字符
    if char in count_map: # 3. 已存在?
    count_map[char] += 1 # 4. 次数+1
    else: # 5. 不存在
    count_map[char] = 1 # 6. 设为1
    return count_map # 7. 返回

    案例 3:判断一个数是否为质数

    伪代码:

    1. 如果 n 小于 2:返回 False
    2. 从 2 遍历到 n-1:
    3. 如果 n 能被当前数整除:
    4. 返回 False(不是质数)
    5. 如果循环结束都没找到能整除的:返回 True(是质数)

    翻译过程:

    • “如果 n 小于 2” -> if n < 2:
    • “从 2 遍历到 n-1” -> for i in range(2, n):
    • “能被整除” -> n % i == 0
    • “返回 False” -> return False
    • “循环结束” -> 函数末尾 return True

    def is_prime(n):
    if n < 2: # 1. 小于2不是质数
    return False
    for i in range(2, n): # 2. 从2到n-1
    if n % i == 0: # 3. 能整除?
    return False # 4. 不是质数
    return True # 5. 是质数

    翻译时最常见的三个错误

    错误 1:忘记冒号

    # 错误
    if x > 0
    print("正数")

    # 正确
    if x > 0: # if 后面必须有冒号!
    print("正数")

    错误 2:缩进不一致

    # 错误
    if x > 0:
    print("正数")
    print("大于0") # 缩进不一致!

    # 正确
    if x > 0:
    print("正数")
    print("大于0") # 缩进一致(都是4个空格)

    错误 3:混淆 = 和 ==

    # 错误:在 if 里用了赋值
    if x = 5: # SyntaxError!
    print("x是5")

    # 正确:if 里用比较
    if x == 5: # 判断 x 是否等于 5
    print("x是5")

    练习:自己翻译几道伪代码

    下面给你三道伪代码,请自己尝试翻译成 Python。翻译完再对照答案。

    练习 1:计算阶乘

    伪代码:

    1. 如果 n 小于 0:返回 "错误"
    2. 如果 n 等于 0 或 1:返回 1
    3. result = 1
    4. 从 2 遍历到 n:
    5. result = result * i
    6. 返回 result

    点击查看答案
    def factorial(n):
    if n < 0:
    return "错误"
    if n == 0 or n == 1:
    return 1
    result = 1
    for i in range(2, n + 1):
    result *= i
    return result

    练习 2:反转字符串

    伪代码:

    1. 创建一个空字符串 reversed_str
    2. 从最后一个字符遍历到第一个字符:
    3. 把当前字符加到 reversed_str 末尾
    4. 返回 reversed_str

    点击查看答案
    def reverse_string(s):
    reversed_str = ""
    for i in range(len(s) 1, 1, 1): # 从末尾到开头
    reversed_str += s[i]
    return reversed_str

    练习 3:找出列表中第一个重复出现的数

    伪代码:

    1. 创建一个空集合 seen
    2. 遍历列表中的每个数:
    3. 如果这个数已经在 seen 中:
    4. 返回这个数
    5. 否则:
    6. 把这个数加到 seen 中
    7. 如果没有找到重复的:返回 None

    点击查看答案
    def first_duplicate(nums):
    seen = set()
    for num in nums:
    if num in seen:
    return num
    seen.add(num)
    return None


    5.3 逆向练习:从代码反推伪代码

    真正检验你是否理解了"翻译过程"的方法,是反过来做——给你一段 Python 代码,你把它翻译成伪代码。

    这听起来有点反直觉,但它能逼你去思考:这段代码的本质逻辑是什么?如果不理解代码"在干什么",你是写不出它的伪代码的。

    练习 1:分析下面的代码,写出它的伪代码

    def mystery(numbers):
    result = []
    for n in numbers:
    if n not in result:
    result.append(n)
    return result

    思考过程:

    • result = [] — 创建了一个空列表
    • for n in numbers — 遍历每个元素
    • if n not in result — 检查当前元素是否已经在 result 中
    • result.append(n) — 如果不在,就加进去
    • 最后返回 result

    对应的伪代码:

    1. 创建一个空列表 result
    2. 遍历 numbers 中的每个数 n:
    3. 如果 n 不在 result 中:
    4. 把 n 追加到 result 末尾
    5. 返回 result

    这个函数实际上就是去重——去掉列表中重复的元素。如果你一开始就能看出"这是在去重",说明你已经开始建立"模式识别"的能力了。


    练习 2:分析这段"难一点"的代码

    def transform(data):
    counts = {}
    for item in data:
    if item in counts:
    counts[item] += 1
    else:
    counts[item] = 1
    return counts

    对应的伪代码:

    1. 创建一个空字典 counts
    2. 遍历 data 中的每个元素 item:
    3. 如果 item 已经在 counts 中(出现过):
    4. 把 counts[item] 加 1
    5. 否则(第一次出现):
    6. 设置 counts[item] = 1
    7. 返回 counts

    这就是一个统计频率的函数。理解了"它在统计个数",你就能知道这个函数应该叫什么名字、用在什么场景——而不是只能看到一堆语法细节。


    练习 3:分析这段"还有逻辑"的代码

    def find_first(nums, target):
    for i in range(len(nums)):
    if nums[i] == target:
    return i
    return 1

    对应的伪代码:

    1. 遍历 nums,同时获取下标 i 和值:
    2. 如果当前值等于 target:
    3. 返回当前下标 i
    4. 如果循环结束都没找到,返回 -1

    你发现了吗?从代码反推伪代码的过程,就是把"怎么写的"还原成"想做什么的"。而编程思维训练的核心,就是让你越来越擅长这个"想做什么"的部分。


    翻译过程中常见的三个"坑"

    坑 1:把"判断条件"翻译错了

    伪代码说"如果是偶数",新手可能会写成:

    if num / 2 == 0: # 错误!

    正确应该是:

    if num % 2 == 0: # 除以 2 的余数为 0

    坑 2:忘了初始化

    伪代码说"统计偶数个数",你写了:

    for num in nums:
    if num % 2 == 0:
    even_count += 1

    但忘了在前面写 even_count = 0。新手经常忘记一个变量在第一次使用前必须先赋值(初始化)。

    坑 3:索引越界

    伪代码说"遍历列表的每个元素",新手可能写:

    for i in range(len(nums) + 1): # 多了一个!
    print(nums[i]) # 最后一个 i 会越界

    正确是 range(len(nums))。


    翻译思考:什么时候该用 for,什么时候该用 while?
    场景用哪个理由
    遍历列表的每个元素 for 次数 = 列表长度,已知
    重复做 N 次 for 次数 = N,已知
    直到用户输入 quit 才停止 while 不知道要循环多少次
    猜数字,猜对为止 while 不知道要猜多少次
    逐行读取文件直到文件尾 for 遍历每一行,次数已知
    迭代求近似值直到满足精度 while 不知道要迭代多少次
    翻译思考:什么时候该用列表,什么时候该用字典?
    伪代码中的描述用哪种数据结构
    “把每个符合条件的元素存起来” 列表(result.append(x))
    “记录每个分数对应的学生名字” 字典(name_map[score] = name)
    “找出最大值/最小值/总和” 不需要额外结构,用变量就行
    “统计每个元素的出现次数” 字典(count_map[item] += 1)
    “去掉重复的元素” 集合(set())
    “按顺序访问” 列表
    “快速查找” 字典或集合

    5.4 实战案例一:两数之和(由浅入深)

    题目:

    给定一个列表 nums 和一个目标值 target,请你在该列表中找出和为目标值的两个数的下标。

    例如:nums = [2, 7, 11, 15], target = 9
    输出:[0, 1](因为 nums[0] + nums[1] = 2 + 7 = 9)

    现在我们按照五步法来走一遍。


    第一步:理解题目

    用自己的话复述:

    输入:一个数字列表 nums,一个目标整数 target
    输出:两个下标 [i, j],使得 nums[i] + nums[j] == target
    条件:
    – 假设一定有解
    – 每个元素只能用一次(不能自己加自己)
    – 返回任意一个有效解即可

    手动推演最简单的例子:

    nums = [2, 7, 11, 15], target = 9
    2 + 7 = 9 -> 下标 0 和 1 -> 输出 [0, 1]

    确认理解正确,进入下一步。


    第二步:设计思路

    最直接的想法——“暴力法”:

    1. 从第一个数(i=0)开始
    2. 让它和它后面的每个数(j=i+1, i+2, …)分别相加
    3. 如果等于 target,返回 [i, j]
    4. 如果第一个数试完了都不行,换第二个数继续
    5. 直到试完所有组合

    用伪代码写出来:

    for i 从 0 到 len(nums)-1:
    for j 从 i+1 到 len(nums)-1:
    if nums[i] + nums[j] == target:
    return [i, j]
    如果循环结束都没找到,返回 []

    思路清晰到了足够翻译成代码的程度。进入下一步。


    第三步:转化为代码

    把上面的伪代码逐行翻译成 Python:

    def two_sum(nums, target):
    # 外层循环:遍历每个数
    for i in range(len(nums)):
    # 内层循环:只遍历 i 后面的数
    for j in range(i + 1, len(nums)):
    if nums[i] + nums[j] == target:
    return [i, j]
    return [] # 没找到

    注意几个关键细节:

    • range(len(nums)) 生成 0 到 n-1 的下标
    • range(i+1, len(nums)) 确保 j 始终在 i 后面(避免重复和自加)
    • return 找到了就立刻结束函数

    写完后,在脑子里迅速过一遍:

    • nums = [2, 7, 11, 15], target = 9
    • i=0, j=1: 2+7=9 -> return [0, 1]

    第一版代码能跑了。


    第四步:测试验证

    测试用例 1:正常情况

    nums = [2, 7, 11, 15]
    target = 9
    预期:[0, 1]
    print(two_sum(nums, 9)) # [0, 1]

    测试用例 2:答案在中间(不是第一个)

    nums = [3, 2, 4]
    target = 6
    预期:[1, 2](2+4=6)
    print(two_sum([3, 2, 4], 6)) # [1, 2]

    测试用例 3:两个相同的数

    nums = [3, 3]
    target = 6
    预期:[0, 1]
    print(two_sum([3, 3], 6)) # [0, 1]

    测试用例 4:包含负数

    nums = [-1, -2, -3]
    target = -3
    预期:[0, 1](-1 + -2 = -3)
    print(two_sum([-1, -2, -3], -3)) # [0, 1]

    测试用例 5:只有两个数

    nums = [5, 3]
    target = 8
    预期:[0, 1]
    print(two_sum([5, 3], 8)) # [0, 1]

    所有测试通过!


    第五步:回顾优化

    现在代码能跑了,我们来想想怎么让它更好。

    当前算法的问题:双重循环 -> 时间复杂度 O(n^2)。
    如果 nums 有 10000 个元素,最坏情况下要比较约 5000 万次。

    更好的思路:用字典来优化。

    1. 创建一个空字典 seen = {},用来存"数字 -> 下标"
    2. 遍历 nums,对于每个数 num:
    3. 计算 complement = target – num(和它配对的数)
    4. 如果 complement 已经在 seen 中 -> 找到了!返回 [seen[complement], i]
    5. 否则,把当前 num 和它的下标存到 seen 中

    def two_sum_optimized(nums, target):
    seen = {} # 数字 -> 下标
    for i, num in enumerate(nums):
    complement = target – num
    if complement in seen:
    return [seen[complement], i]
    seen[num] = i
    return []

    这个优化版的思路是:用空间换时间——用一个字典记住"已经见过哪些数",这样每个数只需要检查一次就能知道它是否和之前的某个数配对。

    时间复杂度从 O(n^2) 降到了 O(n),对于大数据量来说提升巨大。

    但正如我之前说的:先用暴力法做出来,再考虑优化。 能做出来,你已经赢了 80% 的人。


    5.5 实战案例二:回文数判断

    让我们用第二个案例来巩固五步法。

    题目:

    判断一个整数是否是回文数。回文数是指正序和倒序读都是一样的整数。

    例如:121 -> True(从左到右和从右到左都是 121)
    -121 -> False(从左到右是 -121,从右到左是 121-)
    10 -> False(从左到右是 10,从右到左是 01)


    第一步:理解题目

    输入:一个整数 x
    输出:布尔值 True(是回文)或 False(不是回文)
    关键理解:
    – 负数一定不是回文(因为有负号)
    – 个位数一定是回文(0-9)
    – 末尾为 0 的正整数一定不是回文(除了 0 本身)
    – 例如:10 -> False, 0 -> True

    手动推演:

    x = 121
    正序:121
    倒序:121
    相同 -> True

    x = -121
    正序:-121
    倒序:121- -> 不同 -> False

    确认理解正确。


    第二步:设计思路

    思路一(最简单):转换为字符串,反转后比较

    1. 把整数转换成字符串
    2. 反转字符串
    3. 比较原字符串和反转后的字符串是否相等
    4. 如果相等返回 True,否则 False

    思路二(不转字符串,纯数学方式):反转一半数字

    1. 处理特殊情况:负数返回 False,个位数返回 True
    2. 每次取原数的最后一位,加到反转数上
    3. 直到原数 <= 反转数
    4. 比较原数和反转数(考虑奇位数的情况)

    对于新手,用思路一就好。 简单、直观、不容易出错。

    1. 把 x 转成字符串 s
    2. 反转 s 得到 reversed_s
    3. 比较 s == reversed_s


    第三步:转化为代码

    def is_palindrome(x):
    # 转成字符串
    s = str(x)
    # 反转字符串
    reversed_s = s[::-1] # 这是 Python 的切片反转语法
    # 比较
    return s == reversed_s

    只用三行代码就搞定了。测试一下:

    print(is_palindrome(121)) # True
    print(is_palindrome(-121)) # False
    print(is_palindrome(10)) # False
    print(is_palindrome(0)) # True
    print(is_palindrome(12321)) # True

    注意 s[::-1] 是 Python 中反转字符串最简洁的方式:

    • s[start:stop:step] -> 当 step 为 -1 时,就是从尾到头取

    你完全可以用更"原始"的方式来反转,以加深理解:

    def is_palindrome_manual(x):
    s = str(x)
    reversed_s = ""
    for char in s:
    reversed_s = char + reversed_s # 把每个字符加在前面
    return s == reversed_s

    代码运行正确。


    第四步:测试验证

    # 测试用例 1:三位数回文
    assert is_palindrome(121) == True

    # 测试用例 2:负数
    assert is_palindrome(-121) == False

    # 测试用例 3:末尾为 0
    assert is_palindrome(10) == False

    # 测试用例 4:0
    assert is_palindrome(0) == True

    # 测试用例 5:个位数
    assert is_palindrome(7) == True

    # 测试用例 6:偶数位数
    assert is_palindrome(1221) == True

    # 测试用例 7:非回文偶数位数
    assert is_palindrome(1231) == False

    print("所有测试通过!")


    第五步:回顾优化

    当前代码已经很简洁了,可以做一些小的改进:

  • 提前排除不可能的情况——负数直接返回 False,可以节省转换字符串的开销
  • 增加可读性——用更清晰的变量名
  • def is_palindrome(x):
    # 负数不可能是回文
    if x < 0:
    return False
    # 个位数一定是回文
    if x < 10:
    return True

    original = str(x)
    reversed_str = original[::-1]
    return original == reversed_str


    5.6 实战案例三:斐波那契数列

    第三个案例来练习"从问题中抽象出模式"的能力。

    题目:

    斐波那契数列是这样定义的:

    • F(0) = 0
    • F(1) = 1
    • F(n) = F(n-1) + F(n-2)(当 n >= 2 时)

    请写一个函数,输入 n,输出 F(n) 的值。

    例如:n = 5 -> 输出 5(因为数列是 0, 1, 1, 2, 3, 5)


    第一步:理解题目

    输入:非负整数 n
    输出:第 n 个斐波那契数
    关键理解:
    – 这是递归定义:F(n) 依赖于 F(n-1) 和 F(n-2)
    – n=0 -> 0
    – n=1 -> 1
    – n=5 -> 0,1,1,2,3,5 -> 第5个是 5

    确认理解。


    第二步:设计思路

    思路一(递归):直接按照数学定义写

    如果 n == 0: 返回 0
    如果 n == 1: 返回 1
    否则: 返回 F(n-1) + F(n-2)

    思路二(循环):从底向上计算

    1. 如果 n == 0: 返回 0
    2. 如果 n == 1: 返回 1
    3. 从 2 开始循环到 n:
    当前值 = 前一个值 + 前两个值
    更新"前两个值"和"前一个值"
    4. 返回当前值

    对于新手,建议先用思路一(递归)来理解概念,再用思路二(循环)来解决实际问题。 因为递归虽然代码简洁,但当 n 较大时效率非常低。


    第三步:转化为代码

    版本 1:递归实现(理解概念)

    def fib_recursive(n):
    if n == 0:
    return 0
    if n == 1:
    return 1
    return fib_recursive(n – 1) + fib_recursive(n – 2)

    这个版本完全按照数学定义来写,非常清晰。但运行 fib_recursive(40) 会非常慢——因为重复计算太多了。

    版本 2:循环实现(实际可用)

    def fib(n):
    if n == 0:
    return 0
    if n == 1:
    return 1

    prev2 = 0 # F(n-2)
    prev1 = 1 # F(n-1)

    for i in range(2, n + 1):
    current = prev1 + prev2 # F(n) = F(n-1) + F(n-2)
    prev2 = prev1 # 往后移一位
    prev1 = current # 往后移一位

    return prev1

    重点理解循环体中的"滚动更新":

    • 开始时:prev2=F(0)=0, prev1=F(1)=1
    • 第 1 轮:current = 1+0 = 1 = F(2),然后 prev2=1, prev1=1
    • 第 2 轮:current = 1+1 = 2 = F(3),然后 prev2=1, prev1=2
    • 第 3 轮:current = 2+1 = 3 = F(4),然后 prev2=2, prev1=3
    • 第 4 轮:current = 3+2 = 5 = F(5)

    你在用**两个变量"滚动"**地记录数列中的值,每次循环都往前推一步。这种滚动更新的模式在很多编程题中都会用到。


    第四步:测试验证

    # 基础情况
    print(fib(0)) # 0
    print(fib(1)) # 1

    # 小数字
    print(fib(5)) # 5
    print(fib(10)) # 55

    # 大数字(验证效率)
    print(fib(30)) # 832040
    print(fib(50)) # 12586269025

    # 对比递归版本的效率
    import time

    start = time.time()
    print(fib_recursive(35)) # 9227465
    print("递归耗时:", time.time() – start)

    start = time.time()
    print(fib(35)) # 9227465
    print("循环耗时:", time.time() – start)
    # 你会发现递归比循环慢了几个数量级!


    第五步:回顾优化

    我们的循环版本已经足够好了——时间复杂度 O(n),空间复杂度 O(1)。但还可以考虑:

  • 增加输入验证
  • def fib(n):
    if not isinstance(n, int) or n < 0:
    return "输入必须是非负整数"
    # … 后续代码不变

  • 使用列表来缓存(适合需要多次查询的场景)
  • class Fibonacci:
    def __init__(self):
    self.cache = {0: 0, 1: 1}

    def get(self, n):
    if n not in self.cache:
    for i in range(len(self.cache), n + 1):
    self.cache[i] = self.cache[i-1] + self.cache[i-2]
    return self.cache[n]

    但对于新手来说,不需要过度优化。能写出正确、可读的循环版本,已经很好了。

    深入理解递归:斐波那契的递归调用树

    递归版本的代码只有短短几行,但它的执行过程远比你想象得复杂。让我们以 fib_recursive(5) 为例,画出它的递归调用树:

    fib_recursive(5)
    ├── fib_recursive(4)
    │ ├── fib_recursive(3)
    │ │ ├── fib_recursive(2)
    │ │ │ ├── fib_recursive(1) -> 1
    │ │ │ └── fib_recursive(0) -> 0
    │ │ └── fib_recursive(1) -> 1
    │ └── fib_recursive(2)
    │ ├── fib_recursive(1) -> 1
    │ └── fib_recursive(0) -> 0
    └── fib_recursive(3)
    ├── fib_recursive(2)
    │ ├── fib_recursive(1) -> 1
    │ └── fib_recursive(0) -> 0
    └── fib_recursive(1) -> 1

    看到问题了吗?fib_recursive(2) 被计算了 3 次,fib_recursive(3) 被计算了 2 次,fib_recursive(1) 被计算了 5 次! 这就是递归版的效率灾难——大量重复计算。

    当 n=5 时,一共调用了 15 次函数。
    当 n=40 时,调用了超过 3.3 亿次——你的计算机要算很久很久。

    记忆化优化:用"备忘录"解决重复计算

    优化思路非常简单:算过的结果就记下来,下次直接用,不用再算。

    def fib_memo(n, memo={}):
    if n in memo: # 查备忘录:算过没有?
    return memo[n] # 算过了,直接取结果

    if n == 0:
    return 0
    if n == 1:
    return 1

    memo[n] = fib_memo(n1, memo) + fib_memo(n2, memo) # 算完记下来
    return memo[n]

    有了备忘录后,每个 n 只计算一次,时间复杂度从 O(2^n) 降到了 O(n)。

    "滚动更新"模式的通用性

    循环版的核心模式——用变量"滚动"地记录最近几个值——是一种非常通用的编程技巧。来看几个经典变体:

    变体 1:求前 N 个斐波那契数的和

    def fib_sum(n):
    if n == 0:
    return 0
    if n == 1:
    return 1

    prev2, prev1 = 0, 1
    total = 1 # 初始 sum = F(0) + F(1) = 0 + 1 = 1
    for i in range(2, n + 1):
    current = prev1 + prev2
    total += current
    prev2, prev1 = prev1, current
    return total

    print(fib_sum(5)) # 0+1+1+2+3+5 = 12

    变体 2:求第 N 个泰波那契数(Tribonacci,三个数相加)

    def tribonacci(n):
    if n == 0:
    return 0
    if n == 1:
    return 1
    if n == 2:
    return 1

    t0, t1, t2 = 0, 1, 1
    for i in range(3, n + 1):
    tn = t0 + t1 + t2
    t0, t1, t2 = t1, t2, tn
    return t2

    print(tribonacci(5)) # 0,1,1,2,4,7 -> 7

    变体 3:爬楼梯问题(经典的斐波那契应用)

    你要爬上一个有 n 级台阶的楼梯,每次可以爬 1 级或 2 级。有多少种不同的爬法?

    这个问题实际上就是斐波那契数列:

    • 爬到第 n 级台阶的方法数 = 爬到第 n-1 级的方法数 + 爬到第 n-2 级的方法数
    • 因为最后一步要么爬 1 级(从第 n-1 级上来),要么爬 2 级(从第 n-2 级上来)

    def climb_stairs(n):
    if n == 1:
    return 1
    if n == 2:
    return 2

    prev2, prev1 = 1, 2
    for i in range(3, n + 1):
    current = prev1 + prev2
    prev2, prev1 = prev1, current
    return prev1

    print(climb_stairs(5)) # 8 种方法

    这些变体展示了同一个核心模式的强大力量:用一个"滚动窗口"记录最近几个状态,每次循环都往前推一步。


    5.7 从问题到代码的通用思维模板

    最后,送你一个我每次解题都会用的思维模板。把它放在手边,每次做题都按这个模板来思考:

    解题思维模板
    =============

    【第一步:理解问题】
    输入:____
    输出:____
    约束条件:____
    手动推演示例:____

    【第二步:设计思路】
    用中文/伪代码写出步骤:
    1. ________
    2. ________
    3. ________
    4. ________

    【第三步:翻译成代码】
    (把第二步的每一步翻译成 Python)

    【第四步:测试验证】
    测试1:____ -> 预期:____ -> 实际:____
    测试2:____ -> 预期:____ -> 实际:____
    测试3:____ -> 预期:____ -> 实际:____

    【第五步:回顾优化(可选)】
    – 代码有重复吗?
    – 变量名清晰吗?
    – 边界情况都考虑了吗?

    使用这个模板时的三条铁律:

  • 没完成第一步,绝不开始第二步。 没理解题目就写代码,是最浪费时间的做法。
  • 没完成第二步,绝不开始写代码。 思路都没想清楚,写出来的代码一定一团糟。
  • 写完代码一定测试。 至少测试正常情况和边界情况。
  • 5.8 从新手到高手的进阶建议

    当你用上面的五步法练习了 20-30 道题之后,你会发现自己的思维方式在不知不觉中发生了变化:

    初级阶段(0-20 题):

    • 关注点:语法对不对?能不能跑起来?
    • 常见问题:看不懂题、不知道从哪下手、不知道怎么把思路变成代码

    中级阶段(20-80 题):

    • 关注点:思路对不对?有没有遗漏的情况?
    • 常见问题:暴力法能做出来,但不知道如何优化

    高级阶段(80+ 题):

    • 关注点:哪种方法效率更高?这个题和之前做过的哪道题类似?
    • 思维方式:看到题目自动开始分类、联想、模式匹配

    你现在在哪里都没关系,重要的是——动手开始做第一道题。

    记住:编程不是弹钢琴,不是越练越熟那么简单;编程是像学游泳——不下水永远学不会。 现在就去打开 LeetCode 或任何刷题平台,做一道题吧。


    例题六:找质数(经典数学问题,从暴力到优化)

    题目:

    输入一个正整数 n,输出 2 到 n 之间所有的质数(素数)。

    质数定义:大于 1 的自然数中,除了 1 和它本身以外不再有其他因数。

    例如:n = 20 -> 输出 [2, 3, 5, 7, 11, 13, 17, 19]


    第一步:理解题目

    输入:一个正整数 n
    输出:从 2 到 n 之间所有的质数列表
    关键理解:
    – 质数只能被 1 和自身整除
    – 1 不是质数(质数的定义是大于 1 的自然数)
    – 2 是最小的质数,也是唯一的偶数质数
    – 例如 n=10 时,质数有:2, 3, 5, 7

    手动验证:判断 7 是不是质数

    7 能不能被 2 整除?7 % 2 = 1 -> 不能
    7 能不能被 3 整除?7 % 3 = 1 -> 不能
    7 能不能被 4 整除?7 % 4 = 3 -> 不能
    7 能不能被 5 整除?7 % 5 = 2 -> 不能
    7 能不能被 6 整除?7 % 6 = 1 -> 不能
    没有能整除的 -> 7 是质数


    第二步:设计思路

    思路一(最暴力的方法):逐个检查

    1. 创建一个空列表 primes
    2. 从 2 遍历到 n:
    3. 假设当前数 num 是质数(设置一个标记 flag = True)
    4. 从 2 遍历到 num-1:
    5. 如果 num 能被当前数整除:
    6. 标记 flag = False,跳出内层循环
    7. 如果 flag 仍然是 True:
    8. 把 num 加入 primes 列表
    9. 输出 primes

    思路二(优化版):只需要检查到 sqrt(num)

    核心优化: 如果 num 不是质数,那么它一定可以写成 a x b 的形式。其中 a 和 b 中至少有一个 <= sqrt(num)。所以只需要检查从 2 到 sqrt(num) 的数就够了。

    例如:检查 37 是不是质数。sqrt(37) ~ 6.08。所以只需要检查 2, 3, 4, 5, 6 能不能整除 37 即可。如果这些都不能,那 37 一定是质数。

    优化后的伪代码:

    1. 创建一个空列表 primes
    2. 从 2 遍历到 n:
    3. 假设当前数 num 是质数
    4. 从 2 遍历到 int(sqrt(num)) + 1:
    5. 如果 num 能被当前数整除:
    6. 标记为不是质数,跳出循环
    7. 如果仍然是质数:
    8. 把 num 加入 primes
    9. 输出 primes

    思路三(埃拉托斯特尼筛法,适合找大量质数):

    思想:从 2 开始,把每个质数的倍数都"筛掉"。

    具体过程(以 n=30 为例):

    1. 先列出 2 到 30 的所有数
    2. 从 2 开始:2 是质数,筛掉所有 2 的倍数(4, 6, 8, 10, …)
    3. 下一个没被筛掉的是 3:3 是质数,筛掉所有 3 的倍数
    4. 下一个没被筛掉的是 5:5 是质数,筛掉所有 5 的倍数
    5. 继续直到 sqrt(30) ~ 5.5
    6. 所有没被筛掉的数就是质数


    第三步:转化为代码

    版本 1:暴力法(最直观,理解原理用)

    def is_prime_v1(num):
    if num < 2:
    return False
    for i in range(2, num): # 从 2 检查到 num-1
    if num % i == 0: # 如果能被整除
    return False # 不是质数
    return True # 是质数

    def find_primes_v1(n):
    primes = []
    for num in range(2, n + 1):
    if is_prime_v1(num):
    primes.append(num)
    return primes

    print(find_primes_v1(20)) # [2, 3, 5, 7, 11, 13, 17, 19]

    版本 2:优化版(只需要检查到 sqrt(num))

    import math

    def is_prime_v2(num):
    if num < 2:
    return False
    if num == 2:
    return True # 2 是特殊的质数
    if num % 2 == 0:
    return False # 大于 2 的偶数一定不是质数

    # 只需要检查到 sqrt(num),步长为 2(跳过偶数)
    for i in range(3, int(math.sqrt(num)) + 1, 2):
    if num % i == 0:
    return False
    return True

    def find_primes_v2(n):
    primes = []
    for num in range(2, n + 1):
    if is_prime_v2(num):
    primes.append(num)
    return primes

    效率对比:

    • v1(暴力法)检查 num=97 时:要从 2 检查到 96,共 95 次除法
    • v2(优化版)检查 num=97 时:sqrt(97) ~ 9.8,只需检查 3, 5, 7, 9 共 4 次!快了 20 多倍

    版本 3:筛法(适合求大范围内的所有质数)

    def sieve_of_eratosthenes(n):
    is_prime = [True] * (n + 1)
    is_prime[0] = is_prime[1] = False # 0 和 1 不是质数

    for i in range(2, int(math.sqrt(n)) + 1):
    if is_prime[i]:
    for j in range(i * i, n + 1, i):
    is_prime[j] = False

    primes = [i for i in range(2, n + 1) if is_prime[i]]
    return primes

    print(sieve_of_eratosthenes(30))

    重点理解筛法的执行过程(以 n=30 为例):

    初始状态:假设所有数都是质数
    [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, …]

    i=2: 2 是质数,筛掉 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30
    [2, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]

    i=3: 3 是质数,筛掉 9, 15, 21, 27
    [2, 3, 5, 7, 11, 13, 17, 19, 23, 25, 29]

    i=5: 5 是质数,筛掉 25
    [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

    筛除完毕!剩下的全是质数。

    筛法的巧妙之处在于:它不是"检查每个数是不是质数",而是"从数字表中批量删除非质数"。 后者的效率远高于前者。


    第四步:测试验证

    print(find_primes_v2(20)) # [2, 3, 5, 7, 11, 13, 17, 19]
    print(find_primes_v2(2)) # [2]
    print(find_primes_v2(1)) # []

    print(sieve_of_eratosthenes(100))
    # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

    print(is_prime_v2(17)) # True
    print(is_prime_v2(1)) # False
    print(is_prime_v2(2)) # True
    print(is_prime_v2(4)) # False


    第五步:回顾优化

    质数问题教给我们三个重要的优化思想:

    1. 减少搜索范围: 检查质数时只需要到 sqrt(n),因为因子成对出现。这是数学规律带来的效率提升。

    2. 跳过必然不成立的情况: 大于 2 的偶数一定不是质数,直接跳过。

    3. 用空间换时间: 筛法用一个布尔列表记录了"每个数是不是质数"的信息,用额外的空间换来了极快的时间。

    三种算法的效率对比(n=100000):

    方法执行次数时间(约)
    暴力法 v1 ~5 亿次 几十秒
    优化版 v2 ~15 万次 0.1 秒
    筛法 v3 ~10 万次 0.02 秒

    这道题完美地展示了:同样的结果,不同的算法,效率可以相差上千倍。 这就是算法思维的威力。


    例题七:特殊的因子数(来自 CSDN 专栏的进阶题)

    题目:

    存在一类特殊的数字,其因子只包含 3、5、7 这三个质数。
    例如:3, 5, 7, 9, 15, 21, 25, 27……
    (注意:9=3×3,15=3×5,21=3×7,25=5×5,27=3x3x3)
    请问第 50 个这样的数是多少?

    题目来源: CSDN 专栏「Python习题小测」- 特殊的因子数

    这道题比前面的题要难不少。如果你觉得太难,可以先跳过,等水平提高后再回来挑战。但如果你想挑战一下自己——那就来吧!


    第一步:理解题目

    输入:无(要求直接输出第 50 个数)
    输出:第 50 个"因子只包含 3, 5, 7"的数
    关键理解:
    – 这些数的因子只能由 3, 5, 7 组成(可以重复使用)
    – 也就是说:这些数 = 3^a x 5^b x 7^c(a, b, c 是非负整数)

    手动列出前几个数,找规律:

    序号数字因子分解
    1 1 3^0 x 5^0 x 7^0
    2 3 3^1 x 5^0 x 7^0
    3 5 3^0 x 5^1 x 7^0
    4 7 3^0 x 5^0 x 7^1
    5 9 3^2 x 5^0 x 7^0
    6 15 3^1 x 5^1 x 7^0
    7 21 3^1 x 5^0 x 7^1

    第二步:设计思路

    思路一(暴力法,不可行):
    遍历从 1 开始的所有正整数,对每个数分解质因数,检查是否只包含 3, 5, 7。
    -> 要找第 50 个,需要遍历非常大的范围,效率极低。

    思路二(生成法,可行):
    因为每个数 = 3^a x 5^b x 7^c,我们可以按从小到大的顺序"生成"这些数。

    技巧:用三个指针,类似"合并三个有序序列"

    1. 用一个列表 special 来存放已生成的数字,从 [1] 开始
    2. 用三个指针 p3, p5, p7,分别指向 special 中下一个要乘以 3、5、7 的数
    3. 每次从三个候选中选出最小的:
    候选1 = special[p3] x 3
    候选2 = special[p5] x 5
    候选3 = special[p7] x 7
    最小的那个就是下一个特殊的因子数
    4. 如果某个候选被选中了,它对应的指针就向前移动一位
    5. 如果多个候选值相同,对应的指针都要往前移(避免重复)
    6. 重复直到 special 中有 50 个数

    这个思路和"丑数"问题完全一样(Ugly Number II)。


    第三步:转化为代码

    def find_special_number(n):
    special = [1] # 已经生成的数字列表
    p3 = p5 = p7 = 0 # 三个指针

    while len(special) < n:
    next3 = special[p3] * 3
    next5 = special[p5] * 5
    next7 = special[p7] * 7

    next_num = min(next3, next5, next7)
    special.append(next_num)

    if next_num == next3:
    p3 += 1
    if next_num == next5:
    p5 += 1
    if next_num == next7:
    p7 += 1

    return special[1]

    print(find_special_number(50))

    用前几个数字的手动推演来理解:

    初始:special = [1], p3=0, p5=0, p7=0

    第1轮:next3=3, next5=5, next7=7
    min = 3 -> special = [1, 3], p3=1

    第2轮:next3=9, next5=5, next7=7
    min = 5 -> special = [1, 3, 5], p5=1

    第3轮:next3=9, next5=25, next7=7
    min = 7 -> special = [1, 3, 5, 7], p7=1

    第4轮:next3=9, next5=25, next7=49
    min = 9 -> special = [1, 3, 5, 7, 9], p3=2

    这个过程保证每次选出的都是"最小的下一个",special 一定是从小到大排列的。


    第四步:测试验证

    # 输出前 10 个
    special = [1]; p3 = p5 = p7 = 0
    while len(special) < 10:
    next3 = special[p3] * 3
    next5 = special[p5] * 5
    next7 = special[p7] * 7
    next_num = min(next3, next5, next7)
    special.append(next_num)
    if next_num == next3: p3 += 1
    if next_num == next5: p5 += 1
    if next_num == next7: p7 += 1
    print(special) # [1, 3, 5, 7, 9, 15, 21, 25, 27, 35]

    print(find_special_number(50))


    第五步:回顾优化

    这道题的核心思想是**「用生成替代检查」**——不在一大堆数中逐个检查"是不是我要的",而是直接"造出"我要的数。

    这个方法的精妙之处:

  • 三个指针各自独立地"遍历"已经生成的数列——把当前已有的数乘以 3、5、7,得到的候选中一定包含下一个要的数。
  • 使用 min() 保证顺序——每次只取最小的候选,保证了结果有序。
  • 处理重复值——当一个值可以通过多种方式得到(如 15=3×5=5×3),多个指针同时前移,避免重复。
  • 这道题在 LeetCode 上有一个几乎一模一样的问题——第 264 题「丑数 II」(Ugly Number II),区别只是丑数的因子是 2, 3, 5。如果完全理解这道题,你就不仅掌握了一个经典算法,也掌握了"多指针生成序列"这一类问题的解法。


    这道题给你的启示:

    • 不能用的方法: 暴力遍历 -> 范围太大,效率太低
    • 应该用的方法: 直接生成 -> 从已知推未知,每次只取最小
    • 核心模式: 多指针 + 三个有序序列的归并

    当你遇到"找第 N 个满足某种条件的数"的问题时,优先想一想:能不能不去"找",而是直接"造"?

    5.9 更多经典例题详解

    这一节,我们从 CSDN 专栏「Python习题小测」中挑选了几道有代表性的题目,用五步法逐一拆解。这些题目覆盖了列表操作、字符串处理、字典应用、数学计算等多个知识点,非常适合用来巩固前面学到的思维方法。


    例题一:移动零元素(列表操作经典题)

    题目:

    对于一个列表,在保持非零元素相对顺序的同时,将元素中所有的数字 0 移动到末尾。

    例如,输入 [0, 1, 0, 3, 12],输出 [1, 3, 12, 0, 0]

    第一步:理解题目

    输入:一个整数列表
    输出:一个新的列表,所有非零元素保持原来的相对顺序,所有 0 移到末尾
    关键理解:
    – "保持相对顺序"意味着不能排序,只能移动 0
    – 0 的数量不变,只是位置变了
    – 例如 [0,1,0,3,12] -> [1,3,12,0,0](两个 0 都在末尾)

    第二步:设计思路

    思路一(新建列表法):

    1. 创建一个空列表 result
    2. 遍历原列表:
    3. 如果当前元素不是 0:
    4. 把它加到 result 中
    5. 计算原列表中有多少个 0
    6. 在 result 末尾补上相应数量的 0
    7. 返回 result

    思路二(双指针法,更高级):

    1. 用两个指针:一个遍历所有元素,一个记录"非零元素应该放的位置"
    2. 遍历列表:
    3. 遇到非零元素,把它放到"非零位置",然后非零位置后移
    4. 遍历完后,从"非零位置"到末尾全部填 0

    对于新手,用思路一即可。 思路二虽然更高效,但理解成本更高。

    第三步:转化为代码

    def move_zeros(nums):
    # 1. 收集所有非零元素
    non_zeros = []
    zero_count = 0
    for num in nums:
    if num != 0:
    non_zeros.append(num)
    else:
    zero_count += 1

    # 2. 补上 0
    result = non_zeros + [0] * zero_count
    return result

    第四步:测试验证

    # 测试1:正常情况
    print(move_zeros([0, 1, 0, 3, 12])) # [1, 3, 12, 0, 0]

    # 测试2:没有 0
    print(move_zeros([1, 2, 3])) # [1, 2, 3]

    # 测试3:全是 0
    print(move_zeros([0, 0, 0])) # [0, 0, 0]

    # 测试4:空列表
    print(move_zeros([])) # []

    # 测试5:只有一个元素
    print(move_zeros([0])) # [0]
    print(move_zeros([5])) # [5]

    第五步:回顾优化

    当前代码已经很好了,但可以更简洁——用列表推导式:

    def move_zeros(nums):
    non_zeros = [x for x in nums if x != 0]
    zeros = [0] * (len(nums) len(non_zeros))
    return non_zeros + zeros

    或者一步到位:

    def move_zeros(nums):
    return [x for x in nums if x != 0] + [0] * nums.count(0)

    但新手不需要追求这种"一行代码"的写法。清晰、可读、正确,比"炫技"重要得多。


    例题二:判断列表是否有重复元素(集合的经典应用)

    题目:

    从键盘输入一个元素个数大于 2 的列表,判断该列表中是否有重复元素。

    例如:

    • 输入 [1, 2, 1, 3, 1, 4] -> 输出 有重复元素
    • 输入 [1, 2, 3, 4] -> 输出 没有重复元素

    第一步:理解题目

    输入:一个列表(元素个数 > 2)
    输出:字符串 "有重复元素" 或 "没有重复元素"
    关键理解:
    – 只要列表中有任意两个元素相同,就算有重复
    – 不需要找出具体哪些重复,只需要判断"有"或"没有"

    第二步:设计思路

    思路一(集合去重法):

    1. 把列表转成集合(集合会自动去重)
    2. 比较集合的长度和原列表的长度
    3. 如果长度不同 -> 有重复
    4. 如果长度相同 -> 没有重复

    思路二(遍历检查法):

    1. 创建一个空集合 seen
    2. 遍历列表中的每个元素:
    3. 如果这个元素已经在 seen 中 -> 有重复,返回
    4. 否则 -> 把这个元素加到 seen 中
    5. 如果遍历完都没找到重复 -> 没有重复

    第三步:转化为代码

    版本 1:集合去重法(最简洁)

    def has_duplicates(lst):
    if len(lst) != len(set(lst)):
    return "有重复元素"
    else:
    return "没有重复元素"

    版本 2:遍历检查法(更直观,适合理解原理)

    def has_duplicates(lst):
    seen = set()
    for item in lst:
    if item in seen:
    return "有重复元素"
    seen.add(item)
    return "没有重复元素"

    第四步:测试验证

    print(has_duplicates([1, 2, 1, 3, 1, 4])) # 有重复元素
    print(has_duplicates([1, 2, 3, 4])) # 没有重复元素
    print(has_duplicates([1, 1, 1])) # 有重复元素
    print(has_duplicates([1, 2, 2])) # 有重复元素

    第五步:回顾优化

    版本 1 非常简洁,但有一个小问题:它把整个列表转成了集合,对于大列表会占用额外的内存。版本 2 是"提前退出"的——一旦发现重复就立即返回,不需要遍历完整个列表。

    对于新手来说,两个版本都可以。重点理解:集合的「自动去重」特性是解决"重复"问题的利器。


    例题三:求最大公约数(辗转相除法)

    题目:

    从键盘输入两个正整数,求它们的最大公约数。

    例如:输入 36, 24 -> 输出 36和24的最大公约数为:12

    第一步:理解题目

    输入:两个正整数 a, b
    输出:它们的最大公约数(GCD)
    关键理解:
    – 最大公约数是能同时整除 a 和 b 的最大正整数
    – 例如 36 和 24 的公约数有 1, 2, 3, 4, 6, 12,最大的是 12

    第二步:设计思路

    思路一(暴力法):从最小数开始往下试

    1. 令 min_val = min(a, b)
    2. 从 min_val 开始,逐个往下检查:
    3. 如果当前数能同时整除 a 和 b:
    4. 这就是最大公约数,返回它
    5. 否则继续往下检查

    思路二(辗转相除法/欧几里得算法):

    1. 用 a 除以 b,得到余数 r
    2. 如果 r == 0:b 就是最大公约数
    3. 否则:令 a = b, b = r,重复步骤 1

    对于新手,先用思路一理解概念,再用思路二掌握高效算法。

    第三步:转化为代码

    版本 1:暴力法

    def gcd_brute_force(a, b):
    min_val = min(a, b)
    for i in range(min_val, 0, 1): # 从大到小检查
    if a % i == 0 and b % i == 0:
    return i

    版本 2:辗转相除法

    def gcd(a, b):
    while b != 0:
    r = a % b # 求余数
    a = b # 原来的除数变成新的被除数
    b = r # 余数变成新的除数
    return a

    重点理解辗转相除法的执行过程:

    gcd(36, 24):
    第1轮: a=36, b=24 -> 36 % 24 = 12 -> a=24, b=12
    第2轮: a=24, b=12 -> 24 % 12 = 0 -> a=12, b=0
    b=0,循环结束,返回 a=12

    第四步:测试验证

    print(gcd(36, 24)) # 12
    print(gcd(48, 18)) # 6
    print(gcd(7, 5)) # 1(互质)
    print(gcd(100, 25)) # 25
    print(gcd(1, 1)) # 1

    第五步:回顾优化

    辗转相除法的时间复杂度是 O(log(min(a,b))),比暴力法快得多。对于大数字,暴力法可能需要几十亿次循环,而辗转相除法只需要几十次。

    这道题的经典之处在于:它展示了「数学算法」如何大幅提升程序效率。 很多编程题的背后,都有数学规律在支撑。


    例题四:阿姆斯特朗数(水仙花数的推广)

    题目:

    求 1000 以内的阿姆斯特朗数。

    阿姆斯特朗数:一个 n 位数,其各位数字的 n 次方之和等于它本身。

    • 例如:153 是 3 位数,1³ + 5³ + 3³ = 1 + 125 + 27 = 153
    • 例如:9474 是 4 位数,9⁴ + 4⁴ + 7⁴ + 4⁴ = 6561 + 256 + 2401 + 256 = 9474

    第一步:理解题目

    输入:范围上限 1000
    输出:1000 以内所有的阿姆斯特朗数
    关键理解:
    – 1 位数:1^1 = 1,所以所有 1 位数都是阿姆斯特朗数
    – 2 位数:没有(可以验证)
    – 3 位数:需要检查各位立方和是否等于本身

    第二步:设计思路

    1. 从 1 遍历到 999:
    2. 把当前数转成字符串,获取每一位数字
    3. 计算位数 n(字符串长度)
    4. 计算各位数字的 n 次方之和
    5. 如果和等于当前数 -> 是阿姆斯特朗数,输出

    第三步:转化为代码

    def is_armstrong(n):
    s = str(n) # 转成字符串,方便获取每一位
    length = len(s) # 位数
    total = 0
    for digit in s:
    total += int(digit) ** length # 每位数字的 length 次方
    return total == n

    # 找出 1000 以内的所有阿姆斯特朗数
    result = []
    for i in range(1, 1000):
    if is_armstrong(i):
    result.append(i)

    print(result) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 153, 370, 371, 407]

    第四步:测试验证

    # 验证已知的阿姆斯特朗数
    print(is_armstrong(153)) # True (1^3 + 5^3 + 3^3 = 153)
    print(is_armstrong(370)) # True (3^3 + 7^3 + 0^3 = 370)
    print(is_armstrong(371)) # True (3^3 + 7^3 + 1^3 = 371)
    print(is_armstrong(407)) # True (4^3 + 0^3 + 7^3 = 407)

    # 验证非阿姆斯特朗数
    print(is_armstrong(123)) # False (1^3 + 2^3 + 3^3 = 36 ≠ 123)
    print(is_armstrong(100)) # False (1^3 + 0^3 + 0^3 = 1 ≠ 100)

    第五步:回顾优化

    这道题的关键技巧是**「把数字转成字符串来处理各位数字」**。这是一种非常常见的编程技巧——数字不方便逐位操作时,转成字符串就好处理了。

    如果你想挑战更高难度,可以试着找出 10000 以内的所有阿姆斯特朗数(提示:需要检查 4 位数)。


    例题五:二分查找(从线性到对数的思维飞跃)

    题目:

    用户输入一个升序排列的、无重复数字的整数列表,以及一个目标值 target。
    搜索 target 是否在列表中。如果存在,输出目标值的索引;否则输出 “not find”。

    例如:

    • 输入 [-1, 0, 3, 4, 6, 10, 13, 14],target = 13 -> 输出 6
    • 输入 [-1, 0, 3, 4, 6, 10, 13, 14],target = 5 -> 输出 not find

    第一步:理解题目

    输入:一个升序列表 lst,一个目标值 target
    输出:target 在列表中的索引,或 "not find"
    关键条件:
    – 列表是升序排列的(这是使用二分查找的前提!)
    – 列表中没有重复数字

    第二步:设计思路

    思路一(线性查找):从头到尾逐个检查

    1. 遍历列表的每个元素:
    2. 如果当前元素等于 target -> 返回当前索引
    3. 如果遍历完都没找到 -> 返回 "not find"

    思路二(二分查找):每次砍掉一半

    1. 定义左边界 left = 0,右边界 right = len(lst) – 1
    2. 当 left <= right 时循环:
    3. 计算中间位置 mid = (left + right) // 2
    4. 如果 lst[mid] == target -> 找到了,返回 mid
    5. 如果 lst[mid] < target -> target 在右半边,left = mid + 1
    6. 如果 lst[mid] > target -> target 在左半边,right = mid – 1
    7. 循环结束还没找到 -> 返回 "not find"

    第三步:转化为代码

    版本 1:线性查找(简单直观)

    def linear_search(lst, target):
    for i in range(len(lst)):
    if lst[i] == target:
    return i
    return "not find"

    版本 2:二分查找(高效,必须掌握)

    def binary_search(lst, target):
    left = 0
    right = len(lst) 1

    while left <= right:
    mid = (left + right) // 2
    if lst[mid] == target:
    return mid
    elif lst[mid] < target:
    left = mid + 1 # 目标在右半边
    else:
    right = mid 1 # 目标在左半边

    return "not find"

    重点理解二分查找的执行过程:

    lst = [-1, 0, 3, 4, 6, 10, 13, 14], target = 13

    第1轮: left=0, right=7, mid=3 -> lst[3]=4 < 13 -> left=4
    第2轮: left=4, right=7, mid=5 -> lst[5]=10 < 13 -> left=6
    第3轮: left=6, right=7, mid=6 -> lst[6]=13 == 13 -> 返回 6

    只用了 3 次比较就找到了!线性查找需要 7 次。

    第四步:测试验证

    lst = [1, 0, 3, 4, 6, 10, 13, 14]

    # 测试存在的元素
    print(binary_search(lst, 13)) # 6
    print(binary_search(lst, 1)) # 0
    print(binary_search(lst, 14)) # 7

    # 测试不存在的元素
    print(binary_search(lst, 5)) # not find
    print(binary_search(lst, 100)) # not find

    # 测试边界
    print(binary_search([5], 5)) # 0
    print(binary_search([5], 3)) # not find

    第五步:回顾优化

    二分查找的核心优势:每次比较都能排除一半的元素。

    • 线性查找:1000 个元素最坏需要 1000 次
    • 二分查找:1000 个元素最多只需要 10 次(因为 2^10 = 1024)

    但二分查找有一个重要前提:列表必须是有序的。 如果列表无序,你只能使用线性查找。

    这道题教会我们:选择正确的算法,可以让效率提升成百上千倍。


    5.10 练习题库(自己动手做)

    下面是几道从 CSDN 专栏中精选的练习题,请用五步法独立完成。做完后可以对照专栏中的参考答案。

    练习 1:统计最高最低分和不及格人数

    从键盘输入 5 个考试成绩(0-100 的整数),计算最高分、最低分和不及格的人数(低于 60 分)。

    练习 2:删除字符串的指定字符

    输入一个字符串和一个要删除的字符,去掉字符串中该字符的所有出现,输出新字符串。
    例如:输入 "hello world" 和 "l" -> 输出 "heo word"

    练习 3:生成随机密码

    生成 5 个 6 位密码,每个密码由大小写字母和数字随机组成。将密码升序排序后输出。

    练习 4:单词个数统计

    输入一个英文句子,统计其中不同单词的个数(不区分大小写,单词之间用空格隔开)。
    例如:输入 "one little two little three little boys" -> 输出 5

    练习 5:用字典统计字符出现次数

    输入一个字符串,统计每个字符出现的次数,按字符顺序输出。
    例如:输入 "中国人中国魂Fighting" -> 输出每个字符及其出现次数


    通过这一节的 5 道经典例题和 5 道练习题,你应该对「如何把思路变成代码」有了更深的体会。记住:每道题都走一遍五步法,不要跳步。 坚持下去,你会越来越熟练的。

    第六章:常见思维误区与「解毒」

    这一章,我们来聊聊那些阻碍你进步的错误观念。每个误区,我都会告诉你怎么"解毒"。

    误区一:背代码 = 学编程

    症状:

    • 把代码当课文背
    • 觉得「记不住某个函数就等于不会」
    • 花大量时间背语法细节

    真相:
    编程不是背课文。专业的编程者也会频繁查文档、查 Stack Overflow、查 Google。没有人能把所有东西记住。

    解毒:

    • 允许自己记不住。忘了就查。
    • 重点记住的是「思路」而不是「具体写法」
    • 学会用「搜索引擎 + 文档」来弥补记忆力
    • 写代码的频率比背代码的时长重要一百倍

    误区二:追求「标准答案」

    症状:

    • 觉得每道题只有一个「正确写法」
    • 写出来的代码和别人不一样就觉得是自己错了
    • 花很长时间纠结「这个写法是不是最标准的」

    真相:
    编程不像选择题,没有标准答案。一百个程序员写同一个功能,可能有一百种不同的写法。 只要代码正确、可读、能维护,就是好代码。

    解毒:

    • 先保证正确,再考虑优雅
    • 比较不同写法时,关注的是「哪种更清晰」,而不是「哪种更标准」
    • 你的代码可以有你的风格

    误区三:害怕犯错

    症状:

    • 写代码时战战兢兢,怕写错
    • 看到报错就慌
    • 不敢自己尝试新东西,怕把代码弄坏了

    真相:
    我认识的所有优秀的程序员——包括工作了十几年资深工程师——写代码的第一版都会出错。没有人能一次写对,没有人。

    解毒:

    • 把报错当成「计算机在帮你检查作业」
    • 每次报错都是学习机会,你在修复的过程中会变得更理解代码
    • 心态转换:「又出错了,太好了,我又可以学到东西了!」

    误区四:只看不练

    症状:

    • 看了很多教程、很多视频,但自己很少动手
    • 觉得「理解」就够了
    • 觉得写代码是「懂了之后自然就会的事」

    真相:
    编程是一门手艺,不是一门知识。你可以通过看书来学习历史知识,但你不能通过看书来学会游泳。编程也是一样——它是一种「动手的技能」,必须通过动手来掌握。

    解毒:

    • 学习比例:看 20%,练 80%
    • 每学一个知识点,立刻自己写代码练习
    • 看完一个教程的章节,不要急着看下一章——先关掉教程,自己把代码写出来
    • 每天至少写 30 分钟代码,保持手感

    结语:编程思维是一生的财富

    写到这里,这份教程已经很长了。如果你能看到这里,说明你是真的想学好编程。恭喜你——你比大多数人已经多了一份坚持。

    最后,我想对你说几句心里话。

    当你刚开始学编程的时候,你会觉得很难、很挫败、很不适应。这是一种非常正常的感受。每一个人都经历过这个阶段。

    但请相信我:编程思维一旦建立起来,它改变的不只是你写代码的方式——它会改变你思考问题的方式。

    你会发现:

    • 你开始习惯把复杂的问题一步一步地拆解
    • 你开始习惯用精确的语言表达你的想法
    • 你开始习惯从错误中学习而不是被错误打倒
    • 你开始习惯「试错 → 调整 → 再试」的迭代方式

    这些能力,不只是编程需要——生活中的很多问题,都可以用编程思维来解决。

    所以,坚持下去。

    如果你遇到困难,不妨重新翻开这份教程的第一章,问问自己:

    • 我「理解」题目了吗?
    • 我「分解」问题了吗?
    • 我「设计思路」了吗?
    • 我「一步步翻译」成代码了吗?

    这是一条不容易的路,但绝对是一条值得走的路。

    加油 🚀


    赞(0)
    未经允许不得转载:171主机测评 » Python 编程思维训练指南 from NWAFU
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址