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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

x-cmd pkg | public-ip-cli - 公共 IP 地址查询工具

简介 public-ip-cli 是一个用 Javascript 编写的命令行工具&#xff0c;用于获取当前计算机或网络所使用的公共 IP 地址。 它可以让用户在命令行界面上查询 OpenDNS、Google DNS 和 HTTPS 服务的 DNS 记录以获取与互联网通信时所分配的公共 IP 地址。 首次用户 使用 x env us…

[开源]EBO机器人改装 动力增强板 最简单的4G无线距离小车

前言&#xff1a; 相信在疫情期间见了很多人做的4G小车&#xff0c;取外卖&#xff0c;取快递&#xff0c;买物资等用途&#xff0c;博主前段时间买了一个EBO逗猫机器人。就是他&#xff1a; 期初是为了逗猫玩的&#xff0c; 这个机器人4G低延迟图像&#xff0c;无线喊话&am…

快速玩转 Mixtral 8x7B MOE大模型!阿里云机器学习 PAI 推出最佳实践

作者&#xff1a;熊兮、贺弘、临在 Mixtral 8x7B大模型是Mixtral AI推出的基于decoder-only架构的稀疏专家混合网络&#xff08;Mixture-Of-Experts&#xff0c;MOE&#xff09;开源大语言模型。这一模型具有46.7B的总参数量&#xff0c;对于每个token&#xff0c;路由器网络选…

Hadoop详解

Hadoop 概念 就是一个大数据解决方案。它提供了一套分布式系统基础架构。 核心内容包含 hdfs 和mapreduce。hadoop2.0 以后引入 yarn. hdfs 是提供数据存储的&#xff0c;mapreduce 是方便数据计算的。 hdfs 又对应 namenode 和 datanode. namenode 负责保存元数据的基本信息…

实战纪实 | 某配送平台zabbix 未授权访问 + 弱口令

本文由掌控安全学院 - 17828147368 投稿 找到一个某src的子站&#xff0c;通过信息收集插件wappalyzer&#xff0c;发现ZABBIX-监控系统&#xff1a; 使用谷歌搜索历史漏洞&#xff1a;zabbix漏洞 通过目录扫描扫描到后台&#xff0c;谷歌搜索一下有没有默认弱口令 成功进去了…

HackTheBox - Medium - Linux - Health

Health Health 是一台中型 Linux 计算机&#xff0c;在主网页上存在 SSRF 漏洞&#xff0c;可利用该漏洞访问仅在 localhost 上可用的服务。更具体地说&#xff0c;Gogs 实例只能通过 localhost 访问&#xff0c;并且此特定版本容易受到 SQL 注入攻击。由于攻击者可以与 Gogs …

PyTorch 内 LibTorch/TorchScript 的使用

PyTorch 内 LibTorch/TorchScript 的使用 1. .pt .pth .bin .onnx 格式1.1 模型的保存与加载到底在做什么&#xff1f;1.2 为什么要约定格式&#xff1f;1.3 格式汇总1.3.1 .pt .pth 格式1.3.2 .bin 格式1.3.3 直接保存完整模型1.3.4 .onnx 格式1.3.5 jit.trace1.3.6 jit.scrip…

AtCoder Regular Contest 115 E. LEQ and NEQ(容斥 单调栈优化dp)

题目 n(n<5e5)个数&#xff0c;第i个数ai(1<ai<1e9) 构造一个序列b&#xff0c;要求bi∈[1,ai]&#xff0c;且b[i]不等于b[i1] 求方案数&#xff0c;答案对998244353取模 思路来源 洛谷题解Xu_brezza 一模一样的cf题&#xff1a; Codeforces Round 759 (Div. 2…

解析智能酒精壁炉不完全燃烧的成因及潜在问题

解析智能酒精壁炉不完全燃烧的成因及潜在问题 智能酒精壁炉作为一种环保、高效、现代化的取暖工具&#xff0c;其采用酒精作为燃料进行燃烧&#xff0c;但在一些情况下&#xff0c;可能会出现酒精燃烧不完全的问题。下面将深入探讨这一现象的成因以及可能引发的问题。 成因分析…

php isset和array_key_exists区别

在PHP中&#xff0c;可以使用array_key_exists函数或者isset函数来判断一个字典&#xff08;关联数组&#xff09;中是否存在某个下标。 使用 array_key_exists 函数: $myArray array("key1" > "value1", "key2" > "value2",…

基于动态顺序表实现通讯录项目

本文中&#xff0c;我们将使用顺序表的结构来完成通讯录的实现。 我们都知道&#xff0c;顺序表实际上就是一个数组。而使用顺序表来实现通讯录&#xff0c;其内核是将顺序表中存放的数据类型改为结构体&#xff0c;将联系人的信息存放到结构体中&#xff0c;通过对顺序表的操…

【数据结构与算法】1.时间复杂度和空间复杂度

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f64f;小杨水平有限&#xff0c;欢迎各位大佬指点&…