谈谈算法中的时间复杂度和空间复杂度

在这里插入图片描述

一、算法的复杂度

我们写代码要实现某种功能的时候,可以使用很多方法来解决,但是哪一种方法才是最好的呢?又怎么来衡量?所以就要引出算法复杂度的概念了。
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

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

所以衡量算法的好坏是靠时间复杂度和空间复杂度来比较的,下面我们接着分析时间复杂度和空间复杂度。

二、时间复杂度

1.时间复杂度的概念

那么什么是时间复杂度呢?又是怎么定义的?
时间复杂度的定义:

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

所以找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
注意: 上面定义基本操作的执行次数并不是指语句或者指令执行的次数,而是算法的逻辑走了多少次。比如循环了多少次,递归了多少次,这个才是基本操作的执行次数。

举例分析:
下面的函数执行的基本操作次数为多少?

void Fun1(int n)
{
	int i = 0, j = 0;
	int count = 0;
	for (i = 0; i < n; i++)
	{
		for (j = 0; j < n; j++)
		{
			count++;
		}
	}

	int k = 0;
	for (k = 0; k < 2 * n; k++)
	{
		count++;
	}

	int M = 10;
	while (M--)
	{
		count++;
	}
}

再次强调题目要分析 基本操作次数 ,所以我们就得看算法的逻辑走了多少次,比如循环了多少次,递归了多少次。
所以可以得出Fun1函数的基本操作数是:F(N) = N2 + 2 * N + 10
随着N的增长有以下规律:
N = 10       F(N) = 130
N = 100     F(N) = 10210
N = 1000      F(N) = 1002010
……………………
随着N的增大函数表达式中最高阶对结果是影响最大的,而后面的表达式就可以忽略不计了,所以实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
因此我们就可以用来表示时间复杂度了,下面我们接着分析大O的渐进表示法。

2.大O的渐进表示法

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

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

所以使用大O的渐进表示法以后,Func1的时间复杂度为:O(N2)
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
还有算法的时间复杂度存在最好、平均和最坏情况:
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
但是在实际中一般情况关注的是算法的最坏运行情况。

三、空间复杂度

分析完时间复杂度接着分析空间复杂度。
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。

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

四、常见复杂度的计算例题

1.常见用例的时间复杂度

求时间复杂度:

void Fun2(int N)
{
	int count = 0;
	int i = 0;
	for (i = 0; i < 2 * N; i++)
	{
		count++;
	}

	int M = 10;
	while (M--)
	{
		count++;
	}
	printf("%d\n", count);
}

从上面的函数中我们可以得出Fun2函数的基本操作数是:F(N) = 2 * N + 10
我们时间复杂度要表达式中的最高阶是2 * N,所以根据大O的渐进表示法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。
得到的结果就是大O阶可以得出算法的时间复杂度为:O(N)

计算下面函数Fun3的时间复杂度:

void Fun3(int N, int M)
{
	int count = 0;
	int i = 0;
	for (i = 0; i < M; i++)
	{
		count++;
	}
	for (i = 0; i < N; i++)
	{
		count++;
	}
	printf("%d\n", count);
}

我们从函数中可以得出Fun3函数的基本操作数是:F(N) = M + N
当M远大于N的时候,时间复杂度为:O(M)
当N远大于M的时候,时间复杂度为:O(N)
当N和M差不多的时候,时间复杂度为:O(N)或者O(M)都行

求下面函数Fun4的时间复杂度:

void Fun4(int N)
{
	int count = 0;
	int i = 0;
	for (i = 0; i < 100; i++)
	{
		count++;
	}
	printf("%d\n", count);
}

注意: Fun4函数的基本操作数是:F(N) = 100
而100是常数根据大O的渐进表示法得出时间复杂度为:O(1)
所以当我们以后看见时间复杂度为 O(1)的时候并不是只执行一次操作数,而是常数次。

2.冒泡排序的复杂度

