时间复杂度和空间复杂度的概念

简介: 本文介绍了如何评估算法的执行效率和内存占用,重点讲解了时间复杂度和空间复杂度的概念及其计算方法。通过大O记法,可以量化算法的运行时间和内存使用情况,从而在不同算法间做出合理选择。

上节《算法是什么》中提到,解决一个问题的算法可能有多种,这种情况下,我们就必须对这些算法进行取舍,从中挑选出一个“最好”的。


算法本身是不分好坏的,所谓最好的算法,指的是最适合当前场景的算法。挑选算法时,主要考虑以下两方面因素:

  • 执行效率:根据算法所编写的程序,执行时间越短,执行效率就越高;
  • 占用的内存空间:不同算法编写出的程序,运行时占用的内存空间也不相同。如果实际场景中仅能使用少量的内存空间,就应该优先选择占用空间最少的算法。


当问题对应的算法数量较少时(比如 2、3 种),我们可以编写出各个算法对应的程序,逐个在机器上运行,记录它们各自的执行时间和占用内存空间的大小,最终挑选出最好的算法。如果问题对应的算法数量有很多(比如 10 种,20 种),先前的挑选方式将不再适用,因为将各个算法一一编写成程序的工作量是巨大的,得不偿失。


实际开发中,我们往往采用“预先估值”的方法挑选算法。具体来讲,就是分析各个算法的实现过程(步骤),估算出它们各自的运行时间和占用的内存空间,进而挑选出最好的算法。用“预先估算”方式挑选算法时,我们习惯用时间复杂度表示一个算法的运行时间,用空间复杂度表示算法占用存储空间的大小。


接下来,我们就来了解一下如何估算一个算法的时间复杂度和空间复杂度。

时间复杂度

时间复杂度用于表示算法的执行时间。


接下来,我们以《算法是什么》一节中求 n! 的算法为例:

输入 n          // 接收 n 的值

p <- 1          // p 的初值置为 1

for i<-1 to n:  // i 的值从 1 一直到 n

   p <- p * i  // 将 p*i 的值赋值给 p

Print p         // 输出 p 的值

计算它的时间复杂度,只需经历以下几个步骤:

1) 统计算法中各个步骤的执行次数

整个算法中共有 5 行伪指令,它们各自的执行次数分别是:

输入 n          <- 执行 1 次

p <- 1          <- 执行 1 次

for i<-1 to n:  <- i 的值从 1 遍历到 n,当 i 的值为 n+1 的时候退出循环,总共执行 n+1 次

   p <- p * i  <- i 从 1 到 n 的过程,共执行 n 次

Print p         <- 执行 1 次

统计算法中所有伪指令执行的总次数,结果为 2*n+4。显然,2*n+4 不是一个固定值,整个表达式值的大小取决于 n 的值。


2*n+4 可以直接作为算法执行时间的估值,也可以对它进行简化,用规范的形式表示算法的运行时间。

2) 简化算法的执行次数

通过统计各个算法中每条伪指令的执行次数,每个算法的运行时间都可以用类似 2*n+4、3*n2+4*n+5 这样的表达式表示。这就产生一个问题,如何比较各个表达式的大小呢?


首先,我们可以尝试对每个表达式进行简化,简化方法是:假设表达式中变量的值无限大时,去除掉那些对表达式结果影响较小的项。以 3*n2+4*n+5 为例,简化过程为:

  • 当 n 无限大时,3*n2+4*n 与 3*n2+4*n+5 的值非常接近,是否加 5 对表达式的值影响不大,因此表达式可以简化为 3*n2+4*n;
  • 当 n 无限大时,3*n2 的值要远远大于 4*n 的值,它们之间类似于 10000 和 1 之间的关系,因此是否加 4*n 对表达式最终的值影响不大,整个表达式可以简化为 3*n2
  • 当 n 无限大时,n2 的值已经超级大,是否乘 3 对最终结果影响不大,整个表达式可以简化为  n2


简化表达式的过程可以总结为:

  • 去掉表达式中所有的加法常数项,3*n2+4*n+5 中的 5 就是加法常数项;
  • 只保留表达式中变量指数最大的项,3*n2+4*n 中 n 的最大指数为 2,所以只保留 3*n2
  • 去掉常数系数,3*n2 中的 3 就是 n2 的常数系数。


基于“n 值无限大”的思想,3*n2+4*n+5 最终可以简化为 n2。无论多么复杂的表达式,都可以采用这种方式进行简化。

3) 大O记法表示时间复杂度

除了用 n 外,一些人还可能会用 a、b、c 等字符作为表达式中的变量。为此,人们逐渐达成了一种共识,即都用 n 作为表达式中的变量,并采用大 O 记法表示算法的执行时间。


采用大 O 记法表示算法的执行时间,直接套用如下的格式即可:

O(频度)

频度指的就是简化后的表达式。


采用大 O 记法,2*n+4 可以用 O(n) 表示,3*n2+4*n+5 可以用O(n2)表示。注意,如果一个算法对应的表达式中没有变量(比如 10,100 等),则用O(1)表示算法的执行时间。


