算法的时间复杂度介绍
创始人
2025-05-30 20:52:45

本文主要算法时间复杂度的相关知识。

1 概述

算法(Algorithm)是指用来操作数据、解决程序问题的方法。

对于同一个问题,使用不同的算法,也许最终得到的结果是相同的,但在执行该算法所需的时间和(存储)资源可能会有很大的区别,那么我们应该如何去衡量不同算法之间的优劣呢?目前主要是从算法所占用的“时间”和“空间”两个维度去进行评价。

时间维度是指执行算法所消耗的时间,通常用“时间复杂度”来描述,空间维度是指执行算法需要占用的内存空间大小,通常用“空间复杂度”来描述。

2 大O表示法与常见的时间复杂度

大O表示法是一种算法复杂度的“相对”“表示”方式,在这个句子中:

相对(relative):我们只能比较相同的事物,不能把一个做算数乘法的算法和排序整数列表的算法进行比较,这是没有意义的;

表示(representation):大O表示法把算法间的比较简化为了一个单一的变量,这个变量的选择基于观察或假设。例如,排序算法之间的比较通常是基于比较操作(比较2个结点来决定这2个结点的相对顺序)的,这里面就假设了节点间的比较操作的计算开销很大。但是,如果比较操作的计算开销不大,而交换操作的计算开销很大,又会怎么样呢?这就改变了先前的比较方式。

常见的时间复杂度量级如下:

  • O(1):常数阶

  • O(n):线性阶

  • O(n^2):平方阶

  • O(logn):对数阶

  • O(nlogn):线性对数阶

  • O(2^n):2的n次方阶

  • O(n!):阶乘阶

说明:在表示算法复杂度时,对数logn的底数为2。

这几种时间复杂度的优劣性如下图所示:

下面分别介绍几种常见的时间复杂度。

2.1 O(1)

对于O(1)常数阶时间复杂度,仅需固定次数(如1次、5次等等)即可完成算法的操作,而不会受其他因素影响,即其他因素不会参与到算法的操作中。

下面给出一个交换参数a和b位置的示例代码:

void swapTwoInts(int &a, int &b) {int temp = a;a = b;b = temp;
}

在上面的示例代码中,仅需一次操作即可完成变量交换算法(操作),而在此过程中,其他因素不会影响到该算法的时间复杂度。上述代码的运行过程示意图如下:

2.2 O(n)

对于O(n)线性阶时间复杂度,需要执行n次操作(如变量交换、加法等算法运算等)方可完成算法的完整操作。

下面给出一个求累加和的示例代码:

int sum (int n) {int ret = 0;for (int i = 0; i <= n; i++) {ret += i;}return ret;
}

在上面的示例代码中,for循环中的代码(加法运算)会被执行n次,即需要n次操作方可完成累加算法(操作),因此该算法的时间复杂度为O(n)。上述代码的运行过程示意图如下:

注意,c*O(n)表示的时间复杂度与O(n)相同。例如2*O(n)或(1/2)*O(n)均表示时间复杂度为O(n)。

2.3 O(n^2)

对于O(n^2)平方阶时间复杂度,需要执行(n^2)次操作(如变量交换、加法等算法运算等)才能完成算法的完整操作。

下面给出一个选择排序(selection sort)的示例代码:

void selectionSort(int arr[],int n) {for (int i = 0; i < n; i++) {int minIndex = i;for (int j = i + 1; j < n; j++) {if (arr[j] < arr[minIndex]) {minIndex = j;}}swap(arr[i], arr[minIndex]);}
}

在上面的示例代码中,for循环中的代码中又嵌套了一层for循环(双重循环),即把O(n)的代码再嵌套循环一遍,因此该算法的时间复杂度就是O(n^2)了。上述代码的运行过程示意图如下:

这里推导一下该算法的执行过程:

  • 当”i = 0“时,第二重循环需要运行(n – 1)次;

  • 当”i = 1“时,第二重循环需要运行(n – 2)次;

依此类推,可以得到公式:

(n - 1) + (n - 2) + (n - 3) + ... + 0
= (0 + n - 1) * n / 2
= (n ^ 2 - n) / 2

因此,该算法的时间复杂度为O(n^2)。

当然,并非所有双重循环的时间复杂度都是O(n^2),如果内层循环次数为常数c,则时间复杂度为c*O(n)=O(n)。示例代码如下:

