当数学遇上编程,所有的边界都打破了(修订版)

说明:本文为作者原创,第一时间会发到微信公众号:「大圣不是圣」,由于微信公众号文章发布后不方便修改,因此这里也会同步一篇,错误或修订都更新在这里。

欢迎分享,若需转载请联系微信(ddupxyz),谢谢!

用此文向尊敬的 Seymour Papert(西摩.派珀特)致敬!作为一名教育家、数学家和人工智能专家,Papert 在他 30 年前的著作 《因计算机而强大》 中就已经不可思议地把通过编程去学习的本质讲得非常清楚了!


上面这句话源自 1995 年的纪录片《乔布斯:遗失的访谈》(Steve Jobs: The Lost Interview)中对乔布斯的采访。

很多地方,尤其是各类编程机构都在引用上面乔布斯这句话,作为宣传用语,但很少见到有人真正从实践上去解读或印证这句话背后的意义:编程是如何教会我们思考的?

当初创业开始做青少年编程教育时,也用了一句类似的话作为我们的教学理念:

Learn to code, but code to learn
学习编程,更是通过编程去学习。

通过这五六年的教学实践,对这句话的理解也更加深刻了,希望接下来通过系列文章和大家分享一下我对 「通过编程去学习」 的看法和实践,也算是对编程教会我们如何思考这个命题的回应。

本文适合关注青少年编程教育的各位同学、家长及编程老师,当然,如果你恰好是一位专业的程序员,又对教育感兴趣,希望也能带给你一种不同的看待编程的思考方式!


这是第一篇,聊一聊编程与数学的关系:学编程,更是通过编程去学数学。文章比较长,我会尽可能通过一些示例把这个话题讲清楚,聊透。

下面的内容中,涉及到具体案例,会通过 Scratch3 和 Python 两种编程语言来演示。

Scratch3 是由麻省理工学院媒体实验室专为孩子设计的图形化编程语言,利用可拖拽的积木块来代替键盘输入的纯代码指令,让写程序更像搭积木,孩子们可以创建自己的互动游戏、故事和动画,非常适合孩子编程入门学习。

Python 是由荷兰人 Guido van Rossum (吉多.范罗苏姆) 开发的一门编程语言,它的设计理念就是简洁,Python 语法与自然语言比较接近,因此非常适合青少年与初学者学习编程使用。

Scratch3 属于图形编程语言,Python 是一种纯代码编程语言,大家也正好可以观察一下两种不同类型的编程语言的不同表达方式。

数学遇上编程,是件非常美妙的事情,下面这幅简单的示意图呈现了编程对数学带来的帮助,实际上,这种帮助是相互的,两者相遇,无论是对于编程学习,还是数学学习都有非常好的促进作用。

我下面的文章内容,也会大致按照图中的几个模块来展开讨论。

一、培养数感

在数学课标中,明确要求:教师在教学中要培养学生的数感,并且数感排在数学素养 10 个核心概念的首位。可见数感培养的重要性。

那如何培养数感呢?有生活中有很多种方法,而在编程中,数的应用无处不在,对数感的培养完全是潜移默化的。

1)数的表示

数字的意义,在小学刚开始学数字时,最常见的一种方式便是物与数的对应,1 个苹果、 2 个梨子、10 个人。

在 Scratch 编程的世界里,数字往往与角色相关,它所表示的意义更加丰富、直观。比如说,90 这个数字,它可以表示

  • 移动 90 步
  • 90% 的比例大小
  • 也可以是指 90 度方向
  • 也可以是旋转 90 度角
  • 还可以表示一种接近透明的状态
  • ......

在运用编程去创造的过程中,数字不仅仅是抽象的数字,更是表示了一种非常具体的状态,可以说,编程对于数感的培养不仅更有趣,而且在潜移默化中让我们对数的理解更加深刻。

2)数的估算

在解决实际问题的过程中,数量的大小往往并不是精确的,也不需要精确,这时候就需要我们去估一估、算一算,在估算中,内化数感。

比如说,我们设计了一堂 Scratch 课,目标是完成下面这个开心农场的作品,完成效果如下图所示:

在空旷的农场放养一些鸡,鸡是通过 Scratch 中的克隆功能生成,正常情况下,鸡活动的范围应该是房屋右侧面的空地,它不会跑到房子上去,也不会跑到后面山上去(都不符合常理),我们编程实现的过程自然就需要对鸡的位置范围进行估算

这里的估算很有意思,要完成估算,有两个前提条件:

  • 一个是要理解 Scratch 中舞台大小及位置如何表示
  • 一个是要知道在 Scratch 中如何去估算位置范围

如下图所示,黄色的坐标轴显示了在 Scratch 中舞台的大小与位置定位;而绿色的框则是我们大概标识出来鸡的活动范围。

对坐标的表示稍微熟悉后,我们可以尝试估算了,直接在舞台区拖动角色(这里的母鸡)到绿框的边角,通过对角色的位置属性观察去得到一个大致的估算范围(x 与 y 的值)。

实际上,在开心农场这个作品中,还有好几个地方都需要用到估算,比如说,克隆多少只鸡出来合适(不拥挤也不冷清)?鸡离我们的远近会有一定的透视比例缩放(近大远小),大概缩放多少合适?

估算,不用精确,但一定要合理。在使用 Scratch 进行编程学习的过程中,有太多的地方都需要孩子们有一定的估算能力,坦克大战游戏中坦克炮管的攻击范围、气球升空时的移动速度、贪吃蛇进阶时需要增大的比例,等等。

数无处不在,有数的地方,就有运算、有比较、有估算,何愁对数感的培养不会好!

3)对大数的感受

