算法空间复杂度计算

目录

空间复杂度定义

影响空间复杂度的因素

算法在运行过程中临时占用的存储空间讲解

例子

斐波那契数列递归算法的性能分析

二分法(递归实现)的性能分析


空间复杂度定义

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。
一个算法在计算机存储器上所占用的存储空间,包括程序代码所占用的空间输入数据所占用的空间辅助变量所占用的空间这三个方面。

影响空间复杂度的因素

注意:
一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小,它包括为参数表中形参变量分配的存储空间和为在函数体中定义的局部变量分配的存储空间两个部分。若一个算法为递归算法,其空间复杂度为递归所使用的堆栈空间的大小。它等于一次调用所分配的临时存储空间的大小乘以被调用的次数(即为递归调用的次数加1,这个1表示开始进行的一次非递归调用)
递归的空间复杂度: 每次递归所开空间*深度。

算法在运行过程中临时占用的存储空间讲解

1、有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,我们称这种算法是“就地”进行的,是节省存储的算法,下面会介绍。

2、有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况。

计算方法
①忽略常数,用O(1)表示
②递归算法的空间复杂度=递归深度n*每次递归所要的辅助空间
③对于单线程来说,递归有运行时堆栈,求的是递归最深的那一次压栈所耗费的空间的个数,因为递归最深的那一次所耗费的空间足以容纳它所有递归过程。

为什么要求递归的深度呢?

        因为每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和算法里的栈原理是一样的),一次递归结束,这个栈就是就是把本次递归的数据弹出去。所以这个栈最大的长度就是递归的深度。

例子

1、空间算法的常数阶

如上图,这里有三个局部变量分配了存储空间,所以f(n) = 1 + 1 + 1 = 3,根据上面的法则该函数不受n的影响且为常数项,所以空间复杂度记作O(1)。这种与问题的大小无关(n的多少),执行时间恒定的算法,我们称之为具有O(1)的空间复杂度,又叫常数阶。

2、两个函数,空间复杂度分别为O(1), O(n)

# O(1)
def f1(n):
    j = 0
    for i in range(n):
        j += 1
    return j
 
# O(n)
def f2(n):
    a = []
    for i in range(n):
        a.append(i)
    return a

在第一个函数中,所需开辟的内存空间并不会随着n的变化而变化,即此算法空间复杂度为一个常量,所以表示为O(1)。在第二个函数中,随着n的增大,开辟的内存大小呈线性增长,这样的算法空间复杂度为O(n)。在递归的时候,会出现空间复杂度为logn的情况,比较特殊。

3、空间算法的线性阶(递归算法)

如上图,这是一个递归算法(计算从n + (n-1) + (n-2) + … + 2 + 1的和)
每当执行一次该函数就会为tmp分配一个临时存储空间,所以f(n) = 1*(n-1+1) = n,函数是受n影响的所以空间复杂度记为O(n)

斐波那契数列递归算法的性能分析

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……
这个数列从第3项开始,每一项都等于前两项之和。

如果设F(n)为该数列的第n项(n∈N*),那么这句话可以写成如下形式::F(n)=F(n-1)+F(n-2)

显然这是一个线性的递推数列。

通项公式 :

上面就是斐波那契数列的递推公式,这样一个完全是自然数的数列,通项公式却是用无理数来表达的。而且当n趋向于无穷大时,前一项与后一项的比值越来越逼近黄金分割0.618

​​​​​​​

递推是公式是求解斐波那契数列的一个方法,我们当然也可以用计算机编写程序来求解。

方法一(迭代法)
  /// <summary>
    /// 斐波那契(迭代法)
    /// </summary>
    /// <param name="n"></param>
    /// <returns></returns>
    int Fibonacci(int n)
    {
        if (n <= 0)
            return -1;
        if (n == 1 || n == 2)
            return 1;
        else
        {
            int num = 0;
            int a = 1;
            int b = 1;
            while (n - 2 > 0)
            {
                num = a + b;
                a = b;
                b = num;
                n--;
            }
            return num;
        }

    }

