数据结构学习之路--算法的时间复杂度与空间复杂度(精讲)

本文详细介绍了算法效率的衡量标准,重点讲解了时间复杂度的定义、计算步骤,通过代码示例分析了不同情况下的时间复杂度,并对比了空间复杂度的概念和常见复杂度等级。
摘要由CSDN通过智能技术生成

     嗨嗨大家!本期带来的内容是:算法的时间复杂度与空间复杂度。


目录

前言

一、算法效率

算法效率的衡量标准

二、时间复杂度

1 时间复杂度的定义

2 求解时间复杂度的步骤

2.1 找出算法中的基本语句: 

2.2计算基本语句执行次数的数量级:

2.3大O阶的渐进表示法:

3 代码示例与解析

3.1 计算1+2+3+4+...+100的和

(1)常规算法

(2)高斯算法

3.2 求两个n 阶方阵C=A*B的乘积 

3.3 分析下面代码的时间复杂度

(1)代码1

(2)代码2 

4 情况的判断 

 三、空间复杂度


前言

   时间复杂度是衡量一个算法的好坏,也是算法题的重中之重。但是很多初学者认为时间复杂度不好理解,对于其复杂程序的估算无从下手。那么本篇文章将从简单到复杂来介绍时间复杂度的计算方法,希望给迷茫的你们指明一条前进的方向。

一、算法效率

算法效率的衡量标准

   当给定一个算法时,我们要做两项分析。第一项是以数学的角度来证明算法的正确性,这一步主要用到形式化证明的方法及相关的推理模式,例如数学归纳法。然而在证明算法正确的基础上,便需要第二项的内容了。第二项就要分析算法的时间复杂度与空间复杂度,算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

二、时间复杂度

1 时间复杂度的定义

   在计算机科学中,算法的时间复杂度是一个函数。一个算法执行所耗费的时间,从理论上说,是不能被算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是若每个算法都需要上机测试,会很麻烦,所以才有了时间复杂度的分析方式。

   一个算法中的语句执行次数称为语句(时间)频度,记作T(n) ,这里的n是问题的规模。当n不断变化时,时间频度T(n) 也会不断变化。那我们就会想:它变化时呈现的规律是怎样的?为此,来引入时间复杂度的概念。

   时间(计算)复杂度又称时间复杂性,它是一个算法运行时间的相对度量。一个算法的运行时间长短,大致等于执行简单操作(赋值、比较、计算、转向、返回、输入和输出)所需要的时间与算法中进行简单操作次数的乘积

2 求解时间复杂度的步骤

2.1 找出算法中的基本语句: 

   一般来说,算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。

2.2计算基本语句执行次数的数量级:

   这里仅计算基本语句执行次数的数量级,意味着只要保留基本语句执行次数的函数中的最高次幂正确即可,从而可以忽略所有低次幂和最高次幂的系数,进而能够简化算法分析。通常情况下,算法的基本操作重复执行的次数是问题规模n 的某个函数,用T(n) 表示

2.3大O阶的渐进表示法:

   大O:用来描述函数渐进行为的数学符号。

   如果有某个辅助函数f(n) ,使得在n趋近于无穷大时,T(n) /f(n) 的极限值不等于0 的常数,则称f(n) 是T(n) 的同数量级函数。记作T(n)=O(f(n))。

其实,简单来说,就是保留求出次数的最高次幂,并把系数去掉。如:T(n)=2n^2+n+1=O(n^2)。

为方便大家理解,用代码举例:

#include<stdio.h> 	  
int main()  
{  
   int i, j, x = 0, sum = 0, n = 100;  //执行1次//  
    for( i = 1; i <= n; i++)           //执行n+1次//  
	 {  
	   sum = sum + i;                  // 执行n次 //     
           for( j = 1; j <= n; j++)    // 执行n*(n+1)次//  
	       {  
	          x++;                     //执行n*n次//  
            	  sum = sum + x;       //执行n*n次//  
              }  
	 }  
	    printf("%d", sum);             //执行1次//  
}  

