【数据结构和算法学习笔记 】 第一篇: 时间复杂度和空间复杂度

终于开始写数据结构和算法的博客了,迈出了2019年的第一步,今年的计划,首先是写完数据结构和算法的博客,然后开始学习Android源码,另外,小程序,flutter,kotlin也开始要学起来,学习的中途,也会做自己的项目,是一个MVP+retrofit+rxjava+dagger2的项目,另外,公司的项目已经截止,接的私活开始做起来,买了扔物线的HencoderPlus,两个月学完,今年是很忙的一年,也是会进步巨大的一年。今年加薪50%跳槽,是一个非常不错的开始,希望下一次跳槽,能有更大的涨幅,加油吧!

要分析数据结构和算法,离不开复杂度的分析,复杂度分析包括:时间复杂度和空间复杂度。数据结构和算法,解决的就是两个问题,第一:如何让代码运行的更快;第二:如何让代码更节省空间。这两个问题对应的就是时间复杂度和空间复杂度。所以学习数据结构和算法,首先要学习的就是时间复杂度和空间复杂度的分析。

一、时间复杂度

1.1 大o复杂度表示法

首先,我们看下面一段代码

public int sum(int n){
  int sum = 0; 
  int i = 1;
  for(; i < n;++i){
     sum = sum + I; 
   }
  return sum;
}

在这里,我们粗略估计,cpu在运行这段代码时,每一行代码的执行时间都相同为unit_time,(当然,每一行代码的运行时间必然不相同,但这里我们只是粗略的估计,视为每行代码运行时间都相同。)那么估算一下,这段代码的运行时间是多少呢?
我在下面的代码块里标注出每行代码的运行时间:

public int sum(int n){
  int sum = 0; // 1 * unit_time
  int i = 1;// 1 * unit_time
  for(; i < n;++i){ // n * unit_time
     sum = sum + i; // n * unit_time
   }
  return sum; // 1 * unit_time
}

因此,我们可以算出,这段代码总的运行时间为:T(n) = (3+n+n)* unit_time。在这里,代码执行时间T(n)与代码执行次数n成正比。

再看下面这段稍微复杂一点的代码:同样的,我们依然假设CPU执行每一行代码的时间都是相同的为unit_time,在代码块中标注出每一行代码执行的时间:

public int sum(int n){
 int sum = 0;  // 1*unit_time
 int i = 1; //  1*unit_time
 int j = 1; // 1* unit_time
 for(;i <= n;++i){  // n * unit_time
     j = 1; //n* unit_time
   for(; j <= n ; ++j){ // n² * unit_time
     sum = sum + i*j; // n² * unit_time
     }
   }
}

如上所示,可以算出,这段代码执行的总时间T(n) = (3 + n + n + n² + n² )* unit_time ,这里,我们也可以得出一个结论,代码执行时间T(n)与执行次数n成正比。也就是我们常说的大O复杂度表示法:

大O复杂度表示法

公式中,T(n)表示的是代码执行的总时间,n表示的是数据的规模,f(n)表示的是代码执行次数的总和,公式中的O表示代码的执行时间T(n)与f(n)成正比。

上面第一个例子和第二个例子使用大O时间复杂度表示法就可以表示成:
T(n) = O((3+n+n)* unit_time) 和 T(n) = O( (3 + n + n + n² + n² )* unit_time ) 这就是大O时间表示法。大O时间复杂度实际上并不具体表示代码真正的执行时间,而是代表代码执行时间随数据规模增长的变化趋势,所以,也叫做渐进时间复杂度,简称时间复杂度。
当n很大时,公式中的低阶、常量、系数三部分不能左右增长趋势,都可以忽略,只需要记录一个最大量级就可以了,上述两个例子中,用大O复杂度表示,可以记为T(n) = O(n)和 T(n) = O(n²)

1.2 时间复杂度分析

了解了时间复杂度的概念,怎么分析一段代码的时间复杂度呢?三个方法

1. 只关注循环执行次数最多的一段代码。
2. 总复杂度等于量级最大的那段代码的复杂度。
3. 嵌套代码的复杂度等于嵌套内外代码复杂度的乘积。
1.2.1 几种常见的时间复杂度分析
几种常见的时间复杂度分析

常见的复杂度量级,可以粗略的分为两类,多项式量级和非多项式量级。其中非多项式量级只有两个:O(2的n次方) 和 O(n!)。当数据规模越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长,所以,非多项式量级的算法都是非常低效的算法,应该尽量避免。我们来看几种常见的多项式时间复杂度。

1.常量阶时间复杂度 O(1)

只要代码执行时间不随n的增大而增大,这样的代码的时间复杂度我们都记作O(1) ,一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万条代码,其时间复杂度也是O(1)。

2.对数阶时间复杂度 O(logn)、O(nlogn)

对数阶时间复杂度非常常见,看下面这段代码:

  I=1;
 while (i <= n)  {
   i = i * 2;
 }

对上面这段代码进行分析:变量i的值从1开始取,每循环一次就乘以2。当大于n时,循环结束。当i < n时,i的值分别为 1、2、4、8、16....也就是2的n次方。


image.png

所以,我们只要知道x的值,就知道这段代码的执行次数了,x=log2n ,这段代码的时间复杂度就是O(log2n)(2是底数),如果把这段代码修改一下,改为:

  I=1;
 while (i <= n)  {
   i = i * 3;
 }

