数据结构与算法:复杂度

友友们大家好啊,今天开始正式学习数据结构与算法有关内容,后续不断更新数据结构有关知识内容,希望多多支持!

数据结构:
数据结构是用于存储和组织数据的方式,以便可以有效地访问和修改数据。不同的数据结构适用于不同类型的应用,并且具体的数据结构可以大幅影响程序的性能。数据结构分为两大类:线性数据结构和非线性数据结构。

算法:
算法是完成特定任务的一系列操作步骤,是解决问题的明确规范。算法的效率通常通过时间复杂度和空间复杂度来评估,即算法执行所需的时间和空间资源。

那么本节课我们进入第一节,复杂度!

复杂度

  • 时间复杂度
    • 大O的渐进表示法
    • 常见时间复杂度计算举例:
  • 空间复杂度
    • 例(十)计算Fibonacci的空间复杂度

算法效率
算法效率通常是指算法运行所需的资源量,评价算法效率主要依据两个重要指标:时间复杂度和空间复杂度

时间复杂度

时间复杂度是在计算机科学中衡量一个算法执行所需时间的量化指标。更准确来说,它不直接度量实际的时间(如秒或毫秒),而是衡量算法需要执行的操作步骤数量。计算时间复杂度通常假设每个基本操作的执行时间是固定和相同的,即使在现实中不同的操作可能需要不同的时间。算法中的基本操作的执行次数,为算法的时间复杂度

即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

例:请计算一下Func1中++count语句总共执行了多少次?

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--)
{++count;
}
printf("%d\n", count);
}

Func1 执行的基本操作次数F(N)=N2+2*N+10;

  • N=10,F(N)=130
  • N=100,F(N)=10210
  • N=1000,F(N)=1002010

实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法

大O的渐进表示法

大O渐进表示法是数学和计算机科学中用来描述函数增长率的一种表示方法。它是分析算法复杂度(如时间复杂度和空间复杂度)时最常用的工具之一。大O表示法通过忽略常数因子和低阶项,专注于描述最主要的影响因素,从而提供了一种比较算法效率的方法。

定义

大O符号,记作O(f(n)),表示随着输入大小n的增加,算法的运行时间或所需空间的增长率与f(n)增长率相同或者更慢。在这里,f(n)是一个数学函数,代表随着输入规模n的变化,算法的资源消耗如何变化。

关键概念

  • 最坏情况分析:大O通常表示最坏情况下的复杂度,即算法在任何输入上的性能不会比这个界限更差。

  • 渐进上界:大O表示的是一个上界,说明了算法复杂度的一个上限。

  • 忽略非主要项:在大O表示法中,我们只关注主要项(即最大影响项),忽略常数因子和低阶项。

推导大O阶方法

  • 用常数1取代运行时间中的所有加法常数
  • 在修改后的运行次数函数中,只保留最高阶项
  • 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶

有些算法的时间复杂度存在最好、平均和最坏情况(例四):

  • 最坏情况:任意输入规模的最大运行次数(上界)
  • 平均情况:任意输入规模的期望运行次数
  • 最好情况:任意输入规模的最小运行次数(下界)

例如上述例题:
Func1 执行的基本操作次数F(N)=N2+2*N+10;

使用大O的渐进表示法以后,Func1的时间复杂度为:O(N2)

  • N = 10 F(N) = 100
  • N = 100 F(N) = 10000
  • N = 1000 F(N) = 1000000

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

常见时间复杂度计算举例:

例(一):计算Func2的时间复杂度

void Func2(int N)
{int count = 0;for (int k = 0; k < 2 * N ; ++ k){++count;}int M = 10;while (M--){++count;}printf("%d\n", count);
}

基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为 O(N)

例(二):计算Func3的时间复杂度

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;}printf("%d\n", count);
}

基本操作执行了M+N次,有两个未知数M和N,时间复杂度为 O(N+M)

例(三):计算Func4的时间复杂度

void Func4(int N)
{int count = 0;for (int k = 0; k < 100; ++ k){++count;}printf("%d\n", count);
}

基本操作执行了10次,通过推导大O阶方法,时间复杂度为 O(1)

O(1)代表常数次!!!