void printInformation(int n) {for (int i = 1; i <= n ; i++) {for (int j = 1; j <= 30; j++) {cout << "Hello World!" << endl;}}
}

2.4 O(logn)

对于O(logn)对数阶时间复杂度,需要执行(logn)次操作(如变量交换、加法等算法运算等)才能完成算法的完整操作。

下面给出一个二分查找(binary search)的示例代码:

int binarySearch(int arr[], int n, int target) {int left = 0, right = n - 1;while (left <= right) {int mid = left + (right - left) / 2;if (arr[mid] == target) {return mid;}if (arr[mid] > target) {right = mid - 1;}else {left = mid + 1;}}return -1;
}

在上面的示例代码中,通过while循环中的除2操作,将每次搜索范围缩小至前一次的一半,因此最坏的情况需要经过logn次操作跳出循环,因此该算法的时间复杂度为O(logn)。上述代码的运行过程示意图如下:

这里推导一下该算法的执行过程:

  • 在第1次查找完成后,剩余的搜索范围为n*(1/2);

  • 在第2次查找完成后,剩余的搜索范围为n*(1/2)*(1/2);

依此类推,可以得到公式:

n*((1/2)^k)=1
k=logn

说明:

  • n为数组长度;

  • k为最坏情况下查找到目标元素所需的次数(或者跳出循环的次数);

  • 等式“n*((1/2)^k)=1”中右侧的“1”表示剩余的待搜索范围。当剩余的搜索范围为“1”时,该次搜索操作完成后,无论是否找到了目标元素,此算法都将结束执行。

因此,该算法的时间复杂度为O(logn)。

2.5 O(nlogn)

将时间复杂度为O(logn)的代码循环n次,则该算法的时间复杂度为n*O(logn),即O(nlogn)。

示例代码如下:

void hello() {for (i = 1; i < n; i++) {int j = 1;while (j < n) {j = j * 2;}}
}

3 其他几种时间复杂度

本章介绍以下几种复杂度:

  • 递归算法(recursive algorithm)的时间复杂度;

  • 最好情况(best case)时间复杂度;

  • 最坏情况(worst case)时间复杂度;

  • 平均情况(average case)时间复杂度;

  • 均摊(amortized)时间复杂度。

3.1 递归算法时间复杂度

3.1.1 一次递归调用

如果递归函数中,只进行了一次递归调用,且递归深度为depth,同时递归函数中操作的时间复杂度为T,则该递归算法的总体时间复杂度为O(T * depth)。

下面给出一个使用递归函数的二分查找(binary search)的示例代码:

int binarySearch(int arr[], int left, int right, int target) {if (left > right) {return -1;}int mid = left + (right - left) / 2; if (arr[mid] == target) {return mid;  }else if (arr[mid] > target) {return binarySearch(arr, left, mid - 1, target);} else {return binarySearch(arr, mid + 1, right, target);}
}

在上述代码中,递归函数中每次只可进行一次递归调用(if-else判断条件),由于递归函数中的除2操作,使得递归深度为logn,同时递归函数中操作的时间复杂度为1(无循环等),因此该算法的时间复杂度为O(1 * logn) = O(logn)。

这里再给出一个使用递归函数计算累加和的代码示例:

int sum(int n) {if (n == 0) {return 0;}return n + sum(n - 1);
}

在上述代码中,递归深度随着n的增加而线性递增,即递归深度为n,同时递归函数中操作的时间复杂度为1(无循环等),因此该算法的时间复杂度为O(1 * n) = O(n)。该代码的运行过程示例图如下:

3.1.2 多次递归调用

递归算法中比较难计算的是多次递归调用的情况,在这种情况下,通常需要根据具体情况分析算法时间复杂度。

先看下面一段示例代码:

int f(int n) {if (n == 0) {return 1;}return f(n - 1) + f(n - 1);
}

在上述这段代码中,递归算法包含了2次递归调用,递归树中节点数就是代码操作的调用次数。假设n=3,则上述代码的运行过程示意图如下:

在上述示意图中,代码操作的调用次数计算公式如下:

1 + 2 + 4 + 8 = 15

从中可以寻找出如下规律:

2 ^ 0 + 2 ^ 1 + 2 ^ 2 + ... + 2 ^ n
= 2 ^ (n + 1) – 1
= 2 ^ n + 1

