数据结构与算法:复杂度

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

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

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

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

复杂度

  • 时间复杂度
    • 大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;
 else
 return 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 的增大,函数的内存占用也以线性方式增加。

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

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

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

相关文章

翻译: GPT-4 Vision从图像转换为完全可编辑的表格 升级Streamlit四

GPT-4 Vision 系列: 翻译: GPT-4 with Vision 升级 Streamlit 应用程序的 7 种方式一翻译: GPT-4 with Vision 升级 Streamlit 应用程序的 7 种方式二翻译: GPT-4 Vision静态图表转换为动态数据可视化 升级Streamlit 三 当您需要从不可复制或不可下载的表中提取数据时&#x…

Java+Spring Cloud +Vue+UniApp微服务智慧工地云平台源码

目录 智慧工地云平台功能 【劳务工种】所属工种有哪些&#xff1f; 1.管理人员 2.信息采集 3.证件管理 4.考勤管理 5.考勤明细 6.工资管理 7.现场统计 8.WIFI教育 9.课程库管理 10.工种管理 11.分包商管理 12.班组管理 13.项目管理 智慧工地管理平台是以物联网、…

C++进阶(七)AVL树

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、AVL树的概念二、AVL树的旋转1、左单旋2、右单旋3、左右双旋4、右左双旋 三、AVL树的基本实…

跨平台框架Flutter工作原理初探

前言 Flutter是开发跨平台应用的框架&#xff0c;支持将应用打包到几乎市面所有平台&#xff0c;本文较浅层次探究flutter框架的工作原理 参考来源为flutter中文社区官方文档 Flutter 开发文档 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter flutter的布局 组合…

防御保护--第一次实验

目录 一&#xff0c;vlan的划分及在防火墙上创建单臂路由 二&#xff0c;创建安全区域 三&#xff0c;配置安全策略 四&#xff0c;配置认证策略 五&#xff0c;配置NAT策略 1.将内网中各个接口能够ping通自己的网关 2..生产区在工作时间内可以访问服务器区&#xff0c;仅…

vivado 2018.3 烧写固化FPGA verilog代码以及出现的问题解决

vivado一般是与SDK同时使用的,像zynq系列,通过SDK烧写固化代码很方便,但是有的时候比如本人目前使用的是XC7K325T FPGA进行的开发,不会用到SDK软件,所以烧写固化代码想通过vivado直接操作。 1、按照网上百度的方法进行设置,如下 遇到的第一个问题就是在vivado2018.3的fl…

Blender教程(基础)-物体添加-03

1、打开的界面如下图会存在3个物体、英文状态下按键盘字母A全选、然后按键盘delete删除。 删除后一片空白 2、新增物体 方式1&#xff1a;在英文状态下按键盘shiftA组合键弹出如下添加物体弹窗 方式2&#xff1a;在菜单下找到添加点击弹出添加选项 3、举例新增物体 采用上述…

【数字通信】数字带通传输

数字调制和数字带通传输系统 数字调制解调 数字调制 用数字基带信号控制载波&#xff0c;把数字基带信号变换为数字带通信号的过程 目的&#xff1a;数字基带信号含大量低频分量&#xff0c;无法通过具有带通特性的信道传输。需对数字基带信号进行数字调制使信号与信道的特…

log4j2 java api 入门介绍

概述 Log4j 2 API 提供了应用程序应该编码的接口&#xff0c;并提供了实现者创建日志实现所需的适配器组件。 虽然 Log4j 2 在 API 和实现之间被分解&#xff0c;但这样做的主要目的不是允许多个实现&#xff0c;尽管这当然是可能的&#xff0c;而是明确定义在“正常”应用程…

YOLOv8融合改进 更换检测头同时改进C2f模块

一、Detect_DyHead检测头和C2f-EMSC,C2f-EMSCP模块 详细介绍和代码在往期的博客里: Detect_DyHead: (YOLOv8改进检测头Detect为Detect_Dyhead-CSDN博客) C2f-EMSC和C2f-EMSCP: (YOLOv8改进之多尺度转换模块C2f-EMSC和C2f-EMSCP-CSDN博客) 二、算法实现 1、将检测…