例(四):计算strchr的时间复杂度

const char * strchr ( const char * str, int character )
{
while(*str)
{if(*str==character)return str;++str;
}
}

这道题就涉及最好情况,平均情况和最坏情况

  • 最好情况
    最好情况发生在要查找的字符 character 正好是字符串 str 的第一个字符。此时,循环会在第一次迭代时找到匹配,立即返回指向该字符的指针。在这种情况下,该函数的时间复杂度为 O(1),因为无论字符串多长,只需进行一次比较操作。

  • 平均情况:
    平均情况会假设字符在字符串中均匀分布或者一定概率出现在任何位置。由于字符可以出现在字符串的任何位置,因此平均而言,我们可能需要检查字符串的一半才能找到字符或确定字符不在字符串中。平均来看,时间复杂度与字符串的长度成正比,即 O(N/2),但由于大O表示法忽略常数因子,因此简化为 O(N),其中 N 是字符串的长度。

  • 最坏情况
    最坏情况发生在两种情况下:

    • 要查找的字符不存在于字符串中,则必须遍历整个字符串直至终结符 ‘\0’,进行 N 次比较,其中 N 是字符串的长度。
    • 要查找的字符正好是字符串的最后一个字符(紧邻终结符 ‘\0’),这同样需要遍历整个字符串。

时间复杂度一般看最坏所以时间复杂度为 O(N)

例(五):计算BubbleSort的时间复杂度

void BubbleSort(int* a, int n)
{assert(a);for (size_t end = n; end > 0; --end){int exchange = 0;for (size_t i = 1; i < end; ++i){if (a[i-1] > a[i]){Swap(&a[i-1], &a[i]);exchange = 1;}}if (exchange == 0)break;}
}

这道题中有两层循环
最坏的情况发生在数组完全逆序。在这种情况下,我们需要对每一个元素做完整的n-1次比较和可能的交换,然后是n-2次,直到最后一次比较。集合中的比较次数 T 可以用以下等式来表示:

T = (n-1) + (n-2) + ... + 2 + 1 = n(n-1)/2

当 n 逐渐增加到非常大时,n2项占据了主导,因此我们可以将时间复杂度简化为 O(n2)

例(六):计算BinarySearch的时间复杂度**(二分查找)**

int BinarySearch(int* a, int n, int x)
{assert(a);int begin = 0;int end = n-1;while (begin <= end){int mid = begin + ((end-begin)>>1);if (a[mid] < x)begin = mid+1;else if (a[mid] > x)end = mid-1;elsereturn mid;}return -1;
}

在每次迭代中,二分查找都会将搜索范围减半。如果数组的大小为n,则迭代如下:

  • 第一次迭代后,搜索范围减为n/2。
  • 第二次迭代后,搜索范围减为n/4。

这一过程持续进行,直到搜索范围无法再分割(即begin > end)。划分过程的次数可以用对数函数表示:

n, n/2, n/4, ..., 1

当我们达到1时,相当于进行了k次迭代,那么n/2k = 1。解这个等式,得到n = 2k。取对数可得k = log₂(n)。

因此,二分查找的时间复杂度是O(log n)。

注意:这里的对数底数是2是因为每次迭代都将搜索区间分为两部分。二分查找的效率与目标值的实际位置无关,从最坏情况来看总是O(log n)。

例(七) 计算阶乘递归Fac的时间复杂度

long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}

每次递归调用减少 N 的值,直到 N 达到 0。对于每个 N,函数只进行一次递归调用。因此,如果初始值为 N,那么会有 N 次递归调用。所以这个函数的时间复杂度是 O(N)。

例(八) 计算斐波那契递归Fib的时间复杂度

long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}

在这里插入图片描述
以 Fib(5) 为例,其递归调用树大致如下:

         Fib(5)/      \Fib(4)     Fib(3)/    \      /    \
Fib(3) Fib(2) Fib(2) Fib(1)/   \
Fib(2) Fib(1)

从这个树状结构中我们可以看到:

  • Fib(5) 分解为 Fib(4) 和 Fib(3)
  • Fib(4) 分解为 Fib(3) 和 Fib(2)
  • Fib(3) 分解为 Fib(2) 和 Fib(1)

