一起学数据结构(1)——复杂度

news2024/11/26 10:41:46

目录

1. 时间复杂度:

1.1 时间复杂度的概念:

1.2 时间复杂度的表示及计算:

1.3 较为复杂的时间复杂度的计算:

2. 空间复杂度:

2.1 空间复杂度的概念:

2.2 空间复杂度的计算:


1. 时间复杂度:

1.1 时间复杂度的概念:

        时间复杂度是一个函数,用于衡量一个算法的运行快慢,对于一个算法而言,在不同配置上的机器运行的速度是不一样的,所以,并不能简单的用运行的时间来衡量算法的运行快慢。虽然用时间来表示并不合理,但是,一个算法运行的时间与这个算法中基本操作的次数成正比,所以,将一个算法中的基本操作的运行次数定义为时间复杂度。

1.2 时间复杂度的表示及计算:

       上面给出了时间复杂度的概念后,这里给出一个例子:

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;
}

通过上面给出的定义可以知道,时间复杂度是一个关于算法中基本操作运行次数的函数,对上述代码,如果将它的运行次数用函数表示,即:

                                          f(n) = N^2 + 2*N + 10

不过,实际中计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要知道执行的大概次数,并且取对结果有决定性因素的一样来表示。例如,对于上面的函数式,取决定性作用的一项是N^2。在表示时,采用 大O的渐进表示法 对于上述代码,可以表示为O(N^2).

对于时间复杂的的计算,在不同的情况下有不同的规则,下面通过举例来引出这些规则:

例1:

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);

按照上面所说的方法,用函数来表示上述代码中基本操作的运行次数,即:

                                                  f(n) = 2*N + 10

取函数中有决定性因素的项,即:2*N,不过在用大O的渐进法进行表示时,在N的前面存在一个常数,需要满足用常数1取代运行时间中的所有加法常数这个规则,所以,时间复杂度表示为O(N).

例2:

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);

此时的时间复杂度为:O(M+N),因为不能分辨,M、N二者的大小,所以也就不能分辩二者谁在时间复杂度中起决定性作用。 

  如果给出的条件足以分别二者之间的大小关系:

 当M>>N时,时间复杂度可以表示为:O(M)

M<<N时,时间复杂度可以表示为:O(N)

M = N时,时间复杂度可以表示为;O(M)或者O(N)

例3:

void Func4(int N) 
{
  int count = 0;
  for (int k = 0; k < 100; ++ k)
     {
        ++count;
     }

  printf("%d\n", count); 
}

对于例3所给出的代码,执行次数为100次,并不是一个函数式,对于这种只有常数的类型,用O(1)来表示。 

例4:

const char * strchr ( const char * str, int character );

strchr函数的功能实在一个字符串中,寻找和某个目标字符相同的字符。假设,字符串的长度为N,查找的次数表示为K对于在这个字符串中查找目标字符,可以分为三种情况:

 第一种情况: 第一次就找到目标字符,执行次数为1.

第二种情况:在1 < K < N时找到目标字符。

第三种情况: 最坏的情况,即K = N时找到目标字符。

对于这种多情况的代码的时间复杂度,按照最坏的情况进行表示,所以,例4的时间复杂度为O(N)

在了解了时间复杂度的基本运算规则即表示后,下面引入一些较为复杂的时间复杂度的计算:

1.3 较为复杂的时间复杂度的计算:

  例1:

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)
            {
                 (a[i-1] > a[i])
                   {
                      Swap(&a[i-1], &a[i]);
                      exchange = 1;
                   }
            }
if (exchange == 0)
break
      } 
}

对于例1,即冒泡排序的复杂度的计算:需要注意,当有循环嵌套时,时间复杂度的计算应按照累加,而不是累乘,对于冒泡排序,可以理解为,共有N中情况,每种情况中代码的基本操作的执行次数一次为N-1 ,N-2...... 1,即满足一个等差数列。但是需要注意,冒泡排序的执行次数依旧有三种情况,假设代码的执行次数为K:

   第一种情况:最好的情况,给定的数组有序。此时K = 1

   第二种情况:1 < K < (N-1)*N/2

   第三种情况:最坏的情况,此时K = (N-1) *N/2

通过前面对时间复杂度的介绍,可以得出,冒泡排序的时间复杂度为:O(N^2)

例2:

