数据结构一:算法效率分析(时间复杂度和空间复杂度)-重点

       在学习具体的数据结构和算法之前,每一位初学者都要掌握一个技能,即善于运用时间复杂度和空间复杂度来衡量一个算法的运行效率。所谓算法,即解决问题的方法。同一个问题,使用不同的算法,虽然得到的结果相同,但耗费的时间和资源肯定有所差异。这也就意味着,如果解决问题的算法有多种,我们就需要从中选出最好的那一个。那么,怎么判断哪个算法更好(或者更优)呢?在满足准确性和健壮性的基础上,还有一个重要的筛选条件,即通过算法所编写出的程序的运行效率。程序的运行效率具体可以从 2 个方面衡量,分别为:程序的运行时间和程序运送所需的内存空间大小。本篇博客详细总结如何计算一个算法的时间复杂度和空间复杂度,根据代码的编写形式分为:非递归式(循环)和递归代码的时间复杂度,空间复杂度计算,根据出题的形式又可分为:给定代码计算时间复杂度和空间复杂度,或者给定题目请设计算法同时给出时间复杂度和空间复杂度;后者作为笔试面试的重点考查方式,需要重点掌握!

一、算法复杂度

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


 二、复杂度在校招中的考察

三、时间复杂度     

3.1 时间复杂度的概念,如何理解?

        在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。记作:T(n)=O(f(n)),其中f(n)是问题规模n的某个函数。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。O(语句总的执行次数),即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

理解概念的两个关键点:

     1. 用计算语句总的执行次数,来度量时间的;

     2. 把握好问题的规模这个概念,抓住问题规模;

3.2 推导大O阶方法

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

推导大O阶方法:

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

       通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。 另外有些算法的时间复杂度存在最好、平均和最坏情况:

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

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

      总结:O(执行次数)    ---->   O(阶次/数量级)

举个栗子:

实现getsum函数求和1+2+3+...+n
int getsum(int n)
{
    int result=0;   执行1次
    for(int i=1;i<=n;i++)   执行次数为:1+n+n
    {
         result+=i;        执行次数为:n
    }
    return result;         执行1次
}

 分析:

   问题规模体现在形参n上,当n不同,计算求和的项数就不同,计算总的执行次数为f(n)=1+1+n+n+n+1=3n+3;  经过化简可知:O(n) ,可以总结出:像定义变量、循环初始条件、循环判断条件、循环自增变量等这些都不会影响最终的结果(当n较大时),在计算总的执行次数时可以不计算它们!只需要看循环内部语句总的执行次数。

3.3  常见的时间复杂度

3.3.1 常数阶

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

分析:这个算法的运行次数函数是f(n)=4。根据我们推导大O阶的方法,第一步就是把常数项 4改为1。在保留最高阶项时发现,它根本没有最高阶项,所以这个算法的时间复杂度为O(1)

#include <stdio.h>
int main()
{
   int res=0,n=100; 执行1次
   res=(1+n)*n/2;   执行第1次
   res=(1+n)*n/2;   执行第2次
   res=(1+n)*n/2;   执行第3次
   res=(1+n)*n/2;   执行第4次
   res=(1+n)*n/2;   执行第5次
   res=(1+n)*n/2;   执行第6次
   res=(1+n)*n/2;   执行第7次
   res=(1+n)*n/2;   执行第8次
   res=(1+n)*n/2;   执行第9次
   res=(1+n)*n/2;   执行第10次
   printf("%d",res); 执行1次
   return 0;         执行1次
}

     事实上无论n为多少,上面的两段代码就是4次和13次执行的差异。这种与问题的大小无关(n的多少),即没有问题规模,执行时间恒定的算法,我们称之为具有 0(1)的时间复杂度,又叫常数阶。O(K)=O(1)

      对于分支结构而言,无论是真,还是假,执行的次数都是恒定的,不会随着 n的变大而发生变化,所以单纯的分支结构 (不包含在循环结构中),其时间复杂度也是0(1)。

3.3.2 线性阶

      线性阶的循环结构会复杂很多。要确定某个算法的阶次,我们常常需要确定某个特定语句或某个语句集运行的次数。(关键是要分析O(1)语句执行的总次数)。因此,我们要分析算法的复杂度,关键就是要分析循环结构的运行情况。

