一定要会的算法复杂度分析

news2025/1/20 18:28:46

本文首发自「慕课网」,想了解更多IT干货内容,程序员圈内热闻,欢迎关注"慕课网"!

原作者:s09g|慕课网讲师


我们知道面对同一道问题时可能有多种解决方案。自然地,我们会将多种方法进行比较。那么怎么样才能知道是A方法好,还是B方法好?这时候我们就需要对算法的复杂度进行分析。

这次我们介绍两个概念:时间复杂度与空间复杂度。并且用Two Sum作为案例,用时间空间复杂度分析Two Sum的三种解法。

时间复杂度

时间复杂度描述的是算法执行需要消耗的时间。同等条件下,消耗时间越少,算法性能越好。但是,算法执行的确切时间无法直接测量,通常只有在实际运行时才能知道。所以我们通过估算算法代码的方法来得到算法的时间复杂度。

空间复杂度

空间复杂度描述的是算法在执行过程中所消耗的存储空间(内存+外存)。同等条件下,消耗空间资源越少,算法性能越好。

大O符号

大O符号是用于描述函数渐近行为的数学符号,在分析算法效率的时候非常有用。

借用wikipedia上的一个例子,解决一个规模为n的问题所花费的时间可以表示为:T(n)=4n2+2n+1。当n增大时,n2项将开始占主导地位,而其他各项可以被忽略。比如当n=500,4n2项是2n项的1000倍大,因此在大多数场合下,省略后者对表达式的值的影响将是可以忽略不计的。

长远来看,如果我们与任一其他级的表达式比较,n2项的系数也是无关紧要的。例如:一个包含n2项的表达式,即使T(n)=1,000,000n2,假定U(n)=n3,一旦n增长到大于1,000,000,后者就会一直超越前者。

案例:Two Sum

给出一个整数数组nums和一个target整数,返回两个和为target的整数。

假定我们正在面试,让我们用面试的方法来分析一下这道题。

1.向面试官确认输入、输出
通过询问面试官,我们可以知道:输入是一个int类型的数组和一个target;返回值是两个下标,并且以数组的形式返回;方法名没有特殊要求。这样一下我们就确定了函数的签名

public int[] twoSum(int[] nums, int target) {
  // Solution
}

2.向面试官确认输入、输出是否有特例

接下来我们要确认一下输入输出的细节

  • 输入是否可以为空?
  • 输入的数组范围是正整数,还是任意范围?
  • 输入数组会不会特别大,甚至无法载入内存,比如300GB的数据量?
  • 如果输入不合法或者没有正确答案,我们已经返回空数组还是抛出异常?
  • 输入的数组中有重复么?如果没有重复,可以同一个数字用两次么?
  • 如果有多个解,那么返回第一个,还是所有解?
  • 你希望答案写成class,还是只提供方法本身即可?
  • ……

有些问题即使题目中已经提到,最好还是再次向面试官确认。如果以上这些问题你没有想到的话,那么说明思路仅限于做题,缺乏面试的沟通技巧。可以多找小伙伴Mock面试,注意多交流。

假设面试官告诉我们:只需要写函数本身。输入数组可能为空,但不会大到无法读进内存。数字的范围就是int类型的范围,可能有重复。对于不合法或者没有正确答案的情况,请自行判断。多个解法是,返回任意一个答案都可以。

得到了这些信息,我们可以先进行防御性编程。

public int[] twoSum(int[] nums, int target) {
  if (nums == null || nums.length < 2) {
    return new int[0];
  }
  
  // TODO: solution here
  
  return new int[0];
}

3.举几个例子

接下来,我们可以要求面试官举几个例子,或者自己提出几个例子,来确保双方对题目没有异议。

Example 1:
Input: nums = [], target = 0
Output: []

Example 2:
Input: nums = [2], target = 4
Output: []

Example 3:
Input: nums = [2, 3, 4, 2], target = 6
Output: [2, 4] or [4, 2]

