算法的时间复杂度介绍
创始人
2025-05-30 19:06:20
0次

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

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)常数级别了。

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

相关内容

热门资讯

在大吉订购APP订购交易亏损被...   近期,不少投资者反映在“大吉订购App”上投资白银、铂金等商品遭遇严重亏损。该平台宣称提供现货订...
铂银壹号APP商品订购交易亏损...   许多中小投资者误以为网上的“商品现货订购”是合规合法的理财渠道,殊不知这背后隐藏着巨大的合规风险...
兴鑫淘金APP贵金属订购交易亏...   白银、贵金属或其他商品类投资,除国家批准的正规期货交易场所及持牌期货公司外,市面上大量第三方交易...
铂银掘金APP无资质的违规平台...   铂银掘金等平台的投资者,往往是被“注册送现金”“充值返现券”“首单免亏”等广告话术吸引,进而下载...
八方淘金APP订购交易不合规,...   “八方淘金APP”“铂银掘金APP”“铂银壹号APP”等软件被指以“现货订购”为名,实际采用高杠...
在指尖订购APP订购商品亏损被...   指尖订购APP这类平台,表面是“订购交易”,实则是披着电商外衣的非法期货陷阱。绝大多数投资者被蒙...
在犀贝易购APP做商品订购交易...   市面上打着“现货投资”“贵金属交易”旗号的APP并不少,但真正合规、受监管的平台并不多。犀贝易购...
易达淘金APP虚假商品订购交易...   易达淘金APP存在严重的虚假宣传与违规交易行为。该平台打着“商品订购”的旗号,实则通过不合规的交...
大浪淘金APP白银订购亏损被骗...   大浪淘金APP最隐蔽的套路,在于其完全封闭的内盘交易系统。表面上,它宣称与国际贵金属市场接轨;实...
一品好购APP上订购白银出现亏...   一品好购APP原名“一品订购”,运营方为上海宝智未来信息技术有限公司。近期不少投资者反映在该平台...
九州订购APP贵金属订购投资骗...   九州订购APP正通过抖.音、快手、今日头条等平台大量投放投资广告,用“5元起投”“充值返现”“0...
坚锅淘金APP交易软件并不正规...   当前,市面上大量所谓的“现货商城订购”交易软件,早已偏离了正常的实物商品买卖,演变为高杠杆、高手...
斗金订购APP贵金属期货交易骗...   斗金订购APP的投资者往往被铺天盖地的广告宣传所诱导——注册即送现金、充值返现券等看似诱人的福利...
警惕!云启购APP违规经营,披...   "5元起投、充值返现、稳赚不赔"——这些诱人的广告语背后,可能是一个精心设计的金融陷阱。  近期...
量子淘金APP贵金属投资不合规...   "4元起投""充值返券""月入过万"——这些极具诱惑力的广告语在短视频平台、社交软件上铺天盖地。...
鑫汇智投APP贵金属商品订购骗...   鑫汇智投APP非法期货投资骗局,受害者在投资过程中根本不懂交易,也不去研究和验证这个平台是否正规...
矅光智策APP平台违规经营非法...   矅光智策APP对外宣称是“商城订购”“现货交易”,但查询其经营范围,并未显示具备开展现货期货交易...
黄金珠宝怎么选?内侧刻字都代表... 潮新闻客户端 记者 马焱 通讯员 任倩影 9月8日下午,“云游杭州 乐购金秋”——“浙城·浙放心”活...
原创 冰... 最近全球国债市场正在上演魔幻一幕: 美债、日债、欧债遭遇猛烈抛售,收益率冲到几十年新高,美国财政部都...
日韩股市开盘表现分化 上证报中国证券网讯 9月9日,日韩股市开盘表现分化,韩国综合指数开盘上涨0.26%,报6972.87...