int BinarySearch(int* a, int n, int x) 
{
  assert(a);
  int begin = 0;
  int end = n-1;
// [begin, end]:begin和end是左闭右闭区间,因此有=号
   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,每次查找后,数据的总量会/2,也就是说,查找K次后的数据总量为N/(2)^K,对于二分查找而言,最坏的情况就是查找K次后,N = 1

此时,K和N的关系可以表示为2^K = N, k = logN (注:此时的log以2为底)。由于文本编辑的原因,以2为底的对数不容易编写,所以,将以2为底的对数写成logN,对非2为底的对数不成立!

例3:

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

对于递归算法,其时间复杂度是多次调用的代码的次数的累加,所以,对于例3给出的递归,调用的次数最大是N+1次,所以,时间复杂度是O(N)

如果,对于例3中的代码做一点简单的修改,即:

long long Fac(size_t N)
{
   if(0 == N)
     return 1;
}
  for( size_t i = 0; i < N; i++)
     {
        .......
     }
    
     return Fac(N-1)*N;
}

与例3不同的是,在例3中的最后一行代码表达的意思就是递归运算后的结果×N,时间复杂度O(N)只与递归的调用次数有关,对于递归后面×的N,只是对结果进行乘法。

但是,在这个例子中,递归的次数一共是N次,但是,在每次递归的时候,中间会有一个for循环,所以,代码一共会递归N+1次,但是,每次在递归中,循环的次数是N,N-1,N-2......0次,前面说到对于递归算法的时间复杂度,是多次调用的代码的次数的累加,并不是类乘,因此,对于次代码整体的逻辑可以理解为一个等差数列,时间复杂度为O(N^2)

例4:

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

例4是一个双动递归,对于这种递归的时间复杂度的运算,由下面的一张图来表示:

 如图所示是参数为5时的调用情况。如果,当参数为N时,调用情况会变成下图所示:

稍加观察会发现,图中每一行的项的个数都满足2^N的数量。再结合上面的图像不难得出一个结论,如果按照图中的计算方法一直进行下去,右边数据到达Fib(1),Fib(2)的速度大于左边,所以,当所有的项都变成Fib(1),Fib(2)时,图形的结构可以用下面的图片表示:

其中白色和蓝色的交界处表示此时递归变成了 Fib(1),Fib(2)。蓝色部分表示下面没有项,所以,当蓝色部分出现后,每一行的元素数量不再严格的满足2^N.但是,当N很大是,即使缺少了一部分,此时所有项之和的数量级,依旧和2^N类似,在此处也可以理解为,三角形项的和的极限无限趋近于2^N。 所以,对于上面给出的递归,其时间复杂度为O(2^N)

2. 空间复杂度:

2.1 空间复杂度的概念:

   在介绍时间复杂度的概念时说过,时间复杂度是一个函数表达式,同样,对于空间复杂度来说也是一个函数表达式 ,对算法运行过程中临时占用存储空间大小的量度。但是对于空间复杂度,并不是指程序占用了多少bytes的空间,空间复杂度针对的目标是变量的个数。

2.2 空间复杂度的计算:

对于空间复杂度的计算,依旧通过给出例子来计算不同情况下的空间复杂度:

例1:

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;
        }
  }

还是拿冒泡排序举例,在计算时间复杂度时,最后得出冒泡排序的时间复杂度是O(N^2)

对于冒泡排序时间复杂度的计算,通过上面给出的概念,可以知道,时间复杂度针对的对象是代码中临时占用内存空间的变量,可以理解为,为了解决某个问题或者为了完成代码而创建的变量,对于上面的代码,可以看出,为了完成冒泡排序,创建了三个变量分别是end,exchange,i。所以,上述代码中临时占用内存空间的变量的数量为3,所以,冒泡排序的空间复杂度为O(1)

例2:

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;

例2中,为了完成代码,所以,利用动态内存函数malloc创建了N+1个临时占用空间的变量。下面虽然也创建了变量i,但是,空间复杂度仍然是取起决定性作用的值,所以,例2的空间复杂度为O(N)

例3:

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

对于递归而言,每次运算都要开辟常数量的空间,需要开辟N次,所以,空间复杂度为O(N)

例4:

long long Fib(int N)
{
   { 
     if(N < 3)
     return 1;
   }

return Fib(N-1) + Fib(N-2);
}

