【数据结构】时间复杂度

目录

一、算法的复杂度

二、时间复杂度

2.1 时间复杂度的概念

2.2 大O渐进表示法

2.3 计算时间复杂度步骤

三、常见时间复杂度举例

3.1 ❥ 常数阶

3.2 ❥ 线性阶

3.3 ❥ 平方阶

3.4 ❥ 对数阶

3.5 ❥ 指数阶

3.6 ❥ 多个未知数的复杂度

四、最好,最坏,平均情况

五、时间复杂度优劣对比


一、算法的复杂度

如何衡量一个算法的好坏呢?

是从时间效率空间效率这两个方面进行衡量。

因为算法在编写成可执行程序后,运行需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏,一般从时间和空间这两个维度来衡量。即时间复杂度和空间复杂度。

时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小,所以对空间的复杂度很是在乎。现如今计算机行业迅速发展,计算机存储容量很高,因此就不再特别关注算法的空间效率,通常更加注重时间效率。

注意:

        不能认为时间复杂度就比空间复杂度重要,在某些场景下空间复杂度反而比时间复杂度重要,在程序中我们需要综合考虑让时间和空间的消耗达到一个平衡点。

二、时间复杂度

2.1 时间复杂度的概念

在计算机科学中,算法的时间复杂度是一个函数,它定性描述了该算法的运行时间。

一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

例题:

int func(int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("haha\n");
	}
	return 0;
}

以上代码共执行了多少次呢?

分析如下:

作为衡量代码速度的依据,当代码较多的时候,一条一条地去数就比较麻烦,而且在函数调用函数的时候,运行起来也比较麻烦。

所以,算法一般使用估算值来衡量代码的执行速度,这里用大O的渐进表示法来表示时间复杂度。

2.2 大O渐进表示法

大O渐进表示法:用来粗略度量算法的效率。

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。

推导大O阶方法:

  1. 用常数1取代运行时间中的所有加法常数。
  2. 在修改后的运行次数函数中,只保留最高阶项。
  3. 如果最高阶项存在且不是1,则去除与这个项目相乘的常数,得到的结果就是大O阶。

简而言之,去掉常数项,低次幂,最高次的系数就是该函数的时间复杂度。

本质:计算算法时间复杂度(次数)属于哪个量级(level)。

只需要计算算法的时间复杂度属于哪个量级的原因:

  • 因为cpu的运行速度超级快,每秒钟的运算速度达到上亿次,如果所运算的规模比较小的话,其实计算它的时间复杂度是没有意义的,都可以在一秒钟运行完毕。
  • 这里表示的是:当问题规模无限大的情况下(也就是数学中接近于无穷大的情况下),那么常数项,低次幂,以及最高次的系数就该函数的影响不大,可以忽略,所以直接计算该复杂度属于哪个量级即可。

使用大O的渐进表示法以后,func的时间复杂度为:O(n)

2.3 计算时间复杂度步骤


 1、找出算法中的基本语句。

        算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。

 2、计算基本语句的执行次数的数量级。

  计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可。

  3、用O记号表示算法的时间性能。

  将基本语句执行次数的数量级放入O记号中


三、常见时间复杂度举例

3.1 ❥ 常数阶

常数阶的时间复杂度是:O(1)

说明算法的执行时间是常量,不随输入规模的增加而增加。

例题:

//计算Print的时间复杂度
void Print()
{
	int i = 0;
	for (i = 0; i < 100; i++)
	{
		printf("lala\n");//层次最深的语句
	}
	printf("haha\n");
}

可知,最深的打印语句执行了100次,也就是常数次。用O(1)表示。

注意:

其实printf("haha\n");也算一次运算,但是它整体影响不大,(因为cpu运行速度超级快)所以可以忽略这些细枝末节,不用计算。

易错提醒:

  • O(1)是代表的是常数次,不是1次。
  • 只要是常数,都用O(1)进行表示。

3.2 ❥ 线性阶

线性阶的时间复杂度是:O(N)

例题:

// 计算阶乘递归Fac的时间复杂度
long long Fac(int N)
{
	if (0 == N)
		return 1;
	return Fac(N - 1) * N;
}

分析如下:

由此可知:

递归的时间复杂度:所有递归调用的次数累加

3.3 ❥ 平方阶

平方阶的时间复杂度是:O(N^2)

例题:

// 计算阶乘递归Fac的时间复杂度
long long Fac(int N)
{
	if (0 == N)
		return 1;

	for (int i = 0; i < n; ++i)
	{
		//...
	}

	return Fac(N - 1) * N;
}

分析如下:

由于计算的是算法时间复杂度(次数)属于哪个量级,所以只需要看决定性的项,也就是最高阶的项,不需要关注那些细枝末节。