求冒泡排序的时间复杂度?

//冒泡排序
void BobbleSort(int* p, int sz)
{
	int i = 0, j = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int flag = 1;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (p[j] > p[j + 1])
			{
				int tmp = p[j];
				p[j] = p[j + 1];
				p[j + 1] = tmp;
				flag = 0;
			}
		}
		if (flag == 1)
		{
			return;
		}
	}
}

当算法随着输入不同,时间复杂度也会随之变化,所以时间复杂度做悲观预期,看最坏的打算。 举一个复杂的数组 10 9 8 7 6 5 4 3 2 1     进行冒泡排序,变化如下:

把10移到后面,发生了9次交换,数组变为 :9 8 7 6 5 4 3 2 1 10
把9移到后面,发生了8次交换,数组变为: 8 7 6 5 4 3 2 1 9 10
把8移到后面,发生了7次交换,数组变为: 7 6 5 4 3 2 1 7 9 10
………………
把3移到后面,发生了2次交换,数组变为 : 2 1 3 4 5 6 7 8 9 10
把2移到后面,发生了1次交换,数组变为 : 1 2 3 4 5 6 7 8 9 10
规律如下: 冒泡排序在最复杂的时候交换的次数为 等差数列,所以我们想要知道要交换多少次就全部加起来即可,根据等差数列的求和公式 :((首项 + 尾项)* 项数) / 2
得出 n * (n - 1) / 2 ,再根据大O的渐进表示法得出时间复杂度为:O(n2)

3.二分查找法的复杂度

求二分查找法的时间复杂度?

int BinarySearch(int* p, int sz,int k)
{
	int left = 0;
	int right = sz - 1;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (k < p[mid])
		{
			right = mid - 1;
		}
		else if (k > p[mid])
		{
			left = mid + 1;
		}
		else
		{
		   //找到目标元素就返回此下标
			return mid;
		}
	}
	//找不到就返回-1
	return -1;
}

二分查找法的思想是折一半查找,当数组中间元素不是目标元素的时候,下次查找的范围就缩小二分之一。在最复杂的条件下,数组要缩小2的几倍才能找到目标元素,也就是说,2x = n ,所以x =logn 即时间复杂度为:O( logn )

4.阶乘递归的复杂度

求阶乘的时间复杂度和空间复杂度

long long Fun(int n)
{
	if (n == 0)
	{
		return 1;
	}
	return Fun(n - 1)* n;
}

在这里插入图片描述

求阶乘的递归,操作递归了N次,时间复杂度为O(N)
递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间,空间复杂度为O(N)。

5.斐波那契递归的复杂度

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

画图分析:
在这里插入图片描述
时间复杂度: 基本操作递归了2N次方,时间复杂度为O(2N) 。
空间复杂度: 这个不同于时间复杂度,时间是一去不复返,累计的;而空间是可以重复利用,不累计的。当递归到F(1)结束后,函数的栈帧就销毁了,每一个函数结束后函数的栈帧都会销毁,所以空间复杂度是递归的深度即 O(N)

五、常见复杂度对比图

请添加图片描述

请添加图片描述

六、复杂度的相关练习

1.消失的数字

题目:
数组nums包含从0到n的所有整数,但其中缺了一个,请编写代码找出那个缺失的整数。
比如:数组[3,0,1,4] 中缺的是2,目标要找到2
数组[9,6,4,2,3,5,7,0,1]中缺的是8,目标要找到8
我们有非常多的思路来解决这个问题,但是每种方法的时间复杂度和空间复杂度可能不同,下面来分析不同的方法:

方法1: 暴力解决

根据题目信息,数组缺了一个整数,那么我就从0开始在数组中找有没有相同的数字,找到就继续找下一个,直到找不到相同的整数为止。
这样方法时间复杂度为 O(N2),空间复杂度为O(1)
代码如下:

