数据结构与算法:复杂度

news2024/12/27 13:56:45

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

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

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

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

复杂度

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

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

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

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

相关文章

python222网站实战(SpringBoot+SpringSecurity+MybatisPlus+thymeleaf+layui)-友情链接管理实现

锋哥原创的SpringbootLayui python222网站实战&#xff1a; python222网站实战课程视频教程&#xff08;SpringBootPython爬虫实战&#xff09; ( 火爆连载更新中... )_哔哩哔哩_bilibilipython222网站实战课程视频教程&#xff08;SpringBootPython爬虫实战&#xff09; ( 火…

Modern C++ std::tuple的size

不知道大家读过《Modern C std::unique_ptr的实现原理》没有&#xff1f; 里面提到了std::tuple<void*, default_delete()>的大小是4&#xff0c;而不是41或者44&#xff0c;是不是很奇怪&#xff0c;本文不会揭晓答案&#xff0c;只是会扩展测试各种情况。 #include<…

打开 IOS开发者模式

前言 需要 1、辅助设备&#xff1a;苹果电脑&#xff1b; 2、辅助应用&#xff1a;Xcode&#xff1b; 3、准备工作&#xff1a;苹果手机 使用数据线连接 苹果电脑&#xff1b; 当前系统版本 IOS 17.3 通过Xcode激活 两指同时点击 Xcode 显示选择&#xff0c;Open Develop…

私人漫画图书馆:分类管理,一目了然 | 开源日报 No.157

tachiyomiorg/tachiyomi Stars: 26.9k License: Apache-2.0 tachiyomi 是一个免费开源的安卓漫画阅读器。 该项目的主要功能、关键特性、核心优势包括&#xff1a; 从多种来源在线阅读本地阅读已下载内容可配置的阅读器&#xff0c;具有多个查看器、翻页方向和其他设置支持追…

Bug: git stash恢复误drop的提交

Bug: git stash恢复误drop的提交 前几天在写ut时突然需要通过本地代码临时出一个包&#xff0c;但是本地ut又不想直接作为一个commit提交&#xff0c;所以为了省事就将ut的代码暂时stash起来。出完包后想apply stash&#xff0c;但是手误操作点成了drop stash&#xff0c;丢失了…

容器化部署 Jenkins,并配置SSH远程操作服务器

目录 一、Jenkins是什么 二、常见的部署Jenkins的方法 三、为什么选择容器化部署 四、容器化部署Jenkins步骤 1、安装 Docker 2、获取 Jenkins 镜像 3、创建并运行容器 4、访问 Jenkins 4.1 查看初始密码问题 5、配置 Jenkins 5.1 安装插件 5.2 创建管理员用户 5.3…

Git安装详细步骤

目录 1、双击安装包&#xff0c;点击NEXT​编辑 2、更改安装路径&#xff0c;点击NEXT 3、选择安装组件 4、选择开始菜单页 5、选择Git文件默认的编辑器 6、调整PATH环境 7、选择HTTPS后端传输 8、配置行尾符号转换 9、配置终端模拟器与Git Bash一起使用 10、配置额外…

web前端---------盒子模型

1.内容 盒子的内容可以包含文字、图片等多种类型。 浏览器在加载网页时&#xff0c;会将元素按照内容区分为替换元素与非替换元素。 &#xff08;1&#xff09;替换元素指的是HTML中的一些形如<img>、<input>等非文本元素。 这些元素本身不包含任何内容&#x…

【VB测绘程序设计】案例13——几种常用的角度转换子程序Function功能的使用(附源代码)

【VB测绘程序设计】案例13——几种常用的角度转换子程序Function的使用(附源代码) 文章目录 前言一、界面展示二、程序说明三、程序代码1.角度转换子程序jdzh()四、数据演示总结前言 使用VB编写测绘程序,最基础的对于角度在导线测量计算中频繁需要角度的计算,从度分秒转…

Redis核心技术与实战【学习笔记】 - 3.Redis服务高可靠

1.数据同步&#xff1a;主从库如何实现数据一致&#xff1f; 前面我们学习了 AOF 和 RDB&#xff0c;如果 Redis 发生了宕机&#xff0c;它们可以分别通过回放日志和重新读入 RDB 文件的方式恢复数据&#xff0c;从而保证尽量较少丢失数据&#xff0c;提升可靠性。 不过&…

qemu单步调试arm64 linux kernel