Example 4:
Input: nums = [2, 7, 11, -2], target = 9
Output: [2, 7] or [7, 2] or [11, -2] or [-2, 11]

  • 根据例子1、2,确定没有正确解时返回空数组。
  • 根据例子2,确定数字不可重复使用。
  • 根据例子3、4,确定如果有多个合适的解,返回任意一个都可以。
  1. 开始解题

完成了之前的步骤,需要找到正确的思路。这道题有三种思路,我们需要一一分析判断,找到合适的解法之后,和面试官进行讨论。得到面试官的允许之后,才可以开始写代码。(如果一上来就埋头解题,即使做对了也不能拿到最高评价。)

解法1 Brute Force

没有具体思路的时候,暴力破解法应该是第一个想法。几乎任何后续更高效的算法都是在暴力破解法的基础上优化而来的。即使无法优化成功,一个可行解也好过一个高效但不可行的算法。

对于Two Sum这道题,最直观的想法大概是找到所有可能的数字组合,挨个计算他们的和,返回第一个满足条件的组合。这种解法并没有什么技术含量,但是可以作为我们下一步优化的基础。

public int[] twoSum(int[] nums, int target) {
    if (nums == null || nums.length < 2) {
        return new int[0];
    }

    for (int i = 0; i < nums.length; i++) { // O(N)
        int firstNum = nums[i]; // 确定第一个可能的数字
        for (int j = i + 1; j < nums.length; j++) { // O(N)
            int secondNum = nums[j]; // 确定第二个可能的数字
            if (firstNum + secondNum == target) {
                return new int[]{firstNum, secondNum};
            }
        }
    }
    return new int[0];
}

假设我们的输入大小为N(即nums的长度为N),for循环遍历每个数字时,假设每访问一个数字需要消耗的1个单位的时间,那么对于长度为N的数组,一共需要消耗N的时间。在计算机领域,我们使用大O记号来表示这种量化方法,将for循环的消耗记为O(N)。由于解法1中,我们使用了嵌套了两重for循环,这说明我们对于N个数字,每个数字除了消耗1个单位时间用于访问,还消耗了N个时间第二次遍历数组,总体的时间消耗为O(N2).

解法2 使用HashSet

反思解法1的步骤,我们利用了两重for循环。第一层for循环我们有不得不使用的理由:因为我们至少需要遍历每个数字。第二个for循环的目的是找到与firstNum相加等于target的数字,在这里我们又使用了for循环。如果有一种办法能够让我们记住已经见过的数字,并且在O(1)的时间内检查是否有数字与firstNum相加等于taget,那么就可以省下一个O(N)的for循环。

有一个已知的数据结构可以解决这个问题——Set。Set对应数学意义上的集合,每个元素在集合中只出现一次,Set提供了add/remove/contains … 等API,并且非常高效消耗均为O(1)。

在遍历数组的过程中,每遇到一个新的数字num,计算target - num的值并记为potentialMatch。检查set中是否包含potentialMatch,如果包含说明存在这么一组数字对,他们的和等于target;如果不包含,那么将当前的num加入set,然后检查下一个数字。

public int[] towSum(int[] nums, int target) {
    Set<Integer> set = new HashSet<>();
    for (int num : nums) { // O(N)
        int potentialMatch = target - num;
        if (set.contains(potentialMatch)) { // O(1)
            return new int[]{potentialMatch, num};
        } else {
            set.add(num); // 空间消耗增加O(1)
        }
    }
    return new int[0];
}

这个方法利用了Set的特性:以O(1)的速度快速查询元素是否存在。从而省去了一个for循环,将时间复杂度降到了O(N)。但是Set消耗了额外的空间,在最差的情况下,Set可能保存了每一个数字但依旧返回了空数组。所以,解法二消耗了O(N)的空间和O(N)的时间。

解法3 使用排序

