关于算法的时间复杂度和空间复杂度的详细介绍 【数据结构】

算法的复杂度

衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度

  • 时间复杂度主要衡量一个算法的运行快慢
  • 空间复杂度主要衡量一个算法运行所需要的额外空间

时间复杂度

算法中的基本操作的执行次数,为算法的时间复杂度

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 执行的基本操作次数 :
在这里插入图片描述
在这里插入图片描述

实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法

大O的渐进表示法

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

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

假如在一个长度为N数组中搜索一个数据k
最好情况:1次找到k
最坏情况:N次找到k
平均情况:N/2次找到k
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)

常见时间复杂度计算举例

// 计算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阶方法知道,时间复杂度为 O(N)


// 计算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);
}

这里没有说明M和N的关系,在一般情况下时间复杂度计算时未知数都是用的N,但也可以是M、K等其他未知数

基本操作执行了M+N次,有两个未知数M和N,时间复杂度为 O(N+M)


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

基本操作执行了10次,通过推导大O阶方法,时间复杂度为 O(1)


// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );

基本操作执行最好1次,最坏N次,当一个算法随着输入的不同,时间复杂度不同,时间复杂度做悲观预期,看最坏的情况,时间复杂度为 O(N)


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

算时间复杂度不能只是去看有几层循环,而是要看背后的思想
冒泡排序的核心思想是比较相邻两个元素,把大的元素交换到后面

假设我们只冒泡排序4个数,也就是我们需要排序3趟,第一趟俩俩比较3次,第二趟2次,第三趟1次,合起来一共3+2+1=6次。
依次类推,如果排序N个数,也就是我们需要冒泡排序N-1趟,也就是说我们要算出(N-1)+(N-2)+(N-3)+(N-4)+…+3+2+1 即N*(N-1)/2。
根据时间复杂度的算法,冒泡排序法的时间复杂度为O(N^2)


// 计算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;
}

假设序列里有n 个元素
第一次二分后,需要继续在n/2 个元素中进行查找
第二次二分后,需要继续在n/2^2个元素中进行查找
最坏的情况下,只剩下一个元素,也就是继续在n/2^t个元素中进行查找

在这里插入图片描述

基本操作执行最好1次,最坏O(logN)次,时间复杂度为 O(logN) ps:logN在算法分析中表示是底数为2,对数为N。有些地方会写成lgN。


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

递归算法:递归次数*每次递归调用的次数

实例7通过计算分析发现基本操作递归了N次,时间复杂度为O(N)


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

实例8通过计算分析发现基本操作递归了2N次,时间复杂度为O(2N)。(建议画图递归栈帧的二叉树
讲解)

空间复杂度

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

空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法

计算空间复杂度

空间复杂度算的是变量的个数

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

// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{
      assert(a);
	 for (size_t end = n; end > 0; --end)
	 {
	        int exchange = 0;
	      for (size_t i = 1; i < end; ++i)
	     {
	         if (a[i-1] > a[i])
	         {
	           Swap(&a[i-1], &a[i]);
	           exchange = 1;
	         }
	     }
	   if (exchange == 0)
	      break;
	 }
}

冒泡排序的空间复杂度 就是在交换元素时那个临时变量所占的内存空间

冒泡排序的辅助变量空间仅仅是一个临时变量,并且不会随着排序规模的扩大而进行改变,所以空间复杂度为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次递归,每次递归所需的栈空间也是一样的。

所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是O(1)

求递归的空间复杂度,我们已经知道了每次递归的空间复杂度,我们还需要知道递归深度

每次递归所需的空间都被压到调用栈里 ,看递归算法的空间消耗,就是要看调用栈所占用的大小 , 当一次递归结束,这个栈就会把本次递归的数据弹出去。即这个栈最大的长度就是 递归的深度
如图所示:

在这里插入图片描述

在这里插入图片描述