在计算机出现之前,对大数的认识,尤其对于孩子来说,大都是停留在想像之中,并没有一个实际的对比感受,毕竟在生活中大部分人都很少有机会去接触到超大数的应用场景。

而有了计算机后,我们完全可以利用编程去模拟对大数的运算,这就很容易通过数字的变化过程来感受大数究竟有多大

下面是一个我们在课堂中经常会讲到的经典折纸问题。

首先拿出一张普通的 A4 纸,让孩子们估算一下它的厚度大概是多少?通过估算,让孩子们对于长度单位(米、分米、厘米、毫米等)有一个直观的感觉。

实际上,通过查阅资料验证后,我们知道一张普通 A4 纸的厚度大概在 0.1 毫米左右,如果用米作单位就是 0.0001 米。

过程中鼓励孩子们尝试去对折一下,看看大家能将一张 A4 纸对折多少次?通过尝试,大家发现最多只能对折六七次。

引出这样一个问题:如果给你一张无限大这样的白纸,将它对折 20 次,它会变得有多厚呢?对折 30 次后又是多厚呢?

在课堂中问到这个问题的时候,孩子们的回答都很积极,回答也是五花八门,从大部分孩子的回答情况来看,一般都不会很厚,大概也就是几本书那么厚,厚度绝对不会超过一人高(这实际上为后面令人惊讶的结果对比做了辅垫)。

这个问题我们完全可以通过一段非常简短的 Scratch 程序模拟出来。

可以看到,当对折 23 次后,厚度就达到了 838.86米,竟然超过了目前世界的最高楼:位于迪拜,总共 828 米的哈利法塔,这是大家完全想像不到的!实际上对折 20 次后,竟然就有 100 多米了,也有 30 层楼那么高!

这个结果对于孩子们(对于很多成人也是一样)是完全出乎意料的!都觉得不可思议!

惊讶之余,引出另一个问题,再让大家猜一猜,我们对折多少次后,纸的厚度就可以超过世界最高峰珠穆朗玛峰的高度(8848米)呢?

再编写一段程序,经过测试,只需要对折 27 次,纸的厚度就已经有 13421.7728 米了,远远超过珠穆朗玛峰的 8848 米了。

在实际生活中,我们没办法找到这样一张无限大的纸,就算有这样的纸,用人力去完成这样的对折也不太可能,但如果你懂一点编程,写一段上面的程序并不是一件困难的事情,我们带着二三年级的孩子去完成,也是一点问题都没有。

实际上,每对折一次,纸张的层数翻倍,纸张的厚度也要翻倍,而且这个厚度会随着对折的次数增加得很快!很快我们就没办法继续对折了。对于稍大年龄的孩子,我们会告诉他们,这其实是数学中的一种指数增长效应,而指数增长是非常快的

2011 年,一位美国的中学老师 James Tanton 带着他的 17 名学生在麻省理工学院的无限走㾿(Infinite Corridor)用超过 16 公里的厕所卷纸做了对折的实验,花了八小时,最终成功对折了 13 次,对折后的厚度大概有 0.8 米,下图是他们当时参与对折的人员合影。

有趣的是,James Tanton 教授,正是爆炸点(Explding Dots)的发明人,爆炸点是一种全新的关于数学的表示与运算方式,我在博客也写了两篇介绍的文章:

感兴趣的可以去了解一下。

编程给我们提供了一种可操作的实验手段,可以去模拟一些在现实中没办法完成的问题,尽管只是模拟,但这种实践模拟所带来的对数学的体验却是真实的

4)随机数

随机数本身是一个很有意思的话题,生活中有很多的事情我们也是当做随机事件来研究的。在数学中,甚至有专门的分支是研究与随机相关的事件与现象的。

不过人工的话却很难去模拟这些随机的事件,而通过编程,模拟随机事件就很简单了:

  • 模拟一个随机出现的位置、方向或颜色
  • 模拟掷一个骰子可能出现的点数
  • 随机生成 30 份数学成绩分数用于测试
  • ...

在 Scratch 中,随机数的应用随处可见,少了随机性,很多作品就失去了灵性。

下面这个简单的作品没太多实际意义,不过用于感受随机数的应用效果还是不错。

程序代码其实很简单,就是把随机数应用到角色的不同属性上去,包括方向、大小、颜色、位置等。

在上编程课时,我还经常用到下面这样一个非常简单的 Python 程序,帮助我实现随机点名抽问,既公平,又有趣。

import os
import random
 
# 学生姓名列表
students = ["Moon", "Yancy", "Kody", "Amy", "Poli", "Coco"]
 
# 从姓名列表中随机选一个名字
name = random.choice(students)
 
# 调用系统命令把名字读出来
os.system(f"say '{name}'")

实际上,随机数在编程中也是一个非常重要的概念,它被广泛应用于模拟、游戏开发、密码学、数据分析等多个领域。

二、感受几何与空间

西摩.派珀特(Seymour Parpert)在《因计算机而强大》一书中很较大篇幅讲到了通过编程去帮助理解几何与空间,有了 Scratch,配合上图形与动画,对于理解几何、位置、方向等概念再适合不过了,大家或多或少都接触过一些这方面的例子。

被称为 Scratch 之父的米切尔.雷斯尼克(Mitchel Resnickoy)就是西摩.派珀特(Seymour Parpert)的学生,Scratch 的设计理念本身也是受到了 Parpert 发明 Logo 语言的很大影响。

下面我们一起再来看看几个例子 ~

1)坐标与位置

当我们利用编程去完成一些作品时,所有我们能在舞台上看见的东西,都是有一定的空间位置关系的。

最简单的,我们要将一个角色放到舞台中央,就需要知道「舞台中央」这个位置如何去表示?