以此类推,直到基础情况 Fib(1) 或 Fib(2),返回 1。

注意在这棵树中,Fib(3) 被计算了两次,Fib(2) 被计算了三次。随着 N 的增大,重复计算的问题会指数性增长。

在这样的递归调用中,每增加一个 N,递归树的层数加一,每一层的结点数几乎是上一层的两倍(除了在接近底部,当 N 小于 3 时,不再继续拆分)。

因此,如果我们考虑每个函数调用是树中的一个节点,那么整个递归过程涉及的节点总数(即函数调用的总数)大约是一个满二叉树中的节点数,这是因为除了最底层,几乎每个节点都会分裂成两个子节点。

满二叉树的节点总数与树的深度(在这里即N)有关,大约是 2N(为简化分析,这里忽略了精确的计数,特别是在树的最底层)。因此,递归解决方案的时间复杂度被认为是指数级的,即 O(2N)。

空间复杂度

空间复杂度是一个衡量算法在运行过程中临时占用存储空间大小的一个量度,它和时间复杂度一样,是用来评价算法效率的一个重要指标。空间复杂度不仅包括在算法执行过程中,输入和输出所占据的空间,还包括算法执行过程中临时占用的额外空间。

空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

举例如下:

例(九) 计算BubbleSort的空间复杂度

void BubbleSort(int* a, int n)
{
assert(a);for (size_t end = n; end > 0; --end){int exchange = 0;for (size_t i = 1; i < end; ++i){if (a[i-1] > a[i]){Swap(&a[i-1], &a[i]);exchange = 1;}}if (exchange == 0)break;}
}

BubbleSort的空间复杂度计算主要依据算法在执行过程中所需的额外内存空间。在讨论空间复杂度时,我们关注的是除了输入数据本身占用的空间外,算法运行时所需的附加空间。这通常包括栈空间(用于存储函数调用和局部变量)和堆空间(用于动态内存分配)。然而,在冒泡排序的实现中,我们主要关注的是栈空间的使用。

让我们逐一查看 BubbleSort 函数中的元素:

  1. 局部变量
    • end: 用于标记数组中尚未排序部分的末尾。
    • exchange: 用于标记在一次遍历中是否发生了交换,以此判断数组是否已经排序完成。
    • i: 循环计数器,用于遍历数组中的元素。
  2. 参数:
    • a: 指向数组的指针,它引用了函数外部的数组,因此不增加内部空间复杂度。
    • n: 表示数组的大小,是一个整型值。
  3. 递归调用:
    冒泡排序是一个迭代算法,不涉及递归调用,因此不会因为递归调用导致栈空间显著增加。
  4. 动态分配的内存:
    在此实现中,没有动态分配的内存;算法仅在原始数组上进行操作。

计算空间复杂度

  • 固定大小的局部变量: end、exchange 和 i 是固定大小的整型变量,它们占用的空间量与数组的大小 n 无关。
  • 无递归调用: 算法不使用递归,因此不会因为递归调用而在栈上占用额外的空间。
  • 无动态内存分配: 算法运行过程中没有使用如 malloc, new 等动态内存分配函数,因此不会在堆上占用额外的空间。

BubbleSort 函数的空间复杂度主要由固定数量的局部变量决定,这意味着它的空间需求不随输入数组的大小 n 而变化。根据空间复杂度的定义,我们可以得出 BubbleSort 的空间复杂度是 O(1),即常量空间复杂度。

例(十)计算Fibonacci的空间复杂度

long long* Fibonacci(size_t n)
{if(n==0)return NULL;long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));fibArray[0] = 0;fibArray[1] = 1;for (int i = 2; i <= n ; ++i){fibArray[i] = fibArray[i - 1] + fibArray [i - 2];}return fibArray;返回斐波那契数列的前n项
}

空间复杂度分析

  1. 动态内存分配:

    • 这个函数中最重要的部分是 fibArray 的动态内存分配。分配的空间大小直接与输入 n 成正比。即 fibArray 的大小是 n+1 个 long long 类型的大小。
      固定大小的局部变量:
  2. i 是一个整型变量,它的大小是固定的,与 n 无关。