如果一个算法的执行时间最终估算为O(n),那么该算法的时间复杂度就是O(n)。如下列举了常用的几种时间复杂度以及它们之间的大小关系:

O(1)< O(logn) < O(n) < O(n2) < O(n3) < O(2n)

O(1)是最小的,对应的算法的执行时间最短,执行效率最高。

空间复杂度

空间复杂度衡量的是算法执行过程占用的内存空间的大小。


比较多个算法占用的内存大小,本质上比较的是各个算法执行过程中额外申请的内存空间的大小。举个简单的例子:

输入 n

A[i...n] = {1...n}  <- 额外申请 n 个空间

根据 n 的值,算法执行时需要申请 n 个整数的内存空间,n 的值越大,额外申请的内存空间就越多。


与时间复杂度的表示方法一样,空间复杂度也采用大 O 记法表示。算法空间复杂度的估算方法是:

  • 如果算法中额外申请的内存空间不受用户输入值的影响(是一个固定值),那么该算法的空间复杂度用 O(1) 表示;
  • 如果随着输入值 n 的增大,算法申请的存储空间成线性增长,则程序的空间复杂度用 O(n) 表示;
  • 如果随着输入值 n 的增大,程序申请的存储空间成 n2 关系增长,则程序的空间复杂度用 O(n2) 表示;
  • 如果随着输入值 n 的增大,程序申请的存储空间成 n3 关系增长,则程序的空间复杂度用 O(n3) 表示;

总结

时间复杂度和空间复杂度是衡量算法好坏的两个关键指标,但是在不同的开发场景中,它们的重要程度可能不同,比如:

  • 在处理大规模数据时,时间复杂度可能更为重要,因为快速处理数据可以在第一时间拿到结果。
  • 在内存受限的环境中,空间复杂度可能更为关键,因为需要确保算法在有限的内存内运行。


不过现在硬件便宜了,可以为计算机增加大量存储空间,所以程序员往往更加注重时间复杂度。至于空间复杂度,只要处于一个合理的范围即可。

相关文章
|
11月前
|
算法 Java C语言
弗洛伊德算法求最短路径
弗洛伊德算法用于寻找加权图中各顶点间的最短路径,适用于无向图和有向图。算法基于动态规划思想,通过枚举中间顶点来更新路径,确保最终得到最短路径。该算法要求路径权值非负,否则可能出错。
788 0
|
11月前
|
算法
回溯算法的基本思想
本节介绍回溯算法,通过图1中从A到K的路径查找示例,说明其与穷举法的异同。回溯算法通过“回退”机制高效试探各种路径,适用于决策、优化和枚举问题。
371 0
|
11月前
|
算法 Java 定位技术
迷宫问题
迷宫问题是指在给定区域内寻找从起点到终点的可行路径。可以使用回溯算法解决,通过不断尝试四个方向(上下左右)移动,若无法前进则回退,直到找到终点或遍历所有可能路径。文中还给出了C语言、Java和Python的实现代码,并展示了运行结果。
401 0
|
11月前
|
算法 Java C语言
汉诺塔问题
汉诺塔问题源自印度古老传说,涉及将一组圆盘从一根柱子移动到另一根,遵循特定规则。文章详细介绍了该问题的背景、解决思路以及如何使用分治算法实现,同时提供了C语言、Java和Python的代码示例。
750 0
|
11月前
|
存储 算法 Java
寻找图中是否存在路径
本节介绍了如何使用并查集判断图中两个顶点之间是否存在有效路径。通过将图的边信息存储到并查集中,同一连通区域的顶点会处于同一集合。只需比较两个顶点的根节点是否相同,即可判断它们是否连通。文中提供了C语言、Java和Python的实现代码,并通过示例验证了算法的正确性。
280 0
|
11月前
|
存储 算法
最小生成树的概念与思想
数据结构中的图存储结构包含顶点和边,分为连通图与非连通图。生成树是包含所有顶点、任意两点间仅有一条通路的极小连通图。最小生成树则是权值总和最小的生成树,常用于解决道路建设等实际问题,常用算法有普里姆算法和克鲁斯卡尔算法。
290 0
|
11月前
|
存储 算法 Java
哈希查找算法
哈希查找算法,又称散列查找算法,是一种通过哈希表快速定位目标元素的查找方法。其核心在于利用哈希函数将元素映射到表中的一个位置,从而实现高效查找,平均时间复杂度为O(1)。该算法适用于有序或无序序列,关键在于构建合适的哈希表并处理可能出现的哈希冲突。
1174 0
|
11月前
|
存储 算法 Java
求数组中的最大值和最小值
本文介绍了在程序中如何查找数组中的最大值和最小值,重点讲解了两种算法:普通算法和分治算法。普通算法通过遍历数组直接比较元素大小,找出最值;而分治算法则通过递归将数组划分成更小的部分,分别找出各部分的最大值,最终合并结果得到整个数组的最大值。文章以 {3,7,2,1} 为例,详细演示了两种算法的实现过程,并提供了 C、Java 和 Python 的代码示例。
794 0