因此,该算法的时间复杂度为O(2^n)。

再看一个归并排序的代码示例:

void mergeSort(int sourceArr[], int tempArr[], int startIndex, int endIndex)
{if(startIndex < endIndex){int midIndex = startIndex + (endIndex-startIndex) / 2;mergeSort(sourceArr, tempArr, startIndex, midIndex);mergeSort(sourceArr, tempArr, midIndex+1, endIndex);merge(sourceArr, tempArr, startIndex, midIndex, endIndex);}
}

在上述这段代码中,递归算法包含了2次递归调用。假设n=8,则上述代码的运行过程示意图如下:

通过观察上面的归并排序的递归树可知:

  • 归并排序的递归树深度为logn;

  • 归并排序的递归树中每个节点处理的数据规模是逐渐缩小的,但每一层的时间复杂度保持不变,为O(n);

因此,归并排序的算法时间复杂度为O(nlogn)。

说明:归并排序算法时间复杂度的计算方式与3.1.1节“一次递归调用”中的计算方式类似。这里要强调的是,涉及到对此递归调用的时间复杂度计算时,需根据实际情况进行分析。

3.2 最好/最坏情况时间复杂度

最好或最坏情况下的时间复杂度指的是特殊情况下的时间复杂度。

现有如下示例代码(作用是查找某数值在数组中首次出现时的位置):

int find(int array[], int n, int x) {for (int i = 0; i < n; i++) {if (array[i] == x) {return i;break;}}return -1;
}

上述代码的运行过程示意图如下:

通过上图可知,当数组中第一个元素就是要查找的数值时,时间复杂度就是O(1);而当最后一个元素才是要找的数值时,时间复杂度则是O(n)。

简单总结一下,最好情况时间复杂度就是在最理想情况下执行代码的时间复杂度,它所消耗的时间是最短的;最坏情况时间复杂度就是在最糟糕情况下执行代码的时间复杂度,它所消耗的时间是最长的。

3.3 平均情况时间复杂度

最好、最坏时间复杂度反应的是极端条件下的时间复杂度,通常发生的概率不大,不能代表平均水平。因此为了更好的表示一般情况下的算法时间复杂度,就需要引入平均情况时间复杂度。

平均情况时间复杂度可用代码在所有可能情况下执行次数的加权平均值表示。

这里仍以3.2节中的find函数为例,从概率的角度看, 数值x在数组中每一个位置出现的可能性是相同的,都是1/n,同时对应位置的权重为n的实际值(如1、2、3...),因此,该算法的平均情况时间复杂度就可以用如下的方式计算:

(1/n)*1 + (1/n)*2 + (1/n)*3 + ... + (1/n)*n
= (n+1)/2

因此,find函数的平均时间复杂度为O(n)。

3.4 均摊时间复杂度

这里通过一个动态数组的push_back操作来理解均摊时间复杂度。示例代码如下:

template 
class MyVector {
private:T* data;// 存储数组中的元素个数int size;// 存储数组中可以容纳的最大的元素个数int capacity;// 复杂度为O(n)void resize(int newCapacity) {T *newData = new T[newCapacity];for (int i = 0; i < size; i++) {newData[i] = data[i];}data = newData;capacity = newCapacity;}
public:MyVector() {data = new T[100];size = 0;capacity = 100;}// 平均复杂度为 O(1)void push_back(T e) {if (size == capacity) {resize(2 * capacity);}data[size++] = e;}// 平均复杂度为 O(1)T pop_back() {size --;return data[size];}
};

上述代码的运行过程示意图如下:

在上述代码中,push_back实现的功能是向数组的末尾增加一个元素。如果数组没有填满,则直接向其后面插入元素;如果数组已经填满了(即size == capacity),则将数组扩容一倍,然后再执行插入元素操作。

假设数组长度为n,则前n次调用push_back函数的时间复杂度都是O(1)级别,而在第(n+1)次则需要先进行n次元素转移操作,然后再进行1次元素插入操作,因此,第(n+1)操作的时间复杂度为O(n)。

所以,对于容量为n的动态数组,前面添加元素需要消耗了(1*n)的时间,同时扩容操作消耗n时间,总共消耗(2*n)的时间,因此,完整操作的均摊时间复杂度为:O(2n/n) = O(2),也就是O(1)常数级别了。