解法2利用了O(N)的额外空间去记录已经访问过的数组。那么是否存在一种办法可以不消耗额外的空间,同时提供高效地查询。

当然没有这种好事?……

除非我们做一步预处理:将输入的数组排序处理。比如下图的例子:nums = [2, 4, 9, 7, 1], target = 6

  1. 先将原数组进行排序(这里可以使用编程语言自带的排序方法)
  2. 创建left、right两根指针。left指向第一位,right指向最后一位
  3. 只要left和right不重合,循环比较left、right指向的两个数字的和sum:
    • 如果sum等于target,那么left、right所指向的数字就是我们要找的结果
    • 如果sum大于target,那么将right向左移动一位,让下一个sum变小
    • 如果sum小于target,那么将left向右移动一位,让下一个sum变大
  4. 当循环结束,依旧没有答案,说明没有正确解
public int[] twoSum(int[] nums, int target) {
    Arrays.sort(nums); // O(NlogN)
    int left = 0;
    int right = nums.length - 1;
    while (left < right) { // O(N)
        int sum = nums[left] + nums[right];
        if (sum == target) { 
            // 如果sum等于target,那么left、right所指向的数字就是我们要找的结果
            return new int[] {nums[left], nums[right]};
        } else if (sum < target) {
            // 如果sum小于target,那么将left向右移动一位,让下一个sum变大
            left++;
        } else if (sum > target) {
            // 如果sum大于target,那么将right向左移动一位,让下一个sum变小
            right--;
        }
    }
    return new int[0];
}

这个算法的优势在于每次只会让较大的值减小、或者较小的值增大,得到的sum是连续的。如果存在正确的解,就一定可以找到对应的left和right。left、right的单调移动,每次会排除一部分错误答案,减小搜索空间,而且保证了数组中每个数字仅被访问一次,消耗是O(N)的。但是在预处理的时候使用了排序,所以会有O(NlogN)的时间消耗。总体上消耗了O(NlogN)的时间和O(1)的空间。缺点是改变了原数组的元素位置。

时间-空间的取舍

让我们来回顾这三种解法:

  • 解法1消耗了O(N2)的时间和O(1)的空间
  • 解法2消耗了O(N)的时间和O(N)的空间
  • 解法3消耗了O(NlogN)的时间和O(1)的空间

与解法1的暴力算法相比,解法2是用了空间换时间,增加了Set的消耗,减短了查询的消耗。解法3则相反,用了时间换空间,通过原地排序,省去了Set。这两类操作统称space-time trade-off 空间-时间权衡。

通过对算法的复杂度分析,我们有了量化算法效率的方法。我们可以明确地指出,解法2比解法1更好,解法3比解法2消耗更少的内存。

数据结构关键信息
array通过下标访问O(1),查询O(N),插入O(N),删除O(N)
string在内存中的形式与array等价
linked list通过下标访问O(N),查询O(N),插入O(1),删除O(1)
stacklast-in first-out,在内存中的形式等价于linked list
queuefirst-in first-out,在内存中的形式等价于linked list
heap查询极值O(1),插入O(logN),删除极值O(N)
hash table插入、删除、查询O(1)
binary search tree插入、删除、查询、找最大最小值、访问前驱结点、访问后继节点均为O(1)

大多数情况下,算法的过程是基于对基础数据结构的操作。因此分析算法复杂度也要求我们掌握常见的数据结构。上表给出了常用数据结构和操作的时间复杂度。记住这张表,能帮助我们更快的分析一个新算法的复杂度。


欢迎关注「慕课网」帐号,我们会一直坚持内容原创,提供IT圈优质内容,分享干货知识,大家一起共同成长吧!

本文原创发布于慕课网 ,转载请注明出处,谢谢合作
 

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

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

相关文章

【Linux】网络协议(应用层与传输层)

