【数据结构】时间、空间复杂度实例分析

跌倒了,就重新站起来,继续向前走;傻坐在地上是没用的。💓💓💓

目录

•✨说在前面

🍋知识点一:算法的效率

[• 🌰1.斐波那契数列的第n项](#• 🌰1.斐波那契数列的第n项)

[• 🌰2.算法的复杂度](#• 🌰2.算法的复杂度)

🍋知识点二:时间复杂度

[• 🌰1.时间复杂度的概念](#• 🌰1.时间复杂度的概念)

[• 🌰2.大O的渐进表示法](#• 🌰2.大O的渐进表示法)

🔥复杂度的一般分析法则

🔥总结求解复杂度的方法

[• 🌰3.时间复杂度的量级](#• 🌰3.时间复杂度的量级)

[• 🌰4.时间复杂度增长趋势](#• 🌰4.时间复杂度增长趋势)​​​​​​​

[• 🌰5.时间复杂度计算案例](#• 🌰5.时间复杂度计算案例)

🔥案例1:单个同量级for循环

🔥案例2:多个同量级for循环

🔥案例3:常数控制的for循环

🔥案例4:strchr函数的时间复杂度

🔥案例5:冒泡排序的时间复杂度

🔥案例6:调整语句为i=i*2的for循环

🔥案例7:二分查找的时间复杂度

🔥案例8:递归求阶乘

🔥案例9:递归求斐波那契数列的第n项

🍋知识点三:空间复杂度

[• 🌰1.空间复杂度的概念](#• 🌰1.空间复杂度的概念)

[• 🌰2.空间复杂度计算案例](#• 🌰2.空间复杂度计算案例)

🔥案例1:冒泡排序的空间复杂度

🔥案例2:递归求阶乘

[• ✨SumUp结语](#• ✨SumUp结语)


•✨说在前面

亲爱的读者们大家好!💖💖💖,我们又见面了,在之前的阶段我们学习了顺序表、链表,包括单链表和双向链表,还刷了一些算法OJ练习。**这些练习,我们有时可以有多种思路和方法解决,那我们如何对这些方法进行取舍呢?哪些方法是最优的呢?**为此,我们必须进入学习时间复杂度和空间复杂度的相关知识,相信你学习完后就可以回答这个问题了。

👇👇👇

💘💘💘知识连线时刻(直接点击即可)

🎉🎉🎉复习回顾🎉🎉🎉

【数据结构】顺序表专题详解(带图解析)

【数据结构】单链表专题详细分析

【数据结构】双向循环链表专题解析

博主主页传送门:愿天垂怜的博客

🍋知识点一:算法的效率

• 🌰1.斐波那契数列的第n项

在讲解时间复杂度与空间复杂度之前,我们先看一个简单的例子:

练习:写一个程序,求出斐波那契数列的第n项的值。

**方法1:**迭代法

cpp 复制代码
long long Fibonacci(int n)
{
	int x1 = 1;
	int x2 = 1;
	int x3 = 1;
	while (n >= 3)
	{
		x3 = x1 + x2;
		x1 = x2;
		x2 = x3;
		n--;
	}
	return x3;
}

**方法2:**递归法

cpp 复制代码
long long Fibonacci(int n)
{
	if (n == 1 || n == 2)
		return 1;
	return Fibonacci(n - 1) + Fibonacci(n - 2);
}

由观察不难发现,递归的写法明显要比迭代的写法要短的多,那是不是就说明递归的写法就比迭代的写法更好呢?其实不然,实际上用递归来写的话它的运行效率将会大大降低。

比如求第50项,就要先得到49项和48项,要得到第49项,就要得到48项和47项......它会执行很多很多次,这是它效率不高的原因。

所以,项数较大时,我们还是用循环(迭代)的方式来实现。

所以说,我们不能只根据程序的长短就果断地判断程序的好坏。

• 🌰2.算法的复杂度

那究竟如何衡量算法的好坏呢?就是要看程序的时间复杂度空间复杂度。

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

时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小,所以对于空间复杂度很是在乎,但结果计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度,所以我们如今已经不需要再特别关注一个算法的空间复杂度,转而更加关注他的时间复杂度。

🍋知识点二:时间复杂度

• 🌰1.时间复杂度的概念

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

一个算法执行所消耗的时间,从理论上来说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?这显然非常麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比,算法中的基本操作的执行次数,为算法的时间复杂度。

即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

• 🌰2.大O的渐进表示法

实际问题中我们不需要精确的计算执行次数,而只需要大概的执行次数,即使用大O的渐进表示法。

公式如下:

🎉参数:

**• T(n):**代码执行所需要的时间,它是n的函数,T(n)代表了算法的时间复杂度。

**• n:**数据规模的大小,但有可能不止一个,如两个for循环分别循环m、n次,则该参数为m、n。

**• f(n):**每行代码执行的次数总和,它是n的函数,但我们只关注最大量级的那一项。

**• O:**表示T(n)与f(n)之间的关系为正比例,即一个算法所花费的时间与其中语句的执行次数成正比。

🔥 复杂度的一般分析法则

1)单端代码看高频:比如for、while循环。

2)多段代码取最大:比如一段代码中有单循环和多重循环,那么取多重循环的复杂度。

3)嵌套代码求乘积:例如递归、多重循环的结构。

4)多个规模求加法:比如算法中有两个参数控制了两个for循环,那么此时复杂度取二者复杂度之和。

🎉举例:

cpp 复制代码
//请计算一下Func1中++count语句总共执行了多少次?
void Func(int n)
{
	int count = 0;
	for (int i = 0; i < n; ++i)
	{
		for (int j = 0; j < n; ++j)
		{
			++count;
		}
	}

	for (int k = 0; k < 2 * n; ++k)
	{
		++count;
	}
	int M = 10;
	while (M--)
	{
		++count;
	}
	printf("%d\n", count);
}

根据复杂度的分析原则,在上面的这个函数Func1中,第一个循环是嵌套结构,for循环嵌套结构执行了n^2次,第二个for循环执行了2n次,最后一个while循环执行了10次。

由此,这个函数中基本语句的执行次数f(n)的表达式为:f(n)=n^2+2n+10,所以T(n)=O(n^2+2n+10)。但是,大O的渐进表示法并不具体代表代码真正的执行时间,而是代表代码执行时间随数据规模n增长的变化趋势。

当n很大时,比如1000、100000,此时公式中的低阶、常量、系数三部分不左右增长趋势,所以都可以忽略,只关注最高阶的那一项就可以了。

所以最终,Func的时间复杂度表示为:T(n)=O(n^2)。

🔥总结求解复杂度的方法

1)只关注循环执行次数最多的一段代码,总复杂度等于量级最大的那段代码的复杂度

2)加法法则:若控制两个for循环的数量级相同,则总复杂度取二者复杂度之和。

3)乘法法则:嵌套循环的复杂度等于嵌套内外代码复杂度的乘积。

• 🌰3.时间复杂度的量级

**多项式阶:**随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长,包括:O(1)(常数阶)、O(logn)(对数阶)、O(n)(线性阶)、O(nlogn)(线性对数阶)、O(n^2)(平方阶)、O(n^3)(立方阶)。

**非多项式阶:**随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。包括,

O(2^n)(指数阶)、O(n!)(阶乘阶)。

• 🌰4.时间复杂度增长趋势

常见不同数量级的复杂度增长趋势图如下:

另外有些算法的时间复杂度存在最好、平均和最快的情况:

**🎉最坏情况:**代码在最坏情况下执行的时间复杂度,即任意输入规模的最大运行次数(上界)。

**🎉平均情况:**代码在所有情况下执行的次数的加权平均值,即任意输入规模的期望运行次数。

**🎉最好情况:**代码在最理想情况下执行的时间复杂度,任意输入规模的最小运行次数(下界)。

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

最坏情况:N次找到

平均情况:N/2次找到

最好情况:1次找到

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

• 🌰5.时间复杂度计算案例

🔥案例1:单个同量级for循环

计算Func2的时间复杂度。

cpp 复制代码
void Func2(int N)
{
	int count = 0;
	for (int k = 0; k < 2 * N; ++k)
	{
		++count;
	}
	int M = 10;
	while (M--)
	{
		++count;
	}
	printf("%d\n", count);
}

函数Func2中,第一个for循环执行了2N次,第二个while循环的执行次数是M,但是M为已知量10,量级最大的为2N,所以Func2的时间复杂度为O(N)

🔥案例2:多个同量级for循环

计算Func3的时间复杂度。

cpp 复制代码
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);
}

函数Func3中,第一个for循环执行了M次,第二个for循环执行了N次,且M、N量级相同,所以Func3的时间复杂度为O(M+N),或O(max(M,N))

注意:若M>>N,则时间复杂度为O(M),若M<<N,则时间复杂度为O(N)。

🔥案例3:常数控制的for循环

计算Func4的时间复杂度。

cpp 复制代码
void Func4(int N)
{
	int count = 0;
	for (int k = 0; k < 100; ++k)
	{
		++count;
	}
	printf("%d\n", count);
}

函数Func4中,控制for循环的次数为常数,代码不随n的增长而增长,常数阶的时间复杂度为O(1),即Func4的时间复杂度为O(1)

🔥案例4:strchr函数的时间复杂度

**strchr:**strchr() 用于查找字符串中的一个字符,并返回该字符在字符串中第一次出现的位置。

strchr函数模拟实现代码如下:

cpp 复制代码
const char* my_strchr(const char* str, int character)
{
	assert(str);
	while (*str)
	{
		if (*str == character)
			return str;
		else
			str++;
	}
}

在strchr查找的过程中,最好的情况是字符character就在下一个字符,一次就可以找到,也就是常数阶,此时时间复杂度为O(1);最坏的情况是字符character在离位置的无穷远处,也就是线性阶。此时时间复杂度为O(N);取最坏的情况,所以strchr的时间复杂度为O(N)

🔥案例5:冒泡排序的时间复杂度

计算冒泡排序BubSort的时间复杂度。

cpp 复制代码
void bubSort(int arr[], int length)
{
	assert(arr);
	int flag = 1;
	while (flag && length--)
	{
		flag = 0;
		for (int i = 0; i < length; i++)
		{
			if (arr[i] > arr[i + 1])
			{
				int temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
				flag = 1;
			}
		}
	}
}

冒泡排序中,由类似案例2的嵌套循环结构,但是像案例2这样的嵌套循环,它的内层for循环的控制参数是不变的,也就是说,内部循环一共执行了M次,每次执行内部循环都需要循环N次,所以总共的执行次数是M*N,这很好理解,但现在冒泡排序的内部循环一共执行了length次,而第一次for循环length次,第二次length-1次,第三次length-2次...,若考虑最坏的情况,最后到1次。这种情况怎么处理呢?

**显然内部for循环的循环次数是一个等差数列,其中公差d=2,首项a1=1,尾项an=length(简写为n),项数为length。**对于这样一个等差数列,我们可以对其求和,所得到的结果不就是所有的执行次数了吗?

根据高斯求和公式,很容易计算出等差数列的前n项和,即基本操作的执行次数,显然为平方阶,量级为N^2,所以冒泡排序Bubsort的时间复杂度为O(N^2)

其实对于案例2这样每次都是循环固定次数的嵌套,或者说,**任意的双层嵌套都可以转化为数列求和的问题,**如案例2,显然就是一个公差d=0,首项为N,尾项为N,项数为M的等差数列

根据高斯求和公式,也能够分析出基本操作的执行次数,即时间复杂度为O(MN)

🔥案例6:调整语句为i=i*2的for循环

计算Func5的时间复杂度。

cpp 复制代码
void Func5(int n)
{
	int x = 0;
	for (int i = 1; i < n; i *= 2)
	{
		x++;
	}
}

函数Func5中,这个for循环的调整语句为i*=2,即i每次都是前一次的两倍。这种循环是有规律的,这个我们后面再说。我们先直接分析这个函数,假设基本语句x++执行了N次,那么有循环语句可得:

所以我们得到,执行次数N为对数阶,所以时间复杂度为O(logN)这里需要注意,为了方便起见,当底数为2时,我们直接将底数2省略不写,

🔥案例7:二分查找的时间复杂度

计算二分查找BinarySearch的时间复杂度。

cpp 复制代码
int BinarySearch(int arr[], int length, int x)
{
	assert(arr);
	int left = 0;
	int right = length - 1;
	while (left <= right)
	{
		int mid = left + right - ((left - right) >> 1);
		if (arr[mid] < x)
			left = mid + 1;
		else if (arr[mid] > x)
			right = mid - 1;
		else if (mid == x)
			return x;
	}
	return -1;
}

在二分查找函数BinarySearch中,如果我们直接观察while循环的话,其实是不太好看出来的,因为left和right时不时都在改变,此时我们不要死盯代码,一定要理解它的实际意义,也可以看图进行观察

最好的情况我们很容易想到,就是要查找的x就在中间,我们一下就找到了,此时是时间复杂度为O(1)。那最坏的情况呢?其实就是当要查找的x在数组的两端的时候,比如x是第一个元素,此时我们设mid到x的距离为n,**则mid会以每次靠近一半的速度逼近第一个元素x,也就是每次都除以2,直到这个值等于1,**就找到了第一个元素。我们设循环执行了N次,mid第一次的位置为n,则

同样地,如果x是最后一个元素,也是同样的道理。显然为对数阶,所以二分查找BinarySearch的

的时间复杂度为O(logN)

🔥案例8:递归求阶乘

计算Fact的时间复杂度。

cpp 复制代码
long long Fact(size_t N)
{
	if (0 == N)
		return 1;

	return Fac(N - 1) * N;
}

这是一个递归求阶乘的函数。在函数Fact中,我们假设N=5的情况如下:

可以发现,每次递归的单个函数都是常数阶,即O(1),而递归总共调用了5+1=6次(Fact(5)、Fact(4)、Fact(3)、Fact(2)、Fact(1)、Fact(0))。

以递归的函数我们先看单个函数内部的阶,再看递归了多少次。如果是N,那么每个函数为O(1),递归了N+1次,那么总共为(N+1)O(1),所以递归求阶乘函数的时间复杂度为O(N)

🔥案例9:递归求斐波那契数列的第n项

计算Fibonacci函数的时间复杂度。

cpp 复制代码
long long Fibonacci(int n)
{
	if (n == 1 || n == 2)
		return 1;
	return Fibonacci(n - 1) + Fibonacci(n - 2);
}

同样的道理,我们看单个函数内部的阶,显然为O(1),那递归了多少次呢?

我们以n=5为例,此时我们能画出面类似于树状图的结构,在数的每个节点都进而伸出两个节点,第一行为个数为1(2^0),第二行个数为2(2^1),第三行个数为4(2^2)...以此类推,第n行的个数为2^n。但其实上大家能看到,这颗树是歪的,它的底部缺失了一块三角形的部分,但是当n很大时,这些缺失的部分相对于整体的个数其实就很少了,由此我们可以将每一行的递归次数看做一项,那整体就可以看做首项a1=1,公比q=2的等比数列,求和得到的即是全体递归的总次数。

虽然真正的递归次数没有这么多,但是它的量级是不会被影响的,依然是指数阶,所以用递归求斐波那契数列的第n项,它的时间复杂度为O(2^N)

稍许有些不严谨,在二叉树的部分我们还会提到。

🍋知识点三:空间复杂度

• 🌰1.空间复杂度的概念

**空间复杂度也是一个数学表达式,**是对一个算法在运行过程中临时占用存储空间大小的亮度。

空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。

空间复杂度计算规则基本和时间复杂度类似,也是大O渐进表示法。

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式的额外空间来确定。

• 🌰2.空间复杂度计算案例

🔥案例1:冒泡排序的空间复杂度

计算冒泡排序BubSort的空间复杂度。

cpp 复制代码
void bubSort(int arr[], int length)
{
	assert(arr);
	int flag = 1;
	while (flag && length--)
	{
		flag = 0;
		for (int i = 0; i < length; i++)
		{
			if (arr[i] > arr[i + 1])
			{
				int temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
				flag = 1;
			}
		}
	}
}

在冒泡排序BubSort中,创建了:int flag = 1,int i = 0两个变量,为常数个,所以冒泡排序的空间复杂度为O(1)

注意:空间复杂度算的是算法中额外开辟的空间,所以数组arr和长度length并不算在内。

🔥案例2:递归求阶乘

计算Fact函数的空间复杂度。

cpp 复制代码
long long Fact(size_t N)
{
	if (0 == N)
		return 1;

	return Fac(N - 1) * N;
}

对于这样的一个递归函数,每次递归都会在栈上创建栈帧空间(函数栈帧),每个栈帧使用了常数个空间,所以空间复杂度为O(N)

• ✨SumUp结语

数据结构的学习一定要多画图,多理解,多思考,切忌直接抄写代码,就认为自己已经会了,一定到自己动手,才能明白自己哪个地方有问题。

如果大家觉得有帮助,麻烦大家点点赞,如果有错误的地方也欢迎大家指出~

相关推荐
猷咪25 分钟前
C++基础
开发语言·c++
IT·小灰灰26 分钟前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧28 分钟前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q28 分钟前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳028 分钟前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾28 分钟前
php 对接deepseek
android·开发语言·php
CSDN_RTKLIB32 分钟前
WideCharToMultiByte与T2A
c++
2601_9498683632 分钟前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
飞机和胖和黄1 小时前
考研之王道C语言第三周
c语言·数据结构·考研
yyy(十一月限定版)1 小时前
寒假集训4——二分排序
算法