示例一:
#include <stdio.h>
int main()
{
   int i=0;
   for(i=0;i<n;i++)
   {
      /*时间复杂度为O(1)的程序步骤序列 */
   }
   return 0;       
}

     上面这段代码,它的循环的时间复杂度为 O(n),因为循环体中的代码须要执行 n次。

总结:对于单层for循环,循环条件与问题规模n有关,时间复杂度一般都是O(n)

3.3.3 对数阶

实例1:
#include<stdio.h>
int main()
{
    int  count=1;
    while (count <n)
    {
         count *=2;
         /*时间复杂度为O(1)的程序步骤序列*/
    }
    return 0;
}


实例2:
#include<stdio.h>
int main()
{
    int  num;
    while (num!=0)
    {
        num/=2;
       /*时间复杂度为O(1)的程序步骤序列*/
    }
    return 0;
}

示例1:

     由于每次 count 乘以2之后,就距离n 更近了一分。也就是说,有多少个2相乘后大于 n,则会退出循环。由2^x=n 得到 x=log2n(x也代表次数)。所以这个循环的时间复杂度为0(logn)。

实例2:

     由于每次num除以2之后,就距离0 更近了一分。也就是说,有多少个2相除后等于0,则会退出循环。由n/2^x=1 ,得到 x=log2n(x也代表次数)。所以这个循环的时间复杂度为0(logn)。

3.3.4 平方阶

实例1:
#include<stdio.h>
int main()
{
    int i,j;
    for(i=0;i<n;i++)
    {
       for(j=0;j<n;j++)
        {
            /*时间复杂度为O(1)的程序步骤序列*/
        }
    }
    return 0;
}


实例2:
#include<stdio.h>
int main()
{
    int i,j;
    for(i=0;i<m;i++)
    {
       for(j=0;j<n;j++)
        {
            /*时间复杂度为O(1)的程序步骤序列*/
        }
    }
    return 0;
}

实例3:
#include<stdio.h>
int main()
{
    int i,j;
    for(i=0;i<n;i++)
    {
       for(j=0;j<=i;j++)
        {
            /*时间复杂度为O(1)的程序步骤序列*/
        }
    }
    return 0;
}

实例1:

       是一个循环嵌套,它的内循环刚才我们已经分析过,时间复杂度为O(n)。而对于外层的循环,不过是内部这个时间复杂度为 O(n)的语句,再循环n次。所以这段代码的时间复杂度为O(n^2)。

实例2:

    如果外循环的循环次数改为了m,时间复杂度就变为O(mxn)。

实例3:

     由于当i=0,内层循环执行1次,当i=1,内层循环执行2次,当i=2,内层循环执行3次......当i=n-1,内层循环执行n次,因此总的执行次数为:1+2+3+4+...+n即等差数列求和问题:(1+n)*n/2=n/2+n^2=O(n^2)

总结:总结:对于双层for循环,内外循环条件与问题规模n有关,时间复杂度一般都是O(n^2)

四、空间复杂度 

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

     空间复杂度计算的是与问题规模有关的额外开辟的内存空间!!!比如:临时定义的变量与问题规模无关不能算进去。

五、常用的时间复杂度 

六、经典题目(非递归和递归代码比较)

6.1 计算1+2+3...+n的和

1.非递归方式
int getsum(int n)
{
   int res=0;
   for(int i=1;i<=n;i++)
   {
       res+=i;
   }
   return res;
}

2.递归方式
int getsum(int n)
{
    if(n==1)
      {
          return 1;
      }  
    else
      {
         return getsum(n-1)+n;
      }
}
  

分析:非递归方式:问题规模体现在形参上n

       时间复杂度:单层for循环,语句执行次数为n,所以时间复杂度为:O(n)

       空间复杂度:没有与问题规模相关额外开辟的内存 ,空间复杂度为O(1)

注:临时变量res和i都与问题规模无关,无论求和项数是多少,都只开这两个O(2)-----O(1)