void Find(int* p, int n)
{
	int i = 0, j = 0;
	//产生0到sz的数字
	for (i = 0; i < n; i++)
	{
		//依次遍历数组
		for (j = 0; j < n; j++)
		{
			if (i == p[j] )
			{
				break;
			}
		}
		//数组元素没有相等的就是缺失的数字
		if (j == n)
		{
			printf("缺失的数字是%d ", i);
		}
	}
}

方法2:qsort排序

把数组中的元素都排成有序,然后再遍历一遍数组,如果数组元素不等于此元素下标,说明就是此下标就是缺失的整数。 所以我们利用快速排序的库函数 qsort来实现,qsort函数在我这篇文章中有详细介绍 :qsort函数的解析与模拟实现
qsort快排的时间复杂度为O(N * logN) ,空间复杂度为O(1)
实现代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

int compar(const void* a, const void* b)
{
	if ((*(int*)a - *(int*)b) > 0)
	{
		return 1;
	}
	else if ((*(int*)a - *(int*)b) == 0)
	{
		return 0;
	}
	else
	{
		return -1;
	}
	//return ( *(int*)a - *(int*)b );
	//这样有可能会产生溢出
}

int main()
{
	int arr[] = { 9, 6, 4, 2, 3, 5, 7, 0, 1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//把数组变成有序
	qsort(arr, sz, sizeof(arr[0]), compar);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		//如果元素不等于自己对于的下标,就是缺失的数字
		if (i != arr[i])
		{
			printf("缺失的数字是:%d\n ", i);
			break;
		}
	}
	return 0;
}

执行结果如下:
在这里插入图片描述

方法3:以空间换时间

数组中包含从0到n的所有整数,要找缺失的一个数字,我们可以开辟一块整形数组空间,这块数组的下标是从0到n,接着遍历数组,把数组中元素放到等于开辟的空间下标位置,比如元素5 就放到开辟空间的下标为5的位置,最后再遍历新开辟的数组,验证元素是否等于自己对于的下标即可。

例如上面的数组[9,6,4,2,3,5,7,0,1]放到新开辟的空间形成的结果为:
旧的数组排放位置:0 1 2 3 4 5 6 7 9
新的数组对应下标:0 1 2 3 4 5 6 7 8 9
发现下标8的元素不是8,则8就是缺失的数字
时间复杂度为O(N),空间复杂度为O(N)
实现代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

int Find(int* arr, int sz)
{
	//开辟一块空间
	int* p = (int*)malloc(sz * sizeof(int));
	if (p == NULL)
	{
		perror("erron");
		exit(-1);
	}
	int i = 0;
	//把数组中元素放到等于开辟的空间下标位置
	for (i = 0; i < sz; i++)
	{
		p[arr[i]] = arr[i];
	}

	for (i = 0; i <= sz; i++)
	{
		//验证元素是否等于自己对于的下标
		if (p[i] != i)
		{
			break;
		}
	}
	free(p);
	p = NULL;
	return i;
}

int main()
{
	int arr1[] = { 9, 6, 4, 2, 3, 5, 7, 0, 1 }; //8
	int arr2[] = { 3,0,1,5,4,6 };  //2
	int arr3[] = { 5,3,4,0,2 };   //1
	int sz1 = sizeof(arr1) / sizeof(arr1[0]);
	int sz2 = sizeof(arr2) / sizeof(arr2[0]);
	int sz3 = sizeof(arr3) / sizeof(arr3[0]);

	printf("数组arr1缺失的数字为:%d\n", Find(arr1, sz1));
	printf("数组arr2缺失的数字为:%d\n", Find(arr2, sz2));
	printf("数组arr3缺失的数字为:%d\n", Find(arr3, sz3));
	return 0;
}

程序执行结果:
在这里插入图片描述

方法4:整体异或

有两个最优的方法分别是 第一:0到n的数字全部加起来,然后减去数组中的每一个元素,剩下的结果就是那个缺失的数字。
第二:先求出异或0到n全部数字的结果,然后再异或数组中的每一个元素,剩下的结果就是缺失的数字。
代码如下:

