数据结构之时间复杂度与空间复杂度


前言

对于一份代码,我们是以如何的标准来评判它的意义的呢?
当前我们主要以一份代码的时间复杂度和空间复杂度的大小来评判一份代码的意义;如果对于一份代码来说它的时间复杂度和空间复杂度都比较小,那么我们可以认为这是一份有意义的代码,反之;但是当经技术的发展已尽使我们不在那么重视空间复杂度了;说了这么多,那到底什么是时间复杂度,什么又是空间复杂度呢?;

一、时间复杂度?

时间复杂度并不是我们想的那样去统计时间,因为这样去统计的话毫无意义,同一份代码,在性能更好的机器下,固然跑的时间会更少,这样就失去了对于代码意义的评估,而转为对机器性能的要求;那末到底什么才是真正的时间复杂度呢?

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

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

从上面的介绍我们可以看出算法的时间复杂度是一个函数,也就是类似于
f(n)=…//函数表达式;
而且我们统计的是执行次数,不是时间!!!!
有了上面的理解,我们来看看一段代码,并计算一下它的时间复杂度;

void fun(int N)
{
	int count = 0;
	for (int i = 0; i < N; i++)
		count++}

这个fun函数的时间复杂度是多少呢?
是不是N啊写出函数的形式就是 :f(N)=N;
通过代码我们可以看见这个for循环也就执行了N次,也就是说,你传进来的N是多少,for循环就得执行多少次;当然我们是看的执行次数,不是看的每条命令执行了多少次;
既然理解了上一道我们再来看看下一道:

void fun(int N)
{
	int count = 0;
	for (int i = 0; i < N; i++)
		count++;
	for (int k = 0; k < N; k++)
		count++;
	int M = 10;
	while (M--)
		count++;
}

我们再来看看这段代码的时间复杂度:
我们可以看出第一个for循环执行了N次,第二个for循环执行了N次,最后一个while循环执行了M次(M是常数),所以总的来说fun的时间复杂度就是
f(N)=2N+M;对不对,这是精确的次数,但是我们有必要搞这么精确吗?
就好比某一天你对你的同行说,我的代码跑了12233次?显然没必要说的这么精确我们只需要说我的代码跑了1w多次就行了,说个大概就行了;
还有就是:就已此题为例:
f(N)=2
N+M;
N=1 --------------f(1)=22+10=14;
N=10 --------------f(10)=2
10+10=30;
N=100 --------------f(100)=2100+10=210;
N=…
N=100000000---- -----------f(100000000)=2
100000000+10=20000000010;
我们可以发现随着N的增大,f(N)与N趋于一个数量级,这是我们就没必要区分什么2*N+M和N了直接让复杂度大概用N来表示,O( )表示渐近表示,不表示精确的次数,表示大概次数;比如这个fun、的时间复杂度就是
O(N)当然()里面的英文我们可以随便换,但是我们习惯用N表示;

至此我们来梳理一下时间复杂度的表示方法:
1、运用常熟1来取代时间中的所有加法常数;
2、再修改后的运行次数函数中,只保留最高阶项;
3、如果最高阶项存在,且不为1,则去除与这个项目相乘的常数,得到的结果就是大O阶。

空间复杂度的表示方法也是如此;

接下来我们再看一段代码:

void fun1(int N)
{
	int count = 0;
	for (int i = 0; i < N; i++)
	{

		for (int j = 0; j < N; j++)
			count++;
	}
}

分析:我们可以发现:外层循环每变化一次,里层循环是不是会执行N次,而外层循环总共会执行N次,也就是说我有N个N相加,也就是N^2,再看看时间复杂度的表示方法,没什么需要去除的,所以该函数的时间复杂度也就是
O(N^2);
我们再来看看冒泡排序的时间复杂度;

void fun2(int *a,int N)
{

	for (int i = 0; i < N; i++)
	{
		for (int j = 0; j < N - 1 - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				int tmp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = tmp;
			}
		}
	}

}

分析:我们可以看到对于这种代码我们不能简单地认为是两个for循环相乘,我们得去理解一下冒泡排序的思想;冒泡排序的主要思想就是,每一趟冒出一个最小或最大的到最后面去;然后需要排序的个数-1;在依次继续上述操作;于是我们可以发现,外层循环没执行一次,内成循环的执行次数就会间少一次;外层循环第一次,内层循环N-1次,外层循环第2次,内层循环N-2次,外层循环第三次,内层循环N-3次;依次往后,直到外层循环到第N次为止;也就是说,这个函数基本操作执行了N-1+N-2+N-3+N-4…+1+0;
是一个等差数列:算出来F(N)=1/2N^2-1/2N;然后根据表示方法算出冒泡排序的时间复杂度为O(N ^2);
我们再来看一段代码:

void fun3(int N,int M)
{
	int count = 0;
	for (int i = 0; i < N; i++)
	{
		count++;
	}
	for (int j = 0; j < M; j++)
		count++;
}

