算法分析:时间和空间复杂度

本文转载自算法市, java全栈技术,不靠谱的猫

算法分析:时间和空间复杂度_第1张图片

一、什么叫算法

算法(Algorithm):是对特定问题求解方法或步骤的一种描述。一个算法可以用多种方法描述,主要有:

  • 使用自然语言描述;
  • 使用形式语言描述;
  • 使用计算机程序设计语言描述。

注:算法和程序是两个不同的概念。一个计算机程序是对一个算法使用某种程序设计语言的具体实现。

算法一般具有以下五个特性:

  • 输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象集合。
  • 输出:一个算法有一个或多个输出,这些输出是同输入有着某些特定关系的量。
  • 有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成。
  • 确定性:算法中每一条指令必须有确切的含义,不存在二义性。
  • 可行性:算法描述的操作都可以通过已经实现的基本运算执行有限次来实现。

二、什么叫好算法

评价一个好的算法有以下几个标准:

  • 正确性(Correctness):算法应满足具体问题的需求。
  • 可读性(Readability):算法应容易供人阅读和交流,方便理解和修改。
  • 健壮性(Robustness):算法应具有容错处理。当输入非法或错误数据时,算法应能适当地作出反应或进行处理,而不会产生莫名其妙的输出结果。
  • 通用性(Generality):算法应具有一般性 ,即算法的处理结果对于一般的数据集合都成立。
  • 效率与存储空间需求: 效率指的是算法执行的时间;存储空间需求指算法执行过程中所需要的最大存储空间。一般这两者与问题的规模有关。

三、算法的时间复杂度

算法分析:时间和空间复杂度_第2张图片

算法中基本操作重复执行的次数是问题规模n的某个函数,其时间量度记作:T(n)=O(f(n)),称作算法的渐近时间复杂度(Asymptotic Time complexity),简称时间复杂度。一般地,常用最深层循环内的语句中的原操作的执行频度(重复执行的次数)来表示。

表示时间复杂度的阶有:

  • O(1):常量时间阶
  • O(n):线性时间阶
  • O(logn):对数时间阶
  • O(n*logn):线性对数时间阶
  • O (n^k):k≥2,k次方时间阶

时间复杂度实例

例:两个n阶方阵的乘法

算法分析:时间和空间复杂度_第3张图片

两个n阶方阵的乘法

由于是一个三重循环,每个循环从1到n,则总次数:n * n * n=n^3,时间复杂度为T(n)=O(n^3)。

基本操作执行次数

关于代码的基本操作执行次数,我们用四个生活中的场景来做一下比喻:

场景1. 给小灰一条长10寸的面包,小灰每3天吃掉1寸,那么吃掉整个面包需要几天?

算法分析:时间和空间复杂度_第4张图片

答案自然是 3 X 10 = 30天。

如果面包的长度是 N 寸呢?

此时吃掉整个面包,需要 3 X n = 3n 天。

如果用一个函数来表达这个相对时间,可以记作 Tn = 3n

场景2. 给小灰一条长16寸的面包,小灰每5天吃掉面包剩余长度的一半,第一次吃掉8寸,第二次吃掉4寸,第三次吃掉2寸......那么小灰把面包吃得只剩下1寸,需要多少天呢?

这个问题翻译一下,就是数字16不断地除以2,除几次以后的结果等于1?这里要涉及到数学当中的对数,以2位底,16的对数,可以简写为log16。

因此,把面包吃得只剩下1寸,需要 5 X log16 = 5 X 4 = 20 天。

如果面包的长度是 N 寸呢?

需要 5 X logn = 5logn天,记作 Tn = 5logn

场景3. 给小灰一条长10寸的面包和一个鸡腿,小灰每2天吃掉一个鸡腿。那么小灰吃掉整个鸡腿需要多少天呢?

算法分析:时间和空间复杂度_第5张图片