因为函数的主要内存消耗来自于与输入大小成正比的 fibArray,所以 Fibonacci 函数的空间复杂度是 O(n)。这表明所需的存储空间随着输入 n 的增长而线性增长。

例(十一) 计算阶乘递归Fac的空间复杂度

long long Fac(size_t N)
{if(N == 0)return 1;return Fac(N-1)*N;
}

空间复杂度分析

  1. 递归调用:
    • 这个函数的关键在于它使用了递归调用。每一次递归调用都需要在调用栈上增加一层,用于保存当前调用的状态(包括局部变量和参数)。
  2. 递归深度:
    • 递归的深度直接与输入参数 N 相关。对于每个大于 0 的 N,都会产生一个递归调用,直到 N 降至 0。

由于递归调用会造成调用栈大小随 N 线性增长,因此 Fac 函数的空间复杂度是 O(N)。这意味着随着 N 的增大,函数的内存占用也以线性方式增加。

第一节知识点大概到此结束,后续我们会根据遇见的题再进行分析,感谢大家的阅读!!!!

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

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

相关文章

E5071C 是德科技网络分析仪

181/2461/8938产品概述&#xff1a; E5071C ENA 矢量网络分析仪&#xff0c;9 kHz 至 20 GHz&#xff0c;配有增强型 TDR 测量选件。 E5071C 是大规模无源元器件测试的理想解决方案。 它具有出色的测量性能&#xff0c;有助于提高测试吞吐量&#xff0c;尤其是与 E5092A 多端…

REVIT二次开发万能刷

将这两个参数赋予其他参数 步骤2 将来做个可以调控的版本 using System; using System.Collections.Generic; using System.Lin

C动态内存那些事

为什么存在动态内存分配&#xff1f; 首先&#xff0c;动态内存分配是计算机中一种重要的内存管理方法&#xff0c;它主要解决了静态内存分配无法灵活应对变化需求的问题。以下是几个存在动态内存分配的原因&#xff1a; 灵活性&#xff1a;动态内存分配允许程序在运行时根据需…

Allure 内置特性

章节目录&#xff1a; 一、内置特性概述二、展示环境信息三、测试结果分类四、用例步骤说明五、添加附件六、添加用例描述七、设置动态的用例标题八、报告中添加链接九、组织测试结果9.1 使用与理解9.2 指定运行 十、划分用例级别十一、动态生成附加信息十二、清空历史报告记录…

【GitHub项目推荐--如何构建项目】【转载】

这是一个 138K Star 的开源项目&#xff0c;这个仓库汇集了诸多优质资源&#xff0c;教你如何构建一些属于自己的东西&#xff0c;内容主要分为增强现实、区块链、机器人、编辑器、命令行工具、神经网络、操作系统等几大类别。 开源地址&#xff1a;https://github.com/danist…

vue 样式隔离原理

日常写单文件组件时&#xff0c;会在style添加scoped属性&#xff0c;如<style scoped>&#xff0c;目的是为了隔离组件与组件之间的样式&#xff0c;如下面的例子&#xff1a; <template><p class"foo">这是foo</p><p class"bar&q…

ubuntu下docker卸载和重新安装

卸载&#xff1a;步骤一&#xff1a;停止Docker服务 首先&#xff0c;我们需要停止正在运行的Docker服务。打开终端&#xff0c;执行以下命令&#xff1a; sudo systemctl stop docker 步骤二&#xff1a;删除Docker安装包 接下来&#xff0c;我们需要删除已经安装的Docker软件…

微信小程序如何自定义单选和多选

