好 我们接下来讨论算法的设计与实现
所谓算法的设计和实现
就是要构造算法来解决实际的问题
就像原来讲的
你一定要按照自顶向下
逐步求精的方式来进行才行
为啥要这么去写呢
因为几十年来的程序设计经验表明
如果你不按照这个方式来书写
你写的算法花的时间就多
就是说效率不高
最典型的一个特点就是
我要解决这个问题
总是要搞清楚我这个算法的执行逻辑
我第一步应该做什么
第二步应该做什么
第三步应该做什么
想清楚了才能写程序
这就叫自顶向下
你从顶层开始思考问题
然后一点点地向细节进发
这是一个非常重要的设计思路
如果你倒过来写
先从细节开始 然后再去顶层
原来主框架的逻辑都没有搞清楚
你就开始写细节
写了半天了最后发现主框架不对
那不白做吗
这是最重要的地方
一定要自顶向下逐步求精
这里面就会通过两个例子
一个是素性判定的例子
一个是最大公约数的例子
给同学们展示
我们怎么设计一个优良的算法
通过这几个例子的不同实现
你也能够看出来
同样一个问题 换了一个解决思路
对于最终程序执行来讲
它的执行效率是千差万别的
先看第一个 就是素性判定的问题
判定某个给定的自然数n
n大于2 先不考虑2的问题
先说说大于2是不是素数
按照定义 很容易写出对应的伪代码
注意在此之前 严格的算法书写模式
应该在前面写上输入部分
输出部分是什么
输入是个大于2的正整数n
输出 就是n值 是素数还是不是素数
如果是 返回true
否则返回false 这样把它写上
有什么好处呢
这就很容易地
决定这个函数的原型是什么样子
有了输入和输出
函数的原型基本上就呼之欲出了
写伪代码的时候同学们一定要注意
接下来就是步骤
步骤1、步骤2、步骤3和步骤4
就按照它的定义来 写出来就行了
伪代码我们就不解释了
看我们的实现
给出素性判定函数的第一版的实现
bool IsPrime(unsigned int n)传过来的是一个n值
然后unsigned int i初始化成2
从2开始除起 2、3、4 一直到n-1
如果全部都不能整除
那就说明它就是素数了
所以while(i<n)
if(n % I == 0)
除了以后取余数
如果它的余数为0 就return false
它是合数不是素数
如果不成 怎么办呢
它没有return false 就i++
2除不尽
然后试着用3去除 看看能不能除尽
3不行看4、4不行看5
按照这个方式一点点地除 一直到n-1
这个循环做完 它也没有返回false
那没辙了 只能return true
这就是算法的第一版的实现
你可以验证一下是不是算法
对照算法的五个特征
有穷性、确定性、输入、输出、有效性
看它是不是满足要求 是的
那么它就是算法
然后我们就要证明这个算法
这个算法吻合我们的素数定义
所以它实际上也是算法
然后你会测试 就写一个主程序
然后调用这个函数 运行一下
有的放矢地给一些特定的数
看一看满不满足要求
最后得到结论是不是正确的
一测试发现算法是对的
刚才那个算法效率不高
那么现在就可以考虑做一个更快一点的
怎么样让算法做得更快呢
那就有一个很重要的观察
就是如果一个数是合数
那么它一定会有两个因子
假设这两个因子是p、q
p、q的乘积是n
那p、q肯定有一个大一个小
假设较小那个是p
那p肯定是小于等于q
在这种情况下p一定是小于等于根号n的
这意味着是什么呢
如果这个数n是一个合数
那么它一定有一个小于根号n的因子
如果2到根号n之间
都找不到它的任何一个因子
比根号n还大的那个整数
我们就不用找了
那个因子肯定不存在
有了这个方案的话
那就说while循环
我压根都不需要到i<n这个地步
我只到i<=sqrt(n)就可以
这样的话 我的测试就从2到n-1
一口气降低到了2到根号n这个级别
程序比原来快多了
你想原来如果n是10000的话
那我原来要比较2到9999
要比较这么多个数
这是多少个数 这是9998个数
你才能得到结论
现在呢 我只要比较2到100就行了
我只要比较99个数
刚才是多少啊 刚才9998
现在是多少 接近原来1%
速度立马就快了还跟多
非常非常明确
但是同学们要记住
sqrt这个函数 是C标准库里面的函数
那个函数写在“math.h”头文件里面
你要想用它 你必须包含数学库
数学库的名字叫什么呢
在C里面叫“math.h”
在C++里面呢 你可以用“math.h”包含它
也可以使用“cmath”去包含它
这两种包含策略都可以
但是你在Linux下面写程序的时候
要特别记住
Linux缺省情况下 是不把数学库
库代码装载进去的
你要装载它
应该在链接的时候g++后面写上“-lm”
把数学库 链接进去
才能够编译通过
这是第一个地方需要特别解释的
第二个地方呢 就是sqrt函数
它接受的实际上是一个
double类型的浮点数
返回值还是一个double类型的浮点数
比较这个浮点数没有意义
对于我们来说实际上是一个整数
所以我们要把sqrt得到的结果
转型成一个unsigned int
所以我们的程序代码
是按照这样方式写的
这就是素性判定的第二版
明显比第一版的效率实际上就要高的
接下来就要想
这个程序能不能做得更快
我们还有一个招
什么招呢 就是说如果这个数是个偶数
那它一定不是素数 它是个合数
刚才讲了吧 我们是大于2的数嘛
那就很明确了
if(n%2==0) 那说明它是合数 return false
再此之后 从3开始除
还除4吗 不用除 我们除5
我们只需要除奇数就行了
所以每次我不再i++ 我i+=2
整个程序运行效率
比刚才第二版又快了一倍
几乎所有的偶数被跳过
只不过前面多了一个If
后面的偶数就全眺过了
所以效率显然比刚才又快了
这是我们第三版
但是我们现在要注意到一个地方就是
这两个版本实现
实际上隐含着一个问题
这个问题一般人注意不到
早期的计算机上面
这是有可能存在的一个隐含的bug
现在新式的计算机
这个问题一般就不太突出
问题在哪里呢
就是sqrt这个求平方根这个函数
最后得到的结果它是一个浮点数
如果这个数是完全平方数
假设一个数字121
它是完全平方数 是11的平方
那么它要求根号
理论上应该等于11.00000
但是因为它表示的问题
计算机内部表示我们以前讲过
浮点数它是有误差的
存在误差的情况下面
这个11.00000
有可能被表示成了11.00001
它也可能被你表示成10.99999
可能会存在这样一种情况
如果很不幸
计算机把11这个整数
作为浮点数保存的时候
会保存成10.99999的话
那这个程序就崩掉了
为啥呀 当它转化成unsigned int的时候
它的小数部分会被砍掉
它不会四舍五入的
它一砍掉以后结果就是10
也就是说会漏过一些完全平方数
实际上是合数 但是被你误认为是素数
怎么解决这个问题呢 简单
就把这个地方加1
把sqrt取出平方根以后转换成整数
然后把它加1 多判断一次
就把这个隐含的bug消灭掉了
我自己在现代计算机上做了测试
我一直测试到100万
所有完全平方数
它最终求平方根以后得到结果
都是完全精确的
后面小数点部分全是0
所以说在现在计算机上
一般看不见它的误差
那么现在我就要问
从第二版一直到第四版
我们的程序真得很快吗
理论应该比我们第一版快
但是它真得很快吗 还真不一定
为啥不一定呢 很重要一条
你注意看我原来代码是怎么写的
我把那个求平方根的函数写在
while循环条件表达式里
这就意味着每做一次循环迭代
我就要求一次平方根 没必要
对于我们这一道题来讲
n的值在我们的函数内部是不变的
取它的平方根
只需要取一次就够了
我没必要反复计算它的平方根
所以我们最好的方案
是将这个求平方根的函数
从while循环条件表达式里面
给它提取出来 放到循环之前
这意味着我们做while循环的时候
再也不会去算根号n了
根号n在我们进入while循环之前
就已经算好了 只算一遍就够了
那么整个程序效率提升
就会非常非常明显
我们给出了五版的素性判定函数
你就看得很清楚
当我解决一个问题 设计算法的时候
应该怎么去做
第一个 必须保证你这个算法
写出来是正确的
第二个 衡量一个算法的好坏
它实际上有两个主要的出发点
一个我们称它为效率
一个称它为可理解性
算法写得好坏与否
那第一个就体现你算法做得快不快
能够迅速地准确地得到答案
这就要迅速
这就叫效率 效率要高 这是一个
第二个就是可理解性
你这个算法写出来
别人能不能够很容易看懂
这就叫可理解性好
如果可理解性不好
那么别人来维护你这个算法
想对你这个算法进行修改、更新、升级
怎么去处理呢
你的代码读都读不懂 可理解性太差
那根本没有办法维护
涉及到效率和可理解性
你设计这样一个算法就必须综合考虑
在设计一个算法的时候
效率和可理解性 有的时候是属于
鱼与熊掌不可得兼的一个东西——
你不可能既要求效率又高
可理解性又好
这种情况往往很难出现
经常出现一个场合就是
一个算法效率很高
但是很难读懂
如果这个算法很简单 一眼就能懂
它效率往往就不高
所以当我们进行算法评估的时候
衡量这个算法的好坏
必须在效率和可理解性下面
做一个折中
虽然我们说算法的好坏主要是效率
但是对于初学者来讲
可理解性其实更重要
同学们写算法的时候要特别去注意
要尽可能用一种简单的方式来实现
哪怕效率低一点也能接受
这是我们算法选择 效率和可理解性
两者要进行一次折中 注意这一点
我们来看第二个例子
这就是最大公约数这个例子
求两个正整数x和y最大公约数
直接给出函数原型
gcd 后面跟着俩整数
然后返回值就是它的最大公约数
还是一个整数 我们不考虑负数
最大公约数我给出两种设计方案
一种设计方案就是穷举法
就是我们来个最笨的方案 那就除呗
x和y两个数
一直从大往小找共同的因子
一直递减到1 如果互素的话
1就是它的最大公约数 肯定能找到
所以我们就按照它的定义
很容易就写出这个代码
我定义个临时变量t
设成x、y较小的那一个
然后用while循环
看x%t!==0 或者y%t!==0
只要有一个不等于0 就说明什么
就不能整除 我就要t--
最终就能够得到一个值t
返回这个t就行了
因为这个循环 不管怎么样
当t递减到1的时候
它就一定能够整除了
就是说这个循环是能够结束的
t就是最大公约数 这就是穷举法
当两个数互素的时候
比如说1000和1001
这两个数是互素的
所以你要按照这个方式除
先除1000 除999 除998 一直除到1
你要除1000遍
才能够得到它们的最大公约数是1
这个穷举法效率显然是低的
看第二个解决方案
这个第二种解决方案叫欧几里得算法
在中国古代称之为辗转相除法
它的基本意思就是什么
两个正整数x和y
用x整除以y 设它的余数是r
如果r是0
那么y就是它的最大公约数
如果不是的话
那我们就把y作为新的x
把r作为新的y 然后重复
就按照这个重复的模式去计算
我们三两下
就能够把它的最大公约数给除出来
明显比刚才那个算法的效率快很多
但是同学们要知道
这样一个算法别看它代码很短
它很容易就能够发现吗
不那么容易
你别觉得 中国古代很久很久之前
就有这样一个算法
如果你以前没有学过数论方面知识
让你现在自己凭空去想这个算法的实现
你就会发现这个算法真不那么容易发现