这个位置的表示其实就是对应于数学中的坐标,不过我们并不会去强调坐标这个抽象的数学概念,而是通过移动和变换角色的位置去体验这一组数的变化,从而更深刻的去理解这组数与角色位置之间的对应关系。

根据实践经验,孩子在编程创作的过程中,对这种对应关系会越来越熟悉,到后面坐标就只是一个名称而以,他们甚至不会觉得「坐标」是一个非常抽象的数学概念,这正是编程带来的非凡作用!当孩子以后在数学课程中学习到坐标概念时,应该会非常亲切的。

2)运动与方向

在知名的袋鼠数学思维挑战竞赛(Math Kangaroo)中出现过这样一道数学思维挑战题:

对于小学低龄段的孩子来说,尽管他们辨别左右没问题,但要在运动的过程中去判断转弯方向,还是有点难度的,因为需要他们将自己代入小猫的角色,每次左转还是右转都取决去当前的位置与方向。

如果通过编程去完成,这种代入感会更直接,在编写代码的过程中,孩子们会很自然的把自己代入小猫这个角色,而且还可以通过尝试去调整,去感觉,这样的过程可以很好的帮助孩子去理解运动中的方向与位置。

下图是我们在 Scratch3 让小猫去完成的同样路线,左转和右转不再是靠想像,小猫成了孩子的替身,孩子通过左转和右转指令去控制小猫前行或转向。

甚至你可以发现,最终小猫是面朝下的,如果不转弯,它会一直朝下前行。

程序写完,只要效果达到,至于右转多少次,左转多少次,这就是一个顺带的结果,看看程序就一目了然了。

3)几何图形与特点

众所周知,通过编程去画图是非常有助于理解数学中的几何图形及其特性的。在 Scratch 中,我们可通过画笔模块去绘制各种几何图形。

动态的多边形绘制,是我经常在课堂中使用的一个例子,整个过程从简单到复杂,逐步演进。

  • 绘制基本的线段
  • 绘制正方形
  • 绘制三角形
  • 绘制五边
  • 绘制多边形

要正确的绘制出这些几何图形,从数学的角度来看,关键点在于要清楚要绘制的多边形的边数与每次旋转角度之间的关系。

这个关系在中学阶段是有公式去计算,但在小学阶段,我们不知道公式,怎么办呢?可以通过尝试 → 修正 → 再尝试 → 再修正 这样的迭代过程去一步步逼近最终效果。先尝试画正方形,再画三角形,再到画五边形 ……

从我们的教学实践数据来看,通过试错和迭代,孩子是完全可以理解重复次数与旋转角度之前的关系的

重复次数(边数) x  旋转角度 = 360 度

实际上,在传统的学校教育中,这样的内容要在小学高年级甚至中学阶段才会接触到。而通过编程,运用简单的左转、右转指令,配合角度(90°、60°、360°)去绘制不同的几何图形, 这些抽象的数学概念对于二三年级的孩子来说也没有任何难度,旋转方向与角度在他们的头脑中的印象,比一些中学生还来得深刻。

三、呈现规律与模式

在很多方面,数学 - 特别是有关空间、运动和重复行为的数学 - 是非常适合儿童的。
--- 西摩.派珀特

重复本质上就是规律与模式,而规律与模式是编程与数学中的一个非常核心的共同主题,规律与模式也是体现数学与编程之美的核心所在。

1)循环与乘法

我们从数学中最简单的乘法运算来看,大家都知道:乘法就是重复的加法。乘法其实就是加法的一种简便运算。

如:5x3 = 3+3+3+3+3,也就是5个3相加。

其实,如果用编程来理解,可以更加深刻,下面是一段刚学 Scratch 编程的孩子就能写出来的程序。

每次走 3 步,一共重复执行 5 次,一共也就是走了 5 x 3 = 15 步,这不就是乘法吗。

如果我们将这里的 5 和 3 交换一下,像下面这样:

每次走 5 步,一共重复执行了 3 次,从结果上来说,也是走了 5 × 3 = 15 步,从数学的角度来说,这不就是乘法满足交换律吗?

继续,在重复执行积木块内部,还可以添加其它代码。比如说,我希望每移动 3 步停 2 秒钟,就可以这样。

本质上就是走 3 步,停 2 秒,这个组合动作一共重复了 5 次。

这个时候,你能把 5 和 3 交换吗?

走 3 步,停 2 秒,和走 5 步,停两秒,感觉上完全不太对了。

如果简化并抽象成数学符号表达,前者应该就是:5 x (3 + 2),等于 25;后者简化的符号表达式为 3 × (5 + 2),等于 21。结果也不同,造成的原因很明显,修改前,共停了 5 个 2 秒,共 10 秒;而修改后,只停了 3 个 2 秒,只有 6 秒。

当然,这里因为 3 代表的是步数,2 代表的是秒数,所以不能直接加起来。我们只是从数学抽象的角度,把所代表的实际含义去掉来看。

有意思的是,在这里,编程为抽象的数学符号和概念找到了一种非常好的应用表达方式。本质上他们是可以互换的,理解了一点,不仅有助于我们编程学习,更有助于我们的数学学习。

2)图形与数学

一个数学的头脑,最显著的特色不是逻辑,而是美感。
-- 《因计算机而强大》

在前面的几何与空间小节,我们利用编程去绘制了各种几何图形,通过编程,可以很好的帮助我们去理解图形、角度和旋转。

在前面创建的几何图形基础上,如果再以不同的方式重复起来,一种特别的美便呈现出来了。

仅仅调整重复次数和旋转度数,效果就很不一样了,这是一种由数学带来的秩序与法则之美,而且这种美通过编程以直观的方式呈现在我们眼前,这种感觉是非常棒的!