答案自然是2天。因为只说是吃掉鸡腿,和10寸的面包没有关系

如果面包的长度是 N 寸呢?

无论面包有多长,吃掉鸡腿的时间仍然是2天,记作 Tn = 2

场景4. 给小灰一条长10寸的面包,小灰吃掉第一个一寸需要1天时间,吃掉第二个一寸需要2天时间,吃掉第三个一寸需要3天时间.....每多吃一寸,所花的时间也多一天。那么小灰吃掉整个面包需要多少天呢?

答案是从1累加到10的总和,也就是55天。

如果面包的长度是 N 寸呢?

此时吃掉整个面包,需要 1+2+3+......+ n-1 + n = (1+n)*n/2 = 0.5n^2 + 0.5n。

记作 Tn = 0.5n^2 + 0.5n

算法分析:时间和空间复杂度_第6张图片

上面所讲的是吃东西所花费的相对时间,这一思想同样适用于对程序基本操作执行次数的统计。刚才的四个场景,分别对应了程序中最常见的四种执行方式:

场景1, T(n) = 3n,执行次数是线性的。

算法分析:时间和空间复杂度_第7张图片

场景2, T(n) = 5logn,执行次数是对数的。

算法分析:时间和空间复杂度_第8张图片

场景3,T(n) = 2,执行次数是常量的。

算法分析:时间和空间复杂度_第9张图片

场景4,T(n) = 0.5n^2 + 0.5n,执行次数是一个多项式

算法分析:时间和空间复杂度_第10张图片

渐进时间复杂度

有了基本操作执行次数的函数 T(n),是否就可以分析和比较一段代码的运行时间了呢?还是有一定的困难。

比如算法A的相对时间是T(n)= 100n,算法B的相对时间是T(n)= 5n^2,这两个到底谁的运行时间更长一些?这就要看n的取值了。

所以,这时候有了渐进时间复杂度(asymptotic time complectiy的概念,官方的定义如下:

若存在函数 fn),使得当n趋近于无穷大时,Tn/ fn)的极限值为不等于零的常数,则称 fn)是Tn)的同数量级函数。

记作 Tn= Ofn)),称Ofn)) 为算法的渐进时间复杂度,简称时间复杂度。

渐进时间复杂度用大写O来表示,所以也被称为O表示法

算法分析:时间和空间复杂度_第11张图片

算法分析:时间和空间复杂度_第12张图片

如何推导出时间复杂度呢?有如下几个原则:

  1. 如果运行时间是常数量级,用常数1表示。
  2. 只保留时间函数中的最高阶项
  3. 如果最高阶项存在,则省去最高阶项前面的系数。

让我们回头看看刚才的四个场景。

场景1:

T(n) = 3n

最高阶项为3n,省去系数3,转化的时间复杂度为:

Tn = On

算法分析:时间和空间复杂度_第13张图片

场景2:

T(n) = 5logn

最高阶项为5logn,省去系数5,转化的时间复杂度为:

Tn = Ologn

算法分析:时间和空间复杂度_第14张图片

场景3:

T(n) = 2

只有常数量级,转化的时间复杂度为:

Tn = O1

算法分析:时间和空间复杂度_第15张图片

场景4:

T(n) = 0.5n^2 + 0.5n

最高阶项为0.5n^2,省去系数0.5,转化的时间复杂度为:

Tn = On^2

算法分析:时间和空间复杂度_第16张图片

这四种时间复杂度究竟谁用时更长,谁节省时间呢?稍微思考一下就可以得出结论:

O1< Ologn< On< On^2

在编程的世界中有着各种各样的算法,除了上述的四个场景,还有许多不同形式的时间复杂度,比如:

Onlogn, On^3, Om*n),O2^n),On!)

其关系为:

O(1) < O(logn) < O(n) < O(n*logn) < O(n^2) < O(n^3)

指数时间的关系为:

O(2^n) < O(n!) < O(n^n)

