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

news2024/11/25 10:40:30

       在学习具体的数据结构和算法之前,每一位初学者都要掌握一个技能,即善于运用时间复杂度和空间复杂度来衡量一个算法的运行效率。所谓算法,即解决问题的方法。同一个问题,使用不同的算法,虽然得到的结果相同,但耗费的时间和资源肯定有所差异。这也就意味着,如果解决问题的算法有多种,我们就需要从中选出最好的那一个。那么,怎么判断哪个算法更好(或者更优)呢?在满足准确性和健壮性的基础上,还有一个重要的筛选条件,即通过算法所编写出的程序的运行效率。程序的运行效率具体可以从 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)

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

  

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【c++leetcode】1913.Maximum Product Difference Between Two Pairs

问题入口 这个问题很容易解决。只要将数组排序&#xff0c;返回 最大元素*第二大元素-最小元素*第二小元素 即可。通过这道题顺便复习一些排序算法 。 直接使用sort函数 class Solution {public:int maxProductDifference(vector<int>& nums) {sort(nums.begin(),…

《WebKit 技术内幕》之三(2): WebKit 架构和模块

2.基于 Blink 的 Chrominum 浏览器结构 2.1 Chrominum 浏览器的架构及模块 Chromium也是基于WebKit&#xff08;Blink&#xff09;开发的&#xff0c;并且在WebKit的移植部分中&#xff0c;Chromium也做了很多有趣的事&#xff0c;所以通过Chromium可以了解如何基于WebKit构建浏…

尝试解决githubclone失败问题

BV1qV4y1m7PB 根据这个视频 似乎是我的linux的github似乎下好了 我没有配置好 比如我的ssh-key 现在根据视频试试 首先需要跳转到ssh的文件夹&#xff1a; cd ~/.ssh 然后生成一个ssh-key&#xff1a; ssh-keygen -t rsa -C "<github资料里的邮箱>" 然后…

Linux之进程间通信(管道)

目录 一、进程间通信 1、进程间通信的概念 2、进程间通信的目的 3、进程间通信的分类 二、管道 1、管道基本介绍 2、匿名管道 3、命名管道 一、进程间通信 1、进程间通信的概念 什么是进程间通信&#xff1f; 我们在学习了进程的相关知识后&#xff0c;知道&#xff…

java SSM网上小卖部管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java SSM网上小卖部管理系统是一套完善的web设计系统&#xff08;系统采用SSM框架进行设计开发&#xff0c;springspringMVCmybatis&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源 代码和数据库&#xff0c;系统主要…

生物信息基础:pysam读写基因组文件

Pysam[1]是一个 Python 模块&#xff0c;它打包了高通量测序库htslib[2]的 C-API&#xff0c;可用于读写基因组相关文件&#xff0c;如 Fasta/Fastq&#xff0c;SAM/BAM/CRAM&#xff0c;VCF 等。本文以 Fasta/Fastq 文件的读写为例&#xff0c;介绍 Pysam 的用法&#xff0c;详…

更新Ubuntu并同步网络时间

ubuntu环境搭建专栏&#x1f517;点击跳转 Ubuntu系统环境搭建&#xff08;九&#xff09;——更新Ubuntu并同步网络时间 文章目录 Ubuntu系统环境搭建&#xff08;九&#xff09;——更新Ubuntu并同步网络时间1.更新Ubuntu1.1 查看ubuntu版本和详细信息1.2 创建root用户1.3 更…

Maven error in opening zip file?maven源码debug定位问题jar包

文章目录 问题发现调试Maven1. 查看maven版本2. 下载对应版本的maven源码3. 打开maven源码&#xff0c;配置启动选项 启动maven debug模式进入maven 源码&#xff0c;打断点调试找jar包算账 已录制视频 视频连接 问题发现 最近使用maven分析jar包的时候遇到了一个很搞的问题。…

智慧文旅运营综合平台:重塑文化旅游产业的新引擎

目录 一、建设意义 二、包含内容 三、功能架构 四、典型案例 五、智慧文旅全套解决方案 - 210份下载 在数字化浪潮席卷全球的今天&#xff0c;智慧文旅运营综合平台作为文化旅游产业与信息技术深度融合的产物&#xff0c;正逐渐显现出其强大的生命力和广阔的发展前景。 该…