一、背景和目的 qemu搭建arm64 linux kernel环境-CSDN博客 之前介绍了qemu启动kernel的配置步骤和方法&#xff0c;现在开始我们的调试&#xff0c;这篇文章主要讲解如何单步调试内核&#xff0c;所有的实验还是基于ARM64&#xff1b; 二、环境准备 需要准备hostx86 target…

vue2中的watch(侦听器)讲解,以及解决深度监听新值和旧值相同的两种方案(手写深拷贝和JSON.parse())。

目录 一&#xff1a;什么是watch? 二&#xff1a;watch的基础使用 1.最基本的使用 2.简写形式 三&#xff1a;watch中的immediate和deep属性 1.immediate属性 2.deep属性 3.解决深度监听新旧值相同的问题 1&#xff09;使用序列化和反序列化。 2&#xff09;手写深…

DevOps系列文章之 GitLabCI汇总

GitlabCI环境搭建 前提 先安装 docker Docker容器化安装 docker pull gitlab/gitlab-ee:12.4.0-ee.0 创建挂载目录 mkdir -p /srv/gitlab mkdir -p /srv/gitlab/config # 映射到 Glitlab 容器中的配置目录 mkdir -p /srv/gitlab/logs # 映射到 Glitlab 容器中的日志目录 m…

换个思维方式快速上手UML和 plantUML——类图

和大多数朋友一样&#xff0c;Jeffrey 在一开始的时候也十分的厌烦软件工程的一系列东西&#xff0c;对工程化工具十分厌恶&#xff0c;觉得它繁琐&#xff0c;需要记忆很多没有意思的东西。 但是之所以&#xff0c;肯定有是因为。对工程化工具的不理解和不认可主要是基于两个逻…

最新国内GPT4.0使用教程,AI绘画-Midjourney绘画V6 ALPHA绘画模型,GPT语音对话使用,DALL-E3文生图+思维导图一站式解决方案

一、前言 ChatGPT3.5、GPT4.0、GPT语音对话、Midjourney绘画&#xff0c;文档对话总结DALL-E3文生图&#xff0c;相信对大家应该不感到陌生吧&#xff1f;简单来说&#xff0c;GPT-4技术比之前的GPT-3.5相对来说更加智能&#xff0c;会根据用户的要求生成多种内容甚至也可以和…

围棋的气 - 华为OD统一考试

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 围棋棋盘由纵横各19条线垂直相交组成&#xff0c;棋盘上一共19x19361个交点&#xff0c;对弈双方一方执白棋&#xff0c;一方执黑棋&#xff0c;落子时只能将棋子…

智慧文旅:未来旅游业的数字化转型

随着科技的快速发展&#xff0c;数字化转型已经成为各行各业的必然趋势。旅游业作为全球经济的重要组成部分&#xff0c;也正经历着前所未有的变革。智慧文旅作为数字化转型的重要领域&#xff0c;正逐渐改变着旅游业的传统模式&#xff0c;为游客带来更加便捷、个性化的旅游体…

锐龙笔记本Windows 11休眠无法唤醒问题的解决(6800h, 7840H/Hs)

锐龙笔记本运行Windows 11时经常会遇到休眠后无法唤醒的问题&#xff0c;表现为休眠后 按键盘或鼠标无反应&#xff0c;只能长按电源开关关机后再开机。网上有很多说法&#xff0c;比如显卡问题或其它问题。但是本质 上这个是电源管理软硬件不兼容导致的&#xff0c;解决办法如…

2024 年 eBPF 和网络趋势预测

本文地址&#xff1a;2024 年 eBPF 和网络趋势预测 | 深入浅出 eBPF 1. eBPF 1.1 eBPF 将继续呈指数增长1.2 eBPF 应用市场1.3 eBPF 在手机中得到更广泛的应用1.4 eBPF 滥用带来的风险2. 可观测 2.1 最受欢迎的可观测性2.2 降低可观测性开销2.3 上下文感知的 Kubernetes 工作负…

python222网站实战(SpringBoot+SpringSecurity+MybatisPlus+thymeleaf+layui)-自定义帖子管理实现

锋哥原创的SpringbootLayui python222网站实战&#xff1a; python222网站实战课程视频教程&#xff08;SpringBootPython爬虫实战&#xff09; ( 火爆连载更新中... )_哔哩哔哩_bilibilipython222网站实战课程视频教程&#xff08;SpringBootPython爬虫实战&#xff09; ( 火…