数据结构之时间复杂度和空间复杂度的相关计算

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏:数据结构(Java版)

目录

时间复杂度 

概念

大O的渐进表示法

相关练习 

例1:

例2:

例3:

例4:

例5:

例6: 

例7: 

例8:

空间复杂度

概念:

相关练习:

例1:

例2:

例3: 


接下来,将开始数据结构的学习了。

我们如果衡量一个算法的好坏呢?这个算法到底怎么样呢?

有的小伙伴可能会说:直接把这个代码拷贝到编译器中,看看运行时间是多少,不就行了嘛。的确这个方法在同样的情况下确实可以。比如说:同样是计算斐波那契数列的第 n 项。下面有两份代码,我们就可以把它们分别给到编译器,让其运行看时间是多少?

public class Test {
    public static int fib(int n) {
        // 递归求
        if (n < 2) {
            return 1;
        }
        return fib(n-1) + fib(n-2);
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        System.out.println(fib(n));
    }
}

public class Test {
    public static int fib(int n) {
        // 迭代求
        int c = 0;
        int a = 1;
        int b = 1;
        if (n < 2) {
            return b;
        }
        for (int i = 2; i <= n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        System.out.println(fib(n));
    }
}

由上可见:用迭代求斐波那契数列的第 n 项比用递归求效率更高,也就是说迭代的算法思想在这里的应用更好。

但是如果每一个代码都用编译器去跑,那就有点浪费时间了,并且还不一定准确。因为电脑的处理器不一样,效率肯定也是不一样的。因此,就提出了用时间复杂度的概念和空间复杂度的概念来重新作为这个标准。 下面就来介绍这两个概念。

时间复杂度 

概念

什么是时间复杂度?时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

大O的渐进表示法

概念我们已经知道了,那么接下来就该了解,时间复杂度的计算了。时间复杂度就是通过代码的执行次数来确定的。实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。

当推导出了代码的执行次数之后,就需要用到下面的规则,来简化得到最终的表达式。

规则:

1、用常数1取代运行时间中的所有加法常数。

2、在修改后的运行次数函数中,只保留最高阶项。

3、如果 最高阶项的系数 不是1,则把这个系数变成1。得到的结果就是大O阶。

相关练习 

求下列代码的时间复杂度。

例1:

    void func1(int N) {
        int count = 0;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                count++;
            }
        }
        for (int k = 0; k < 2 * N; k++) {
            count++;
        }
        int M = 10;
        while ((M--) > 0) {
            count++;
        }
        System.out.println(count);
    }

所以最终的执行次数是: N^2 + 2N + 10 。这个结果肯定是符合上面的化简规则的。

首先,根据规则1,把10变成了1,N^2 + 2N + 1 ;

其次,根据规则2,只保留最高阶项,N^2 ;

最后,去掉最高阶项的系数。因为这里的最高阶项的系数是1,因此就不变:O(N) 。

这里也可以证明一下这个规则:

因为这个N的取值是不固定的,如果这个N的取值是100的话,那么这个10对最终结果的影响不是很大。因此可以用1来代替。既然N的取值可以是100,那也就是10000,甚至更大。我们在数学中学过随着X的增大,X^2 与 2X的差值同样是越来越大。因此 2X 也是可以舍去的。因此最终就只剩下了最高阶项,同样其系数对齐的影响同样不大。

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

例如:在一个长度为N数组中搜索一个数据 x 最好情况:1次找到 ;最坏情况:N次找到。平均情况:N/2次找到。

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

例2:

    void func2(int N) {
        int count = 0;
        for (int k = 0; k < 2 * N ; k++) {
            count++;
        }
        int M = 10;
        while ((M--) > 0) {
            count++;
        }
        System.out.println(count);
    }

例3:

    void func3(int N, int M) {
        int count = 0;
        for (int k = 0; k < M; k++) {
            count++;
        }
        for (int k = 0; k < N ; k++) {
            count++;
        }
        System.out.println(count);
    }

注意:时间复杂度计算的是执行次数最多的,但是这里的M和N并未表明具体值,因此都不能省略。

例4:

    void func4(int N) {
        int count = 0;
        for (int k = 0; k < 100; k++) {
            count++;
        }
        System.out.println(count);
    }

注意:这里的 N 其实就是用来迷惑我们的。 