时间复杂度:
while以外的算法语句都忽略不计(不随n的变化而变化)
while算法语句所有语句
f(n) = 4 *(n - 2) = 4n - 8
时间复杂度记为:O(n)

空间复杂度:
算法中num、a、b只创建1次
s(n) = 1 + 1 + 1 = 3
空间复杂度记为:O(1)

方法二(递归法)

def fibonacci(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)

首先回顾一下时间复杂度:递归算法的时间复杂度 = 递归的次数 * 每次递归中的操作次数。每次递归进行了一次加法操作,时间复杂度是O(1),递归的次数可以抽象成一棵递归树:(以n=5为例)

在这棵二叉树中每一个节点都是一次递归,一棵深度(按根节点深度为1)为k的二叉树最多可以有  个节点,所以该递归算法的时间复杂度为O(2^n),这个复杂度是非常大的,随着n的增大,耗时是指数上升的。

然后再来看空间复杂度:递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度。

为什么要求递归的深度呢?因为每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和算法里的栈原理是一样的),一次递归结束,这个栈就是把本次递归的数据弹出去。所以这个栈最大的长度就是递归的深度。

回到斐波那契数列的例子,每次递归所需要的空间大小都是一样的,所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是 O(1)。递归的深度如图所示:

递归第n个斐波那契数的话,递归调用栈的深度就是n。那么每次递归的空间复杂度是O(1), 调用栈深度为n,所以这段递归代码的空间复杂度就是O(n)。

此算法时间复杂度非常高的主要原因是最后一行的两次递归,所以优化算法的方向就是减少递归的调用次数:

def fibonacci(first, second, n): #初始值 first = second = 1
    if n <= 0:
        return 0
    elif n <= 2:
        return 1
    elif n == 3:
        return first + second
    else:
        return fibonacci(second, first + second, n - 1)

举例来说 n=5 时 fibonacci(1,1,5) → fibonacci(1,2,4) → fibonacci(2,3,3) → 2+3=5。这里相当于用first和second来记录当前相加的两个数值,此时就不用两次递归了。因为每次递归的时候n减1,即只是递归了n次,所以时间复杂度是 O(n)。同理递归的深度是n-2,每次递归所需的空间还是常数,所以空间复杂度依然是O(n)。

最后总结一下:

可以看出,求斐波那契数的时候,使用递归算法并不一定在性能上是最优的,但递归确实简化了代码层面的复杂度。

二分法(递归实现)的性能分析

方法一(迭代法)
	/// <summary>
    /// 二分查找
    /// </summary>
    /// <param name="arr">查找数组</param>
    /// <param name="len">数组长度</param>
    /// <param name="num">查找项</param>
    /// <returns></returns>
    int BinarySearch(int[] arr,int len,int num)
    {
        int left = 0;
        int right = len - 1;
        int mid;
        while (left <= right)
        {
            mid = (left + right) / 2;
            if (arr[mid] > num)
                right = mid - 1;
            else if (arr[mid] < num)
                left = mid + 1;
            else
                return mid;
        }
        return -1;
    }

时间复杂度:
left、right、mid运算次数
f(n1) = 1 + 1 + 1 = 3
我们将While循环中的运算作为一个整体看待,每次都是折半运算次数
f(n2) = log2^n
总运行次数
f(all) = f(n1)+f(n2) = 3 + log2 ^ n
时间复杂度记为:O(log2^n)

空间复杂度:
算法中left、right、mid只创建的次数
s(n) = 1 + 1 + 1 = 3
空间复杂度记为:O(1)

方法二(递归法)

def binary_search(arr, l, r, x):
    if r >= l:
        mid = l + (r - l) // 2
        if arr[mid] == x:
            return mid
        elif arr[mid] > x:
            return binary_search(arr, l, mid - 1, x)
        else:
            return binary_search(arr, mid + 1, r, x)
    else:
        return -1