递归总结分析:

       重点:递归就是函数自己调用自己,发生函数调用就会发生函数栈帧的开辟,每进行一次函数调用就会在内存上开辟一块内存,且需要注意,只有当函数调用完毕(函数语句执行完毕,得到确切的值,返回给被调函数,这块内存才会被释放!!!)

       

       递归的时间复杂度:函数的调用次数

       递归的空间复杂度:递归的深度(二叉树的深度)

分析:递归方式:问题规模体现在形参上n

       在这个问题上,函数调用次数与递归深度都相同,都与问题规模有关,因此,时间复杂度和空间复杂度都是O(n)

6.2 斐波那契数列(递归与非递归实现)-重点理解

1.非递归的方式
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;
	while(n>=3)
	{
		c = a + b;
		a = b;
		b = c;
	}
	return c;
}


2.递归的方式
int Fib(int n)
{
	if(n<=2)
	
		return 1;
	
	else
		return Fib(n - 1) + Fib(n - 2);
}

分析:非递归方式:问题规模体现在形参上n

       时间复杂度:单层while循环,语句执行次数为n-3+1=n-2,所以时间复杂度为:O(n)

       空间复杂度:没有与问题规模相关额外开辟的内存 ,空间复杂度为O(1)

分析:递归方式:问题规模体现在形参上n

       递归的时间复杂度:计算函数的调用总的次数

       递归的空间复杂度:递归的深度(二叉树的深度)

       注意:每一层的递归调用都会在栈上分配一块内存,有多少层递归调用就分配多少块相似的内存,所有内存加起来就是开辟的总的内存大小,同层占用一块内存!! 

6.3  二分查找算法(递归与非递归实现)-重点理解

1.非递归方式
int binarysearch(int* arr, int len, int val)
{
	assert(arr != NULL && len > 0);   //参数检验
	int left = 0, right = len - 1;
	while (left <= right)            //查找的条件
	{
		int midindex = (left + right) / 2;
		//面试进行优化,利用位运算更偏底层运行速度更快。
		//优化一:midindex = (left + right) >> 1; 
 
		// 提示:若: 数据量很大 [0,200] left=150,right = 160; -> 310,  有可能超出范围如何解决? 算术运算符操作的数据范围只能在基本数据类型的范围之内
		//优化二:midindex = ((right - left)>>1) + left;
		if (arr[midindex] == val)
		{
			return midindex;
		}
		else if (arr[midindex] < val)
		{
			left = midindex + 1;
		}
		else
		{
			right = midindex - 1;
		}
	}
	return -1;
}



2.递归方式
int binarysearch0(int* arr, int len, int value, int left, int right)
{
	if (left > right)
		return -1;
	int midindex = (left + right) / 2;
	if (arr[midindex] == value)
	{
		return midindex;
	}
	else if (arr[midindex] > value)
	{
		return binarysearch0(arr, len, value, left, midindex - 1);
	}
	else
	{
		return binarysearch0(arr, len, value, midindex + 1, right);
	}
}
 
int binarysearch(int* arr, int len, int value)
{
	assert(arr != NULL);
	return binarysearch0(arr, len, value, 0, len - 1);
}

分析:非递归方式:问题规模体现在形参上len,数组长度不同,问题规模就不同

       时间复杂度:每次查找查找区间减半,所以减了多少次,代表语句执行的总次数,logn

       空间复杂度:没有与问题规模相关额外开辟的内存 ,空间复杂度为O(1)

分析:递归方式:问题规模体现在形参上查找的区间上,封装成函数。

       时间复杂度:每次查找查找区间减半,所以减了多少次,代表语句执行的总次数,logn

       空间复杂度:没有与问题规模相关额外开辟的内存 ,空间复杂度为O(1)

七、给定题目设计算法,同时给出时间复杂度和空间复杂度(未知代码)

这种题型求解思路如下: 

       时间复杂度:访问问题规模中的元素总个数

       空间复杂度:与问题规模相关额外开辟的内存空间,没有额外开辟跟问题规模相关的内存O(1)

      以上便是我为大家带来的算法效率分析的内容,若有不足,望各位大佬在评论区指出,谢谢大家!可以留下你们点赞、收藏和关注,这是对我极大的鼓励,我也会更加努力创作更优质的作品。再次感谢大家!

  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/335798.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