下面左右两幅图案唯一的差别是循环的次数和旋转角度不同。

配合上变量,封闭的多边形就变成了下面的直角螺旋线,旋转的角度再稍微变化一点,出来的螺旋线图意想不到的漂亮哦!

大家再看看下面这组图形,这是我在实际上课中所用到的一组示例,用 Python 中的 Turtle 绘图模块完成的。直观上看,是不是另一种重复的规则之美呢?

具体如何实现,不是这里的重点,大家可以自己尝试去完成一下呢,在 Scratch 和 Python 都可以实现同样的效果。

以前我们读书的时候,要画几何圆形,需要借助直尺、三角板和圆规,而现在我们有了编程,我们就是拥有了一套真正的魔法工具,使用并不复杂,只需要会基础的一些编程指令,再配合一些简单的数学性质,剩下的就是你的想像和创造力了。

「数」与「形」的结合一直在数学中有一种独特的魅力。直观的「形」往往给抽象的「数」以最生动的呈现和诠释。借助编程,你可以以一种全新的方式去体验数学之美!

我们还设计了一套特别的入门课程,基于 Python 的 Turtle 模块(或者 Scratch3 的绘图模块),编程学习并非从 "Hello World" 开始,而是从画一个基础图形(如圆点)开始,整个课程围绕数字与图形展开,不仅可以把编程的核心概念都覆盖到,而且完全契合了计算思维包括的四个方面:解构、模式、抽象和算法。如果有感兴趣的朋友可以找我继续聊聊这一块。

四、帮助理解数学概念

任何一门学科,在孩子的任何发展阶段,都能以某种智识上诚实的方式,有效地教授给任何孩子。
-- 杰罗姆.布鲁纳(Jerome Bruner)

在大量的教学实践中,越来越能深刻体会到布鲁纳这句话的价值所在。

在传统学校教育中,知识就像一个管道一样,整整齐齐,规规矩矩,按先后顺序排好,按步就班的输送给孩子。

实际上,借助编程这种工具,很多数学知识并不需要按传统数学课程中的安排顺序来学习。好一些数学概念在学校里还没有涉及到,但在我们的教学实践中,借助编程,孩子是完全可以理解的,而且可以理解得非常好。

一些看起来比较难的数学概念,通过编程在孩子的脑袋中已经建立了具体而真实的体验,当他们在学校里再去接触和学习这些抽象概念的时候,会有一种任督二脉被打通的感觉,这种体验是非常棒的。

这方面我选了几个比较典型的例子,来看看编程可以如何帮助到我们去理解一些复杂、抽象的数学概念。

1)排列与组合

没有材料,也没有激励,孩子只能被迫以抽象的和摸索的方式来处理。
-- 《因计算机而强大》

在《因计算机而强大》一书中,Papert 提到过一个组合实验:给孩子们若干彩色的珠子,让他们来对这些珠子进行所有可能的色彩组合。在传统的教学方式下,在进入五六年级之前,大多数孩子都没办法系统地、准确地完成这个操作。

Papert 提到,不是孩子在五六年级之前不能理解,问题出在我们文化的本质上,而对于经常接触计算机和编程的孩子来说,找出珠子组合其实就是编写一个简单的嵌套循环程序:首先选出一种颜色,再挑选出所有可能的第二种颜色,然后重复执行这个过程,直到所有可能的第一种颜色全都选过一次。

例如:一个口袋里有 12 个球,其中有三个红球,三个白球,六个黑球,从其中取出 8 个球,请列出所有的颜色搭配方式。

下面是通过 Python 去实现的代码:

for r in range(4): # r 代表红球,最少 0 个,最多 3 个
  for w in range(4): # w 代表白球,最少 0 个,最多 3 个
    for b in range(7): # b 代表黑球,最少 0 个,最多 6 个
      if r + w + b == 8:
        print(f"{r} {w} {b}")

运行程序,输出结果:

0 2 6
0 3 5
1 1 6
1 2 5
1 3 4
2 0 6
2 1 5
2 2 4
2 3 3
3 0 5
3 1 4
3 2 3
3 3 2

上面列出了所有的组合方式,比如 0 2 6 ,代表选 0 个红球,2 个白球,6 个黑球。共有 13 种搭配方式。

实际上,对于不同的组合,用编程去解决时,是采用了一种叫做枚举的「笨」方法,对于这种笨方法,计算机恰好是非常擅长的,它的能力比人类强太多了。

在教学过程中,也可以让同学们去枚举一下,正好锻炼一下大家枚举(不重复、不遗漏)的能力。

2)枚举

我们用一个类似的,大家更熟悉的小学奥数例子: “鸡兔同笼” ,来看看用编程手段去解决和理解组合问题是多么简单。

这里我们就不细讲各种 “鸡兔同笼” 解法了,后面在讲人脑与机器脑的不同思考方式时还会讲到这一点。

很少人会把 “鸡兔同笼” 看作一个组合问题,其实如果我们把问题换一种说法,大家就明白了,若干只鸡和兔,共有 35 个头,94 条腿,通过鸡和兔的组合搭配(多少只鸡,多少只兔),刚好可以满足头和脚的数量呢?

用编程去解决,思路也很简单,就是用笨办法。从 35 个头中选一个出来,假设它是鸡,那剩下就全是兔了,然后把它们所有的脚算一遍,看是不是刚好有 94 条腿,如果是的话就结束。如果不是的话,就再从 35 个头中选两个出来,假设它们是鸡,依次类推 ...... 直到找到的组合刚好是满足 94 条腿。