mfc110.dll丢失是什么意思?全面解析mfc110.dll丢失的解决方法

在使用计算机的过程中&#xff0c;用户可能会遭遇一个常见的困扰&#xff0c;即系统提示无法找到mfc110.dll文件。这个动态链接库文件&#xff08;DLL&#xff09;是Microsoft Foundation Classes&#xff08;MFC&#xff09;库的重要组成部分&#xff0c;对于许多基于Windows的…

Java多线程并发篇----第二十六篇

系列文章目录 文章目录 系列文章目录前言一、什么是 Executors 框架?二、什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?三、什么是 Callable 和 Future?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分…

海外媒体发稿:满足要求的二十个爆款文案的中文标题-华媒舍

爆款文案是指在营销和推广方面非常受欢迎和成功的文案。它们能够吸引读者的眼球&#xff0c;引发浏览者的兴趣&#xff0c;最终促使他们采取行动。本文将介绍二十个满足要求的爆款文案的中文标题&#xff0c;并对每个标题进行拆解和描述。 1. "XX 绝对不能错过的十大技巧…

专题篇|国芯科技系列化布局车载DSP芯片,满足不同层次车载音频产品的需求

随着高端DSP芯片产品CCD5001的亮相&#xff0c;国芯科技也在积极布局未来的DSP系列芯片群。通过深入研究不同车型音频处理需求&#xff0c;对比国外DSP产品综合性能和成本&#xff0c;国芯科技未来将推出全新DSP芯片家族&#xff0c;包括已经推出的高端产品CCD5001&#xff0c;…

vector讲解

在学习玩string后我们开始学习vector&#xff0c;本篇博客将对vector进行简单的介绍&#xff0c;还会对vector一些常用的函数进行讲解 vector的介绍 实际上vector就是一个数组的数据结构&#xff0c;但是vector是由C编写而成的&#xff0c;他和数组也有本质上的区别&#xff…

排序算法整理

快速排序 C实现 void fastStore(int *a, int start, int end){if(start>end)return ;int leftstart;int rightend;int tempa[left];//设置基准值tempwhile(left < right) //左指针的位置一定小于右指针的位置{while(a[right]>temp && left < right) //左…

VRRP协议负载分担

VRRP流量负载分担 VRRP负载分担与VRRP主备备份的基本原理和报文协商过程都是相同的。同样对于每一个VRRP备份组,都包含一个Master设备和若干Backup设备。与主备备份方式不同点在于:负载分担方式需要建立多个VRRP备份组,各备份组的Master设备可以不同;同一台VRRP设备可以加…

linux(七):I2C(touch screen)

本文主要探讨210触摸屏驱动相关知识。 I2C子系统 i2c子系统组成部分:I2C核心,I2C总线驱动,I2C设备驱动 I2C核心&#xff1a;I2C总线驱动和设备驱动注册注销方法 I2C总线驱动&#xff1a;I2C适配器(I2C控制器)控制,用于I2C读写时序(I2C_adapter、i2c_a…

树的一些经典 Oj题 讲解

关于树的遍历 先序遍历 我们知道 树的遍历有 前序遍历 中序遍历 后序遍历 然后我们如果用递归的方式去解决&#xff0c;对我们来说应该是轻而易举的吧&#xff01;那我们今天要讲用迭代&#xff08;非递归&#xff09;实现 树的相关遍历 首先呢 我们得知道 迭代解法 本质上也…

微信小程序(九)轮播图

注释很详细&#xff0c;直接上代码 新增内容&#xff1a; 1.轮播容器的基本属性 2.轮播图片的尺寸处理 index.wxml <view class"navs"><text class"active">精选</text><text>手机</text><text>食品</text><…

第6章 现代通信技术

文章目录 6.1 图像与多媒体通信6.1.1 图像通信6.1.2 多媒体通信技术1、多媒体通信概念2、多媒体通信的组成3、多媒体通信的业务分类4、实用化的多媒体通信系统类型5、多媒体通信应用系统&#xff08;1&#xff09;多媒体会议电视系统&#xff08;2&#xff09;IPTV 6.2 移动通信…