在计算双动递归的空间复杂度之前,需要了解一个概念,及:时间是累加的,空间是可以重复利用的。对于这句话和求双动递归的空间复杂度有什么关系,可以用计算双动递归的时间复杂度的图来解释:

 图反应的只是双动递归执行的大体结构,但是,不反应双动递归运行时的顺序,对于双动递归运行时的顺序,是Fib(N-1)\rightarrow Fib(N-2)\rightarrow Fib(N - 3)\rightarrow Fib(N-4)。当Fib(N-4)运行后,加上Fib(N)所占用的空间,可以看成一共占用了5个内存空间。返回Fib(N-3),此时Fib(N-4)所占用的内存空间还给操作系统,下次执行时,再执行Fib(N-5),但是对于Fib(N-5)的使用,并不会再开辟一个新的内存空间,而是将Fib(N-4)还给操作系统的空间再给Fib(N-5)使用,这也就对应了上面的话:空间是可以重复利用的。对于其他元素,同样出现重复利用内存空间的情况。所以,计算双动递归的空间复杂度时,计算的应该是递归一直向下执行时(即不出现上面所说的重复利用空间的情况)所占用内存的最大值,所以,对于Fib(N),向下执行时所占用内存空间的最大值为N。因此,双动递归的空间复杂度为O(N)

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

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

相关文章

Kubernetes 简介:容器编排与集群管理的进化

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

Mybatis-plus从入门到精通

1、什么是MyBatis-Plus MyBatis-Plus&#xff08;简称MP&#xff09;是一个基于MyBatis的增强工具&#xff0c;在MyBatis的基础上对其进行扩展&#xff0c;用于简化MyBatis操作&#xff0c;提高开发效率。它继承了MyBatis原生的所有特性&#xff0c;并且添加了一些额外的功能&…

《TCP IP网络编程》第十章

第 10 章 多进程服务器端 10.1 进程概念及应用 并发服务端的实现方法&#xff1a; 通过改进服务端&#xff0c;使其同时向所有发起请求的客户端提供服务&#xff0c;以提高平均满意度。而且&#xff0c;网络程序中数据通信时间比 CPU 运算时间占比更大&#xff0c;因此&#…

探析国内数字孪生引擎技术现状

在数字孪生软件来发中&#xff0c;渲染引擎是一个关键点&#xff0c;国内大多数字孪生平台引擎通常使用的是自研的渲染引擎或者采用开源的渲染引擎。下面通过一些常见的渲染引擎在国内数字孪生引擎中的应用带大家了解数字孪生软件开发的方式。 自研渲染引擎&#xff1a;许多数…

CNN + Vision Transformer 结合学习

介绍三篇结合使用CNNTransformer进行学习的论文&#xff1a;CvT&#xff08;ICCV2021&#xff09;&#xff0c;Mobile-Former&#xff08;CVPR2022&#xff09;&#xff0c;SegNetr&#xff08;arXiv2307&#xff09;. CvT: Introducing Convolutions to Vision Transformers, …

利用 trait 实现多态

我在书上看到基于 std::io::Write 的示例&#xff0c;它是一个 trait 类型&#xff0c;内部声明了一些方法。和 go 语言不同&#xff0c;rust 中类型必须明确实现 trait 类型&#xff0c;而 go 语言属于 duck 模式。 std::io::Write下面的例子中调用 write_all 方式来演示&…

标签是系列色并且加粗帆软