for chicken in range(1, 36): # 鸡的数量可选范围
  rabbit = 35 - chicken # 计算出兔的数量
  if chicken * 2 + rabbit * 4 == 94: # 满足题目总脚数的要求
    print(f"{chicken} {rabbit}")

运行程序,得到结果:

23 12

编程得出结果,共有 23 只鸡,12 只兔。

组合问题本质上就是一个枚举问题,简单的组合问题可以通过列表来完成,但对于数量比较大的组合问题,列表就不太可能了,而通过编程去解决再合适不过了。

3)微分

微积分对于大多人来说,那都是高中甚至大学的课程,很多读过大学的人至今也没弄懂微积分到底是怎么回事。怎么会在小孩子的学习内容中出现呢?小孩子能懂吗?我们直接通过例子在体会一下。

前面在通过编程去画多边形时,大家可能已经发现了,随着多边形的边数越来越多,整个多边形的形状就开始慢慢接近圆形了。

实际上,当我把重复的次数(也就是多边形的边数)设置为 60 的时候,画出图形已经差不多是一个圆形了。

很神奇吧,实际上它仍然是一个多边形,只不过当多边形的边数越来越多时,每次甲虫转的角度也越来越小,多边形的每条边非常短,从视觉上我们看到的就是一个圆了。

同样的,我们前面见过的直角螺旋线图,将重复次数变多,旋转角度变小,线就会越变越光滑,最后就演变成下图右侧这样像蚊香一样的圆形螺旋线了。

如果觉得还不够,再做些改变,把数学中经典的斐波那契数列用图形呈现出来,下面是呈现的过程:

如上图所示,白色正方形的边长恰好对应于斐波那契数列的各项数字,而黄色曲线则是以正方形边长为半径画出的圆弧,最终形成的斐波那契螺旋曲线,又叫做黄金螺旋线,非常漂亮! 

其实微分的本质是从有限到无限,只是通过传统的教学手段,无限这个概念很难去表达,因此微分的概念对于很多人来说很难理解,而通过编程,这一切变得非常简单了,略过复杂的概念与符号,所有抽象的知识被赋予了具体的形态,没有去学微积分,但实实在在触碰到了微积分的本质,这一点是非常重要的。

4)分形

分形几何(Fractal Geometry),属于数学中几何学的一个分支,是一门以不规则形态为研究对象的几何学。分形几何的研究对象普遍存在于大自然中,例如我们熟知的雪花、树叶、云朵、海浪等,因此分形几何学也被称大自然几何学。

分形最大的特点是具有自相似性,无论是放大还是缩小研究对象,都会看到局部与整体具有相似的结构。局部是整体的缩影,整体是局部的放大。

这种自相似天然适合用编程中的递归思想来表达。递归的本质是一种通过将问题分解为同类的子问题去解决问题的一种方法,在编程中,递归是通过在函数中调用函数自身来实现的。

下面是一棵漂亮的分形树,我用 Python 的 Turtle 画图模块来实现的。

代码并不复杂,下面是最主要的绘制函数代码:

# 递归绘分形树的函数
def draw_branch(len, depth, size):
  if depth == 0:
    return
  pensize(size)
  forward(len);
  right(25)
  
  draw_branch(len * 0.8, depth - 1, size * 0.8);
  left(50)
  
  draw_branch(len * 0.8, depth - 1, size * 0.8);
  right(25)
  
  backward(len)
 
draw_branch(50, 8, 6)

上面是迭代的深度参数为 8 的运行效果,你可以修改迭代参数,下面是不同 depth 为不同值的效果,更能观察出分形的自相似特征。

五、理解更底层的概念

数学本质上并不需要依赖于某种装置,而编程一定是依赖于特定的计算机设备才存在的

实际上,如何大家够看到数学与编程的这种区别,可以帮助我们更好地理解一些计算机更底层的机制。

这里举两个例子,大家来感受一下。

1)0.1 + 0.2 = ?

这个问题有点意思,在数学中,当然是一个非常简单的计算问题,所有小学生都会计算,结果是 0.3。

在计算机中呢,可就不那么简单了。

用你熟悉的编程语言,计算一下 0.1 + 0.2 ,看看结果是多少?

在 Python 中:

print(0.1 + 0.2)

在 C++ 中:

#include <iostream>
#include <iomanip>
using namespace std;
 
int main() {
    cout << setprecision(17) << 0.1 +0.2; 
    return 0;
}

运行结果,都是:

0.30000000000000004

实际上,在几乎所有的编程语言中去验证,都有这个问题,0.1 + 0.2 的结果并不等于 0.3。

基于还有一个专门的网站(域名也很有意思),记录了大部分编程语言对这个 0.1 + 0.2 的计算及结果。

30000000000000004.com

这里我们也不是为了要把这一点解释清楚,只是聊一聊这个问题存在的根源是什么。

在我们的日常生活(包括数学教育),绝大部分时候都是使用的十进制计数(逢十进一),在十进制中, 0.1 + 0.2 的结果是精确的数字 0.3。

在计算机内部,不管是什么数(整数还是小数),都是用二进制来表示的,也就是说,当我们在计算十进制的加法 0.1 + 0.2 时,计算机内部会把小数 0.1 和 0.2 转换为二进制来计算。

问题就出在这个转换上,当我们把十进制小数 0.1 转换为二进制数时,会得到一个无限循环的二进制数(就像我们在十进制中计算 10 除以 3 的结果一样)。这样就会存在一个精度问题,需要进行取舍,因此会出现 0.30000000000000004 这样的结果。

如果在输出时把输出的精度再设置高一点,还会得到后面更多位数的数字,比如:

0.3000000000000000444089210

具体如何把一个十进制小数转换为二进制数,这里就不赘述了,感兴趣的可以去查找相关资料。

