财新传媒 财新传媒

阅读:0
听报道

 

1 学编程的好处
 
       由于昍最近偶尔学点编程,因此这段时间关注了些少儿编程的新闻,发现铺天盖地的舆论造势,论证儿童为什么需要学编程,仿佛不学编程就输在了起跑线上。甚至,前两天新华社都专门发了一篇文,题目是《为什么我们的孩子需要学编程?》。
 
       
翻了一下,就是篇编程培训机构的广告软文。文中讲了两个观点:
1)          编程,能让孩子戒掉游戏瘾
2)          编程是问题导向,有助于提高解决问题能力
这两个观点我也赞同,但编程的好处不仅在于此。我在之前写的一篇文章《当儿童编程遇到数学》提到了几点,其中第2和第5点就是这篇文章的两个观点。
 
编程对于孩子认识到粗心的危害确有帮助。前些天,波音737 max机型由于失事而被全球禁飞,而美国航空管理局似乎承认其软件存在问题。所以程序的正确性至关重要,让一个粗心的人去做程序员,着实不让人放心。编程对于避免沉溺于游戏也很有帮助,因为编程为孩子打开了另一个视野,人人都想成为创造者,而不是被动地接受。
 
 
至于编程与数学的关系,可以参考我前一篇文章《少儿编程,我这样教》。
除了上面的几点外,我还想加两点。
一是编程有助于学习英语。这也是昍最近学编程的一点体会。学好了英语,程序中的有些变量名一看就大致知道是什么意思,另外,当程序编译出错时,也可以大概看明白错误信息,做出针对性的修改。
二是学习编程有助于提升宏观思维。程序的执行一步一步是微观的,但最后达到的效果是宏观的。因此在理解一个程序时,常常需要跳出微观,从宏观上去解读。这在后面的例子会看到。
 
      
2 循环结构—少儿编程的第一个坎
 
       有一种观点认为现在的奥数学习套路化了,已承担不了逻辑思维能力培养的重任,因此编程将责无旁贷接过这一重任。编程有助于培养逻辑思维能力毋庸置疑,没有严谨的逻辑思维,必然写不好程序。
程序流程有三种结构:顺序结构、分支结构和循环结构。前两种都比较好理解,所谓顺序结构就是一条道走到黑,沿着这条路一直走就可以到达终点;而分支结构则有岔路口,这时需要选择走哪一条路,不同的人可能会选择不同的路。这两种结构孩子都比较好理解,在学习程序设计中,孩子遇到的第一个坎应该就是循环结构。可以这么说,对少儿来说,熟练使用了循环,编程就学会了一半。
       所谓循环,就是一段程序要反复执行好多遍,甚至是一直执行下去。
       在C++和大部分程序设计语言中,都提供了三种循环语句:for循环,while循环和do-while循环。
       不论是哪种循环,循环都有三个要素:
1)          循环变量
2)          循环控制条件
3)          循环体
循环变量是循环控制条件中的主要输入,用于改变循环执行的逻辑。这里,会出现编程人员常犯的两个错误:(1)循环变量没有初始化或错误地初始化;(2)没有改变循环变量的值,导致死循环。
循环控制条件用于控制循环的结束,这是循环逻辑的重点,其中的表达式可以是简单的关系表达式,还可以是复杂的逻辑表达式。这里,又会出现常犯的的一些错误:(1)循环条件表达式逻辑出错,比如逻辑与和逻辑或用错;(2)边界条件判断出错。后一个错误很多时候是由于基本所有程序设计语言中数组的第一个元素下标都是从0开始,因此遍历所有元素的结束条件是”<”还是”<=”对许多初学者来说都会成为问题。
除了编程犯错,用循环编程将涉及到另一个重要的问题,就是程序的性能。同样是一段代码,循环执行的次数多少极大地影响了程序的运行时间。同一个问题,有可能一个程序需要1秒钟就能得到运行结果,而另一个程序需要超过100秒才能得到运行结果。
循环体是程序执行的主要部分,在这里面比较重要的是进行迭代和改变循环变量。迭代是编程和数学的一大区别,比如sum= sum +i; 在数学上,这个式子只有当i=0时才成立。但在编程中,由于=是赋值号,意思是把sum+i的结果赋值给sum,也就是sum获得了一个新值。在C++和大部分程序设计语言中,与数学上判断两个数是否相等等同的操作符是==。这是包括一些有经验的编程老鸟在编程时也会犯的一个错误,特别是在使用分支语句时,比如: if(i=5)cout i; 那么不管i是否是5,都会输出i,因为i=5是个赋值表达式而不是关系表达式。
      