通过以上内容,可以知道:一个相对比较耗时的操作(如本例中的扩容操作),如果能保证它不会每次都被触发,那么这个相对比较耗时的操作,它需要的时间是可以分摊到其它的操作中的。

相关内容

热门资讯

数字操作方法 系列文章目录 前端系列文章——传送门 JavaScript系列文章——传送门 文章目录系列文章目录...
Cartesi 2023 年 ... 查看 Cartesi Machine、Cartesi Rollups 和 Noether 的更新正在...
JavaWeb——jsp概述入... JSP定义:  在如下一个jsp文件里面有如下的代码  <%@ page content...
一切喜怒哀乐都来自于你的认知 01 有个学子,准备出国,父母请来清华的教授宁向东。请问教授࿱...
JAVA并发编程——synch... 引言         Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,...
git学习----3.21 未... 文章目录前言Git :一个分布式版本控制工具目标一、概述1.1 开发中的实际场景1.2...
Qt优秀开源项目之十七:QtP... QtPromise是Promises/A+规范的Qt/C++实现。该规范的译...
【前端八股文】JavaScri... 文章目录Set概念与arr的比较属性和方法并集、交集、差集Map概念属性和方法String用索引值和...
海康硬盘录像机接入RTSP/o... EasyNVR安防视频云服务平台可支持设备通过RTSP/Onvif协议接入平台,能提供...
在混合劳动力时代如何避免网络安... 在混合劳动力时代如何避免安全网络风险 三年多来,混合工作一直是工作生活中不可或缺的一...
2023还不懂Jmeter接口... 这里介绍的Jmeter接口测试的的实战,如果文章内容没遇看懂的话,我这边...
基于4G/5G弱网聚合的多链路... 基于4G/5G多卡聚合(弱网聚合)的智能融合通信设备技术亮点 增强带宽提供可靠连接 通过将多个有线和...
如何使用Synplify综合v... 文章目录使用Synplify综合的好处synplify的教程方法1(无效)...
2023年全国最新高校辅导员精... 百分百题库提供高校辅导员考试试题、辅导员考试预测题、高校辅导员考试真题、辅导员证考试题库等ÿ...
2022年18个值得期待的Le... 有数百个独特的LearnDash附加组件,您可能很难选择您的LearnDash LMS...
【java基础】Stream流... 文章目录基本介绍流的创建流的各种常见操作forEach方法filter方法map方法peek方法fl...
javaweb高校行政办公自动... 本课题基于我国高校管理信息化建设现状,结合在实际工作中所遇到的问题和收获,...
一款专门为自动化测试打造的集成... 你好,我是不二。 随着行业内卷越来越严重,自动化测试已成为测试工程师的...
【go-zero】golang... 一、casbin 概览 1、casbin基本了解 casbin的GitHub:https://git...
现在开发低代码平台算晚吗? 现在开发低代码平台算晚吗?作为低代码的亲戚——零代码厂商,这篇就以“厂商...
【JavaWeb】书城项目(2... 222.书城项目-第三阶段:修改所有html页面为jsp页面 改成jsp页面之后&#x...
基于jeecgboot的大屏设...      通过前面设计好数据源后,就要进行数据集的设计了。      一、还是在onl...
Linux命令小技巧:显示文件... 工作中会有很多千奇百怪的需求,比如:如何在 Linux 命令行中快速找到...
【找工作】-- 大数据工程师找... 目录 1.前言 2.找工作的理论知识 2.1 分析个人特征 2.1.1 你自身优势是什么?
C++基础算法④——排序算法(... 排序算法 1.插入排序 2.桶排序 1.插入排序 基本思想:将初始数据分为有序部分和...
nginx快速入门.跟学B站n... nginx快速入门.跟学B站nginx一小时精讲课程笔记nginx简介及环境准备nginx简介环境准...
ORACLE存过互相调用之间事... 今天在问答区看到一个问题是 假如有procedureA、procedureB和procedureC&...
基于java中Springbo... 基于java中Springboot框影视影院订票选座管理系统 开发语言:Java 框...
CVE-2018-18086 最近闲来无事,看到青少年CTF平台,感觉对新手还是比较友好的࿰...
【深度学习】基于Hough变化... 💥💥💞💞欢迎来到本博客❤️❤️&#x...