分析:通过上面的学习,我们可以很快的算出该函数的时间复杂度就是
O(M+N);如果题目告诉了我们M远大于N那么时间复杂度就是O(M);
如果是N远大于M,那么时间复杂度就是O(N);如果M和N差不多,那么时间复杂度就是O(M)或者O(N);

char* my_strchr(const char* str1, const char s)
{
	if (str1 == NULL)  
	{
		return NULL;
	}
	while (*str1 != s && *str1)
	{
		str1++;
	}
	if (*str1 != '\0')
		return (char*)str1;
	else
		return NULL;
}

这是一个查找在字符串中查找字符的函数;比如ABDC中查找D找到了就返回D的指针,没找到就返回NULL;
分析:我们发现该函数的时间复杂度似乎并不那么好算,这是我们就要分情况来讨论了,分最好中间和做坏;
最好的话就是O(1),一次就找到了;
最坏的话就是O(N),在最后才找到或者没找到;
中间的话就是,一般情况是最好和最坏加一下,然后/2,
对于这种随着输入的不同时间复杂度不同,时间复杂度做悲观预期,算最坏的执行次数;
所以改函数的时间复杂度就是O(N);

我们再来算算二分查找地时间复杂度


int BinarySearch(int a[], int size, int p) {
	int low = 0;                //查找区间的左端点 
	int high = size - 1;        //查找区间的右端点 
	while(low <= high) {        //如果查找区间不为空就继续查找 
		mid = (low + high) / 2; //取查找区间正中元素的下标 
		if(p == a[mid]) 
			return mid;
		else if(p > a[mid]) 
			low = mid + 1;     //设置新的查找区间的左端点 
		else 
			high = mid - 1;    //设置新的查找区间的右端点 
	}
	return -1;
}

如果直接看的话,我们也是直接看不出来结果的,
我们也是需要分情况,最好o(1);
但是我们不讨论最好;我们讨论最坏
我们也需要结合一下二分查找的主要思想;顾名思义二分查找,也叫折半查找,也就是每查找一次,数目就会减半,最终减少到区间只剩一个才找到或者也没找到;
故N/2/2/2/2/2…2=1;
既N/2^x=1;
x=log2 N;
也就是说二分查找地时间复杂度是O(log2 N);
我们再来看一个数据:
N个数 ------------------- 最多查找次数
N=1000 --------------------10次
N=100w ---------------------20次
N=10亿 - -------------------30次;
由此可见二分查找是一个效率很高的算法,是一个NB的算法;
但是前提是数目得有序;

我们再来看看一个递归的代码:

void fun5(int n)
{
	if (n < 10)
	{
		printf("%d ",n);
		return;
	}
	fun5(n/10);
	printf("%d ",n%10);
}

在这里插入图片描述
递归的时间复杂度=递归的次数 乘以 每次递归调用的次数(每次递归调用里面基本操作执行的次数,有点类似于双层循环,递归次数相当于外层循环,每次递归调用次数相当于内层循环);
我们接下来加大点难度:

我们来算一下斐波那契数列:

int fun6(int n)
{
	if (n <= 2)
		return 1;;
	return fun6(n - 1) + fun6(n - 2);
}

在这里插入图片描述
这是这个函数的递归过程,右边总是比左边快,也就是说右边总是比左边先递归完,右边递归深度比左边总是少,所以图中红色部分代表缺少一部分;但是红色部分相对于蓝色部分加不加上都无所谓啦;
加上的话就是一个等比数列:2^ 0 + 2^ 1+2^ 2 + 2^ 3…+2^ n-1=2^ n;
所以说该函数的时间复杂度就是:O(2^ N);

二、 空间复杂度

空间复杂度也不是我们简单地认为统计字节数,而是统计函数额外开辟的空间;
我们来看一下比较正统的解释:

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

我们来看看冒泡排序的空间复杂度:

void fun2(int *a,int N)
{
	for (int i = 0; i < N; i++)
	{
		for (int j = 0; j < N - 1 - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				int tmp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = tmp;
			}
		}
	}
}

我们发现我们就额外开辟了i,j,tmp三个额外空间;
也就是常数个,也就是O(1);

我们再来看看斐波拉契数的空间复杂度:

int fun6(int n)
{
	if (n <= 2)
		return 1;;
	return fun6(n - 1) + fun6(n - 2);
}

在这里插入图片描述
还是这幅图,递归的话,总是左边先递归完然后再递归右边是吧,也就是说左边递归完过后,函数栈帧是不是会销毁嘛,然后在递归右边,右边又在原来左边的栈帧的基础上建立新的栈帧;所以我们只需看递归最大的深度就行了,也就是最多递归N层,空间复杂度也就是O(N);

换句话说:空间是可以累计的,时间是一去不复返的;

  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 30
    评论
评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南猿北者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值