3 几个例子
 
例1:判断一个数是否为质数
根据质数的定义,如果一个数只能被1和它自身整除,那么这个数就是质数。按照这个定义,可以很快地写出程序,就是用2到n-1的数一个个地去除以这个数,如果都除不尽,那么就是质数。
int i, n
cin>>n;
for(i=2; i
       if(n%i==0){
              cout<<”不是质数”;
              break;
       }
if(i==n)
       cout<<“是质数”;
 
这个程序里,有个break, 表示跳出当前循环。也就是只要有一个数除得尽n,那么就不用再除了,否则会输出多次“是质数“。循环结束时,如果i==n,也就是说2,3, …,n-1都除不尽n,则n是质数。这个简单的程序,初学者也可能会犯一些错误:
易犯错误1:循环初始化,i必须从2开始,而不是1,否则肯定不是质数;
易犯错误2:循环控制条件表达式,应该是i
易犯错误3:if(i==n)而不是if(i=n)
当然,这个程序还有一个小问题,也就是边界问题:如果输入1,那么由于循环并不执行,所以什么都不输出。为此,可以在前面加一句 if(n==1)cout<<“不是质数”。有了循环后,编程就需要多考虑一些极端的边界条件,这是一个好的程序员的基本素养之一。
 
当然,这个循环可以改成如下的while循环:
cin>>n;
if(n==1)
cout<<”不是质数”;
i = 2;
while(i
       i++;
if(i==n)
cout<<”是质数“
else
       cout<<“不是质数”
 
学会分析循环结束条件和循环结束时变量的状态非常重要。这就涉及到循环控制条件表达式的逻辑分析,也是一种宏观思维,我们无需关系循环具体执行了多少次,只需要关注其最终结束时的状态就行。这里的while用了&&这个逻辑表达式,退出条件要么是i>=n, 要么是n%i==0。后者退出时i
需要说明的是,退出循环还有一种可能,就是循环体中使用了break语句,例如上面的for循环中所使用的那样。
 
当然,如果到这就结束了,那就成纯粹的编程语法练习了。程序不仅要正确,还要执行得更快才好。上面的例子中,如果输入的数n是10001,那么假如n是一个质数,循环体要执行9999次。怎么才能减少执行除法的次数呢?
第一个想到的方法是:如果试到n的一半还除不尽,那就不用再试了。也就是把for循环改为:for(i=2;i<=n/2;i++)。这样,就把除法的次数减少了一半。
再进一步,因为所有的质数只有一个偶数,其余都是奇数。如果输入的是个奇数,那么就不用去试偶数了。于是,程序变为:
if(n==1)
cout<<“不是质数“
else if(n==2)
cout<<“是质数”
else
for(i=3; i<=n/2;i+=2)
也就是如果n为奇数,则从3开始到n/2, 每次增加2,即用奇数去除n。这样做的除法次数就减为原来的1/4。
当然,这还不是最少的。如果一个数n不是质数,那么一定可以表示为两个数a和b的乘积,不妨假设n=a×b, 其中a<=b。因此a小于等于n的平方根,即a<=sqrt(n)。也就是说,从2开始试到不超过n的平方根的整数,如果还没有一个除得尽n,那n就一定是质数。从而,程序变为:
for(i=2; i<=sqrt(n); i++)
   …
 
再结合只有一个偶质数的事实,进一步可以变成:
if(n==1)
cout<<“不是质数“
else if(n==2)
cout<<“是质数”
else for(i=3; i<=sqrt(n); i+=2)
这样,尝试的次数只有n的平方根的一半了。对于输入n=10001来说,最多只需要尝试不到50次就可以判断出n是不是质数,相比于原来的9999次大为减少。
 
上面这个程序是判断某一个数是否为质数。如果输入n,要输出所有不超过n的质数,那么可以利用刚才的程序功能作为一个基本功能(一点点重用的思想),也就是对从2到n的每个数,都用上面的办法去判断其是否为质数。这实际上就引出了双重循环的概念:
cin>>n;
for(i=2;i<=n; i++)
{
       j= 2;
       while(j<=sqrt(i)&& i%j!=0)
              j++;
       if(j>sqrt(i))
              cout<
}     
当然,对于这一问题本身还有更好的方法,就是用筛法求不超过n的所有素数,有兴趣的可以参考我之前的文章《孤独而高冷的素数》。    
 
双重循环对孩子来说是一个巨大的挑战,因为第二个循环变量的取值通常受制于第一个循环变量的取值,因此分析清楚两者的关系至关重要。这需要一点抽象思维,更要搞明白第一重循环和第二重循环各自的目的。在上面的例子中,第一重循环遍历2到n中的每个数,判断其是否为质数;第二重循环则是判断某个数i是否为质数的具体做法,即按照素数的优化定义,遍历不超过sqrt(i)的所有数j,看是否除得尽i。
 
例2:在屏幕上输出如下的三角形。
         *
        * * *
       * * * * *
      * * ** * * *
这就需要双重循环,第一重控制输出几行,第二重控制每一行怎么输出。为了写出这个程序,首先得搞清楚数量关系(实际上是一种朴素的函数概念):第i行需要输出多少个*? 
孩子需要分析出这里的数量关系,即第i行输出的‘*’的数量是2×i-1个。在输出*之前,还要分析输出的空格数量与行数i的关系。
 
例3:试说出下面程序的功能
cin >> n;
while(n>0){
       cout<
       n=n/10;
}
通过试着输入几个数并模拟循环执行,孩子可以看出这个程序的功能是把一个输入的大于0的数倒过来输出一遍。即如果输入是123,那么输出是321。
正好最近昍在学进制,因此顺便给他普及一下进制和移位的关系。学过程序设计的人都知道在二进制里,乘2可以用左移1位来实现,除2可以用右移1位来实现。而如果存在一个计算机采用的是十进制,那么n/10就是把n右移1位,n*10则是把n左移1位。所以,即便是不模拟程序的输出,从宏观的角度,也可以看出程序的功能。
当然,这个程序也可以改用for循环;
cin>>n;
for( ; n>0 ; n/=10)
  cout<
 
再对比书上的下一个程序:
 
cin>>n;
while(n>0){
       cout<
       n=n/2;
}
这个程序的功能,从数学的角度,就是将10进制数n不断除2取余,比如6,那么输出是011,也就是十进制对应的二进制数倒过来写。但是,如果抛开我们人类的十进制背景,在计算机里n就是2进制数,这个程序和上面的程序本质上是一样的,就是每次把末位数输出,然后右移一位,实现了把二进制倒过来写的功能。
 
最后说说几种循环的用法。for和while都是先判断循环控制条件是否成立再决定是否执行循环体,循环体可能一次都不执行。而do-while循环则不管三七二十一都会先执行一次循环体,再判断是否要继续执行循环,因此do-while循环至少执行一次循环体。
理论上,这三种循环可以互相转化,用哪个主要看个人喜好。对我自己而言,如果循环执行的数量确定,那更愿意用for循环,而如果循环的数量本身不定,则更愿意用while循环。
话题:



0

推荐

昍爸

昍爸

37篇文章 2年前更新

昍爸,曾获初中和高中全国数学奥林匹克联赛一等奖,江苏赛区第一名,高考数学满分,现在大学计算机专业任教,平时注重提升孩子对数学的自我思考与应用能力。此公众号将伴随昍昍的成长,分享寓教于乐、学以致用的数学教育方式。微信公号:昍爸说奥数(xuanbamath)。

文章