实际上,除了熟悉的十进制外,我们在生活中也会用到一些其它的进制,比如:

  1. 计时中的分钟、秒换算都是 60 进制
  2. 年份,生肖,换算是 12 进制
  3. 在软件开发领域中,经常用 16 进制来表示颜色(比如 0xFF0000 表示红色)

一直认为,在小学阶段刚接触数的时候,多介绍一下不同进制的其它数是有好处的,哪怕是让孩子在心里有一个对不同进制数的模糊概念也好

关于不同进制数的表达和计算方法,不得不又提到一个人: James Tanton 教授,我们在前面讲到纸张对折的小节中提到他带领他的学生创造了 13 次的纸张对折记录。其实,James 还创造了一套名为爆炸点(Exploding Dots)的计数与运算方式,对于我们认识和计数不同进制的数非常有帮助,小学生也可以理解,感兴趣的可以去官方站点看看 explodingdots.org

2)算法效率的考虑

算法,简单来说就是完成一个任务的步骤组合。在编程中,算法被定义为一组序列化的指令,这些指令描述了执行任务所要遵循的一系列步骤。

在计算机中,所有的资源(包括内存、CPU等)都是有限的,也正是有这种限制,在运用编程去解决一个实际问题时,通常会转换为一个可以优化的工程问题(是否省时、是否节约空间)。

对于一个算法来说,这就是一个算法效率问题,一般也会从时间和空间两个维度来考虑如何优化,对应于算法中的时间和空间复杂度分析。

实际上,当我们在谈到各种算法及算法效率时,所涉及到的很多算法(如求最短路径的 Dijkstra 算法),以及数据结构(如树、图)最初也都是源于数学上的研究。从这个层面上来讲,编程与数学就是你中有我,我中有你

下面举一个简单的运用数学知识来提升算法效率的例子。

题目描述:判断一个大于 1 的正整数 nn 是否是质数?

质数(Prime number),又称素数,指在大于 1 的自然数中,除了 1 和该数自身外,无法被其他自然数整除的数。
--- 维基百科

根据质数的定义,判断一个数是否是质数的算法步骤也很简单:

  1. 除开 1 和自身,也就是从 2 开始,一直到 n - 1,检查 n 是否能被这些数整除
  2. 如果 nn 能被其中任何一个数整除,则 nn 一定不是质数
  3. 如果 nn 不能被上面的任何数整除,则 nn 是质数

下面是用 Python 实现的一个判断质数的函数 is_prime()

def is_prime(n):
  for i in range(2, n):
    if n % i == 0:
      return False
  return True

这段代码从逻辑和运算上都没有任何问题,不过其执行的效率还可以优化,利用一点点数学性质,就可以减少很多不必要的判断检查。

先看优化后的代码:

def is_prime(n):
  if n == 2:
	return True
  if n <= 1 or n % 2 == 0:
	return False
  for i in range(3, int(math.sqrt(n)) + 1, 2):
    if n % i == 0:
      return False
  return True

这里优化了两个地方:

  • 如果 nn 是偶数,它一定不是质数。
  • 从范围上来说,其实只需检查到 n\sqrt{n} 。这是因为如果 nn 有一个大于 n\sqrt{n} ​的因子,那么它必然有一个小于或等于 n\sqrt{n} 的因子。

经过简单的优化,算法的时间复杂度从 O(n)O(n) 降到 O(n)O(\sqrt{n}) ,特别是对于很大的数 nn,优化后的方法执行效率提升很明显。

这样的例子其实还有很多,数学隐藏在解决问题的思路背后,时不时跳出来,四两拔千斤,往往会起到一些关键作用

六、思考如何思考

计算机提供了一种非常具象的、实实在在的、特别的思考方式,它让我们更容易理解“思考方式”究竟是怎么一回事。
-- 西摩.派珀特

本质上,计算机和人脑的思考方式是不一样的,在通过编程去学习的过程,你会慢慢体会到这种差别。学习编程也有助于孩子去思考关于思考的问题,学习关于学习的问题

1)调试策略

在传统的学习中,求解一个问题,要么是“正确的”,要么是 “错误的” ,孩子往往被告之的是:尽量少犯错,提高正确率。

而当他们开始编程时,很快就会发现程序几乎不可能一次性就写对,提升编程能力,本质上是不断培养他们发现问题及调试(修正错误)的能力。

计算机给我们提供了一个足够容错的试验环境,在这个环境中,鼓励动手和尝试,出错很正常,我们要做的是学习调试,通过不断迭代来优化,培养解决问题的能力。

从刚开始学习编程,我们就会告诉孩子,程序中含有 Bug(错误)是非常正常的,我们需要做的就是及发现这些问题,并尝试去优化和解决,有意识地不断改善它们。

实际上,专业程序员在他们的日常开发中,调试(查错并修正错误)所占的时间远超过了直接写代码的时间。

调试策略对于孩子的学习成长也有帮助,可以让他们意识到,做一件事不可能一开始就完美的,一定是一个不断改进,不断优化的过程,这也可以培养孩子一种积极的、渐进式成长的态度

2)不同的思考方式

让孩子有机会选择自己的思考方式,实际上是训练他们选择思考方式的能力。
--- 西摩.派珀特

从小到大的学校学习过程中,我们其实很少去思考我们是如何思考的这件事情。

而运用编程去解决问题,它会迫使我们去解构思考的各个环节,去思考机器是如何思考的,人是如何去思考的。自然而然,这个过程也会帮助我们去认识到人脑与机器脑思考的差别。

来看一个我在真实课堂中的两个教学例子:

  1. 累加求和
  2. 爬台阶

累加求和