注:因为for语句会加到n+1,会执行n+1次,而它的函数体在 i==n+1 时不满足判断条件,所以只执行n次

结论:要看清题目中问的是for语句的执行次数,还是它的循环体执行的次数。 

下面我们根据以上的代码,来详细推导“大O阶” 的步骤。

  • 第一步:用常数 1 取代运行时间中的所有加法常数

则上面的算式变为:执行总次数 =3n^2 + 3n + 1,(直接相加的话,应该是T(n) = 1 + n+1 + n +n*(n+1) + n*n + n*n + 1 = 3n^2 + 3n + 3。现在用常数 1 取代运行时间中的所有加法常数,就是把T(n) =3n^2 + 3n + 3中的最后一个3改为1. 就得到了 T(n) = 3n^2 + 3n + 1)

  • 第二步:在修改后的运行次数函数中,只保留最高阶项

这里的最高阶是 n 的二次方,所以算式变为:执行总次数 = 3n^2

  • 第三步:如果最高阶项存在且不是 1 ,则去除与这个项相乘的常数

这里 n 的二次方不是 1 所以要去除这个项的相乘常数,算式变为:执行总次数 = n^2,因此最后我们得到上面那段代码的算法时间复杂度表示为: O( n^2 )。

下面把常见的算法时间复杂度以及它们在效率上的高低顺序记录在这里,使大家对算法的效率有个直观的认识。 

O(1) 常数阶< O(logn) 对数阶< O(n) 线性阶 < O(nlogn) < O(n^2) 平方阶 < O(n^3) < { O(2^n) < O(n!)< O(n^n) }

注:最后用大括号的三项即便是在n 的规模很小的情况下依然要耗费大量的时间,算法的时间复杂度大到无法想象,基本上是不可用的状态。

看到这里,时间复杂度的相关原理就介绍完了。我们通过几个代码示例来具体分析时间复杂度的计算过程:

3 代码示例与解析

3.1 计算1+2+3+4+...+100的和

(1)常规算法
#include<stdio.h>
int main()  
{  
    int i, sum = 0, n = 100;    //执行1次//  
    for( i = 1; i <= n; i++)    //执行n+1次//  
    {  
        sum = sum + i;          //执行n次//  
        //printf("%d\n", sum);  
    }  
    printf("%d", sum);          //执行1次//  
} 

 从附加的注释可以看到各个代码分别执行了多少次。那么这写代码语句执行次数的总和就可以理解为是该算法计算出结果所需要的时间。

该算法所用的时间(算法语句执行的总次数)为: 1 + ( n + 1 ) + n + 1 = 2n + 3。

而当 n 不断增大,比如我们这次所要计算的不是 1 + 2 + 3 + 4 +... + 100 = ? 而是 1 + 2 + 3 + 4 + ... + n = ?其中 n 是一个十分大的数字,那么上述算法的执行总次数(所需时间)会随着 n 的增大而增加,但是在 for 循环以外的语句并不受n 的规模影响(永远都只执行一次)。所以我们可以将上述算法的执行总次数简单的记做: 2n ;又因为系数2可以忽略不记,则最终的结果即为n。

这样我们就得到了我们设计的算法的时间复杂度,我们把它记作: O(n)

(2)高斯算法
#include<stdio.h> 	  
int main()  
{  
	int sum = 0, n = 100;   //执行1次//  
	sum = (1 + n) * n/2;    //执行1次//  
	printf("%d", sum);      //执行1次//  
}  

我们学到这里不难看出这个算法的时间复杂度: O(3),但一般记作 O(1)。

结论:从算法的效率上看,O(1) < O(n) ,所以高斯的算法更快,效率更高。 

3.2 求两个n 阶方阵C=A*B的乘积 