应用层传输层协议 应用层HTTP协议格式请求格式响应格式头部字段中的 Cookie (请求头) & Set-Cookie(响应头)cookiesession&#xff1a;会话cookie vs session HTTPS 协议&#xff1a;对 HTTP 协议进行加密 传输层UDP 协议TCP 协议 应用层 序列化&#xff1a;指将多个数组对…

使用shell封装Linux命令实现自定义Linux命令

前言 在日常工作中&#xff0c;尤其是在Linux上做开发的同学或者运维的同学们肯定会遇到过如下场景&#xff0c;比如在Linxu下通过find查找一个文件并且想看这个文件的详细信息&#xff0c;如果直接使用命令可能会一时想不起来&#xff0c;或者想起来了但是有个别参数忘记了。…

SpringMVC02注解与Rest风格

SpringMVC02 SpringMVC的注解 一、RequestParam 1、RequestParam注解介绍 位置&#xff1a;在方法入参位置作用&#xff1a;指定参数名称&#xff0c;将该请求参数 绑定到注解参数的位置属性 name&#xff1a;指定要绑定的请求参数名称&#xff1b; name属性和value属性互为…

vue-quill-editor富文本编辑框使用

vue富文本中实现上传图片及修改图片大小等功能。 1&#xff0c;配置使用 配置使用网上很多&#xff0c;记录下自己的使用过程 第一步&#xff1a;components/Editor文件夹下创建QuillEditor.vue文件 <template><div :class"prefixCls"><quill-edito…

bitset的用法

bitset的用法 bitset介绍 C的 bitset 在 bitset 头文件中&#xff0c;它是一种类似数组的结构&#xff0c;它的每一个元素只能是&#xff10;或&#xff11;&#xff0c;每个元素仅用&#xff11;bit空间&#xff0c;相当于一个char元素所占空间的八分之一。 bitset中的每个…

MyBatis-Plus Generator v3.5.1 最新代码自动生成器

一、概述 官网&#xff1a;https://baomidou.com/ 官方文档 &#xff1a;https://baomidou.com/pages/56bac0/ 官方源码地址&#xff1a; https://gitee.com/baomidou/mybatis-plus 官方原话&#xff1a; AutoGenerator 是 MyBatis-Plus 的代码生成器&#xff0c;通过 Auto…

【TMT数据传不到MES中间库】-F18

MES中间库有张表:T_Z_ERPSCInfo TMT机台落纱后,会把落纱的数据传到T_Z_ERPSCInfo去。 目前总是有几个机台(以F18举例),落纱了,数据没有过来。 起初以为是没有访问权限的问题,在机台上telnet ip+端口,发现没问题。 后来认为是数据库的账号有问题。 download了一份日…

Oracle EBS数据定义移植工具:FNDLOAD

在实际的EBS二次开发中&#xff0c;我们经常会碰到需要在各个环境之间移植二次开发的程序对象以及数据定义&#xff0c;如在EBS二次开发中并发请求的定义会涉及到&#xff1a; 可执行、并发程序、值集、请求组等的定义&#xff0c;定义需要从开发环境、测试环境、UAT环境一直到…

AI智慧工地视频分析系统 yolov7

AI智慧工地视频分析系统通过yolov7网络模型视频智能分析技术&#xff0c;AI智慧工地视频分析算法模型对画面中物的不安全状态以及现场施工作业人员的不合规行为及穿戴进行全天候不间断实时分析&#xff0c;发现有人不合规行为及违规穿戴抽烟打电话等立即自动抓拍存档告警。在架…

跨平台开发之 Tauri

比起 Electron&#xff0c;Tauri 打包后的安装包体积是真的小。 跨平台开发 最近使用跨平台开发框架写了一个软件&#xff0c;在此记录一下。 说起跨平台开发&#xff0c;我的理解是这样的&#xff1a; 多依赖浏览器环境运行多使用前端语言进行开发只需一次编码&#xff0c;…

JavaScript的this关键字