通过图可知 最大深度为5
可以推出 递归第n个斐波那契数的话,递归调用栈的深度就是n
那么每次递归的空间复杂度是O(1), 调用栈深度为n,

通过计算得出这个递归算法的空间复杂度就是 O(n)


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

实例3递归调用了N+1次,开辟了N+1个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N)

在这里插入图片描述


先看下面的例子


void F2()
{
	int b = 0;
	printf("%p\n", &b);
}
void F1()
{
	int a = 0;
	printf("%p\n", &a);
}
int main()
{
	F1();
	F2();
	return 0;
}

在这里插入图片描述

空间销毁是把当前空间的使用权归还给操作系统 ,并不是空间就消失了

在这里插入图片描述


//计算空间复杂度
long long Fib(size_t N)
{
	if(N < 3)
		return 1;
	return Fib(N-1) + Fib(N-2);
}

在这里插入图片描述


时间复杂度是等比数列 ,O(2^N)

在这里插入图片描述

斐波那契的空间复杂度为O(N)
Fib(N)调用Fib(N-1) , 调用Fib(N-1)之后并不是马上又调用Fib(N-2)
Fib(N-1)调用Fib(N-2) , 调用Fib(N-2)之后并不是马上又调用Fib(N-3)

    往回返的时候调用之前销毁的空间(空间复用)
时间不可重复利用 ,但是空间用了以后归还给操作系统,可以重复利用

复杂度的oj练习

https://leetcode.cn/problems/missing-number-lcci/

思路一:
计算从0到n的和 - 数组所有元素的和
我们假设n = 3 即
0 + 1 +2 + 3 - ( 0 + 1 + 3) =2

int missingNumber(int* nums, int numsSize)
{
        //计算0 到 n 的和  ——等差数列
       int sum =( (0+ numsSize) * (numsSize+1) ) /2;
       int sumNums = 0; //存放数组
    
     //求数组的和
        for( int i = 0  ; i <numsSize ; i++)
        {
            sumNums= sumNums+nums[i];
        }
        return sum - sumNums ;     // 相减

}

思路二:

关于异或

  • a ^ a = 0
  • 0 ^ a = a
  • 异或满足交换律
  • 异或规则 : 相同为0 ,相异为1

先异或 0 到 n 的所有数字,再异或nums数字的所有数字

先异或:0到n的所有数字:X^ 0^ 1 ^ 2 ^3
再异或数组: X^ 0 ^1 ^ 2^ 3 ^ 3^ 0 ^ 1 = 0^ 0^ 1^ 1^ 2 ^3 ^3 = 2;
即2是消失的数字

int missingNumber(int* nums, int numsSize){
        //先异或0-numsSize的所有数字
        int x = 0;
        for(int i = 0 ;i<=numsSize;i++){
                x ^= i;
        }
        //再异或nums数组所有的值
        for(int i = 0;i <numsSize;i++){
            x ^= nums[i];
        }
        //最后的结果就是消失的数字
        return x;
}

https://leetcode.cn/problems/rotate-array/

思路一:
前 n-K个逆置
后k 个逆置
整体逆置
时间复杂度是O(N) ,空间复杂度是O(1)

void reverse(int *nums, int begin, int end)
{

	while (begin < end)
	{
		int tmp = nums[begin];
		nums[begin] = nums[end];
		nums[end] = tmp;
		begin++;
		end--;
	}
}

void rotate(int* nums, int numsSize, int k)
{
    if( k >numsSize)
    {
        k %=numsSize ; 
    }
    reverse ( nums , 0 , numsSize-k-1 ) ;
    reverse ( nums ,numsSize-k ,numsSize-1 ) ;
    reverse ( nums,0 ,numsSize-1 ) ;

}

在这里插入图片描述

如果你觉得这篇文章对你有帮助,不妨动动手指给点赞收藏加转发,给鄃鳕一个大大的关注
你们的每一次支持都将转化为我前进的动力!!!

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

鄃鳕

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

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

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

打赏作者

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

抵扣说明:

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

余额充值