学习数据结构和算法的第一步面试
分析时间复杂度的时候是不考虑前边的系数的,好比说O(1)的话,并不表明它的复杂度是1,也能够是二、三、4...,只要是常数次的,都用O(1)表示算法
最经常使用的方式就是直接看这段代码,它根据n的不一样状况会运行多少次数组
O(1) $n=100000; echo 'hello'; O(?) $n=100000; echo 'hello1'; echo 'hello2'; echo 'hello3';
第一段代码,无论n是多少,都只执行一次,因此它的时间复杂度就是O(1)。第二个其实也同理,咱们不关心系数是多少。虽然第二段代码会执行3次echo输出,可是无论n是多少,它都只执行3次,所以它的时间复杂度也是常数复杂度,也就是O(1)缓存
在看下边两段代码:数据结构
O(n) for($i = 1; $i <= $n; $i++) { echo 'hello'; } O(n^2) for($i = 1; $i <= $n; $i++) { for($j = 1; $j <= $n; $j++) { echo 'hello'; } }
这两段代码都是随着n的不一样,它执行的次数也在发生变化,第一段代码执行的次数和n是线性关系的,因此它的时间复杂度是O(n)。数据结构和算法
第二段代码是一个嵌套循环,当n为100的状况下,里边的输出语句就会执行10000次,所以它的时间复杂度就是O(n^2)。若是第二段代码中的循环不是嵌套的,而是并列的,那么它的时间复杂度应该是O(2n),由于前边的常数系数咱们不关注,所以它的时间复杂度就是O(n)函数
O(log n) for($i = 1; $i <= $n; $i = $i*2) { echo 'hello'; } O(k^2) fib($n) { if ($n < 2) { return $n; } return fib($n-1) + fib($n-2); }
第一段代码,当n=4时,循环执行2次,因此循环内部执行的次数和n的关系为log2(n),所以时间复杂度为对数复杂度O(logn)。第二段是一个Fibonacci(斐波拉契)数列,这里是用了递归的这种形式,这就牵扯到了递归程序在执行的时候,如何计算它的时间复杂度,它的答案是k^n,k是一个常数,是一个指数级的,因此简单的经过递归的方式求Fibonacci数列是很是慢的,它是指数级的时间复杂度。具体指数级的时间复杂度是怎么获得的,后边会详细说明。下边看一下各类时间复杂度的曲线学习
从这张图中能够看到,当n比较小的时候,在10之内的话,不一样的时间复杂度其实都差很少。可是若是当n继续扩大,指数级的增加是很是快的。所以,当咱们在写程序的时候,若是能优化时间复杂度,好比说从2^n降到n^2的话,从这个曲线来看,当n较大的时候,获得的收益是很是高的。所以这也告诉咱们,在平时开发业务代码的时候,必定要对本身的时间和空间复杂度有所了解,并且是养成习惯,写完代码以后,下意识的分析出这段程序的时间和空间复杂度。优化
从图中能够看到,若是你的时间复杂度写砸了的话,其实带给公司的机器或者说资源的损耗,随着n的增大的话,是成百上千的增长,而若是你能简化的话,对公司来讲是节约不少成本的spa
对于不一样的程序,在写法当中完成一样的目标,它可能会致使时间复杂度的不同。下边看一个简单的例题
从1加到2一直加到n,求它的和
小学学数学的时候,你们都知道了有两种方法,方法一的话用程序暴力求解的话,就是从1循环到n累加。这个是一层循环,n为多少,就执行多少次累加,因此它的时间复杂度就是O(n)
$sum = 0; for ($i=1; $i <=$n; $i++) { $sum += $i; }
方法二就是使用一个数学的求和公式:
y = n*(n+1)/2
用这个公式,发现程序就只有一行了,因此它的时间复杂度就是O(1)了。因此能够看到,程序的不一样方法,最后获得的结果虽然是同样的,可是它的时间复杂度很不同
递归的话,关键就是要了解它的递归过程,总共执行了递归语句多少次。若是是循环的话,很好理解,n次的循环就执行了n次。那么递归的话,其实它是层层嵌套,其实不少时候,咱们就是把递归它的执行顺序,画出一个树形结构,称之为它的递归状态的递归树。之前边的求Fibonacci(斐波拉契)数列的第n项为例
Fib:0,1,1,2,3,5,8,13,21... F(n) = F(n-1)+F(n-2)
我以前面试的时候遇到过这么一道题,写的是最最简单的用递归的方式实现
fib($n) { if($n < 2) { retuen $n; } return fib($n-1)+fib($n-2); }
前边有说到它的时间复杂度是O(k^n),那么怎么获得的,能够分析一下,假设此时n取6,要计算Fib(6),就看这段代码如何执行
因此,若是想计算F(6)就引出两个分支,F(5)和F(4),至少多出了两次运算
若是要计算F(5)可同理获得,须要结算F(4)和F(3)。若是要计算F(4)可同理获得,须要结算F(3)和F(2)。这里就能够看到两个现象:
正是由于有这么多大量冗余的计算,致使求6个数的Fibonacci数的话,就变成了2^6次方这么一个时间复杂度。所以在面试中遇到这类题,尽可能别用这种方式写,不然怕是直接凉凉了。能够加一个缓存,把这些中间结果可以缓存下来(用数组或哈希存起来,有重复计算的数值,再从里边找),或者直接用一个循环来写
介绍一个叫主定理的东西,这个定理为何重要,就是由于这个主定理的话,其实它是用来解决全部递归的函数,怎么来计算它的时间复杂度。主定理自己的话,数学上来证实相对比较复杂(关于主定理能够参考维基百科:https://zh.wikipedia.org/wiki...)
也就是说,任何一个分治或者递归的函数,均可以算出它的时间复杂度,怎么算就是经过这个主定理。自己比较复杂的话,那怎样化简为实际可用的办法,其实关键就是这四种,通常记住就能够了
通常在各类递归的情形的话,有上边这四种情形,是在面试和平时工做中会用上
二分查找(Binary search):通常发生在一个数列自己有序的时候,要在有序的数列中找到目标数,因此它每次都一分为二,只查一边,这样的话,最后它的时间复杂度是O(logn)
二叉树遍历(Binary tree traversal):若是是二叉树遍历的话,它的时间复杂度为O(n)。由于经过主定理能够知道,它每次要一分为二,可是每次一分为二以后,每一边它是相同的时间复杂度。最后它的递推公式就变成了图中T(n)=2T(n/2)+O(1)这样。最后根据这个主定理就能够推出它的运行时间为O(n)。固然这里也有一个简化的思考方式,就是二叉树的遍历的话,会每个节点都访问一次,且仅访问一次,因此它的时间复杂度就是O(n)
二维矩阵(Optimal sorted matrix search):在一个排好序的二维矩阵中进行二分查找,这个时候也是经过主定理能够得出时间复杂度是O(n),记住就能够了
归并排序(merge sort):全部排序最优的办法就是nlogn,归并排序的时间复杂度就是O(nlogn)
二叉树的遍历-前序、中序、后序:时间复杂度是多少?
答案是:O(n),这里的n表明二叉树里边树的节点的总数,不论是哪一种方式遍历,每一个节点都有且仅访问一次,因此它的复杂度是线性于二叉树的节点总数,也就是O(n)
图的遍历:时间复杂度是多少?
答案:O(n),图中的每个节点也是有且仅访问一次,所以时间复杂度也是O(n),n为图中的节点总数
搜索算法:DFS(深度优先)、BFS(广度优先)时间复杂度是多少?
答案:O(n),后边的文章会详细介绍这两种算法(n为搜索空间中的节点总数)
二分查找:时间复杂度是多少?
答案:O(logn)
空间复杂度和时间复杂度的状况其实相似,可是它更加的简单。用最简单的方式来分析便可。主要有两个原则:
若是你的代码中开了数组,那么数组的长度基本上就是你的空间复杂度。好比你开了一个一维的数组,那么你的空间复杂度就是O(n),若是开了一个二维的数组,数组长度是n^2,那么空间复杂度基本上就是n^2
若是是有递归的话,那么它递归最深的深度,就是你空间复杂度的最大值。若是你的程序里边递归中又开了数组,那么空间复杂度就是二者的最大值
在快速变化的技术中寻找不变,才是一个技术人的核心竞争力。知行合一,理论结合实践