开发实践8_project

要求&#xff1a; 使用Restful对Chaos模型作基本操作。 结果&#xff1a; post 3 组数据后&#xff0c;get 查询如下&#xff1a; put修改后get&#xff1a; delete pk3之后get&#xff1a; 代码&#xff1a; python manage.py startapp pro8_app 注册 总路由 // path(pr…

免费200万Tokens 用科大讯飞API调用星火大模型服务

简介 自ChatGPT火了之后&#xff0c;国内的大模型发展如雨后春笋。其中的佼佼者之一就是科大讯飞研发的星火大模型,现在大模型已经更新到V3 版本&#xff0c;而且对开发者也是相当友好&#xff0c;注册就送200万tokens,讯飞1tokens 约等于 1.5 个中文汉字 或者 0.8 个英文单词…

JVM 如何判断一个对象可以被回收

Hi&#xff0c; 我是 浮生。 今天分享一道一线互联网公司必问的面试题。 ”JVM 如何判断一个对象可以被回收“ 关于这个问题&#xff0c;来看看高手的回答。 一、问题解析 在 JVM 里面&#xff0c;要判断一个对象是否可以被回收&#xff0c;最重要的是判断这个对象是否还在被…

中仕教育:省考怎么查每个岗位报考人数?一篇文章带你搞定!

参加省考避开热门岗位能够一定程度上提高上岸几率&#xff0c;怎么看岗位报考人数? 1. 官方公告&#xff1a;每年省考发布招录公告时&#xff0c;会公布各个岗位的招录人数&#xff0c;可以关注招录信息。 2. 查询报名数据&#xff1a;在报名结束后&#xff0c;省考招录机关…

debian12.4配置

文章目录 debian12.4配置概述笔记将非root用户添加到sudo组更换国内源配置ssh的客户端访问END debian12.4配置 概述 在虚拟机中装了一个debian12.4, 想配置ssh客户端连接, 出了问题. 配置乱了, 还好长了个心眼, 做了快照. 发现2个问题: debian12.4默认安装完, 有ssh, 先检查…

Python自动化测试【selenium面试题】

一、selenium中如何判断元素是否存在&#xff1f; expected_conditions模块提供了16种判断方法&#xff0c;以下方法是判断元素存在DOM中&#xff1a; presence_of_element_located """ An expectation for checking that an element is present on the DOM of…

【Linux】第三十一站:管道的一些应用

文章目录 一、我们之前的|(竖划线)管道二、自定义shell三、使用管道实现一个简易的进程池1.详解2.代码3.一个小bug4.最终代码 一、我们之前的|(竖划线)管道 cat test.txt | head -10 | tail -5如上代码所示&#xff0c;是我们之前所用的管道 我们拿下面这个举个例子 当我们用…

【SpringBoot】—— 如何创建SpringBoot工程

SpringBoot简化了Spring应用的初始搭建和开发过程。 工程创建 新建模块 出现java: 错误: 无效的源发行版&#xff1a;18这样的错误&#xff0c; 修改pom.xml文件 出现以下信息&#xff0c;即运行成功 修改默认端口 创建application.yml文件 内容&#xff1a; server:port:…

【没学过编程语言,想要做一款游戏应该怎么做?】

*** 【没学过编程语言&#xff0c;想要做一款游戏应该怎么做&#xff1f;】 想让你的创意成为像《堡垒之夜》《原神》这样引爆式的热门游戏吗&#xff1f; 想制作一个能与《我的世界》《模拟城市》一决高下的畅销游戏吗&#xff1f; 即使你手头并没有复杂的代码能力&#xf…

知识图谱KG+大模型LLM

LLM-based KG KnowLM OpenSPGKG-based RAG 基本原理 从query出发的语义解析 pre-LLM方法 思想&#xff1a;直接将问题解析为对应的逻辑表达式&#xff0c;然后到知识图谱中查询。 方法&#xff1a;通常包含逻辑表达式、语义解析算法、语义解析模型训练三部分。一般步骤是将问句…