所以(n^2+n)/2属于平方阶,忽略它的最高项的系数以及低次项,因此算出来的该递归函数的时间复杂度为O(N^2)。

3.4 ❥ 对数阶

对数阶的时间复杂度是:O(logN)

例题:

// 计算BinarySearch的时间复杂度
int BinarySearch(int* a, int n, int x)
{
	assert(a);
	int begin = 0;
	int end = n - 1;
	while (begin <= end)
	{
		int mid = begin + ((end - begin) >> 1);
		if (a[mid] < x)
			begin = mid + 1;
		else if (a[mid] > x)
			end = mid - 1;
		else
			return mid;
	}
	return -1;
}

分析如下:

3.5 ❥ 指数阶

指数阶的时间复杂度是:O(2^N)

例题:

// 计算斐波那契递归Fib的时间复杂度
long long Fib(int N)
{
	if (N < 3)
		return 1;
	return Fib(N - 1) + Fib(N - 2);
}

分析如下:

当时间复杂度达到O(2^N),时间复杂度太高。该算法只有理论意义,无实践意义。

3.6 ❥ 多个未知数的复杂度

一个时间复杂度里面也不一定只有一个未知数。

那遇见存在多个未知数的情况,我们该如何写代码呢?

我们举例来说明,看如下代码:

// 计算Func3的时间复杂度
void Func3(int N, int M)
{
	int count = 0;
	for (int k = 0; k < M; ++k)
	{
		++count;
	}
	for (int k = 0; k < N; ++k)
	{
		++count;
	}
	printf("%d\n", count);
}

分析如下:

该代码的时间复杂度为:O(M+N)

也可以写成O(max(M,N))

如果有说明M和N的关系的,写成下面的形式这样更好

  1. 若M远大于N:O(M)
  2. 若N远大于M:O(N)

四、最好,最坏,平均情况

算法的复杂度存在最好,最坏,和平均情况

  • 最坏情况:任意输入规模的最多运行次数(上界)
  • 平均情况:任意输入规模的期望运行次数
  • 最好情况:任意输入规模的最少运行次数(下界)

例如:在一个长度为N数组中搜索一个数据x

最好的情况:1次找到

最坏的情况:N次找到

平均情况:N/2次找到

我们在实际中一般情况关注的是算法的最坏运行情况(也就是最低的预期),所以数组中搜索数据时间复杂度为O(N)。

五、时间复杂度优劣对比

常见的数量级大小:越小表示算法的执行时间频度越短,则越优。(时间复杂度越,效率越

O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(2n) < O(n!)

速记:常对幂指阶(时间复杂度:低——>高)

所以, 我们编写代码时一定要注意时间复杂度的级别控制,养成良好的代码编写习惯。

### 考研 数据结构 时间复杂度 分析与计算 #### 基本概念 时间复杂度用于描述算法执行所需的时间随输入规模增长而变化的趋势。通常情况下,在评估算法效率时,只需关注操作数量中的最高次项,其余次要项和常数项可被忽略[^2]。 #### 计算原则 对于不同类型的程序控制流结构,存在特定的方法来估算它们对应的时间开销: - **基本操作**:如果一段代码仅涉及简单的赋值、比较或算术运算,则该部分的时间复杂度视为O(1)。 - **顺序结构**:当多条语句依次被执行时,整体的时间复杂度等于各单独语句复杂度相加之和。 - **循环结构**:内嵌于循环体内的指令会重复执行多次,此时应将循环次数与每次迭代内部的操作量相乘得到总的时间消耗。 - **分支结构**:针对if-else这样的条件判断逻辑,最终选取其中一条路径的最大可能代价作为整个表达式的估计值。 #### 单层循环分析方法:“模拟设t法” 考虑如下形式的简单for循环: ```python for i in range(n): # 执行一些固定时间内完成的任务 ``` 假设每轮迭代中所花费的实际时间为`T(i)`,那么n次完整的遍历过程总共耗时为∑_{i=0}^{n-1} T(i),而在大多数实际应用场景下,我们可以近似地认为每一趟处理都是一样的快速——即`T(i)=c`(某个很小且固定的正实数值),从而简化求解公式变为nc=n*O(1)=O(n)。 #### 多重嵌套循环的情况 遇到多个层次相互包裹着的循环时,应当逐级累加各个子组件带来的影响因子。比如双重for-loop模式下的情况: ```python for i in range(m): for j in range(k): # 同样是单位级别的工作负载 ``` 这里总的运行成本大约相当于mk*O(1)=O(mk)。同理推广到更复杂的场景也是类似的思路去拆分解析每一个组成部分再汇总起来考量全局性能表现。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值