github连不上

github连不上 错误提示解决方案 错误提示 fatal: unable to access ‘https://github.com/Ada-design/qianduan.git/’: Failed to connect to github.com port 443 after 21073 ms: Couldn’t connect to server 解决方案 下载steam https://steampp.net/ 安装成功之后&am…

Linux系统明明还有足够的物理内存,调用fork却返回ENOMEM

使用systemtab hook fork&#xff0c;定位到报错调用路径SYSCALL_DEFINE0(fork)-》kernel_clone-》copy_process-》copy_mm-》dup_mm-》dup_mmap-》security_vm_enough_memory_mm-》__vm_enough_memory __vm_enough_memory返回了 -ENOMEM。其源码如下&#xff1a; 从代码可知f…

算法38:子数组的最小值之和(力扣907题)----单调栈

题目&#xff1a; 给定一个整数数组 arr&#xff0c;找到 min(b) 的总和&#xff0c;其中 b 的范围为 arr 的每个&#xff08;连续&#xff09;子数组。 示例 1&#xff1a; 输入&#xff1a;arr [3,1,2,4] 输出&#xff1a;17 解释&#xff1a; 子数组为 [3]&#xff0c;[…

Java集合总览

1.总览 Java中的集合分List、Set、Queue、Map 4种类型。 List&#xff1a;大多数实现元素可以为null&#xff0c;可重复&#xff0c;底层是数组或链表的结构&#xff0c;支持动态扩容 Set&#xff1a;大多数实现元素可以为null但只能是1个&#xff0c;不能重复&#xff0c; …

MySQL 聚集与非聚集索引

文章目录 1.聚集索引1.1 介绍1.2 优点1.3 缺点 2.非聚集索引3.区别参考文献 MySQL 中&#xff0c;根据索引树叶结点存放数据行还是数据行的地址&#xff0c;可以将索引分为两类&#xff1a; 存放数据行&#xff1a;聚集索引存放数据行地址&#xff1a;非聚集索引 InnoDB 使用聚…

Linux学习之文件系统与动静态库

目录 一&#xff0c;文件的管理 什么是磁盘&#xff1f; 磁盘的逻辑抽象结构 格式化 inode 挂载 软硬链接 二&#xff0c;动静态库 什么是动静态库&#xff1f; 1.站在库的制作者角度 静态库&#xff1a; 制作一个静态库 2.站在静态库使用者的角度 动态库 作为制…

windows安装PostgreSQL后进行远程连接,发生SSL错误

1. 报错情况 SSL 关闭 的 pg_hba.conf 记录 (pgjdbc: autodetected server-encoding to be GB2312, if the message is not readable, please check database logs and/or host, port, dbname, user, password, pg_hba.conf) 或是乱码提示&#xff0c;提示中有SSL、 pg_hba.con…

C语言KR圣经笔记 5.12 复杂声明

5.12 复杂声明 C 语言有时会因为声明的语法而受到谴责&#xff0c;特别是涉及函数指针的声明语法。语法试图使声明和使用一致&#xff1b;在简单的情况下它的效果不错&#xff0c;但在更复杂的情况下会让人困惑&#xff0c;因为声明不能从左往右读&#xff0c;而且括号被过度使…

RabbitMQ快速上手

首先他的需求实在什么地方。我美哟明显的感受到。 它给我的最大感受就是脱裤子放屁——多此一举&#xff0c;的感觉。 他将信息发送给服务端中间件。在由MQ服务器发送消息。 服务器会监听消息。 但是它不仅仅局限于削峰填谷和稳定发送信息的功能&#xff0c;它还有其他重要…

Nacos(先解释专属名词,然后大白话讲解+安装配置教程+代码实例 信我,看了必会!!点进来吧各位大佬们)

Nacos 先来一个注意&#xff1a;以下涉及到配置和项目的创建&#xff0c;我的jdk是jdk17&#xff0c;idea是2023.2.4&#xff0c;并且是社区版的idea 1.什么是Nacos Nacos是Dyamic Naming and Configuration Service的首字母简称&#xff0c;一个更易于构建云原生应用的动态…