那么这段代码的时间复杂度就是O(log3n)(3是底数),我们知道,对数之间是可以相互转换的,log3n 就等于 log32 * log2n,其中log32是一个常数,我们前面分析得出的结论,在分析复杂度时,常数可以忽略,所以O(log2n) = O(log3n),在对数阶复杂度表示方法里,忽略对数的底,统一表示为:O(logn)。
如果一段代码的时间复杂度是O(logn),当它循环执行n次时,它的时间复杂度就是O(nlogn)。

3. O(m+n)、O(m*n)

我们先来看一段代码:

int cal(int m, int n) {
  int sum_1 = 0;
  int i = 1;
  for (; i < m; ++i) {
    sum_1 = sum_1 + I;
  }

  int sum_2 = 0;
  int j = 1;
  for (; j < n; ++j) {
    sum_2 = sum_2 + j;
  }

  return sum_1 + sum_2;
}

从代码中可以看出,m和n是两个数据规模。在不知道m、n的值的情况下,无法事先评估m和n谁的量级大,所以在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个,此时时间复杂度就为:T1(n) + T2(m) = O(f(n) + g(m)) .

1.2.2 最好、最坏、平均、均摊时间复杂度
1 最好、最坏时间复杂度

先来看段代码:

// n 表示数组 array 的长度
int find(int[] array, int n, int x) {
  int i = 0; //  1 * unit_time
  int pos = -1; // 1 * unit_time
  for (; i < n; ++i) { // n * unit_time
    if (array[i] == x) pos = I; // n * unit_time
  }
  return pos; // 1 * unit_time 
}

我们来分析下这段代码的时间复杂度,这段代码的目的是查询x在数组中的位置,查找到之后,返回position,如果x不在数组中,返回-1。按照之前的学习的时间复杂度分析,我们可以知道的是,这段代码的时间复杂度是O(n)。如果我们把这段代码改进一下,当在数组中找到x后,中断循环直接返回position。改进后的代码如下:

// n 表示数组 array 的长度
int find(int[] array, int n, int x) {
  int i = 0;
  int pos = -1;
  for (; i < n; ++i) { // 这句代码运行时间是多少个unit_time呢?
    if (array[i] == x) { 
       pos = I; 
       break;
    }
  }
  return pos;
}

这个时候,循环代码执行的次数和x在数组中的位置有关,如果x在数组中的第一个位置,那么循环只会执行一次,这个情况就是最好的情况,那么它对应的时间复杂度就是最好情况时间复杂度;如果x在数组最后一个位置,那么循环执行n次,这个情况是最坏的情况,它对应的时间复杂度就是最坏情况时间复杂度。

由此可以引出最好情况、最坏情况时间复杂度的概念。
最好情况时间复杂度:顾名思义,就是在最理想的情况下,执行这段代码的时间复杂度。上个例子中对应的就是x在数组第一个位置时执行这段代码的时间复杂度。
最坏情况时间复杂度:在最坏的情况下,执行这段代码的时间复杂度。上个例子中对应的就是x在数组最后一个位置时执行这段代码的时间复杂度。

2 平均情况时间复杂度

上面的例子,当x在第一个位置或者x在最后一个位置这种极端情况其实并不常见,发生的概率并不大,事实上,x可以在数组中的任何位置,如果我们假设x在数组中位置的概率都一样,都是1/n,另外还有一种可能是x不在数组中,所以,要查找x在数组中的位置,有n+1种情况。我们把每种情况下,查找需要遍历的元素个数累加起来,然后再除以n+1,就可以得到需要遍历的元素个数的平均值。


平均时间复杂度计算

但是这样计算实际上是不准确的,因为这n+1种情况,出现的概率并不一样,x在数组中和不在数组中,概率为1/2。另外,要查找的数据出现在0 - n-1 这n个位置的概率也是一样的,为1/n,根据概率乘法法则,要查找的数据出现0 - n-1 中任意位置的概率是1/2n。

如果将各种情况发生的概率都统计进去的话,平均时间复杂度要这么计算:


加权平均时间复杂度

这个值就是概率论中的加权平均值,也叫作期望值,所以平均时间复杂度的全称叫做加权平均时间复杂度或者期望时间复杂度。
可以看到,这段代码的加权平均值是(3n+1)/4,用大O表示法来表示,去掉系数和常量,这段代码的加权平均时间复杂度是O(n)。

在大多数情况下,我们并不需要区分最好最坏平均情况时间复杂度三种情况,很多时候只需要一个复杂度久够了。除非一段代码在不同情况下,时间复杂度有量级的差距,我们才需要使用这三种复杂度来区分。

1.3 空间复杂度分析

空间复杂度全称是渐进空间复杂度,表示算法存储空间与数据规模之间的增长关系。
看下面这段代码:

void print(int n) {
  int i = 0;  //申请了一个存储变量空间 
  int[] a = new int[n]; //申请了n个存储变量空间
  for (i; i <n; ++i) {
    a[i] = i * I;
  }

  for (i = n-1; i >= 0; --i) {
    print out a[I]
  }
}

除了标注的两行代码,其他代码都没有占用多少空间,所以整段代码的空间复杂度就是O(n)。常见的空间复杂度一般就是O(1),O(n),O(n²)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容