时间复杂度带你入门数据结构(建议收藏)❥(^_-)

Posted 你帅你先说.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了时间复杂度带你入门数据结构(建议收藏)❥(^_-)相关的知识,希望对你有一定的参考价值。

●🧑个人主页:你帅你先说.
●📃欢迎点赞👍关注💡收藏💖
●📖既选择了远方,便只顾风雨兼程。
●🤟欢迎大家有问题随时私信我!
●🧐版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。

🍎1.算法效率

🍖1.1如何衡量一个算法的好坏

之前在C语言中我们学过求斐波那契数列,用了两种方法,一种是递归,一种是循环。
对于递归求斐波那契数列:

long long Fib(int N) 
{
 if(N < 3)
 return 1; 
 return Fib(N-1) + Fib(N-2);
}

我们知道,这个代码写起来比循环要简洁很多,但效率却不高,因为递归调用函数对内存的消耗非常大。
所以说简洁的代码不一定是好的,既简洁又高效的代码才是好的。

🍗1.2算法的复杂度

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。 时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。 在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

🍊2.时间复杂度

🧇2.1时间复杂度的概念

时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(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 执行的基本操作次数 :
● N = 10 F(N) = 130
● N = 100 F(N) = 10210
● N = 1000 F(N) = 1002010
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。

🍟2.2 大O的渐进表示法

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

1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O的渐进表示法以后,Func1的时间复杂度为:
N = 10 F(N) = 100
N = 100 F(N) = 10000
N = 1000 F(N) = 1000000
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)

🍔2.3常见时间复杂度计算举例

// 计算Func2的时间复杂度?
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);
}

这道题相信大家都会
总次数为2N+10所以时间复杂度是O(N)
这个时候你就会想:

别急,这才刚开始。

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),答案确实是这样。
但我们这样想的前提是什么?题目并没有说M和N之间的大小关系,如果M的大小远大于N,那么这题的答案就是O(N),如果M和N相等或者是接近,可以写成O(M)也可以写成O(N),因为题目没有说明他们之间的大小,所以我们通常都写成O(M+N)

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

这道题是O(100)吗?在这里要强调一下,这种常数次的时间复杂度我们一般用O(1)来表示,不是只执行了一次,是执行了常数次。

// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n) 
{
 	assert(a);
 	int i = 0;
 	int t = 0;
 	for (i = 0; i < n-1; i++)
 	{
 		int j = 0;
 		for (j = 0; j < n-1-i; j++)
 		{
 			if (a[j] > a[j+1])
 			{
 				t = a[j];
 				a[j] = a[j+1];
 				a[j+1] = t;
 			}
 	    }
}

这个冒泡排序与上面的不同了,每一轮的次数都在变动,当i = 0时,执行了n-1次,当i = 1时,执行了n-2次。
n-1->n-2->n-3->…->2->1
这个式子用等差数列求和就能算出结果为 n ∗ ( n − 1 ) 2 \\frac{n*(n-1)}{2} 2n(n1)
所以时间复杂度为O(n)

// 计算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;
 		else
 		return mid;
    }
 	return -1; 
}

这道题可能让很多人懵了,时间复杂度怎么算?
我们先来分析一下二分查找的过程,二分查找每一次查找完都会边界值相加然后除2,那么就相当于是n/2/2/2/2/2.....=1,那么n = 2*2*2*....2,假设有x个2,那就是n = 2 x 2^{x} 2x,两边同时取对数就是x = log ⁡ 2 n \\log_2n log2n
所以最终时间复杂度就是O( log ⁡ 2 n \\log_2n log2n)。

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

这个执行的总次数我们容易计算出是N。
第一次调用Fac(N)
第二次调用Fac(N-1)

第N次调用Fac(1)
这里要说明一下递归的总次数是递归的次数*每次递归中的操作次数
什么意思呢?
如果这段代码里再加一次执行10次的循环,那总次数就是10*N

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

这道题相对来说就比较复杂了。
2 0 2^{0} 20 Fib(N)
2 1 2^{1} 21 Fib(N-1) Fib(N-2)
2 2 2^{2} 22 Fib(N-2)Fib(N-3)Fib(N-3)Fib(N-4)

2 n − 1 2^{n-1} 2n1 Fib(1)Fib(0)
由等比数列的求和公式可求得次数为 2 n 2^{n} 2n-1-x,x是什么?因为靠后的列数在递归时会比前面几列的先递归结束,所以要减去少算的那几次,但对总次数没有太大的影响。所以时间复杂度为O( 2 n 2^{n} 2n)

🍉3.空间复杂度

🌭3.1空间复杂度的概念

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

🌮3.2常见空间复杂度计算举例

// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n) 
{
 	assert(a);
 	int i = 0;
 	int t = 0;
 	for (i = 0; i < n-1; i++)
 	{
 		int j = 0;
 		for (j = 0; j < n-1-i; j++)
 		{
 			if (a[j] > a[j+1])
 			{
 				t = a[j];
 				a[j] = a[j+1];
 				a[j+1] = t;
 			}
 	    }
}

这个算法的空间复杂度是O(1),为什么呢?
空间复杂度算的是额外的空间的个数,*a和n是函数本身就具有的,额外的只有i、j、t,是常数个,所以空间复杂度为O(1)

// 计算Fibonacci的空间复杂度
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{
 	if(n==0)
 	return NULL;
 
 	long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
 	fibArray[0] = 0;
 	fibArray[1] = 1;
 	for (int i = 2; i <= n ; ++i)
 	{
 		fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
 	}
 	return fibArray; 
}

看到这种开辟内存的很明显就是额外的空间,所以首先你能确实开辟了n+1个额外的空间,再看其它的临时变量有i,所以是n+2个空间,所以空间复杂度是O(N)

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

这题就比较简单了,每次递归都开辟一块空间,递归了N次,所以空间复杂度是O(N)

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

很多人可能会这样想,每次递归一次都开辟一块空间,所以和时间复杂度是一样的,会认为是 O ( 2 N ) O(2^{N}) O(2N)。记住,空间是可以重复利用的,不累计。时间是一去不复返的,累计的。在递归时,
2 0 2^{0} 20 Fib(N)
2 1 2^{1} 21 Fib(N-1) Fib(N-2)
2 2 2^{2} 22 Fib(N-2)Fib(N-3)Fib(N-3)Fib(N-4)

2 n − 1 2^{n-1} 2n1 Fib(1)Fib(0)
会先进行第一列的递归,第一列递归完第二列可以接着用第一列的空间,所以空间复杂度为O(N)

讲到这,数据结构的入门级内容就讲完了。

🍉🍉🍉🍉🍉🍉🍉🍉🍉🍉🍉🍉🍉
这样的文章你还不快点赞👍关注💡收藏💖
🥝🥝🥝🥝🥝🥝🥝🥝🥝🥝🥝🥝🥝

以上是关于时间复杂度带你入门数据结构(建议收藏)❥(^_-)的主要内容,如果未能解决你的问题,请参考以下文章

1小时0基础带你 Javascript入门 建议收藏~

保姆级|建议收藏阿ken带你学Java入门及进阶——基本数据类型与数组,文末有彩蛋✨✨

❤️数据结构入门❤️初章 - 算法时间复杂度 (建议收藏)

❤️数据结构入门❤️初章 - 算法时间复杂度 (建议收藏)

保姆级|建议收藏阿ken带你学Java入门及进阶——基本数据类型与数组,文末有彩蛋鸭✨✨✨

一篇博文带你 jQuery入门,万字肝爆! 建议收藏~