当n取得很大时,指数时间算法和多项式时间算法在所需时间上非常悬殊。因此,只要有人能将现有指数时间算法中的任何一个算法化简为多项式时间算法,那就取得了一个伟大的成就。

对于不同运行时间的快速图形表示以及它们之间的比较,大O cheatsheet也非常有用。

算法分析:时间和空间复杂度_第17张图片

时间复杂度的巨大差异

 

算法分析:时间和空间复杂度_第18张图片

算法分析:时间和空间复杂度_第19张图片

我们来举过一个栗子:

算法A的相对时间规模是T(n)= 100n,时间复杂度是O(n)

算法B的相对时间规模是T(n)= 5n^2,时间复杂度是O(n^2),

算法A运行在小灰家里的老旧电脑上,算法B运行在某台超级计算机上,运行速度是老旧电脑的100倍。

那么,随着输入规模 n 的增长,两种算法谁运行更快呢?

算法分析:时间和空间复杂度_第20张图片

从表格中可以看出,当n的值很小的时候,算法A的运行用时要远大于算法B;当n的值达到1000左右,算法A和算法B的运行时间已经接近;当n的值越来越大,达到十万、百万时,算法A的优势开始显现,算法B则越来越慢,差距越来越明显。

这就是不同时间复杂度带来的差距。

四、算法的空间复杂度

空间复杂度(Space complexity):是指算法编写成程序后,在计算机中运行时所需存储空间大小的度量。记作:S(n) = O(f(n)),其中:n为问题的规模或大小。

存储空间一般包括三个方面:

  • 输入数据所占用的存储空间;
  • 指令常数变量所占用的存储空间;
  • 辅助(存储)空间。

算法分析:时间和空间复杂度_第21张图片

一般地,算法的空间复杂度指的是辅助空间。如:

  • 一维数组a[n]:空间复杂度 O(n)
  • 二维数组a[n][m]:空间复杂度 O(n*m)

 

其他正式的符号

  • 大Ω:最好的情况。大Ω的算法描述了如何快速算法可以在最好的情况下运行。
  • 大O:最坏的情况。通常,我们最关心的是大O时间,因为我们最关心的是给定算法的运行速度。我们如何使最坏情况不像它可能的那么糟呢?
  • 大θ:这只能被用来描述一个算法的运行时如果大Ω,大魔神都是相同的。也就是说,算法的运行时间在最好和最坏的情况下都是相同的

常见的算法

递归:递归是指函数调用自身。也许递归的典型例子是实现阶乘函数:

def factorial(n):

if n < 1: #base case

return 1

else: #recursive case

return n * factorial(n-1)

Divide and Conquer(D&C):一种解决问题的递归方法,D&C(1)确定问题的最简单情况(又名基本情况,the base case),(2)减少问题,直到现在成为基本情况。

算法分析:时间和空间复杂度_第22张图片

也就是说,一个复杂的问题被分解成更简单的子问题。这些子问题得到了解决,然后将它们的解决方案组合起来,以解决原来更大的问题。

搜索和排序算法可能是首先要了解的最重要的算法。

搜索

  • 简单搜索

这在前面的电话簿示例中进行了描述,最糟糕的情况是要求您在找到感兴趣的名称之前搜索电话簿中的所有名称。通常,简单搜索具有O(n)时间。所需的最长时间与列表中的元素数量线性相关。

  • 二分搜索

让我们坚持使用电话簿示例。我们仍然有兴趣在电话簿中找到某人的名字,但这次我们将尝试提高效率。我们不是单调乏味地浏览电话簿中的每个名字,而是从电话簿的中间开始,然后从那里开始。