void MaMu(int A[n][n],int B [n][n],int C[n][n])
{  	  
	for(int i=0; i <n; i++)                           //n+1  
	{  
		for (j=0;j < n; j++)                          //n*(n+1)  
		{     
  			C[i][j]=0;                                //n^2  
			for (k=0; k<n; k++)                       //n^2*(n+1)  
			{  
           			 C[i][j]=C[i][j]+A[i][k]*B[k][j]; //n^3  
			}  
		}  
	}  
} 

 该算法所有语句的频度之和为:T(n) = 2n^3+3n^2+2n+1;

 利用大O表示法,该算法的时间复杂度为O(n^3)

3.3 分析下面代码的时间复杂度

(1)代码1
int i,j, n = 100;		
for(i = 0; i < n; i++)
{
	for(j = i; j < n; j++) //j = i而不是0//
    {
		  //时间复杂度为O(1)的程序步骤序列//

	}
}

在该程序中,当 i= 0时,内循环执行了n次,当 i =1时,执行了n-1次,……当i =n-1时,执行了1次。所以总的执行次数为:n+(n-1)+(n-2)+…+ 1= n(n+1)/2= O(n^2)。

(2)代码2 
void test(int arr[], int n)  
{  
	int i, j, k;  
	for (i=0; i<n-1; i++)  
	{  
		k = i;  
		for (j=i+1; j<n; j++)  
		{  
			if (arr[k] > arr[j])  
			{  
 			    k = j;  
			}  
		}  
  		x = arr[i];  
	    arr[i] = arr[k];  
	    arr[k] = x;  
	}  
}  

其中,算法的基本语句是:

if(arr[k]>arr[j]
{
    k=j;
}

这里便可以知道,时间复杂度为O(1)。 执行次数为:n-1 + n-2 + ... + n*(n-1)/2 = O(n^2)。

4 情况的判断 

有些算法的时间复杂度存在最好、平均最坏情况:(时间复杂度取最坏情况

  •  最坏情况:任意输入规模的最大运行次数(上界)
  •  平均情况:任意输入规模的期望运行次数
  •  最好情况:任意输入规模的最小运行次数(下界)

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

  • 最好情况:1次找到
  • 最坏情况:N次找到
  • 平均情况:N/2次找到

例:我们就下面代码进行分析

//计算strchr的时间复杂度
int main()
{
   const char* strchr(const char* str,int character);
   return 0;
}

我们根据该函数原理进行分析: 

strchr函数的作用是在一个字符串中找到目标字符在该字符串中第一次出现的位置,并返回该位置;

最好情况:1次找到(目标字符就在字符串的第一个元素);

最坏情况:N次找到(目标字符在字符串的末尾);

平均情况:N/2次找到。

因此strchr 的时间复杂度为O(N) 。

 三、空间复杂度

   类似于时间复杂度的讨论,一个算法的空间复杂度S(n) 定义为该算法所耗费的存储空间,它也是问题规模n 的函数。渐近空间复杂度也常常简称为空间复杂度。算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n 为问题的规模,f(n) 为语句关于n 所占存储空间的函数。

   空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。算法的输入输出数据所占用的存储空间是由要解决的问题决定的,是通过参数表由调用函数传递而来的,它不随本算法的不同而改变。存储算法本身所占用的存储空间与算法书写的长短成正比,要压缩这方面的存储空间,就必须编写出较短的算法。

如当一个算法的空间复杂度为一个常量,即不随被处理数据量n 的大小而改变时,可表示为O(1);当一个算法的空间复杂度与以2为底的n 的对数成正比时,可表示为O(log2n);当一个算法的空间复杂度与n 成线性比例关系时,可表示为O(n)。

   通常, 我们都使用“时间复杂度”来指运行时间的需求,使用“空间复杂度”指空间需求。当不用限定词地使用“复杂度”时,通常都是指时间复杂度


    本期分享已经接近尾声,那么大家对“复杂度”是否有了新的认识呢?如果这篇文章对你有所帮助,记得给博主一个三连哈~你们的支持是我创作的最大动力!前路漫漫,勿焦灼,勿烦躁。我们要有坚如磐石的信仰,脚下才有行稳致远的力量!诸君加油,不负自己!

  • 20
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值