此算法时间复杂度为O(logn)。每次递归的空间复杂度主要就是参数里传入的这个arr数组,但需要注意的是在C/C++中函数传递数组参数,不是整个数组拷贝一份传入函数而是传入数组首元素地址。也就是说每一层递归都是公用一块数组地址空间的,所以每次递归的空间复杂度是常数即:O(1)。(Python呢?)再来看递归的深度,二分查找的递归深度是logn ,递归深度就是调用栈的长度,那么这段代码的空间复杂度为 1 * logn = O(logn)。

注意:关注语言在传递函数参数时,是拷贝整个数值还是拷贝地址,如果是拷贝整个数值那么该二分法的空间复杂度就是O(nlogn)。

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

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

相关文章

MyBatis3源码深度解析(十一)MyBatis常用工具类(四)ObjectFactoryProxyFactory

文章目录 前言3.6 ObjectFactory3.7 ProxyFactory3.8 小结 前言 本节研究ObjectFactory和ProxyFactory的基本用法&#xff0c;因为它们在MyBatis的源码中比较常见。这里不深究ObjectFactory和ProxyFactory的源码&#xff0c;而是放到后续章节再展开。 3.6 ObjectFactory Obj…

ES6(三):Iterator、Generator、类的用法、类的继承

一、迭代器Iterator 迭代器是访问数据的一个接口&#xff0c;是用于遍历数据结构的一个指针&#xff0c;迭代器就是遍历器 const items[one,two,three];//创建新的迭代器const ititems[Symbol.iterator]();console.log(it.next()); done&#xff1a;返回false表示遍历继续&a…

04_拖动文件渲染在页面中

新建一个文件夹&#xff0c;跟之前一样&#xff0c;在 Vscode 终端里输入 yarn create electron-app Drag。 在 index.html 添加以下代码&#xff0c;JS 文件夹和 render.js 都是新创建的&#xff1a; 首先&#xff0c;css 文件一般和 html 结合使用&#xff0c;相当于 html 是…

Prometheus 监控系统

目录 概述 Prometheus定义 Prometheus 的特点 Prometheus 的生态组件 Prometheus 的工作模式 Prometheus 的工作流程 Prometheus 的局限性 1.部署 Prometheus 上传prometheus包二级制安装 配置系统启动文件&#xff0c;启动 Prometheust 2.部署 Exporters 上传node…

[Spark SQL]Spark SQL读取Kudu,写入Hive

SparkUnit Function&#xff1a;用于获取Spark Session package com.example.unitlimport org.apache.spark.sql.SparkSessionobject SparkUnit {def getLocal(appName: String): SparkSession {SparkSession.builder().appName(appName).master("local[*]").getO…

探索考古文字场景,基于YOLOv8全系列【n/s/m/l/x】参数模型开发构建文本考古场景下的甲骨文字符图像检测识别系统

甲骨文是一种非常历史悠久的古老文字&#xff0c;在前面我们基本上很少有涉及这块的内容&#xff0c;最近正好在做文字相关的项目开发研究&#xff0c;就想着基于甲骨文的场景来开发对应的检测识别系统&#xff0c;在前文中我们基于YOLOv5、YOLOv7和YOLOv9开发构建了在仿真数据…

c++:类和对象中:拷贝构造和赋值运算符重载详解

c:类和对象 构造函数和析构函数详解 文章目录 c:类和对象构造函数和析构函数详解 前言一、拷贝构造怎么写拷贝构造1.拷贝构造也是构造函数的一种,构造函数没有值.所以拷贝构造也没有返回值**2.拷贝构造只有一个形参,正常这个形参是自定义类型对象的引用.3. 如果我们没有显示写…

Excel判断CD两列在EF两列的列表中是否存在

需求 需要将CD两列的ID和NAME组合起来&#xff0c;查询EF两列的ID和NAME组合起来的列表中是否存在&#xff1f; 比如&#xff0c;判断第二行的“123456ABC”在EF的第二行到第四行中是否存在&#xff0c;若存在则显示Y&#xff0c;不存在则显示N 实现的计算公式 IF(ISNUMBER…

文字弹性跳动CSS3代码

文字弹性跳动CSS3代码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c;重定向这个界面 下载地址 文字弹性跳动CSS3代码

YOLOv9实例分割教程|(一)训练教程