//相加再相减法
int Find1(int* p, int n)
{
	int tmp = 0;
	int i = 0;
	//0到n的数字全部加起来
	for (i = 0; i <= n; i++)
	{
		tmp = tmp + i;
	}
	//再依次减去数组的每一个元素
	for (i = 0; i < n; i++)
	{
		tmp = tmp - p[i];
	}
	return tmp;
}

//异或法
int Find2(int* p, int n)
{
	int tmp = 0;
	int i = 0;
	//0到n的数字全部异或起来
	for (i = 0; i <= n; i++)
	{
		tmp = tmp ^ i;
	}
	//利用相同数字异或为0,最后结果就是缺失的数字
	for (i = 0; i < n; i++)
	{
		tmp = tmp ^ p[i];
	}
	return tmp;
}

2.旋转数组

题目:
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数
比如:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]

方法1:暴力求解

要求不是要移动k个位置嘛,那就一个一个地移动即可,移动k个即可
代码如下:

void Rotate(int* p, int sz,int k)
{
	int i = 0;
	//移动k次
	while (k--)
	{
		//先保存最后一个元素
		int tmp = p[sz - 1];
		//从后往前面挪数据
		for (i = sz - 2; i >= 0; i--)
		{
			p[i + 1] = p[i];
		}
		//最后把最后一个元素放在最前面
		p[0] = tmp;
	}
}

方法2:数组法

就是以空间换时间,先把要右移的内容拷贝到新的数组中,然后再把旧数组前面的内容拷贝到新数组的后面,最后把右移好的内容重新拷贝到旧数组中。
代码如下:

void  Rotate1(int* arr, int sz, int k)
{
	//开辟一块空间
	int* p = (int*)malloc(sizeof(int) * sz);
	if (p == NULL)
	{
		perror("erron");
		return;
	}
	int i = 0;
	int j = 0;
	//先把要右移的内容拷贝到新数组前面
	for (i = sz - k; i < sz; i++)
	{
		p[j] = arr[i];
		j++;
	}
	//再把旧数组前面的内容拷贝到新数组
	for (i = 0; i < sz - k; i++)
	{
		p[j] = arr[i];
		j++;
	}
	//最后把新数组右移好的内容拷贝到旧数组中
	for (i = 0; i < sz ; i++)
	{
		arr[i] = p[i];
	}
	free(p);
	p = NULL;
}

方法3:三步反转法

第一步:先把要右移的内容反转过来
第二步:把剩下的内容也反转过来
第三步:把数组中的内容全部反转过来

比如:
输入: nums = [1,2,3,4,5,6,7], k = 3
1、先把5 6 7反转过来变为 7 6 5
2、再把1 2 3 4 反转过来变为 4 3 2 1 ,前面两步数组就变为 4 3 2 1 7 6 5
3、最后再反转数组变为 5 6 7 1 2 3 4
代码实现如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

void Print(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", p[i]);
	}
	printf("\n");
}

void Reverse(int* p, int left, int right)
{
	while (left < right)
	{
		int tmp = p[left];
		p[left] = p[right];
		p[right] = tmp;
		left++;
		right--;
	}
}


int main()
{
	int arr[] = { 9, 6, 4, 2, 3, 5, 7, 0, 1 }; //8
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("数组右旋5个数字前的内容为:\n");
	Print(arr, sz);
	//右旋5个数字
	int k = 5;
	Reverse(arr, sz - k, sz - 1); //反转要右移的内容
	Reverse(arr, 0, sz-k-1);  //反转剩下的内容
	Reverse(arr, 0, sz - 1);  //反转内容全部

	printf("数组右旋5个数字后的内容为:\n");
	Print(arr, sz);
	return 0;
}

程序执行结果:
在这里插入图片描述
在这里插入图片描述

  • 17
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值