Python 编程思维训练指南 from NWAFU

🐍 Python 编程思维训练指南

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


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

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

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

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

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

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

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

什么意思呢?

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

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

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

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

这就像什么呢?

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

编程也是这样。

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

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


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

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

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

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

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

你会自动理解:

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

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

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

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

计算机的反应是:

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

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


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

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

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

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

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

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

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

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

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

  1. 你搞清楚问题怎么解决(这在计算机领域叫做「算法」)
  2. 你用计算机能听懂的语言,把解决步骤写下来(这在计算机领域叫做「编码」)

第一步的核心是 「人脑思考」——你需要自己想清楚怎么做。
第二步的核心是 「翻译」——你把想清楚的步骤,翻译成 Python 代码。

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

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

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


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

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

先有思路,再有代码。

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

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

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

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

不是的。语法也很重要。

一个好的类比是这样的:

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

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

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

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

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


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

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

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

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

还记得那个经典笑话吗?

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

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

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

举个例子:

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

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

但如果你把它分解一下:

  1. 先搞清楚:从哪里下载猫咪图片?(找到图片来源网站,或者知道有哪些提供图片的 API)
  2. 再搞清楚:怎么下载一张图片?(发一个网络请求,把返回的内容存成文件)
  3. 再搞清楚:怎么下载100张?(把第2步重复100次)
  4. 再搞清楚:怎么保存图片?(给每张图片取不同的名字,存到某个文件夹)

你看,分解之后,每个小问题看起来就简单多了。单独来看,「发一个网络请求」— 这个你可以学;「把内容存成文件」— 这个你也可以学;「重复100次」— 用个循环就行。

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

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

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

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

编程中也是这样——

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

比如:

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

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

  1. 先假设第一个是最大的
  2. 遍历剩下的,每次遇到更大的就替换

识别模式的训练方法:
每做完一道题,问自己一个问题:「这道题和我之前做过的哪道题很像?它们的共同点是什么?」时间久了,你就会建立起自己的「模式库」。

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……)—— 类型不对、下标越界
  • 逻辑错误——代码能跑,但结果不对

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

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

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

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

调试思维的训练方法:

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

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

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 的值
1a = 1010
2b = a + 51015
3a = a + 11115
4c = a + b111526

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

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

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

练习 3:变量交换
你有两个变量 a = 5b = 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次1total = 0 + 11
第2次2total = 1 + 23
第3次3total = 3 + 36
第4次4total = 6 + 410

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

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 有两个作用:

  1. 给出函数的计算结果
  2. 立即结束函数的执行
# 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_evensquaresum_of_even_squares 三个函数,层层组合,完成了"求列表中所有偶数的平方和"这个功能。每个函数只做一件事,但组合起来就能做复杂的事。

函数训练的终极目标

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

  1. 这个需求可以拆成哪几个功能?
  2. 每个功能用一个什么函数来实现?
  3. 这些函数之间怎么调用、怎么组合?

这就是函数式的思维方式——以「功能」为基本单位来思考问题。

这个阶段最有效的练习

练习 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. 从一篇文章中统计出现了哪些不同的单词

答案: 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 访问的是 dog1name

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

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

# 每个狗是一个字典
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()  # 清晰:这个狗自己在叫

面向对象的优势:

  1. 数据和操作捆绑在一起——你不需要把字典传来传去
  2. 代码更自然——dog1.bark() 读起来就像"让狗叫"
  3. 便于扩展——要给所有狗增加一个新功能,只需在类中添加一个方法
  4. 继承——可以创建"更具体的种类"
一个更复杂的例子:模拟银行账户
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:想想现实中的对象
观察你周围的事物——手机、书本、咖啡杯……试着用类的思维来描述它们。手机有什么属性(品牌、颜色、电量……)?有什么方法(打电话、发消息、充电……)?这种练习能帮你建立起"万物皆对象"的编程思维。


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

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

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

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

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

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

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

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

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

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

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

具体怎么做:

  1. 用自己的话复述一遍题目。 复述时强迫自己说出:输入是什么?输出是什么?
  2. 划出关键词。 在题目上圈出"递增"、“非空”、“所有可能”、“至少一个”……这些词往往决定了思路。
  3. 找一个最简单的例子,手动推演一遍。 用笔在纸上算一遍,验证你对题目的理解。

检查标准: 如果你能用中文把题目讲给另一个人听,并且对方能听懂,那你就算理解了。

常见错误:

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

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

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

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

具体怎么做:

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

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

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

    • 在代码编辑器里,先用中文注释把框架写出来
    • 然后在每个注释下面填充代码

检查标准: 你的步骤详细到「可以把这些步骤交给另一个人(他完全不懂编程),他按步骤做就能得出答案」。

一种有效的工具:伪代码

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

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

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

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

怎么做:

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

关键心态:

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

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

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


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:
结束循环breakbreak
跳过本次循环continuecontinue
返回结果return 值return result
返回多个值return a, breturn 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把列表第一个元素赋给变量 maxmax_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. 返回 maxreturnreturn 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("所有测试通过!")

第五步:回顾优化

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

  1. 提前排除不可能的情况——负数直接返回 False,可以节省转换字符串的开销
  2. 增加可读性——用更清晰的变量名
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)。但还可以考虑:

  1. 增加输入验证
def fib(n):
    if not isinstance(n, int) or n < 0:
        return "输入必须是非负整数"
    # ... 后续代码不变
  1. 使用列表来缓存(适合需要多次查询的场景)
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(n-1, memo) + fib_memo(n-2, 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:____ -> 预期:____ -> 实际:____

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

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

  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=3x3,15=3x5,21=3x7,25=5x5,27=3x3x3)
请问第 50 个这样的数是多少?

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

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


第一步:理解题目

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

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

序号数字因子分解
113^0 x 5^0 x 7^0
233^1 x 5^0 x 7^0
353^0 x 5^1 x 7^0
473^0 x 5^0 x 7^1
593^2 x 5^0 x 7^0
6153^1 x 5^1 x 7^0
7213^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))

第五步:回顾优化

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

这个方法的精妙之处:

  1. 三个指针各自独立地"遍历"已经生成的数列——把当前已有的数乘以 3、5、7,得到的候选中一定包含下一个要的数。
  2. 使用 min() 保证顺序——每次只取最小的候选,保证了结果有序。
  3. 处理重复值——当一个值可以通过多种方式得到(如 15=3x5=5x3),多个指针同时前移,避免重复。

这道题在 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 分钟代码,保持手感

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

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

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

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

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

你会发现:

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

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

所以,坚持下去。

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

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

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

加油 🚀


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值