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

目录

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)

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

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

相关文章

【C++进阶之路】适配器、反向迭代器、仿函数

文章目录 前言一、适配器①模拟实现栈②模拟实现对列 二、反向迭代器三、仿函数总结 前言 我们先来笼统的介绍一下今天的三个内容。 适配器——简单的理解就是复用&#xff0c;用已经实现的轮子&#xff0c;来继续实现某种功能。 反向迭代器——原理很简单&#xff0c;就是对…

Maven发布中央仓库始终报403

把域名 oss.sonatype.org 全部替换为&#xff1a;s01.oss.sonatype.org

Chatgpt Web API 创建对话,免费,不计token数量,模仿网页提交对话

Chatgpt API 是收费的&#xff0c;按token使用量计费 Chatgpt Web API 免费的&#xff0c;只要有账号就可以使用。 curl https://chat.openai.com/backend-api/conversation \-H authority: chat.openai.com \-H accept: text/event-stream \-H accept-language: zh-CN,zh;q…

52.弱集合 WeakSet

WeakSet与Set相似&#xff0c;Set的方法在WeakSet中基本都能使用 WeakSet只能放对象 WeakSet不能被遍历 目录 1 创建WeakSet 2 弱集合的指向问题 1 创建WeakSet 直接在创建的时候把对象放进去是不行的 你需要用add()方法才可以放进去 2 弱集合的指向问题 当你把obj加…

前后端分离windows本地nginx解决跨域

下载 http://nginx.org/en/download.html 命令 启动Nginx&#xff1a; nginx.exe start 快速停止或关闭Nginx&#xff1a; nginx.exe -s stop 正常停止或关闭Nginx&#xff1a; nginx.exe -s quit 配置文件修改重装载命令&#xff1a; nginx.exe -s reload 强制停用…

Java基本数据类型

Java基本数据类型 字面常量数据类型变量整形变量长整形变量短整型变量字节型变量思考浮点型变量双精度浮点型单精度浮点型 字符型布尔型变量 类型转换类型提升 字面常量 字符串常量&#xff1a;由""括起来的&#xff0c;比如“12345”、“hello”、“你好”。整形常量…

机器学习---经验误差与过拟合、方差与偏差、性能度量、比较检验

1. 经验误差与过拟合 第三张图建立的模型&#xff0c;在训练集中通过x可以很好的预测y&#xff0c;然而我们不能预期该模型能够很好的预 测集外的数据&#xff0c;换句话说&#xff0c;这个模型没有很好的泛化能力。 第一张图建立了一个线性模型&#xff0c;但是该模型并没有…

Nginx 301 https跳转后出现跨域和混合内容问题 —— 筑梦之路

问题 在浏览器地址栏敲入url访问静态资源目录时&#xff0c;发现默认跳转到了http协议的地址 如上图所示&#xff0c;客户端https请求先到达API网关&#xff0c;然后网关将请求通过http协议转发到静态资源服务器。 调出浏览器发现客户端发送的https请求收到了一个301状态码的响…

nodejs+vue+elementui学习交流和学习笔记分享系统

Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台。 前端技术&#xff1a;nodejsvueelementui,视图层其实质就是vue页面&#xff0c;通过编写vue页面从而展示在浏览器中&#xff0c;编写完成的vue页面要能够和控制器类进行交互&#xff0c;从而使得用户在点击网页进…

Kotlin 协程 CoroutineScope

协程定义&#xff1a; 19年官方是这样说的&#xff1a;协程是轻量级的线程&#xff0c;协程就是 Kotlin 提供的一套线程封装的 API&#xff1b; 现在官方是这样说的&#xff1a;协程是一种并发设计模式&#xff1b; 协程作用&#xff1a; 1.处理耗时任务&#xff1b; 2.保…

Kotlin 新版本 1.9.0重要更新预览

释放 Kotlin 新版本 1.9.0 的强大功能 1. Kotlin K2编译器用于多平台 对K2编译器进行了进一步的改进&#xff0c;使其更加稳定。K2编译器针对JVM目标现已进入Beta版本&#xff0c;并且也可以在多平台项目中使用。 您可以通过将K2配置添加到项目的gradle.properties中&#x…

九、数据结构——顺序队列中的循环队列

目录 一、循环队列的定义 二、循环队列的实现 三、循环队列的基本操作 ①初始化 ②判空 ③判满 ④入队 ⑤出队 ⑥获取长度 ⑦打印 四、循环队列的应用 五、全部代码 数据结构中的循环队列 在数据结构中&#xff0c;队列&#xff08;Queue&#xff09;是一种常见的线性数据结…

pytest+allure运行出现乱码的解决方法

pytestallure运行出现乱码的解决方法 报错截图&#xff1a; 这里的截图摘自 悟翠人生 小伙伴的https://blog.csdn.net/weixin_45435918/article/details/107601721一文。 这是因为没有安装allure运行环境或者没有配置allure的环境变量导致&#xff0c;解决方案&#xff1a; 1…

Vue移动端项目--瑞幸咖啡重构优化

来了客官&#xff0c;好久不见&#xff01; 从年初开始&#xff0c;就有个想法&#xff0c;想着把之前做过的项目重新整理一下。毕竟今时不同往日&#xff0c;从现在的角度去看曾经做过的项目&#xff0c;倒是觉得有很多稚嫩的地方。毕竟无论做什么都是熟能生巧&#xff0c;由浅…

深度学习推理和训练

优化和泛化 深度学习的根本问题是优化和泛化之间的对立。 • 优化&#xff08;optimization&#xff09;是指调节模型以在 训练数据 上得到最佳性能&#xff08;即机器学习中的学习&#xff09;。 • 泛化&#xff08;generalization&#xff09;是指训练好的模型在 前所未…

2023JAVA 架构师面试 130 题含答案:JVM+spring+ 分布式 + 并发编程》...

此文包含 Java 面试的各个方面&#xff0c;史上最全&#xff0c;苦心整理最全 Java 面试题目整理包括基JVM算法数据库优化算法数据结构分布式并发编程缓存等&#xff0c;使用层面广&#xff0c;知识量大&#xff0c;涉及你的知识盲点。要想在面试者中出类拔萃就要比人付出更多的…

nginx怎么做负载均衡

Nginx怎么做负载均衡 Nginx 是一个高性能的开源反向代理服务器&#xff0c;可以用于实现负载均衡。负载均衡指的是将用户请求平均分配给多个服务器&#xff0c;以提高整体系统性能和可靠性。下面是一个详细介绍如何使用 Nginx 实现负载均衡的步骤&#xff1a; 步骤 1&#xf…

【Nodejs】Node.js简介

1.前言 Node 的重要性已经不言而喻&#xff0c;很多互联网公司都已经有大量的高性能系统运行在 Node 之上。Node 凭借其单线程、异步等举措实现了极高的性能基准。此外&#xff0c;目前最为流行的 Web 开发模式是前后端分离的形式&#xff0c;即前端开发者与后端开发者在自己喜…

提升Web3安全性和用户体验:元事务和加密技术的应用

在Web3中&#xff0c;去中心化应用程序&#xff08;DApps&#xff09;是一种基于区块链技术的应用程序&#xff0c;它们通过智能合约实现透明、安全、去中心化的业务逻辑。然而&#xff0c;DApps的使用门槛比传统的中心化应用程序更高&#xff0c;需要用户具备一定的技术知识&a…