从上大学第一天开始接触编程,老师便给咱们讲过各式各样的算法。从各类查找、排序,到递归、贪心等算法,大一的时候一直在和这些算法搏斗。直到工做后,为了应付面试,仍不得不回过头去啃算法书或者去刷一些算法习题,才可以拾回一些上学时的记忆。为何算法就这么难以记住呢?或者说,为什么计算机的算法不能更直观一些呢?java
由于计算机的算法就是反人性的,从本质上来讲,这是计算机的思惟方式和人脑思惟方式的区别而形成的。面试
人脑思惟的机制至今没有一个肯定的理论,暂时认为是化学物质和电信号的做用。虽然没有科学的解释,可是咱们每一个人都有一颗大脑,咱们每一个人均可以感觉到本身的思惟方式。算法
而计算机则是人类创造的,从设计之初它便不是以模拟人脑为目的,所以它有其独特的工做方式,只有理解了计算机的工做方式,才能够学会以它的方式去思考, 才能够写出最适合计算机运行的程序代码。编程
咱们经过一个具体的例子,来讲明人脑和计算机的思惟方式不一样,假设咱们想要从一个已经排好序的数组中找出一个特定的数字。数组
已知排序好的数组是1 2 3 5 7 13 34 67 90 127 308,咱们但愿找到是否13这个数在数组内。缓存
人脑是如何去完成任务的呢?函数
人脑处理这样的问题几乎是“做弊”的,咱们能够一目十行,咱们在眼镜一扫视的状况下就发现了13,因此若是我问本身我是如何找到13的,我只能说我“看见”了。性能
而计算机是如何来完成这个任务呢?学习
最简单也是最笨的算法就是从数组开始一个一个的读入数组,我相信每一个学习过编程基础的同窗均可以写出相似下面的代码。优化
boolean isNumInArray(int num, int[] array) { for (int i = 0; i < array.length; i++) { if (array[i] == num) { return true; } } return false; }
计算机须要从数组的第一个元素开始,一个一个的去查当前的数组的元素,和13相比,看看是否是相等。为了找出13这个数,计算机要作6次循环操做,而人几乎是瞬间就看到了答案。
为什么计算机解决问题的方式这么“笨”呢?咱们先得从计算机的工做原理提及。
CPU做为计算机的最核心的部件,也是算法的主要运载体。
CPU并不会像人同样思考,它只懂得一些基本的指令。每个CPU都有其指令集,指令集是存储在CPU内部,对CPU运算进行指导和优化的硬程序。通俗一点说,指令集就是CPU的全部思惟方式。好比常见的指令集中都会有ADD指令,这个指令能够将两个寄存器中的值相加,并将存储到另外一个寄存器中;与此相对应的也会有SUB指令,用于将两个寄存器值相减。若是你去查阅各类CPU指令集的手册,会发现基本上都会包含基本的加减乘除指令,以及向内存中存、取数据的指令。而常见的CPU指令集,最多也就是几百条指令。也就是说CPU只会这几百个命令。
而人脑相对于CPU,有强大的记忆和联想能力,好比你看到1+1,就想到2,看到红灯,就会想到停下来,看到门,就知道去开门把手,这些都是你不假思索能够马上反映出来的东西。
因此,CPU会的东西(指令)比人少多了,那CPU岂不是很笨?没错,CPU就是很笨,可是CPU的优势也是人脑所没法比拟的:
综上所述,CPU是一个既笨又快的家伙。
计算机的常见存储有寄存器、高速缓存、内存、硬盘等。
寄存器就至关于人脑中马上能够想起来的东西,CPU所作的一切运算都是针对于在寄存器中的数据进行的。寄存器存储了计算机当前要作什么计算(指令寄存器),要计算的数据(数据寄存器),计算到哪一步了(段寄存器)等信息。不管是最先的有寄存器的CPU仍是最新最强的的CPU,它们的寄存器数量最多也只有几十个(特殊状况有几百个),也就是说CPU同一时刻可以马上使用过的信息也就是这几十个数字。
内存则是计算机的主力存储设施,它能够存储运行中的程序的信息,内存至关于图书馆的书架,CPU须要用某一段内存中的数据是,须要经过LOAD指令,同时附上一个书架编号(内存地址),而后内存控制器能够将对应的地址的数据经过总线传输给CPU,CPU则将载入的结果放入寄存器中使用。内存存取的速度远小于寄存器,可是访问分布在内存各个区间的数据的速度基本是相等的。
因为大部分时候CPU须要读取连续的一段内存来进行运算,所以一般CPU会有高速缓存将最近使用过的内存整块缓存起来,而使得CPU没必要每执行一步就须要去读一次内存。高速缓存的速度介于寄存器和内存之间,但远高于内存。高速缓存的大小通常在几兆到十几兆之间。
硬盘属于外部存储,老式的机械硬盘中会有一个可转的磁头,在读取磁盘文件的时候须要将磁头转到对应的位置,磁盘的速度远低于内存,而且若是磁盘的磁头若是停留在某个位置时,随机磁盘上不一样位置的信息,会受到磁头运动的物理速度限制而出现速度不均等的状况。新式的固态硬盘采用了和内存类似的存储介质,在随机访问的性能上提高很大。
因此,计算机有一颗只能记得一点点事情的小脑壳(寄存器),可是可以拥有相对较大的快速记忆(缓存),拥有远超过人类的知识储备(内存),而且还随身携带了巨大的移动图书馆(硬盘),因此从存储上来看,计算机像是一个有先天缺陷的雨人(Rain Man)。
因此,咱们来分析一下round 1中为什么计算机到底作了怎样的操做?
首先咱们看咱们函数的定义
boolean isNumInArray(int num, int[] array)
在调用函数的底层实现中,参数是被分配到两个寄存器中。isNumInArray
这个函数,在被调用时,第一个参数num
的值13
会被载入到寄存器(r1), 的第二个参数array
,传入CPU的时候就只是array
在内存中的地址信息,被存储在另外一个寄存器(r2)。
而在第四行array[i] == num
时,CPU须要作三件事才能够完成这工做:
而根据操做3的结果,若是结果不相等,则CPU须要将循环计数器i
加上1存入寄存器r4,再次进行上面的计算。所不一样的是,第二到第N次的步骤二会比第一次要快不少,由于整个数组的内容已经被高速缓存所捕获。
因此,咱们能够看出为什么计算机在解决这个问题上显得如此愚笨:
计算机在上一轮和人脑的PK中败下阵来,然而这并非很公平,由于数组的数量只有短短的几个,而计算机能够存储的上限远不止于如此。因而咱们开始第二次的比拼。 此次咱们将输入扩大
1 2 3 5 7 13 34 67 90 127 308 502 ... 2341245 ... (100万个
查找的数变成了2341245。
此次人脑和计算机的表现又如何呢?
对于一个普通人,咱们假设这100万个数字是打印在一本字典里的,那么他如何找出100万个有序数组中的某个数字呢?
这时人类引以自豪的“一目十行”的能力已经微乎其微,当数字的位数增大时,且不说一眼比较一个数字是否和目标数字相同已经困难,即便真的有一目十行的本事,在100万这样的数字面前也是微乎其微。
因而乎,咱们老老实实的去从头至尾比较数字,一页一页的翻开,去看当前的页中有没有数字,没有的话就去翻下一页。
这个思路是否是很熟悉?没错,这就是计算机的思惟,和咱们上一节中所描述的计算机编码几乎是同样的,除了人能够一眼多看几个数据外。
然而,人类在比较大数是否相等的速度,以及翻字典的速度可远远比不上计算机去读完这100万个数的速度,一样是“笨鸟”,计算机每秒百万次的运算能力几乎能够在瞬间就完成这样的任务。
也就是说,在大规模输入的状况下,人脑的思惟方式“退化”成和计算机近似,可是被计算机压倒性的性能优点给击败。
在第二轮中,人脑败给了计算机,但这样的比拼无疑于两只笨鸟比谁更快。有没有聪明一些的方法呢?
没错,咱们学过二分查找(Binary Search)的算法能够派上用场了。
步骤一:有这么有一本打印了100万个数字的字典摆在咱们的面前,咱们不知道要找的数字会在哪里,那么咱们先折半打开字典(不用那么精确也不要紧),看当前页的第一个数字和最后一个数字,咱们要找的数字是否在这个范围内,若是在那么咱们能够继续在当前页找这个数字。
步骤二:若是当前页的第一个数字仍是比咱们要找的数字大,那么咱们能够将字典的后半部分撕了(由于咱们要找的数字不可能在后半部分了),继续上面的步骤。
步骤三:若是当前页的最后一个数字比咱们要找的数字小,那么咱们能够将字典的前半部分撕了(理由同上),继续步骤一。
这样咱们会讲这本字典越撕越薄,最坏的状况下咱们会撕到最后一页,这一页要么有这个数字,要么没有这个数字,可是咱们保证按照上面的步骤进行咱们不会错过任何可能含有这个数字一页。
这个逻辑和计算机算法中的二分查找原理是同样的,咱们来看看实际的算法代码是如何实现的
boolean isNumInArray(int num, int[] array, int start, int end) { if(num < arr[start] || key > arr[end] || start > end){ return false; } int middle = (start + end) / 2; //找到对折点 if(array[middle] > num) { return isNumInArray(arr, key, start, middle - 1); //撕掉后一半 } else if(array[middle] < num){ return isNumInArray(arr, key, middle + 1, end); //撕掉前一半 }else { return middle; } }
咱们能够看出,和人类的思惟方式比,计算机不会翻“一页”,它只会翻看一个数字,可是其余的思惟方式是如出一辙的。利用这样的算法,人类虽然从结果上仍是比计算机要慢,可是双方都找到了最适合的方法,达到自我效率的最大提高。
那么咱们回过头来看,为何我要假设这100万数字打印在字典上呢?由于字典和计算机内存的模型很像。
计算机能够经过内存地址来直接访问内存,这一点和经过字典的页码来翻到某一页,这一点是近似的。
在计算机编码中咱们能够知道数组的长度,而经过折半的方法找到中间的数,字典有厚度,咱们能够经过厚度减半来找到中间的页码,这一点也是类似的。
试想同样,若是100万的数字不是打印在字典,而是印在一条公路上,咱们是否还能够用上一节的算法来人肉二分查找?答案是不能够,由于跑到公路的一半会消耗你不少的体力,若是采用二分法查找比起round 1中的最笨办法只会让你耗费更多的体力。由于公路这一存储的概念,对应的便不是内存的模型,而是磁带(Tape)的模型,那么对于这样的模型,我相信不管是人或者是计算机, 都须要调整算法,来达到最高的效率。
经过以上的例子,咱们能够看到,计算机的算法反人性,是由于计算机不是一个“正常人”,它有本身的缺陷,也有本身的长处。不少时候咱们觉的算法不直观,不是由于咱们的思惟能力比计算机差,而偏偏是由于做为人类咱们同时接触的信息太多,所会的东西也太多而阻塞了咱们的思惟。那么这种时候,不妨将本身“堕落”成一台“鼠目寸光”和“所知甚少”的计算机,这时可能会有更清晰的思路。
本文已独家受权给脚本之家(ID:jb51net)公众号发布