☆怦然*%心☆_☆动,你好!
我们昨天的内容算是一个铺垫,确立了评判计算机算法好坏的基础和标准。今天我们以计算机科学中最常见的算法——排序算法为例,说说提高效率的本质。
排序是我们在生活中经常会遇到的事情。在学校里老师会把一个年级的学生按照成绩排序,或者按照中学所在的地域排序;做电商的人,可能需要把所销售的各种商品按照收入或者交易量排序。排完序,我们有时就能看出很多规律,或者作进一步的处理了。比如,给学生排完序老师就可以评奖学金或者建议推研了,电商对销售根据一些选项排序后,就能改进自己的业务了。将现实世界的这些问题,变成计算机可以运行的程序,中间的桥梁就是排序算法。
计算机最早的排序算法源于人的生活和经验,这就如同最早的计算机下棋是模仿人,最早设计的飞行器是模仿鸟一样。那么我们人是怎么排序的呢?如果只有三五个数字,我们可以扫一眼就排完了序。但如果到几十个数字,这就有点麻烦了,因为如果没有一个必须严格遵守的流程,排完序经常会有些小错误。我在中学时还没有计算机,我们常常帮助老师统计同学们的期末考试成绩,发现把一个班上五十个人的成绩排序丝毫没有错误,并不容易。后来我们发现比较可靠的办法其实是下面两种笨办法:
方法一:第一次挑出成绩最高的同学,第二次挑出成绩次高的,如此重复,肯定能完成成绩的排序,一定不会错。
方法二:先将成绩单上第一个同学的名字和成绩写到旁边一张白纸的中央,如果第二个同学成绩比他高,就写到第一个同学的上方,如果比他低,就写到下方。等看到第三个同学的成绩后,根据他的成绩与前两个同学成绩的比较,插入到相应的位置。比如他的成绩正好在两个同学之间,就在旁边那张排序的纸上,把他的名字插入到前两个人之间。当然,那张排序的纸要留够空白,以方便插入后来同学的名字。
用这两种方法排50个人的成绩,工作量并不算小,比大家想象的大很多。是否有更好的排序方法,我和我高中的同学都没有想出来,其实也懒得想,因为我们所在的世界周围就那么些人,这两种笨办法够用了。
其实,早期的计算机科学家比我们也强不到哪里去,他们提出的排序算法就是上面两种。第一种算法被称为冒泡排序,因为每一次选出一个最好的,如同从水里冒出的气泡。第二种被称为插入排序,因为每一次要找到合适的位置插入。
接下来我们就用高德纳的思想,分析一下上述算法的复杂度。第一种算法很容易估算,50个人中找出最大的要比较49次,第二大的要比较48次,以此类推,大约是1200多次,是50的平方的一半左右。第二种算法的复杂度,也是和50的平方有关,这里就不再分析了。如果我们把50扩展到任意一个整数N,事实上使用上述排序的复杂度都是N平方左右。我们昨天讲到,在衡量计算机算法复杂度时,科学家们不关心几倍的差别,因此,在用数学公式表达复杂度的时候,高德纳干脆删除了前面的常数因子,只保留后面的变量,他用了微积分中的一个概念——大写的O的概念,所有同等复杂度的算法,都被认为它们在"大O的概念"下是相等的。比如,上述冒泡排序算法的复杂度是O(N平方),插入排序也是O(N平方),属于同一个量级。此外,早期的计算机科学家还想出了其他的排序算法,复杂度都和它们差不多,在一个量级。
接下来怎么提高计算机算法的效率呢?节省20%的计算量是没有意义的事情,就算省个三五倍也没有意义,要省就干脆多省一点,省它个成千上万倍甚至更多。于是,全世界所有的算法专家经过了十多年,终于发现从经验出发的排序速度慢的原因,就是做了无数的无用功。要提高效率,就需要让计算机少做事情。
以冒泡排序为例,之所以慢,是因为每一次选出一个最大的数,都要和其它所有的数字相比,其实并不需要这么麻烦,要想提高效率,就要减少数据之间的相互比较。最早对冒泡排序的改进是一种叫做归并排序的算法,它就利用了少做事情的思想,归并排序的思想大致如下:
首先,科学家们发现,如果我们把全班同学分成两组,分别排序,那么从每一组中挑选出一个最大的,就能省去一半的相互比较时间。于是他们就先将整个班级一分为二,先分别进行排序,再把两个排好序的组,合并成为一个有序的序列。相比排序,对有序的序列合并是很快的。归并排序这个词就是这么来的。这样做大约可以节省一半时间。当然,我们在前面也讲过,节省一半时间意义不大,但是别着急,因为对一个班级分出来的两个小组,排序时也可以采用上述技巧。
第二步,就是对两个组的排序。显然我们不应该再用冒泡排序。聪明一点的人马上会想到,既然能分成两组,就能把每个小组再分为两组,即分成四组,重复上面的算法,分别排序再合并。这样就能省3/4的时间。
再接下来,四组可以分为八组,能省7/8的时间,八组可以分为十六组,时间就不断省得越来越多。分到最后每个小组只剩下两个人的时候,其实就不用排序了,只要比较一次大小即可。
这种方法其实可以理解为两个过程,先是自顶向下细分,再自底向上合并。那么这种算法的复杂度等于多少呢?它相当于N乘以log(N),log(N)就是N的对数函数,大家不必在意N乘以log(N)是什么东西,只要记住它和N平方不一样,而且这个复杂度比前面的N平方小很多就行了。
为了便于你理解它小了多少,我们看看当N分别是100,1万,1百万,1亿时,两种算法的复杂度的情形:
第一种:即N平方,当N是100,1万,1百万,1亿时,它对应1万,1亿,1万亿,1亿亿。
第二种:即N乘以log(N),它对 应700,13万,2000万,23亿。
你可以看出N比较大了以后,N乘以log(N)比N平方要小很多,即23亿和1亿亿的差别,相差大约400万倍。400万是什么概念呢?大约是一支毛笔的长度和北京到上海距离的差别,或者是你和我两个人的重量和瓦良格号航空母舰重量的差别。
这样一对比,你就能感觉到为什么计算机的算法是如此重要。算法没有学好的人,写出来的程序经常是不合格的,因为很容易就浪费掉成千上万倍的计算机资源。大家不要觉得一个亿是什么了不得的大数字,且不说腾讯或者阿里巴巴这样公司的用户数早就超过了一个亿,即使你编写一款游戏,只要有人使用,几天下来的日志都有上亿行,对一亿个数排序,在计算机应用中是很常见的事情。
归并排序算法相比之前的算法为什么效率提高这么多?因为它少做了很多并不需要做的比较。很多人问我,你为什么效率这么高?并不是我效率高,而是做事情比大家少。效率=产出/所做的事情。人的产出是很难提高的,但是所做的事情是可以减少的。
我一直强调工程师水平差一级,贡献可能就差出10倍,有人不服气,说会差这么大么?从上面的一个例子大家可以看出,可能差得还不止十倍。很多人问,我想改行搞计算机,自己找两本书看看是否就可以了?我的答案是不可以,在今天这样一个人多粥少,人的技能普遍高出行业基本要求的时代,进入任何一个行业,都要有点专业精神,把自己当作科班出身来要求。在农耕文明时代,没有力气的人最不济也能产生壮劳力1/3的生产力。在工业时代,靠手工一件件生产商品,可能效率能有机器的1%。但是到了智能时代,本事差一点,效果可能差几百万倍,几亿倍,专业和不专业就差得更远了。
为什么到了智能时代学习变得非常重要了呢?因为一些基本的方法和道理如果不学习,靠自己脑子苦思冥想可能需要十年甚至更多的时间,而学习加练习,可能只要一个月的时间就够了。早期的计算机科学家因为受限于自己的生活环境,也想不出好的排序方法,今天比较常用的排序算法"快速排序(Quicksort)",是在计算机诞生后13年才出现的。这个算法,我下次花十分钟时间,就可以给大家讲清楚,大家可以自己判断,是应该学习,还是自己琢磨呢?
明天我们会谈谈快速排序,以及它背后的方法论。
今天的思考题是这样的:归并排序相比那些效率低的简单排序,其实还说明了一个道理,就是将一个复杂问题分解成很多简单的问题,一个个解决,最后比直接解决复杂问题要省很多时间。能否就这件事发表你的看法?
已读