在准备讲如何通过编程实现累加求和的内容时,通常我会先问问孩子,如何求解:

1+2+3+ ... +100 = ?

① 人的思考方式

大部分同学都听过天才数学家高斯的故事,首尾相加的和再除以项数的一半,甚至有些同学不假思索就可以回答出:5050 。

有同学更厉害,直接搬出了求和公式:

好吧,这是标准的、纯粹的数学解答方式。对程序还比较熟悉的同学,立马可以写出下面的代码:

# 人(数学)的思考方式
n = int(input())
 
s = n * (n + 1) // 2
print(s)

程序很简单,运行结果也正确,这样写当然也没什么问题,但我们会告诉孩子,这是数学的思考方式,并不是机器的思考方式。

这种方式不够灵活。

为了让他们意识到,记住这个公式并不是万能的,我会把问题稍微换一下,比如,你们能帮计算一下 1 到 nn 中所有是 2 的倍数,但不是 3 的倍数的这些数的和吗?

大家一下就懵了,我们脑袋里没这样的数学公式呀,怎么办?

正好我们可以看看如何以编程的方式来思考这一类问题。

② 机器的思考方式

我们来看看机器脑怎么去解决这个问题,思路非常简单,一个一个加呗,有多少个加多少个,如下图所示。对于人来说这不太现实,对机器来说,太容易了。

我们通常称这种方法叫累加求和,Python 的实现代码如下:

# 机器的思考方式,一个一个加
n = int(input())
 
s = 0 # 总和
 
for i in range(1, n + 1):
  s = s + i
 
print(s)

程序也容易理解,就是从 1 开始,一个一个加到结果 s 中去,循环完成,结果就是我们最终得到的数字之和。

程序很听话,而且运算速度很快,哪怕你输入一个 100000,它也会立刻帮你计算出来从 1 加到 100000 的结果。

同样的程序,稍作修改(其实就是加个条件判断),就可以完成求 1 ~ n 当中所有是 2 的倍数,但不是 3 的倍数的数的和。下面是调整后的 Python 代码。

n = int(input())
 
s = 0 # 总和
 
for i in range(1, n + 1):
  if i % 2 == 0 and i % 3 != 0: # 满足是 2 的倍数,不是 3 的倍数的数,才求和
    s = s + i
 
print(s)

而且这一类的问题都可以用这样的「笨」方法来解决,把所有情况都列举出来,然后根据所需的条件进行筛选,满足条件的留下(对满足条件的数可以求和、可以计数,也可以进行其它操作),不满足条件的舍去;最后按要求进行输出即可。

人很聪明,所以会总结上面这样的数学公式;而机器很笨,看起来只会一些非常简单的计算,但它的厉害之处在于,它比人脑计算得更快、更准确,它可以一秒钟内就完成成千上万次这样的计算,这一点人脑就远远赶不上了。而且看起来笨,这样的方法还很灵活,管用!

③ 融合思考法

有没有既能利用人脑的特点,也充分利用机器的特点,把两者的优势结合起来的方式呢?当然有。

假如我们用符号 sum(n) 表示从 1 加到 n 的和,要求 sum(n),我们只需要知道 sum(n-1) 就行了,因为我们可以用 sum(n-1) 来表示出 sum(n)

sum(n) = sum(n-1) + n

这是一种数学上的表达式,要求 sum(n),只需要知道 sum(n-1),要求 sum(n-1),只需要知道 sum(n-2) 就行了,一直往下,最终只需要知道 sum(1) 就行了,而 sum(1) = 1,反推回去,整个问题就解决了。

这种方法在数学中被叫做归纳法,一般要到高中才学。而在编程中有对应的算法策略:递推或递归。

递归的思想其实很好理解,本质就是一种通过将问题分解为同类的子问题去解决问题的方法。但思想虽然好理解,要用人脑去计算却很不方便,人脑不太适合这种大量重复,还是记中间过程的计算方式,而编程刚好特别适合这种重复、有规律的计算方式。

上面求和的问题如果在编程中用递归来解决,下面是 Python 代码:

# 融合的思考方式
n = int(input())
 
def sum_recursive(n):
  if n == 1:
    return 1;
  else:
    return n + sum_recursive(n - 1) # 将原问题分解为更小的子问题
 
print(sum_recursive(n))

同一个问题,三种解法,代表了三种完全不同的思考方式。计算机编程的「魔力」给我们提供了一种强有力的工具,让孩子可以去选择自己的思考方式,实际上也是在锻炼他们选择思考方式的能力

关于不同的思考方式,再来看一个例子,应该会有更多的感觉。

爬台阶

爬台阶,最初这是我给女儿讲过的一个关于思维拓展的例子(忘了这个例子的最初来源了,但很常见)。

在小学阶段,台阶数往往都很少,只需要通过枚举,在保证不重复、不遗漏的前提下,把所有的跳法罗列出来即可。更多是锻炼一种有序思考的基础能力。

① 画图法

一级一级地跳,只有一种方式,两级两级的跳,也只有一种,如下图所示。

跳一级和跳两级混合起来,共有下面三种情况:

总结下来,一共有 5 种上台阶的走法。

② 数字组合法

把上述的方法变得更「数学」一点。

那如要台阶数变得更多后怎么办呢?我们将题目的已知条件用数学的方式再来描述一下。

4 级台阶,一步只能上一级或两级,也就是说,如果我们把 4 做一个的拆分,而且只能拆成 1 或 2,一共有多少种拆的方法?

经过这样的转换,问题就变得很简单了。我们来看看 4 的拆法。