专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;主力高效涨点&#xff01;&#xff01;&#xff01; 一、创建数据集及数据配置文件 创新一个文件夹存放分割数据集&#xff0c;包含一个images和labels文件夹。标签格式如下所示&#xff1a; 创新数据集…

Linux——文件系统与软硬链接

目录 前言 一、物理上的磁盘 二、磁盘存储的逻辑抽象结构 三、块组内容与工作原理 1.inode 2.Data bolcks 3.inode 位图 4.bolck bitmap 5.GDT 6.Super Block 四、软硬链接 五、软硬链接链接目录 前言 我们之前学习了文件标识符&#xff0c;重定向&#xff0c;缓…

Spring框架----AOP全集

一&#xff1a;AOP概念的引入 首先我们来看一下登录的原理 如上图所示这是一个基本的登录原理图&#xff0c;但是如果我们想要在这个登录之上添加一些新的功能&#xff0c;比如权限校验 那么我们能想到的就有两种方法&#xff1a; ①&#xff1a;通过对源代码的修改实现 ②&a…

Android实现点击获取验证码60秒后重新获取功能

本文实例为大家分享了Android实现点击获取验证码60秒后重新获取的具体代码&#xff0c;供大家参考&#xff0c;具体内容如下 上代码 /*** Created by Xia_焱 on 2017/5/7.*/public class CountDownTimerUtils extends CountDownTimer {private TextView mTextView;/*** param…

Requests教程-17-请求代理设置

上一小节我们学习了requests解决乱码的方法&#xff0c;本小节我们讲解一下requests设置代理的方法。 代理基本原理 代理实际上指的就是代理服务器&#xff0c; 英文叫作proxy server &#xff0c;它的功能是代理网络用户去取得网络信息。形象地说&#xff0c;它是网络信息的中…

Android Studio入门——页面跳转

1.工程目录 2.MainActivity package com.example.demo01;import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.TextView;import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCo…

前端开发小技巧 - 【Vue3 + TS】 - 在 TS + Vue3 中使用 Pinia,实现 Pinia 的持久化,优化Pinia(仓库统一管理)

前言 ts 中使用 pinia 和 Vue3 基本一致&#xff0c;唯一的不同点在于&#xff0c;需要根据接口文档给 state 标注类型&#xff0c;也要给 actions 标注类型&#xff1b;以下都是 组合式API 的写法&#xff0c;选项式API 的写法大家可以去官网看看&#xff1b; Pinia&#xff…

Text-to-SQL 工具Vanna进阶|数据库对话机器人的多轮对话

跟数据库对话机器人对话,我可不止一个问题。 可能基于第一句问话,还有第二句、第三句问话。。。第N句对话。所以本文测试了多轮对话功能。 单轮对话的环境搭建参考博客 Text-to-SQL 工具Vanna + MySQL本地部署 | 数据库对话机器人 我的数据是这样 1. 基础配置 import vann…

深入解析Java中锁机制以及底层原理

一、概述 1.1 背景 概念&#xff1a;锁是多线程编程中的机制&#xff0c;用于控制对共享资源的访问。可以防止多个线程同时修改或读取共享资源&#xff0c;从而保证线程安全。 作用&#xff1a;锁用于实现线程间的互斥和协调&#xff0c;确保在多线程环境下对共享资源的访问顺…

前端性能优化——javascript

优化处理&#xff1a; 讲javascript脚本文件放到body标记的后面 减少页面当中所包含的script标记的数量 课堂练习&#xff1a; 脚本优化处理 使用原生JavaScript完成操作过程。 document.querySelector document.querySelectorAll classList以及类的操作API Element.class…

LLM之RAG实战(二十九)| 探索RAG PDF解析

对于RAG来说&#xff0c;从文档中提取信息是一种不可避免的场景&#xff0c;确保从源文件中提取出有效的内容对于提高最终输出的质量至关重要。 文件解析过程在RAG中的位置如图1所示&#xff1a; 在实际工作中&#xff0c;非结构化数据比结构化数据丰富得多。如果这些海量数据无…
最新文章