假设我们的目标名称以P开头。我们打开大致位于字母表中间的M s。我们知道M比字母表中的P早,所以我们可以消除从A到M的部分。现在我们可以看一下电话簿的后半部分(N到Z),将该部分分成中间(到Ts)),并与我们的目标进行比较。T在字母表中比P稍晚。然后我们知道消除后半部分(T到Z)。我们专注于N到S.现在,把它分成两半,依此类推,直到找到我们感兴趣的名字。

通常,在二分搜索中,您可以获取已排序(这很重要)的数据并找到中点。每次,您将目标与中间值进行比较。如果目标值与中间值相同,那么您的工作就完成了。否则,您知道根据比较消除列表的哪一半。您继续分割,直到找到目标或数据集不能再减半。

算法分析:时间和空间复杂度_第23张图片

二分搜索图与数字列表

由于二分搜索涉及数据集减半,因此Big O时间为O(log n)。因此,它比简单搜索更快,特别是当您的数据集增长时(算法的增长不是线性的而是对数的,因此相对于O(n)的线性运行时间,它变得更慢)。

另外,二分搜索可以递归写入,但不被视为D&C算法。尽管较大的输入确实被分解为子集,但如果它们不包含感兴趣的值,则忽略这些子集。不为这些子集生成解决方案,以便它们可以组合以解决更大的输入。

分类

  • 选择排序

就像搜索算法的简单搜索一样,选择排序可能是对数据进行排序的最直接,“强力”方式。实际上,您将遍历列表中的每个元素,并按所需顺序将每个元素附加到新列表中。例如,如果您有兴趣对从最大到最小的数字列表进行排序,您会:

  1. 搜索列表以查找最大的数字
  2. 将该号码添加到新列表中
  3. 转到原始列表,再次搜索它以查找下一个最大的数字
  4. 将该数字添加到新列表中,依此类推......

对于选择排序,你必须遍历列表中的每个项目(这需要n次,就像进行简单搜索一样)并且你必须这样做n次(不只是一次,因为你必须继续回到原始列表,用于查找要添加到新列表的下一个项目)。因此,这需要O(ñ ²)时间。

  • Quicksort

快速排序与选择排序有何不同?如果我们使用数字列表,就像以前一样:

  1. 从列表中选择一个元素,称为pivot。在决定快速排序算法的运行速度时,pivot的选择是非常重要的。现在,我们可以每次选择最后一个元素作为pivot。(关于pivot选择的更多信息,我推荐斯坦福Coursera算法课程。)
  2. 对列表进行分区,使得小于pivot的所有数字都在其左侧,并且所有大于pivot的数字都在其右侧。
  3. 对于列表的每个“half”,您可以将其视为具有新pivot 的新列表,并重新排列每一半,直到它被排序。

算法分析:时间和空间复杂度_第24张图片

快速排序图与数字列表

快速排序是D&C算法的一个例子,因为它将原始列表分成越来越小的有序列表。然后将这些较小的、有序的列表组合起来,得到一个较大的、有序的列表。

Quicksort是独一无二的,因为它的速度取决于pivot选择。在最坏的情况,可能需要O(ñ ²)的时间,这是因为选择排序慢。但是,如果pivot在列表中始终是一个随机元素,则quicksort 平均在O(n log n)时间内运行。

  • Mergesort

假设我们仍在使用我们的数字列表。对于合并排序算法,列表将分解为其各个元素。然后从这些元素创建有序对(左边的数字越小)。然后将这些有序对分组为有序的四个组,并且这将继续,直到创建最终合并的排序列表。

算法分析:时间和空间复杂度_第25张图片

与quicksort一样,mergesort是一种D&C算法,因为输入列表在被组合以生成较大的原始列表的有序版本之前被分解和排序。

Mergesort在O(n log n)时间运行,因为整个列表迭代减半(O(log n))并且这是针对n个项目完成的。

算法和数据结构的知识对数据科学家很有用,因为我们的解决方案不可避免地要用代码编写。因此,理解我们数据的结构以及如何从算法的角度思考非常重要。

你可能感兴趣的:(数据结构和算法)