标签是系列色并且加粗 function(){return <span style"color:this.color;">FR.contentFormat(this.value, #,##0)</span>;}

UI 自动化的 PageObject 设计模式

目录 前言&#xff1a; 什么是 PageObject 模型&#xff1f; 为什么使用 PageObject 模型&#xff1f; PO 模式优点 PageObject 实践 前言&#xff1a; UI 自动化是一种软件测试方法&#xff0c;它主要用于检查应用程序的用户界面是否符合预期。PageObject 是 UI 自动化中…

AI大模型时代下运维开发探索第一篇:ReAct工程初探

引子 人工智能大模型的出现&#xff0c;已渐渐地影响了我们的日常生活和工作方式。生活中无处不在的AI&#xff0c;使我们的生活变得更加智能和便捷。工作中&#xff0c;AI大模型的高效和精准&#xff0c;极大地提升了我们解决问题的效率。 是的&#xff0c;我们不能忽视AI大…

Delphi 开发者,显示图片请忘掉VCL中的 TImage 吧

目录 序言 使用TImageCollection和TVirtualImageList组件支持高分辨率图像 一、总览 二、使用图像收集组件TImageCollection 2.1 图像收集组件编辑器 2.2 将现有 TImageList 载入 TImageCollection 三、使用Virtual ImageList 组件 3.1 Virtual ImageList Component 编辑…

【数据结构】| 王道考研——树的前世今生

目录 一. &#x1f981; 前言二. &#x1f981; 各种树的知识点1. 树1.1 概念1.2 属性1.3 常考性质1.4 树转换成二叉树1.5 森林转换为二叉树1.6 二叉树转换为森林1.7 树的遍历1.8 森林的遍历 2. 二叉树2.1满二叉树2.2 完全二叉树2.3二叉排序树2.4 平衡二叉树2.5 二叉树常考性质…

IDEA使用lombok实体类加上@Data注解后无法找到get和set方法

文章目录 一、问题原因二、解决方法1.File→Settings2.Plugins→搜索"lombok"→Install3.Restart IDE&#xff08;重启IDEA&#xff09; 一、问题原因 IDEA没有安装lombok插件 二、解决方法 1.File→Settings 2.Plugins→搜索"lombok"→Install 3.Restart…

北斗定位导航系统,北斗模块应用领域发展概况_北斗二号模块,北斗三号模块

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、北斗系统概述1.1 空间段1.2 地面控制段1.3 用户段1.4 坐标系统1.5 时间系统 二、北斗系统定位导航授时服务2.1 服务概述2.2 服务区2.3 北斗信号频段2.4 北斗…

服务器中了Locked勒索病毒怎么解决,勒索病毒解密恢复方式与防护措施

服务器是企业重要数据存储和处理的关键设备&#xff0c;然而&#xff0c;众所周知&#xff0c;服务器系统并非完全免受网络攻击的。其中一种常见的威胁是勒索病毒&#xff0c;其中一种恶名昭彰的变种是Locked勒索病毒。Locked勒索病毒采用了对称AES与非对称RSA的加密形式&#…

【C++入门到精通】C++入门 —— 引用、内联函数

目录 一、引用 1.引用的概念 2.引用的特性 3.常引用 4.引用的使用场景 ⭕做参数 ⭕做返回值 5.传值、传引用效率比较 值和引用的作为返回值类型的性能比较 6.引用和指针的区别 引用和指针的不同点 二、内联函数 1.内联函数的概念 2.内联函数的特性 3.宏与内联函数 …

RocketMQ教程-(5)-功能特性-顺序消息

顺序消息为 Apache RocketMQ 中的高级特性消息&#xff0c;本文为您介绍顺序消息的应用场景、功能原理、使用限制、使用方法和使用建议。 应用场景​ 在有序事件处理、撮合交易、数据实时增量同步等场景下&#xff0c;异构系统间需要维持强一致的状态同步&#xff0c;上游的事…

13.4.2 【Linux】sudo

相对于 su 需要了解新切换的使用者密码 &#xff08;常常是需要 root 的密码&#xff09;&#xff0c; sudo 的执行则仅需要自己的密码即可。sudo 可以让你以其他用户的身份执行指令 &#xff08;通常是使用 root 的身份来执行指令&#xff09;&#xff0c;因此并非所有人都能够…

Spring 多数据源方法级别注解实现

Spring框架提供了多种数据源管理方式&#xff0c;其中多数据源管理是其中之一。多数据源管理允许应用程序使用多个数据源&#xff0c;而不是只使用一个数据源&#xff0c;从而提高了应用程序的灵活性和可靠性。 多数据源管理的主要目的是让应用程序能够在不同的数据库之间切换&…

SpringCache 框架使用以及序列化和缓存过期时间问题的解决

目录 为什么使用Spring Cache 如何使用Spring Cache 1 加依赖 2 开启缓存 3 加缓存注解 序列化以及过期时间等问题 解决方案&#xff1a;自定义序列化方式 1.自定义序列化方式并设置白名单 2.配置并设置缓存的过期时间 为什么使用Spring Cache 缓存有诸多的好处&#x…

YOLOv5(v7.0)网络修改实践二:把单分支head改为YOLOX的双分支解耦合head(DecoupleHead)

前面研究了一下YOLOX的网络结构&#xff0c;在YOLOv5(tag7.0)集成了yolox的骨干网络&#xff0c;现在继续下一步集成YOLOX的head模块。YOLOX的head模块是双分支解耦合网络&#xff0c;把目标置信度的预测和目标的位置预测分成两条支路&#xff0c;并验证双分支解耦合头性能要优…