咱们经常在武侠小说中看到一位内力精深的高手在学习新的招式的时候修炼速度异常惊人,我心目中最经典的片断就是倚天屠龙记中张无忌学习乾坤大挪移和太极拳的时候了,他能在极短的时间内领会常人数十年所不能掌握的东西,即便拍了不少版本,每次看到这,我都大呼过瘾,仍然看的津津有味~程序员
数据结构和算法对于程序员来讲就像是武侠世界中江湖人士的内功心法,其重要程度不言而喻,而开启数据结构与算法历练之路大门的钥匙则是复杂度的分析,这里的复杂度主要指的就是时间复杂度。面试
数据结构与算法须要掌握的知识不少,后来通过大牛们的概括总结提炼出图中 10 种数据结构与 10 种经常使用的算法,只须要掌握下面这张图咱们平常工做中就能够游刃有余了:算法
聪明的同窗会发现其实图中知识点仍是不少的,看着少是由于没有展开脑图而已 ,要掌握的知识可能是好事,说明咱们进步的空间很大,学习之路可能很远,不要紧,慢慢来,咱们来日方长~数组
铺垫了这么多,相信你们对数据结构与算法也有了一些认识,我目前也是一名小白,指望经过每次的分享可以在数据结构与算法的道路上走的更远一些。markdown
下面咱们开始入门第一课 :时间复杂度的分析数据结构
主要包括如下 4 点:数据结构和算法
大O复杂度表示法学习
经常使用的时间复杂度表示ui
最好、最坏、平均、均摊时间复杂度spa
空间复杂度
在一些面试题当中常常会出现对某一算法进行时间复杂度分析,在给出的选项中会有相似 O(1),O(n),O(logn)....的写法,那么像这样的O()的写法是什么意思呢?
这就要提到时间复杂度分析经常使用的 大O复杂度表示法。
大O复杂度
大O复杂度实际上并不具体表明代码真正的执行时间,而是表示代码执行时间随数据规模增加的变化趋势,也叫渐进时间复杂度,简称为时间复杂度。
看下面一个例子:
int sum(int n) { int sum = 0; int i = 1; int j = 1; for (; i <= n; ++i) { j = 1; for (; j <= n; ++j) { sum = sum + i * j; } }}
对上述代码而言,假设每一行代码的执行时间都是相同的记为 time(time 为常量),第2-4 行代码都是执行一次,各消耗时间为 time,第五、6行代码各执行 n 次,各消耗时间为 n * time,第七、8 行代码码各执行 n*n 次,各消耗时间为 n*n * time,因此上述代码的总消耗时间为:
2*n*n*time + 2*n*time+3*time = (2n²+2n+3)*time
用大 O 表示法则记为 O((2n²+2n+3)* time), 因为 time为常量,n为变量,即原式能够化简为 O(2n²+2n+3)。
当n很大时,甚至能够认为它趋近于无穷大时,根据极限的知识咱们能够认为 O(2n²+2n+3) 中低阶、常量、系数三部分并不左右增加趋势,因此能够忽略,只用记录最高阶就能够了,即 O(2n²+2n+3) 能够写成 O(n²),这也就是上述代码的时间渐进复杂度,简称时间复杂度。
由上面的例子能够看出,其实在分析时间复杂度的时候,咱们只要关心阶数最高或者说是循环次数最多的一段代码就能够了,由于咱们通常会忽略低阶、常量、系数这三部分。
时间复杂度经常使用到的另外2个法则是:加法法则和乘法法则,都比较简单,这里就不在多说了。
经常使用的时间复杂度表示
O(1) , O(logn) , O(n) , O(nlogn) , O(n²),O(n³),O(2^n) ,O(n!),从左到右时间复杂度依次递增。
O(1)
O(1) 表示常量阶,并非说只执行了一行代码,只要代码的执行时间不随着 n 的增大而增大就能够定为常量阶,哪怕有成千上万行代码。另外若是有循环次数是常量的循环也定义常量阶。
O(logn) 、O(nlogn)
这种对数阶时间复杂度也是比较常见的。
int i = 1;while(i <= n){ i = i * 2;}
从代码中能够看出 i 是成倍往上增加的,当 i 大于 n 时,循环就会结束,这实际上是一道很简单的对数题:
2^x = n, 求 x 的值
咱们都知道 x = log2 n (以 2 为底 n 的对数),那时间复杂度为何不写成 O(log2 n )呢?
其实写成O(log2 n )也并无错,若是咱们把上述代码第4行 改为 i = i * 3,那是否是要写成 O(log3 n )了,显然不是,这样写虽然不错可是太麻烦了。
由咱们仅存的高中数学知识可知,对数是能够相互转化的 :
log3n = log32 * log2n,log32为常数
那么,O(log3n) = O (log32 * log2n)= O(log2n),因此在对数时间复杂度中,就能够忽略对数的底,统一标识为 O(logn)。
而 nO(logn) 就显而易见了,根据乘法法则,在外层再套一层时间复杂度为 O (n)的循环,上述代码复杂度就是 nO(logn) 了。
归并排序、快速排序的时间复杂度都是O(nlogn)。
最好、最坏、平均、均摊时间复杂度
在了解经常使用的复杂度后,咱们在深刻一层,以前的代码都比较简单,不须要考虑相应的状况,下面咱们看一个比较复杂的例子:
//数组中查找变量 target 的位置,有则返回下标,没有则返回1int find(int[] array, int n, int target) { int i = 0; int pos = -1; for (; i < n; ++i) { // n表示数组array的长度 if (array[i] == target) { pos = i; break; } } return pos;}
上面这段代码的时间复杂度是多少呢?
这个时候咱们可能会有这样的疑问:
目标值在不在数组中,若是不在怎么办,若是在,那具体在哪一个位置呢?
这里咱们若是再用以前的方法分析,结果就会有误差了,由于代码中的循环是有可能被中断的(当找到目标值后 break)此时引入最好时间复杂度、最坏时间复杂度、平均时间复杂度的概念了。
顾名思义,最好时间复杂度就是最理想的状况下,也就是目标值就是数组的第一个元素,此时对应的时间复杂度为 O(1)
最坏时间复杂度就是最差的状况下,也就是说目标值不在数组中(或目标值在数组的末尾),此时须要循环n次中才能知道目标值是否在数组中,对应时间复杂度为O(n)。
平均时间复杂度
最好和最坏时间复杂度其实都是极特殊的状况,为了更好的解释平均时间复杂度须要引入一个概念:平均状况时间复杂度,后面简称为平均时间复杂度。
所谓的平均状况与求平均值相似,上述的寻找目标值在数组中的位置,一共有n+1 中状况,包括在数组 0 ~ n-1 的任一下标上 和不在数组中的状况,把须要查找元素的个数累加起来,再除以 n+1 ,就能够获得遍历元素的平均值:
(1+2+3+4……+n+n)/(n+1) = n(n+3)/2(n+1)
将O (n(n+3)/2(n+1)) 根据上述规则转换后,能够得出平均时间复杂度为 O(n)。
加权平均时间复杂度
咱们虽然得出告终果,可是仔细一想这个结果好像并不许确,缘由在于 这 n+1 种状况出现的几率实际上是不一样的,并且目标值出如今数组中某一位置的几率也是不一样的。
为了方便计算,咱们假定目标值出如今数组中与不在数组中的几率是相等的,都为 1/2 ;目标值出如今数组中某一位置的几率也是相等的,都为 1/n,根据乘法法则,咱们要找的目标值出如今数组中的几率应该为 (1/2) * (1/n), 1/(2n)。
因此前面的计算最大的问题是没有考虑几率问题,将几率添加上的算式为:
((1+2+3+4……+n)*(1/2n)+ n*(1/2) ) = (3n+1)/4
这个值就是几率论中的加权平均值,也叫做指望值,因此平均时间复杂度的全称应该叫加权平均时间复杂度或者指望时间复杂度。
实际上通常状况下咱们并不区分最好、最差、平均时间复杂度这三种状况。使用最开始的一个复杂度就能够知足需求了。
若是出现一块代码在不一样状况下,时间复杂度有重量级差距,才会使用这三种复杂度来区分。
均摊时间复杂度
均摊时间复杂度应用场景比较特殊,因此咱们并不会常常用到。
均摊时间复杂度的主要思想是:
对一个数据结构进行一组连续操做中,大部分状况下时间复杂度都很低,只有个别状况下时间复杂度比较高,并且这些操做之间存在先后连贯的时序关系,这个时候,咱们就能够将这一组操做放在一起分析,看是否能将较高时间复杂度那次操做的耗时,平摊到其余那些时间复杂度比较低的操做上。并且,在可以应用均摊时间复杂度分析的场合,通常均摊时间复杂度就等于最好状况时间复杂度 。
空间复杂度
在了解了时间复杂度后,最后再补充一下空间复杂度,其实空间复杂度很简单,它表示算法的存储空间与时间的增加关系。
通常重用的空间复杂度就是 O(1)、 O(n)、 O(n2 ),像O(logn)、 O(nlogn)这样的对数阶复杂度平时都用不到。
经过今天的分享,咱们主要了解了数据结构与算法的重要性,与时间复杂度相关的一些知识,以后咱们会继续学习数据结构与算法相关的知识,一块儿修炼内功,成为“江湖中的大侠”。