You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

10 KiB

13 | 程序设计原则:把计算过程交给计算机

你好,我是胡光,欢迎回来。

上一节中,咱们说了数学思维对于编程的重要性,并且跟你介绍了一种最重要的程序设计思维:数学归纳法。这个思维,不仅可以帮助我们设计程序,而且还可以帮助我们理解及证明程序的正确性。

不过说了这些数学对编程的重要性,可能你还觉得不过瘾,感觉只是停留在理论层面,还是有一层窗户纸没有捅破。今天呢,我就给你带来一道具体的编程问题,从这个具体的问题中,让你过把瘾。

一道简单的数学题

首先,我们先看一道很简单的数学问题,求出 1000 以内所有 3 或 5 倍数的数字的和。什么意思呢我们先缩小范围就是求10 以内,所有 3 或 5 的倍数。我们很快就能找到,这里有 3、5、6、9 ,它们相加之和是 23。注意这里说的是 10 以内,所以不包括 10。

回到1000以内这个原问题这个问题其实很简单可能你现在就想马上撸起袖子开始写代码了。可别急听我给你分析分析怎么做才算是又好又快地用程序解决这个实际的数学问题。

1.把计算过程,交给计算机

一个简单的疑问,我们为什么要写程序,让计算机帮我们算这个问题呢?那是因为,计算机的计算速度,比我们人类要快上几百几千倍不止,出错率也比我们要低得多。我们写程序的一个目的,就是减少我们人类在解决问题中的具体计算过程,那什么叫做具体计算过程呢?

例如,当你写一行代码“ 3 + 5 ”的时候,这是把计算过程交给了计算机,而如果你直接在程序中写上了 8 这个结果的时候,相当于你自己做了这个计算过程。因此,所谓减少我们的具体计算过程,就是能在程序中写 3 + 5就写 3 + 5不要写 8。

这就是我要强调的,要把计算过程交给计算机来做,而不是我们自己来做,毕竟计算机是很擅长做这种事情的,你没必要替它省这个事儿。在这样的指导思想下,我们先来看下面这段程序:

#include <stdio.h>
int main() {
    int sum = 0;
    for (int i = 1; i < 1000; i++) {
        sum += i * (i % 3 == 0 || i % 5 == 0);  
    }
    printf("%d\n", sum);
    return 0;
}

这段程序中循环遍历1000以内的所有整数然后把 3 或 5 的倍数累加到变量 sum 中,最后输出 sum 变量的值,就是 1000 以内,所有 3 或 5 的倍数和。

其中有一个编程技巧,就是利用条件表达式 (i % 3 == 0 || i % 5 == 0) 与数字 i 相乘,条件表达式等于 1 的时候,说明 i 是 3 或 5 的倍数sum 累加的值就是 i * 1 就是 i 的值而当条件表达式不成立的时候sum 累加的值就是 0。掌握这个编程技巧,关键是理解条件表达式的值。

看完了程序的基本逻辑以后,我们来想想,在上述的程序中,有哪个数字,是我们人为计算得到,然后再写到程序中的?你会发现,根本没有。也就是说,我们将所有的计算过程,都交给了计算机,让它来帮我们完成。而我们做的,仅仅是描述这个计算过程,所以这份程序是一份合格的程序。

2. 数学思维:提升计算效率

为什么评价上面的程序,只是一份合格的程序呢?我们想象这么个场景,你是一个老板,手底下有一个工人,你的目的要让工人抬来一桶水。你可能有两种吩咐工人做事的方法:第一种,让工人拿个水瓢,去到 3 里以外,一瓢一瓢的打水,他来来回回跑好几趟,才能打满一桶水。第二种方式,就是你让工人去库房里面拿个水桶,然后再到 3 里以外去打一桶水回来,这样工人只需要跑一趟就能完成任务。

在这两个方法中,第一种工人打满一桶水的效率,明显要差于第二种,而造成这样的结果,是因为你作为老板,教给工人的方法不同,导致效率上的差别。

而在编程中呢,计算机其实就像示例中的工人,你教给它什么方法,它就执行什么方法,任务完成的效率,和计算机没关系,而是和你完成程序,所教给计算机的方法有关系。这个方法呢,就是我们前文中所说的“算法”。

再回到之前那个要求出 1000 以内所有 3 或 5 倍数的数字和的程序,程序虽然完成了任务,可是完成的效率不够高效。

下面我们就把数学类的算法思维,加进程序中,看看效果吧。记住,加入数学思维的同时,也要保证,将计算过程留给计算机。首先来看如下程序:

#include <stdio.h>
int main() {
    int sum3 = (3 + 999 / 3 * 3) * (999 / 3) / 2;
    int sum5 = (5 + 999 / 5 * 5) * (999 / 5) / 2;
    int sum15 = (15 + 999 / 15 * 15) * (999 / 15) / 2;
    printf("%d\n", sum3 + sum5 - sum15);
    return 0;
}