例5:

    void Swap(int[] array, int i , int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    void bubbleSort(int[] array) {
        for (int end = array.length; end > 0; end--) {
            boolean sorted = true;
            for (int i = 1; i < end; i++) {
                if (array[i - 1] > array[i]) {
                    Swap(array, i - 1, i);
                    sorted = false;
                }
            }
            // 如果 sorted 为true,就说明这组数据已经是有序的了
            if (sorted) {
                break;
            }
        }
    }

变型:刚刚我们求的是最坏的情况,现在我们来求最好的情况。

首先,得想一下什么时候冒泡排序的情况最好,既然前面,我们在算最坏的情况是是每一个都要执行,也就是说数组中元素的顺序是刚好和我们要排的序是相反的,因此每一项都得重新排序。那么最好的情况也就可以分析的出来了,就是当这个数组中元素的顺序刚好和我们要排的序是相同的,也就是说这个数组已经是有序的了。怎么知道有序呢?就是通过这个 sorted 来判断的,如果是true,就说明已经有序;否则,就是无序。要知道有序,肯定得把这个数组遍历一遍才行。那么执行的次数就是N次,时间复杂度就是:O(N) = N 。

例6: 

    int binarySearch(int[] array, int value) {
        int begin = 0;
        int end = array.length - 1;
        while (begin <= end) {
            int mid = begin + ((end-begin) / 2);
            if (array[mid] < value)
                begin = mid + 1;
            else if (array[mid] > value)
                end = mid - 1;
            else
                return mid;
        }
        return -1;
    }

上面是一个二分查找的代码,和我们前面的代码有点不一样。这个是 while循环,没有明确表明循环内部代码的执行次数,而 for循环明确表明了会执行多少次。同样计算时间复杂度是按照最坏的情况来分析的,二分查找,什么情况最坏呢? 就是当我们找到了只剩下最后一个元素的时候,这个时候已经没有其他元素了,只有这个元素了或者找不到。只要比较一下,就可以了。

至于为什么在元素个数为1时就可以停止查找,这是因为二分查找的核心思想是通过不断缩小查找区间来定位目标值。当区间缩小到只包含一个元素时,这个元素要么就是我们要找的目标,要么就说明目标不存在于数组中(如果是在查找过程中没有提前终止的话)。在这种情况下,我们不需要也不应该再继续“分半”查找,因为没有更小的区间可以探索了,直接判断这最后一个元素是否为目标即可。因此当只剩下一个元素的时候,不需要再查找了,只需要比较就行了。而比较是在查找的代码次数中,因此不需要再+1了。

数组元素个数为 N

设查找的次数为 X 时,此时元素个数为 1,

那么可得出:N /  2 ^​ X = 1

⇒ N= 2^X

log2 ​N=log2​  2^X = X

X = log2​ N

因此查找的次数就是x,那么对应的时间复杂度也就是:O(log2 N)(已是最简:无常数项、只有一项,无需化简)。 

注意:这里的log2 N,是表示以2为底,N的对数。但是有时候会把这个简写成lg N ,我们也知道这个是以10为底,N的对数,这个也算是对的,注意一下就行了。

例7: 

    long factorial(int N) {
        return N < 2 ? N : factorial(N-1) * N;
    }

递归的时间复杂度 = 递归的次数 * 递归中的代码执行的次数。

因为递归其实也算是另一种意义上的迭代(循环),递归的次数就是外层循环的次数,每一次递归的之后执行的次数就是内层循环执行的次数。

递归执行了N-1次,根据规则化简:常数去掉。因此递归的时间复杂度是:O(N)

注意:这里可能有小伙伴会疑惑:递归的过程中递是1次,归也是一次,不应该递归的次数 X 2吗? 不不不,递归在递的时候,那并没有执行完1次,执行到一半的时候就递下去了,只有归的时候这1次才算执行完毕了。

例8:

    int fibonacci(int N) {
        return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
    }

再经过规则化简(去掉常数),最终的时间复杂度就是O(2^N) 。

上面就是一系列有关时间复杂度的练习。下面我们再来学习空间复杂度。

空间复杂度

概念:

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少字节的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。

同样直接通过练习来感受一下吧。

相关练习:

例1:

    public void Swap(int[] array, int i , int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
    void bubbleSort(int[] array) {
        for (int end = array.length; end > 0; end--) {
            boolean sorted = true;
            for (int i = 1; i < end; i++) {
                if (array[i - 1] > array[i]) {
                    Swap(array, i - 1, i);
                    sorted = false;
                }
            }
            if (sorted) {
                break;
            }
        }
    }

计算空间复杂度就算看这个代码为了做这件事,创建了多少个临时变量。

end、sorted、i、Swap中的i、j 、tmp这些都是的,总共是6个。因为采用的是大O渐进表示法,所以常数化为1,最终的空间复杂度就是O(1)。

注意:

1. 可能有小伙伴会说这个数组为什么不算呢?因为这个数组是存储在堆区的,而我们只是创建了一个形参引用(不过这个形参得考虑进去,但因为是粗略估计,所以可以不算)来指向这个数组而已。并没有真正地在堆区用一块空间来创建数组。

2. 这个end 和 i 并不是说创建了100次,就占用了100分临时空间来存储它,永远只有一份空间,只不过这个空间中的值会发生变化而已。

例2:

    long[] fibonacci(int N) {
        long[] fibArray = new long[N + 1];
        fibArray[0] = 0;
        fibArray[1] = 1;
        for (int i = 2; i <= N ; i++) {
            fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
        }
        return fibArray;
    }

上述代码是为了计算第n个斐波那契数。但是却创建了一个数组。因此空间中的临时变量个数是N+1、i 。根据规则(去掉常数):O(N)。

例3: 

    long factorial(int N) {
        return N < 2 ? N : factorial(N-1)*N;
    }

上述代码是通过递归来计算第N个斐波那契数。每一次递归都会创建一份新的临时空间,递归N次,会创建N-1分临时空间,再加上原本的空间,就是N分空间,因此最终的结果就是O(N)。

上面就是时间复杂度和空间复杂度的全部内容啦!

好啦!本期 初始Java篇(JavaSE基础语法)(8)认识String类(下)的学习之旅就到此结束了! 我们下一期再一起学习吧!

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

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

相关文章

可转债日内自动T+0交易,行情推送+策略触发+交易接口

说明 目前这个项目已编译打包,下载即可测试,直接生成多平台可执行文件&#xff0c;详见运行方法。行情部分与策略弱相关&#xff0c;拆分解耦单独作为一个项目。行情项目请移步GitHub - freevolunteer/hangqing: A股行情订阅工具&#xff0c;支持股票/可转债level2/level2数据&…

MySQL主从复制(三):主从延迟

主备流程图&#xff1a; 谈到主备的复制能力&#xff0c;要关注的是上图中的两个黑色箭头。 一个箭头代表了客户端写入主库&#xff0c;另一个箭头代表的是sql_thread执行中转日志&#xff08;relay log&#xff09;。如果用箭头的粗细来代表并行度的话&#xff0c;那么真实情…

svg中path的直线命令使用

path路径 <path>元素是SVG基本形状中最强大的一个。可以使用它来创建线条&#xff0c;曲线&#xff0c;弧形等等。 另外&#xff0c;path只需要设定很少的点&#xff0c;就可以创建平滑流畅的线条&#xff08;比如曲线&#xff09;。虽然polygon元素也可以实现类似的效…

UPPAAL使用方法

UPPAAL使用方法 由于刚开始学习时间自动机及其使用方法&#xff0c;对UPPAAL使用不太熟悉&#xff0c;网上能找到的教程很少&#xff0c;摸索了很久终于成功实现一个小例子&#xff0c;所以记录一下详细教程。 这里用到的例子参考【UPPAAL学习笔记】1&#xff1a;基本使用示例…

linux-配置服务器之间 ssh免密登录

前言 在管理多台Linux服务器时,为了方便操作和自动化任务,实现服务器之间的SSH免密登录是非常有必要的。SSH免密登录可以避免每次远程连接时输入密码,大大提高效率。本文将详细介绍SSH免密登录的原理和实现步骤。 一、原理解释 SSH免密登录的实现依赖于SSH密钥对,主要是利用…

2.行为参数的演变过程

2.行为参数的演变过程 ​ 行为参数化是软件开发模式&#xff0c;可以处理频繁变更的需求。它让你把一个代码块准备好但不执行&#xff0c;以后可以被其他部分调用&#xff0c;也可以作为参数传递给另一个方法&#xff0c;推迟执行。这样&#xff0c;方法的行为就基于参数化的代…

O2OA(翱途)开发平台数据统计如何配置?

O2OA提供的数据管理中心&#xff0c;可以让用户通过配置的形式完成对数据的汇总&#xff0c;统计和数据分组展现&#xff0c;查询和搜索数据形成列表数据展现。也支持用户配置独立的数据表来适应特殊的业务的数据存储需求。本文主要介绍如何在O2OA中开发和配置统计。 一、先决…

03-ArcGIS For JavaScript结合ThreeJS功能

ArcGIS For JavaScript结合ThreeJS功能 概述three.js中功能实现externalRenderers&#xff08;4.28及以下版本&#xff09;RenderNode&#xff08;4.29版本&#xff09; 概述 ArcGIS For Javacript提供了一些对象可以支持加载webgl上下文信息&#xff0c;这里包括webgl编程的代…

汽车IVI中控开发入门及进阶(二十):显示技术之LCDC

TFT LCD=Thin Film Transistor Liquid Crystal Display LCDC=LCD Controller 薄膜晶体管液晶显示器(TFT LCD)控制器在驱动现代显示技术的功能和性能方面起着关键作用。它们充当屏幕后面的大脑,仔细处理数字信号,并将其转化为精确的命令,决定每个像素的行为,决定它们的…

Linux中gcc/g++的基本使用

目录 gcc/g的使用gcc/g是如何生成可执行文件的预处理编译汇编链接 库.o文件是如何与库链接的&#xff1f; debug版本和release版本 gcc/g的使用 在windows中&#xff0c;我们在VS中编写好了代码之后就可以直接在VS中对源码进行编译等操作后运行 而在Linux下&#xff0c;我们可…

【区域脑图论文笔记】BrainNetCNN:第一个专门为脑网络连接体数据设计的深度学习框架

【区域脑图论文笔记】BrainNetCNN&#xff1a;第一个专门为脑网络连接体数据设计的深度学习框架 信息概览与提炼采用的数据与结果数据集结果概览一眼 重点图与方法概览核心与优劣总结模型与实验论文方法E2E的理解E2N的理解N2G的理解三个卷积层设计的理解 论文实验与讨论 总结与…

差分约束题解

目录 注意点&#xff1a; 思路&#xff1a; SPFA和Dij的不同点&#xff1a; Dij: SPFA: AC代码&#xff1a; 扩展&#xff1a; 题目链接&#xff1a;【模板】差分约束 - 洛谷 注意点&#xff1a; 注意这一题不能用Dij&#xff0c;只能用SPFA 因为这样子才可以得出这个不…

【源码】AVATRADE多语言交易所/15国语言交易所/合约交易/期权交易/币币交易/申购/矿机/风控/前端wap/pc纯源码/带搭建教程

推荐AVATRADE多语言交易所/15国语言交易所/合约交易/期权交易/币币交易/申购/矿机/风控/前端wap/pc纯源码/带搭建教程 语言&#xff1a;15种语言 前端pcwap都是纯源码的 workerman启动有点问题&#xff0c;采集是正常的&#xff0c;wss不能推送。时好时坏&#xff0c;有时候…

树莓派开发需要安装哪些常用库

树莓派是一系列小型、低成本、高性能的单板计算机&#xff08;SBC&#xff09;&#xff0c;旨在促进编程、计算机科学和DIY电子项目。 从英国慈善机构树莓派基金会于 2012 年推出第一代树莓派开始&#xff0c;树莓派被广泛应用于各种项目&#xff0c;包括&#xff1a; 学习和教…

unreal engine 5.0.3 创建游戏项目

根据虚幻官网介绍&#xff0c;虚幻引擎5可免费用于创建线性内容、定制项目和内部项目。你可以免费用它开发游戏&#xff0c;只有当你的产品营收超过100万美元时&#xff0c;才收取5%的分成费用。所以目前国内也有许多游戏厂商在使用UE制作游戏。UE5源码也已开源&#xff0c;有U…

JavaScript表达式和运算符

表达式 表达式一般由常量、变量、运算符、子表达式构成。最简单的表达式可以是一个简单的值。常量或变量。例&#xff1a;var a10 运算符 运算符一般用符号来表示&#xff0c;也有些使用关键字表示。运算符由3中类型 1.一元运算符&#xff1a;一个运算符能够结合一个操作数&…

RFID技术在空调生产流程自动化中的前沿探索

RFID技术在空调生产流程自动化中的前沿探索 应用背景 目前经济环境下&#xff0c;由卖方市场转向买方市场&#xff0c;意味着小批量、多频率、个性化的生产模式日益成为制造业企业面临的一大难题&#xff0c;随着个性化需求的不断增长&#xff0c;大部分空调厂商都选择小批量…

云上聚智——移动云云服务器进行后端的搭建及部署

什么是移动云 移动云是指将移动设备和云计算技术相结合&#xff0c;为移动应用提供强大的计算和存储能力的服务模式。传统的移动应用通常在本地设备上进行计算和存储&#xff0c;而移动云将这些任务转移到云端进行处理。通过移动云&#xff0c;移动设备可以利用云端的高性能计算…

Go团队:Go是什么

2024年的Google I/O大会[1]如期而至。 这届大会的核心主旨毫无疑问是坚定不移的以AI为中心&#xff1a;Google先是发布了上下文长度将达到惊人的200万token的Gemini 1.5 Pro[2]&#xff0c;然后面对OpenAI GPT-4o的挑衅&#xff0c;谷歌在大会上直接甩出大杀器Project Astra[3]…

【加密与解密(第四版)】第十九章笔记

第十九章 外壳编写基础 这章主要是完成一个壳&#xff0c;之前这章看的次数比较多&#xff0c;这里仅仅记录一下关键点 19.1 外壳的结构 19.2 加壳主程序 流程&#xff1a;判断文件是否为PE格式、文件基本数据读入、附加数据的读取、输入表的处理、重定位表的处理、文件的压缩…