将 4 拆成 1 和 2 ,一共有下面五种拆法:

  1. 4 = 1 + 1 + 1 + 1  (一级一级跳)
  2. 4 = 2 + 2  (一级一级跳)
  3. 4 = 2 + 1 + 1 (混合跳)
  4. 4 = 1 + 2 + 1  (混合跳)
  5. 4 = 1 + 1 + 2  (混合跳)

得到同样的答案,一共有 5 种上台阶的走法。

③ 数学与编程拓展

这个任务可以更复杂一点,比如把题目改为:学校大楼前共有 30 级台阶,若规定,一步可以上一级,也可以上两级,还可以上三级台阶,请问要走上这个台阶共有多少种不同的走法呢?

感觉题目一下就难了,想要通过枚举的方式把所有不同的走法罗列出来简直不太可能!

实际上,我们仍然可以通过观察得出一个规律,从第三级台阶之后,要跳到任意第 n 级台阶,只可能有下面三种情况:

  • 从第 n-1 级,跳一级上来;
  • 从第 n-2 级,跳两级上来;
  • 从第 n-3 级,跳三级上来;

我们可以归纳出下面这样的一个数学公式:

f(n)=f(n1)+f(n2)+f(n3)f(n) = f(n-1) + f(n-2) + f(n-3)

这里的 f(n)f(n) 表示如果有 nn 级台阶,共有的走法总数。

从数学的角度来说,就到此为止了,因为就算你得到这样的公式,你也不太可能人工去计算出如果有 30 级台阶,共有多少种走法。

而通过编程,在前面的递推公式基础上,我们可以在上面数学公式的基础上,更进一步,把有多少种走法具体求出来。

下面这段简单的 Python 代码,可以求出任意 nn 级台阶,按上述跳的规则,所有的走法总数。

n = int(input())
f = [0] * (n + 1)
 
f[1] = 1 # 如果只有一级台阶,共有 1 种走法
f[2] = 2 # 如果只有两级台阶,共有 2 种走法
f[3] = 4 # 如果只有三级台阶,共有 4 种走法
 
for i in range(4, n + 1):
  f[i] = f[i-1] + f[i-2] + f[i-3] # 直接利用前面得到的递推公式
  
print(f[n]) # 有 n 级台阶,所有的走法总数

输入 nn 为 30,运行程序,立即会得出结果:

53798080

竟然有 5000 多万种走法,就像前面的纸张对折一样,当得出具体结果时,又是一个令人惊讶的时刻。个人觉得,这就是编程的强大力量所在,尽管求出结果和不求出结果,看起来关系不大,但给你带来的体验是完全不同的。

数学是偏符号化的、抽象程度更高的;编程是更具体的,通过编程更容易得到可观测的结果。

要解决一个实际问题,第一步往往是需要通过一些数学知识去找到解决方案,第二步是借助编程去真正的把这个问题解决掉。

以这样的方式来体验数学与编程,相信你会对学习感上兴趣的。

希望通过这个例子,能给大家带来更多启发和思考。


七、与数学课标的关系

为什么会扯到数学课标上来呢,基础数学是一门发展了很久,已经非常稳定的学科,大纲从某种程度上划定了整个学校教学和考评的框架,从大纲对应来思考与编程的相互作用是有一定参考价值的。

教育部在 2022 年 4 月印发了最新版的义务教育课程标准,其中数学课标里有一段对小学阶段和中学阶段的数学核心素养作了很精彩的说明。

小学阶段侧重对经验的感悟,初中阶段侧重对概念的理解。

小学阶段,核心素养主要表现为:数感、量感、符号意识、运算能力、几何直观、空间观念、推理意识、数据意识、模型意识、应用意识、创新意识。

中学阶段,核心素养主要表现为:抽象能力、运算能力、几何直观、空间观念、推理能力、数据观念、模型观念、应用意识、创新意识。

如果对应上面对编程与数学关系的描述及示例呈现,可以发现,数学与编程结合,其实可以很好的覆盖到课标中对核心素养的培养范围,而且上面这些核心素养,仅仅通过传统的数学教育方式,是比较难去培养和锻炼的,编程在这一块刚好可以起到补充和助力作用。

可以说,有了编程的助力,数学学习就像插上了翅膀,不仅可以飞得更快,还可以飞得更高,当然,更重要的是,更有趣了。

八、后记

We shape our tools and then our tools shape us.
我们塑造了工具,而后工具也在塑造我们。
--- 麦克卢汉

「学编程,更是通过编程去学习」,希望我上面的长篇赘述可以让你体会到这一点。

任何一门学科都不是孤立的,只是在现代的学校教育体制下,为了便于教学才有了分科,学科越来越细分后,不同学科之间的距离也就越离越远了,不只是学生,可能连老师也都慢慢忘了不同学科之间的关联,实际上学科之间天然就是可以相互融合,相互促进的!

计算机与编程为我们提供了一种丰富的文化环境,在这种环境下,每个学科都可以得到更多滋养,生长得更好。尤其数学、物理、艺术这些学科,后面我还会通过更多的文章去聊编程与不同学科相互促进的教学实践。

近两年,伴随着 AI 的出现,时常有言论,程序员都要失业了,还学什么编程呀,希望通过这篇文章,至少能让大家从另一个侧面看到,编程的对于学习的价值所在

这可能也正是为什么 Papert 在 30 年前写的《因计算机而强大》一书,到现在来看也不过时的原因吧!

说明:这篇文章两年前写的第一版,当时有不少人喜欢,通过这篇文章还认识了好一些编程爱好者和教育工作者,这次把一些觉得有必要的内容增补上来,大概三分之一的改动吧,算是一个修订版,一并通过公众号分享出来!

参考资料


关于通过编程去学习这个话题,如果你有一些思考,想要交流或分享,欢迎联系我,期待有更多有趣、深入的讨论!