上面程序中,有三个整型变量分别代表 1000 以内所有 3 的倍数的和 sum3所有 5 的倍数的和 sum5和所有 15 倍数的和 sum15。最后呢用 sum3 + sum5 - sum15 的值,代表了 3 或 5 的倍数的和。你对这个结果可能有点反应不过来,听我继续给你解释。

假设,我们现在手上有两个集合,第一个集合中装的是所有 3 的倍数,第二个集合中装的是所有 5 的倍数,想想两个集合的交集是什么?是不是就是所有 15 的倍数。那么当我们用第一个集合的所有元素和,加上第二个集合中的所有元素和的时候,两个集合交集中的元素,被重复加了一次。所以,最后再减去两个集合交集中的元素和即可。如上所述的程序思路,你可以参考如下示意图。

看完了程序思路以后,我们来具体看一下其中的代码,就拿 sum3 的计算过程来举例,其实使用的就是“等差数列求和公式”,如果你忘了等差数列求和公式,请看下图:

我们再来回顾一下程序在编写这个程序的过程中其中有哪些数字是我们计算得到的么你会发现没有一个是我们直接计算得到的哪怕是5 的倍数995这个数字也是我们通过一段代码算得到的。

而对于这段代码呢,咱们可以详细解释一下,首先用 1000 以内最后一个数字 999 除以 5会得到在1000 以内 5 的倍数有多少个。为什么会得到这个结果呢?这个就要说说 C 语言中的整型间的除法问题了。

在 C 语言中,两个整型数字相除,结果会做向零取整,什么是 向零取整呢?解释这个概念之前,先要介绍一下向下取整的概念,所谓向下取整,就是取小于等于当前数字的第一个整数。

例如4.9 向下取整,就是 4因为小于等于 4.9 的第一个整数就是 4。那么 -1.5 向下取整等于多少呢?这里需要注意,结果是 -2不是 -1因为小于等于 -1.5 的第一个整数是 -2而 -1 比 -1.5 要大。

当你明白了什么是向下取整以后,就很好理解向零取整了,那就是取当前数字和 0 之间,与前数字距离最近的整数。对于正数来说,向零取整的结果和向下取整的结果相同,而对于负数来说结果恰好相反。

咱们还是拿 -1.5 举例,向下取整是 -2可是向零取整就不同了向零取整是在当前数字与 0 之间,取一个距离当前数字最近的整数,取到的就是 -1。

理解了 C 语言中的整数除法规则以后,我们再回到题目中看一下,题目中用 999 / 5 得到的就是 1000 以内有多少个 5 的倍数的数字,然后再用这个数字乘以 5 就得到了 1000 以内,最后一个 5 的倍数的数字。

这时候你可能又问了,为什么要这么麻烦呢?何不直接写一个 995 呢你算得没错995 确实是 1000 以内最后一个 5 的倍数。可你别忘了今天我想教给你的是“把计算过程交给计算机”也就意味着计算5的倍数可能还轻松一点儿那要是计算 7 的倍数呢13 的倍数呢9973 的倍数呢?你会发现,还是计算机比你更适合做具体的计算。所以记住:将计算过程,留给计算机。

一起动手,搞事情

在做今天的思考题之前,我们先来弄清楚两个说法,“平方和”以及“和的平方”。

例如10 以内自然数的平方和就是:

1^2 + 2^2 + 3^2 + 4^2 + 5^2 + 6^2 + 7^2 + 8^2 + 9^2 + 10^2 = 385

也就是 1 到 10 每个数字的平方相加之和。

10 以内自然数的和的平方就是:

(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10) ^ 2 = 3025

也就是 1 到10 所有数字相加之和,然后再取平方的值。

思考题:和的平方减平方和

今天我们的思考题呢,分成两个子问题:

1.请编写一个程序,计算 100 以内自然数“和的平方”与“平方和”的差。
2.通过今天的学习,我们复习了等差数列求和公式,那你能否通过查阅资料,推导得到等差数列的平方和公式呢?

课程小结

好了,最后我们来做一下今天的课程小结吧。通过今天这个简单的小任务,我希望你记住如下三点:

  1. 具体的计算过程,计算机比你更擅长,所以请把具体的计算过程,留给计算机。
  2. 编写程序,其实是在描述和规定计算过程,而描述的方式不同,效率也不同。
  3. 不同的效率过程,就是我们所谓的不同的算法过程,记住:算法很重要。

关于“算法很重要”这句话,你可能有点儿听腻了,可我还是要强调一遍:所谓算法,叫得上来名字的算法是算法,还有很多叫不上来的名字,其实也是算法。两者放在一起,统一被描述成为“算法思维”。你想掌握一个有名字的算法很容易,可要掌握“算法思维”可就没那么容易了,这是需要很长一段时间的锻炼、总结和积累。

好了,今天就到这里了,不积跬步,无以至千里,希望你在看完本节课后,自己也多加练习体会。我是胡光,我们下期见。