数据结构与算法-入门

为什么要学算法和数据结构?

也许对于crud开发者,数据结构和算法毫无用处。但是面对业务量非常大的系统,用不同的算法和数据结构跑出来的功能,性能差距会比较大,当整个项目都是一堆垃圾代码的时候,第一, 业务执行效率低,直接导致用户体验不好。第二,执行占用的内存资源很多,需要更多的硬件资源支撑,扩展了说,也需要花更多的精力去维护这些硬件资源的稳定性。

数据结构与算法的关系

数据结构跟算法相辅相成,数据结构为算法服务,算法要作用在具体的数据结构上。
就好比一个俄罗斯方块的游戏,数据是每次掉下来的砖,数据结构是砖的形状,算法是你怎么把他们叠一起。

重要概念 -- 复杂度分析

数据结构与算法的根本
在资源层面来看:
数据结构 -- 空间
算法 -- 时间
在软件工程层面来看:
时间 = 效率
空间 = 资源

学好数据结构和算法,最终目的就是能在时间和空间之间寻找合理的平衡点。所以面对很多优化问题的时候,根本思想就两点:
1.时间换空间
2.空间换时间

当然 在对面时间和空间的取舍问题时,先得把时间(算法)、空间(数据结构)给整明白了,才能在取舍问题上做文章。最终来优化我们的项目和代码。

所以复杂度分析,就是针对时间和空间的分析。

为什么需要复杂度分析?

  1. 功能测试对场景的要求太多,不同场景需求对 用不同数据结构和算法的 实现,测试结果的性能不一。换个角度说,同一数据结构和算法 在不同场景下表现的性能不一,所以,我们需要一个不用具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法。

复杂度分析之具体落地 -- 大O复杂度表示法

例:

 int cal(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;
     }
   }
 }

上述代码,假设方法体里CPU的一次 读数据-运算-写数据 (一次完整的运算) 的执行时间为 X,那么总时间就是
3X + nX + n2nX = (3+n+2n²)X = T(n)
【注:这里的2n是因为 i*j是一个运算,sum+x是一个运算】

这样我们可以发现 所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。

定义公式: T(n) = O(f(n))
其中,T(n) 表示代码执行的时间;n 表示数据规模的大小;f(n) 表示每行代码执行的次数总和。因为这是一个公式,所以用 f(n) 来表示。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。

所以 上面例子可以表示成 T(n) = O(3+n+2n²) 当n的数量级很大时候,可以简化为T(n)=O(n²) 【注:大 O 这种复杂度表示方法只是表示一种变化趋势。我们通常会忽略掉公式中的常量、低阶、系数,只需要记录一个最大阶的量级就可以了。】
这里面 的 O 当n是常量时,这里面的 代码的执行时间是确定的,但是 在实际场景中,数据规模n是不随随机的,所以需要用O来动态表示 代码执行时间随数据规模变化的趋势。 也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。

时间复杂度分析

  1. 时间复杂度之用关注执行次数最多的代码 (这里会有一个问题,就是如果一个方法体内,有几个关于数据体量,且执行过程互不干涉的内容,根据数据体的不同,执行的效果也可能不同,也就是不能确定哪个是最多的)

  2. 加法法则:总复杂度等于量级最大的那段代码的复杂度
    例:

int cal(int n) {
   int sum_1 = 0;
   int p = 1;
   for (; p < 100; ++p) {
     sum_1 = sum_1 + p;
   }
 
   int sum_2 = 0;
   int q = 1;
   for (; q < n; ++q) {
     sum_2 = sum_2 + q;
   }
 
   int sum_3 = 0;
   int i = 1;
   int j = 1;
   for (; i <= n; ++i) {
     j = 1; 
     for (; j <= n; ++j) {
       sum_3 = sum_3 +  i * j;
     }
   }
 
   return sum_1 + sum_2 + sum_3;
 }

如1所说,这个例子就是 第一段为O(1) ,第二段为O(n),第三段为 O(n²),
所以可以表示为 O(max(n,n²)) 【注:此处是举个例子理解加法运算,在此案例中实际n是大于等于1的 ,所以肯定是O(n2)】