文章目录 一、JavaScript this 关键字总结 一、JavaScript this 关键字 面向对象语言中 this 表示当前对象的一个引用。 但在 JavaScript 中 this 不是固定不变的&#xff0c;它会随着执行环境的改变而改变。 在方法中&#xff0c;this 表示该方法所属的对象。 如果单独使用&a…

2023/4/18总结

项目 实现了服务器和客户端的连接&#xff0c;在登录注册上面。 然后去实现了密码MD5化&#xff0c;通过java自带的&#xff0c;去实现了MD5. public String getMD5(String str) throws NoSuchAlgorithmException {MessageDigest mdMessageDigest.getInstance("MD5&quo…

SSTI模板注入小结

文章目录 一、漏洞简述&#x1f37a;二、flask模板注入&#x1f37a;三、shrine&#xff08;攻防世界&#xff09;&#x1f37a;四、SSTI注入绕过&#x1f37a; 一、漏洞简述&#x1f37a; 1、SSTI&#xff08;Server-Side Template Injection&#xff0c;服务器端模板注入&am…

5个面向Python高级开发者的技巧

使用这些用于自定义类行为、编写并发代码、管理资源、存储和操作数据以及优化代码性能的高级技术来探索 Python 的深度。 本文探讨了 Python 中的五个高级主题&#xff0c;它们可以为解决问题和提高代码的可靠性和性能提供有价值的见解和技术。从允许您在定义类时自定义类行为的…

SpringBoot基础学习之(二十):Shiro与Thymeleaf的整合版本

还是一样&#xff0c;本篇文章是在上一篇文章的基础上&#xff0c;实施再次进阶 Shiro是一种特别的流行的安全框架&#xff0c;Thymeleaf则是spring boot架构中使用的一种特别引擎。今天介绍的则是它们俩的整合版本。 实现的功能&#xff1a;前端的显示的内容&#xff0c;是根…

vi/vim命令,使用vi编辑器命令详解

linux常用命令:vi/vim vi命令有三种模式&#xff1a;一般模式&#xff0c;编辑模式&#xff0c;命令模式&#xff08;底行模式&#xff09; 可以通过 vi [文件路径]文件名 的命令启动vi&#xff0c;并且打开指定的文件进行查看、编辑&#xff0c;其中[文件路径] 是可选参数。如…

微信小程序开发:实现毛玻璃效果

前言 在微信小程序开发的时候&#xff0c;也会遇到一些和在前端开发一样的样式需求&#xff0c;二者的相通类似性非常的高&#xff0c;就拿样式相关的需求来说&#xff0c;可以说是一模一样的操作。那么本文就来分享一个关于实现高斯模糊效果的需求&#xff0c;微信小程序和前端…

【Linux网络服务】FTP服务

FTP服务 一、FTP服务1.1FTP服务概述1.2FTP服务的特点1.3FTP服务工作过程 二、设置FTP服务2.1实验一&#xff1a;设置匿名用户访问FTP服务&#xff08;最大权限&#xff09;2.2实验二&#xff1a;设置本地用户验证访问ftp&#xff0c;并禁止切换到ftp以外的目录&#xff08;默认…

Linux- 进程的切换和系统的一般执行过程

我想在介绍进程切换之前&#xff0c;先引入中断的相关知识&#xff0c;它是我们理解进程切换的重要前提&#xff0c;也是Linux操作系统的核心机制。 中断的类型 • 硬件中断&#xff08;Interrupt&#xff09;&#xff0c;也称为外部中断&#xff0c;就是CPU的两根引脚&…

微服务学习-SpringCloud -Nacos (集群及CP架构相关学习)

文章目录 Nacos集群下心跳机制相对于单机会有怎样的改变&#xff1f;CAP原则和BASE原则常见的注册中心实现对比Nacos集群实现协议Nacos CP架构实现源码Nacos CP架构leader是如何选举的呢&#xff1f; Nacos集群下心跳机制相对于单机会有怎样的改变&#xff1f; 在上一遍单机模…