实现单选 实现效果:点击显示单选状态,每次仅能点击一个元素。 实现方式: wxml: <view wx:for="{{item_list}}" data-info="{{index}}" class="{{menu_index===index?choose:no_choose}}" bind:tap="changeColor">{{ite…

25考研每日的时间安排

今天要给大家分享一下25考研每日的时间安排。 没有完美的计划&#xff0c;只有合适的计划。 仅供参考 很多人说复习不要只看时长而是要看效率&#xff0c;所以学多长时间不重要&#xff0c;重要的高效率完成任务。 完美的计划 这个计划看起来很完美&#xff0c;从早到晚有学习…

尚无忧球馆助教系统源码,助教小程序源码,助教源码,陪练系统源码

特色功能&#xff1a; 不同助教服务类型选择 助教申请&#xff0c;接单&#xff0c;陪练师入住&#xff0c;赚取外快 线下场馆入住 设置自己服务 城市代理 分销商入住 优惠券 技术栈&#xff1a;前端uniapp后端thinkphp 独立全开源

【C++基础】析构函数和构造函数面试知识点总结

&#x1f308;欢迎来到C基础专栏 &#x1f64b;&#x1f3fe;‍♀️作者介绍&#xff1a;前PLA队员 目前是一名普通本科大三的软件工程专业学生 &#x1f30f;IP坐标&#xff1a;湖北武汉 &#x1f349; 目前技术栈&#xff1a;C/C、Linux系统编程、计算机网络、数据结构、Mysq…

大脑的漏洞:你是如何走向狭隘和顽固的?

在这篇文章的最开始&#xff0c;我想请大家思考一个问题&#xff1a; 为什么谣言的传播总是非常容易&#xff0c;但辟谣却一点也不容易呢&#xff1f; 有一个非常简单的答案&#xff0c;你或许立刻就能想到&#xff1a;因为谣言一般都非常简单&#xff0c;但辟谣一般都不怎么简…

linux安装docker--更具官网教程

1.访问https://docs.docker.com/ 2.进入download 3输入cento 或者直接访问地址Install Docker Engine on CentOS | Docker Docs 4一步一步根据官网命令走 2安装 3 4 方式一&#xff1a; service docker start&#xff08;开启&#xff09; service docker status&#xff08…

如何使用 NFTScan API 检索 NFT 合约地址下 Transactions 数据

对于大多数人而言&#xff0c;获取某 NFT 合约地址下的全量交易记录是十分有挑战性的&#xff0c;不仅涉及到对区块链技术的深入了解以及使用相应的工具和资源&#xff0c;还需要处理区块链上的智能合约和交易数据&#xff0c;并将其与外部数据源进行整合分析。通常&#xff0c…

简单介绍----微服务和Spring Cloud

微服务和SpringCloud 1.什么是微服务&#xff1f; 微服务是将一个大型的、单一的应用程序拆分成多个小型服务&#xff0c;每个服务负责实现特定的业务功能&#xff0c;并且可以通过网络通信与其他服务通信。微服务的优点是开发更灵活&#xff08;不同的微服务可以使用不同的开…

「研发部」GitFlow规范-升级版(二)

前言 上一篇文章简单整理过一次产研团队的GitFlow《Git 分支管理及Code Review 流程 (一)》 GitFlow是一种流行的Git分支管理策略&#xff0c;它提供了一种结构化的方式来管理项目的开发和发布流程。以下是GitFlow规范的主要组成部分&#xff1a; 主要分支&#xff1a; mast…

Mybatis-Plus入门

Mybatis-Plus入门 MyBatis-Plus 官网&#xff1a;https://mp.baomidou.com/ 1、简介 MyBatis-Plus (简称 MP) 是一个 MyBatis 的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、 提高效率而生。 https://github.com/baomidou/mybatis-p…

python数据类型-字符串

1 表示方式 python单行字符串用单引号’内容’或双引号"内容"表示&#xff0c; 多行字符串用三引号表示&#xff0c;‘’‘换行内容’或"““换行内容””"&#xff0c; str()函数可将其它类型转换为字符串类型 a henry b "Tom" c 窗前明月…

什么是协方差矩阵?

协方差矩阵&#xff08;Covariance Matrix&#xff09;是一个用于衡量多个变量之间相互关系的工具&#xff0c;在统计学和数据分析领域中非常重要。这个矩阵展现了每一对变量之间的协方差。协方差是衡量两个变量如何一起变化的度量&#xff1b;如果两个变量的协方差是正的&…

C/C++ - Auto Reference

目录 auto Reference auto 当使用auto​​关键字声明变量时&#xff0c;C编译器会根据变量的初始化表达式推断出变量的类型。 自动类型推断&#xff1a;auto​​关键字用于自动推断变量的类型&#xff0c;使得变量的类型可以根据初始化表达式进行推导。 初始化表达式&#x…