3.乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

这一点很好理解,嵌套的执行都是这样。

常见的时间复杂度

常见时间复杂度.png
  1. O(1)
    因为时间复杂度表示的是一种趋势,对这种趋势的细节不太关注 ,所以 ,用次思想来看待 O(1),O(2),O(3) ,都可以认为是 O(1)。
    所以,一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。
  1. O(logN)
    对数阶时间复杂度非常常见,如下:
 i=1;
 while (i <= n)  {
   i = i * 2;
 }

此例中 T(n) = x² 也就是时间复杂度为O(log2 N) 以2为底 N的对数
在采用大 O 标记复杂度的时候,可以忽略系数,O(log2n) 就等于 O(logn)。因此,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O(logn)。

理解了O(logn),那 O(nlogn) 就很容易理解了。结合乘法法则,如果一段代码的时间复杂度是 O(logn),我们循环执行 n 遍,时间复杂度就是 O(nlogn) 了。而且,O(nlogn) 也是一种非常常见的算法时间复杂度。比如,归并排序、快速排序的时间复杂度都是 O(nlogn)。

  1. O(m+n)、O(mn)
    当数据不只有一个时,时间复杂度就需要根据每个数据体量变量来计算,所以当加法时候,需要表示为O(m+n) 。乘法时需要表示成 O(m
    n)

空间复杂度分析

类比时间复杂度,空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。

例:

void print(int n) {
  int i = 0;
  int[] a = new int[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(n2 ),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。

常见复杂度曲线关系

复杂度曲线.png

复杂度分析 -- 方法论

四个角度:
最好情况时间复杂度(best case time complexity)
顾名思义,假设情况最完美;
例如一个数组中找到某个数并返回,那个数就在数组的第一个位置。所以最好情况就是O(1)

最坏情况时间复杂度(worst case time complexity)
假设情况最完美;
例如一个数组中找到某个数并返回,那个数就在数组的最后一个位置。所以最坏情况就是O(n)

平均情况时间复杂度(average case time complexity)
最好和最坏是两种极端且特殊的情况,不能衡量整体情况,这时候就需要取平均值:
要查找的变量 x 在数组中的位置,有 n+1 种情况:在数组的 0~n-1 位置中和不在数组中。如果结果会根据数据的权重而变化,此时算平均情况时间复杂度便需要加上数据的权重。

实际上,在大多数情况下,我们并不需要区分最好、最坏、平均情况时间复杂度三种情况。很多时候,我们使用一个复杂度就可以满足需求了。只有同一块代码在不同的情况下,时间复杂度有量级的差距,我们才会使用这三种复杂度表示法来区分。
例:

 // array 表示一个长度为 n 的数组
 // 代码中的 array.length 就等于 n
 int[] array = new int[n];
 int count = 0;
 
 void insert(int val) {
    if (count == array.length) {
       int sum = 0;
       for (int i = 0; i < array.length; ++i) {
          sum = sum + array[i];
       }
       array[0] = sum;
       count = 1;
    }
 
    array[count] = val;
    ++count;
 }

对每一次调用而言,这里最好情况就是 不走if里面,所以是O(1),最坏的情况是走if里面所以是O(n))。平均复杂度就是 ( O(1)*(n-1)+O(n) ) /n ,所以是O(1)。

均摊时间复杂度
基于上面那两个例子,当每次调用的时间复杂度都很不确定的情况下,可以用平均复杂度来判定,但是下面数组插曲的例子,此时我们的调用,大部分情况下时间复杂度都是比较低的,只有很少的情况比较高,此时可以把较高的操作的复杂度平摊到其他低的上面,其实也就是一种特殊的平均时间复杂度。一般均摊时间复杂度等于最好情况复杂度。

实际项目中 时间复杂度应该调用执行的角度去分析,而不是调用数据的大小来分析。譬如数据大小可能是1到10,但是调用操作大部分数据都是以2为入参,我们就不能简单的只分析数据层面的时间复杂度。

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

推荐阅读更多精彩内容