【51单片机Keil+Proteus8.9+ADC0804】ADC实验 模拟转数字实验

一、实验名称 ADC实验 模拟转数字实验 二、设计思路 电路设计 1.选用AT89C51单片机作为电路核心单元&#xff0c;外接8位单通道AD转换器ADC0804芯片和LM016L显示器以及滑动变阻器等其它常用元器件构成电路。 2.将ADC0804芯片的控制引脚RD,WR,INTR接到AT89C51芯片对应引脚&…

管理信息系统知识点复习

目录 一、名词解释题1.企业资源规划(ERP)2.面向对象方法&#xff1a;3.电子健康&#xff1a;4.供应链5.数据挖掘6.“自上而下”的开发策略&#xff1a;7.业务流程重组8.面向对象&#xff1a;9.决策支持系统10.聚类11.集成开发环境&#xff1a;12.供应商协同13.数据仓库14.深度学…

pytorch集智-6手写数字加法机-迁移学习

1 概述 迁移学习概念&#xff1a;将已经训练好的识别某些信息的网络拿去经过训练识别另外不同类别的信息 优越性&#xff1a;提高了训练模型利用率&#xff0c;解决了数据缺失的问题&#xff08;对于新的预测场景&#xff0c;不需要大量的数据&#xff0c;只需要少量数据即可…

STM32407用汇顶的GT911触摸芯片调试实盘

这个配置很关键 代码 #include "stm32f4xx.h" #include "GT9147.h" #include "Touch.h" #include "C_Touch_I2C.h" #include "usart.h" #include "delay.h" #include "LCD.h" #incl…

Java String基础学习

目录 1、String的构造方法 2、String内存模型 3、字符串的比较 4、字符串的练习 1、用户登录系统 2、遍历字符串 3、统计字符次数 4、拼接字符串 5、字符串的反转 6、金额转换 7、手机号屏蔽 * 8、身份证信息查看 9、敏感词替换 5、StringBuilder 1、概念及练习…

新手也能看懂的【前端自动化测试入门】!

前言 最近在网上搜索前端自动化测试相关的文档&#xff0c;但是发现网上的文章都是偏使用&#xff0c;没有把一些基础概念说清楚&#xff0c;导致后续一口气遇到一些karma、Jasmine、jest、Mocha、Chai、BDD等词汇的时候很容易一头雾水&#xff0c;这次一方面整理一下收获的知…

YOLOv8改进 | 进阶实战篇 | 利用YOLOv8进行视频划定区域目标统计计数

一、本文介绍 Hello,各位读者,最近会给大家发一些进阶实战的讲解,如何利用YOLOv8现有的一些功能进行一些实战, 让我们不仅会改进YOLOv8,也能够利用YOLOv8去做一些简单的小工作,后面我也会将这些功能利用PyQt或者是pyside2做一些小的界面给大家使用。 在开始之前给大家推…

解决Spring Boot跨域问题(配置JAVA类)

什么是跨域问题 跨域问题指的是不同端口之间&#xff0c;使用 ajax 无法相互调用的问题。跨域问题本质是浏览器的一种保护机制&#xff0c;它是为了保证用户的安全&#xff0c;防止恶意网站窃取数据。 比如前端用的端口号为8081&#xff0c;后端用的端口号为8080&#xff0c;后…

Linux下安装docker

1、查看系统版本 Docker支持64位版本的CentOS 7和CentOS 8及更高版本&#xff0c;它要求Linux内核版本不低于3.10。查看Linux版本的命令这里推荐两种&#xff1a;lsb_release -a或cat /etc/redhat-release。 显然&#xff0c;当前Linux系统为CentOS7。再查一下内核版本是否不低…

SpringBoot+dynamic-datasource实现多数据源(msyql、sqlserver、postgresql)手动切换

场景 SpringBootMybatisPlusdynamic-datasources实现连接Postgresql和mysql多数据源&#xff1a; SpringBootMybatisPlusdynamic-datasources实现连接Postgresql和mysql多数据源-CSDN博客 上面实现通过注解和配置文件的方式去进行多数据源操作。 如果业务需求&#xff0c;比…