【C】初阶数据结构1 -- 时间复杂度与空间复杂度

目录

1  数据结构 

2  算法 

3  复杂度 

1) 时间复杂度

2) 空间复杂度

4  提升算法能力的两点建议 

1) 画图

2) 多实践,多上手写代码


重点一  数据结构的定义

1  数据结构 

  数据结构是计算机存储、组织数据的方式,主要是指相互之间存在一种或多种特定关系的数据元素的集合。在计算机中,包含多种数据结构,如:顺序表、链表以及树、图、哈希表等多种数据结构,需要知道的一点是,没有任何一种数据结构能够适用于所有的情况,有时候需要综合多种数据结构才能解决一个实际问题,所以才会有多种数据结构。


重点二  算法

2  算法 

  数据结构是与算法紧密相关的,所谓算法(Algorithm)就是定义良好的计算过程,取一个值或者一组值为输入,然后通过一系列的计算步骤,用来将输入的数据输出结果

  算法也是分好坏的,用有的算法写出来的程序可能运行时间只需要4毫秒,而有的算法写出来的程序可能就会需要8秒甚至9秒,如,堆排序和冒泡排序(以后会讲解),都是100000个数据进行排序,但是堆排序只需要4毫秒,而冒泡排序却需要8秒,所以衡量一个算法的好坏就显得尤为重要,那么该用什么来衡量算法的好坏或者执行效率呢?

  这里就不得不提到这篇文章的重点,复杂度了。


重点三  复杂度

3  复杂度 

  算法在编写成程序后,运行效率无非是从两个角度来分析,一个就是运行时间的长短,另一个就是运行所占空间的大小,所以复杂度也就区分为了时间复杂度与空间复杂度

重点四  时间复杂度

1) 时间复杂度

  时间复杂度主要是用来衡量一个算法的运行快慢的,如果一个算法的时间复杂度越小,那么这个算法也就越好。

  那么时间复杂度是否是直接计算程序运行时间的长短呢?答案是并不是,因为一个程序运行时间的长短不仅是跟算法的好坏相关,也与计算机的配置相关,一个计算机的配置越好,运行时间越快,也与编译器本身相关,而且最重要的一点是,用程序运行时间衡量的话,是不能事前估计的,只能在程序运行后才能衡量,综合来说,是不能用一个程序的具体运行时间来衡量一个算法的好坏的。

  在计算机科学中,时间复杂度是一个函数式T(n),这个函数式定量描述了一个算法的运行时间,体现了一个算法的运行时间的规模,规模越大,算法运行时间也就越长,就没有那么好,比如,算法A时间复杂度T(n)是n^2,算法B的时间复杂度T(n)是n,所以算法B是优于算法A的。

  计算一个算法的时间复杂度时,主要是看某个语句的运行次数,如以下这个代码:

//计算跟count相关语句的执行次数
void Func1()
{
  int count = 0;
  for (int i = 0;i < n;i++)
  {
    for (int j = 0;j < n;j++)
    {
      count++;
    }
  }
 
  for (int i = 0;i < n;i++)
  {
    count++;
  }
}

  运行次数如下面表格所示:

count语句运行次数
语句运行次数
int count = 01
第一个count++n^2
第二个count++n

 所以这个程序中的T(n) = n^2 + n + 1,但是只有当n很小的时候,T(n)中的n和1才会对T(n)起影响作用,但是当n特别大的时候,甚至是趋于无穷量的时候,n^2趋于无穷的速度远远快于n趋于无穷的速度的,所以当n特别大的时候,n和1也就对T(n)的影响很小了,由于T(n)只是衡量一个算法运行时间的规模,所以n和1也就可以舍弃了,这时候T(n) = n^2了

  所以在计算时间复杂度时,只需要计算运行次数中量级最大的那个语句的执行次数(一般是循环中的语句),也就是能够代表增长量的大概执行次数就可以了,这种表示时间复杂度的表示法叫做大O表示法。

  大O表示法规则:

1) 在计算时间复杂度时,只保留高阶项,去掉低阶项,因为当n特别大时,低阶项对于T(n)的影响可以忽略不计

2) 如果高阶项前面有系数且不为1,那么就把系数变为1,因为时间复杂度只是描述运行时间规模,常数项不影响

3) 如果程序的运行次数与n没有关系,只有常数次,那就统一用 1 来表示,表示这个算法的T(n)为常数阶

接下里我们来看几个例子:

例1:计算Func1函数的时间复杂度

void Func1(int n)
{
  int count = 0;
  for (int i = 0; i < 3 * n; i++)
  {
    count++;
  }

  int m = 100;
  
  for (int i = 0; i < m;i--)
  {
    count++;
  }

}

运行次数如下面表格所示:

Func1函数运行次数
语句执行次数
第一个count++2 * n
第二个count++100

总执行次数:2 * n + 1

时间复杂度:根据以上大O表示法规则,舍弃低阶项以及系数,可得T(n) = n,所以时间复杂度为:O(n)

例2:计算Func2函数的时间复杂度

void Func2(int n, int m)
{
  int count = 0;
  for (int i = 0;i < n; i++)
  {
    count++;
  }

  for (int j = 0;j < m; j++)
  {
    count++;
  }

}

运行次数如表格所示:

Func2函数运行次数
语句运行次数
第一个count++语句n
第二个count++语句m

总执行次数:m + n

这个函数的运行次数与两个变量n,m都相关,当m < n时,T(n) = n,时间复杂度:O(n);当m == n时,T(n) = n 或者 m,时间复杂度:O(n) 或者 O(m);当m > n时,T(n) = m,时间复杂度:O(m)

例3:计算Func3函数的时间复杂度

int Func3(int n)
{
  int count = 0;
  for (int i = 0;i < 1000;i++)
  {
    count++;
  }
  
  return count;
}

运行次数:

Func3函数语句的运行次数
语句运行次数
count++100

这个函数里count++语句的运行次数为100次,跟n是没有关系的,T(n) = 100,根据上面的大O推导规则,运行次数为常数次,时间复杂度为:O(1)

  需要注意的是,这里的O(1)并不是代表运行次数为1次,而是代表该算法的消耗时间的规模是常数阶,跟输入的数据n是没有关系的

例3:计算Func4函数的时间复杂度

const char* Func4(const char* str, char character)//查找character字符在str字符串中的位置
{
  const char* p = s;
  while (*p != character)
  {
    if (*p == '\0')
    {
      return NULL;
    }
    
    p++;
  }

  return p;
}

执行次数(这里假设字符串长度为n):

Func4函数语句的运行次数
查找情况执行次数
若查找字符在第一个位置1次
若查找字符在中间位置n/2次
若查找字符在最后位置或者找不到n次

所以可以看到上述程序的运行次数是与查找字符的位置相关的,当查找字符位于前面位置时,T(n)是常数次,所以时间复杂度是:O(1)当查找字符位于偏中间位置时,T(n) = n/2,时间复杂度为:O(n)当查找字符位于末尾位置时,T(n) = n,时间复杂度为:O(n)

  这种分不同情况时,复杂度也随之不同的算法,是有最好时间复杂度、平均时间复杂度和最坏时间复杂度的:

最坏时间复杂度:任意输入规模的最大运行次数(上界)。

平均时间复杂度:任意输入规模的期望(均值)运行次数。

最好时间复杂度:任意输入规模的最小运行次数(下界)。

  显然,这个算法的最坏和平均时间复杂度为O(n),最好时间复杂度为O(1)。

例4:计算Func5函数的时间复杂度

void Func5(int n)
{
  int count = 1;
  while (count < n)
  {
    count *= 2;
  }
  
}

这个函数的运行次数跟之前函数不太一样,之间count都是++,这里是每次乘2,要分析运行次数就得根据运行次数和count值之间的关系来推导出运行次数,count *= 2 语句运行次数(也就是循环次数)与count值变化如下表格:

Func5函数语句的运行次数
count的值运行次数
10
1*21
1*2*22
1*2*2*23
1*2*2*2*24
............
2^i

通过推导运行次数和count值之间的关系,不难得出规律,就是当运行次数为 i 次时,count的值是2^i ,由于循环停止条件是count >= n,所以运行次数和n之间的关系就是2^i >= n,这里取等号就是2^i = n,也就是 i = \log_{2}n所以T(n) = \log_{2}n,时间复杂度为:O(\log_{2}n),这里写O(logn)也是可以的,这里还是因为时间复杂度表示的运行时间的规模,而\log_{2}n就代表其时间复杂度为对数阶,所以写logn也是可以的。

例5:计算Func6函数的时间复杂度

//计算阶乘的递归函数
int Func6(int n)
{
  if (n == 0)
  {
    return 1;
  }
  
  return Func6(n - 1) * n;
}

Func6函数是用来计算一个数n的阶乘的递归(递归是指直接或者间接调用自身函数的一种算法思想)定义的函数,而计算一个跟递归相关算法的时间复杂度公式为:
 

递归函数的时间复杂度 = 单次递归的时间复杂度 * 递归的深度(次数)

Func6函数单次执行的时间复杂度为:O(1)

Func6函数递归的次数为:n次

所以Func6函数的时间复杂度为:O(n)

  要注意的是这里的相乘并不是n * O(1),而是直接将O(1)里面的1乘以n,其实递归函数计算时间复杂度的内在原理为:C语言在每次调用一个函数的时候,都会为其开辟一块函数栈帧(可以理解为在调用函数时,会为每一次多用的函数额外开辟一块空间,每个函数之间的函数栈帧是独立的),也叫运行时堆栈,而每递归一次,就相当于对该函数调用了一次,虽然每次运行次数为常数次,但是当递归次数达到n次时,也就相当于常数次的运行次数变为了n次,所以时间复杂度会变为O(n)

重点五  空间复杂度

2) 空间复杂度

  空间复杂度类似于时间复杂度,也是一个表达式,用来描述程序在运行过程中临时额外开辟空间的大小,同样的,也是描述一个开辟空间的规模大小。

  在时间复杂度中,时间复杂度并不是程序运行时间的大小;同样的,空间复杂度也不是实际开辟了多少个字节的空间,而是算的是额外使用变量的个数,把额外使用的一个变量抽象为一块空间。

  空间复杂度的计算与时间复杂度一样,也采用大O渐进表示法

  需要注意的一点是:函数在运行时所需要的空间(形参,局部变量等)已经确定好了,所以这些变量不被计算到空间复杂度中,计算的是在运行过程中额外开辟的空间

例6:计算Func7函数的空间复杂度

//数组打印函数
void Print(int* arr, int n)
{
  for (int i = 0;i < n;i++)
  {
    printf("%d ", arr[i]);
  }

  printf("\n");
}

  在这个函数中,形参arr,n,以及局部变量 i 在运行时他们的空间就已经确定,所以是不会算到额外开辟空间中的,而这个函数有没有其他额外的变量,所以额外开辟空间为0,根据大O表示法,空间复杂度为:O(1),这里的 1 仍表示额外开辟的空间为常数阶的。 

例7:计算Func6函数的空间复杂度

//计算阶乘的递归函数
int Func6(int n)
{
  if (n == 0)
  {
    return 1;
  }
  
  return Func6(n - 1) * n;
}

这个函数还是上面那个求阶乘的递归函数,该函数的递归次数与额外开辟的空间如下:

Func6函数的空间复杂度
每次递归额外开辟空间大小递归次数总额外开辟空间大小
1nn

由此可以得出递归函数的额外开辟空间的大小的公式:

总的额外开辟空间大小 = 递归次数 * 单次函数额外开辟空间大小

根据大O渐进表示法,所以求阶乘的递归函数的空间复杂度为:O(n)

  其实其内在原理和递归函数求空间复杂度是一样的:每次递归调用函数时,都会为每次函数调用开辟一块空间,这个空间叫做函数栈帧,所以每递归调用一次函数,就相当于额外开辟了一块空间,所以当递归调用n次时,也就额外开辟了n块空间

例8:计算Func8函数的时间复杂度与空间复杂度

//求递归函数的时间复杂度与空间复杂度
void Func8(int n, int count, int stop)
{
  if (count >= stop)
  {
    printf("%d ", count);
    return;
  }

  for (int i = 0;i < n;i++)
  {
    count++;
  }

  Func8(n, count, stop);
}

先看时间复杂度:

count 的值与n和stop同时相关,count++单次递归运行次数和递归次数如表格所示:

Func8函数的执行次数(这里假设n是10,stop是100,刚开始count=0)
语句单次递归执行次数count的值是否进行下一轮递归
count++1010
count++1020
count++1030
........................
count++1090
count++10100
count++10--------

  当count的值达到100时,由于 if 条件在递归之前,所以还是会再调用一次函数,在最后一次函数栈帧里面(也就count自增到 100 的函数栈帧的下一次递归调用函数的函数栈帧),才会进行count值是否等于100,然后停止递归调用,所以Func8函数是会递归调用10次函数的。

  由以上分析不难得出,Func8函数的count++语句单次运行次数是n次,而递归调用的次数是(stop - count) / n,所以总的执行次数就是[(stop - count) / n] * n,即(stop - count),所以时间复杂度为O(stop - count)

  而空间复杂度是取决于递归调用的次数(深度)的,而递归调用的次数为(stop - count) / n,由于递归调用的次数是跟3个变脸相关的,所以是分最好情况和最坏情况的:

 1) 当count == stop时,刚进入函数就会直接return,一次递归也不会进行,所以空间复杂度为O(1)

2) 当n == stop时,只会进行一次递归,所以空间复杂度也为O(1)

3) 当count != stop,n != stop时,空间复杂度就是递归调用的深度,所以空间复杂度为O((stop - count) / n)

  所以这个算法的最好空间复杂度为O(1),最坏时间复杂度为O((stop - count) / n)


4  提升算法能力的两点建议 

1) 画图

  在刚开始学习数据结构的时候,由于各种数据结构比较抽象,不好理解,所以画图来了解算法的执行过程是十分有必要的,可以使得思路更加清晰,更容易理解执行过程。

2) 多实践,多上手写代码

  在学习数据结构与算法的时候,最忌讳的就是只学不实现代码,认识的过程是一个认识,实践,再认识,再实践的过程,只有不断通过写代码理解算法执行过程以及各种数据结构,才能熟练的写出各种算法以及数据结构。

  刚开始学习数据结构与算法是比较困难的,但是相信只要跨过这个坎,就会越来越轻松的,加油!

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

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

相关文章

TypeScript Jest 单元测试 搭建

NPM TypeScript 项目搭建 创建目录 mkdir mockprojectcd mockproject初始化NPM项目 npm init -y安装TypeScript npm i -D typescript使用VSCode 打开项目 创建TS配置文件tsconfig.json {"compilerOptions": {"target": "es5","module&…

一.项目课题 <基于TCP的文件传输协议实现>

客户端代码 需要cJSON.c文件和cJSON.h文件 在这里插入代码片#include "myheadth.h" #include "myfun.h"#define TIME 10 int sockfd; void heartbeat(int signum) {cJSON* root cJSON_CreateObject();cJSON_AddStringToObject(root,"request"…

C#调用OpenCvSharp实现图像的膨胀和腐蚀

图像膨胀和腐蚀操作属于图像处理中常用的形态学操作&#xff0c;其原理都是采用特定小矩形&#xff08;核矩形&#xff09;&#xff0c;将其中心位置与图像中的每个像素对齐后&#xff0c;对重合位置的像素执行特定处理后&#xff0c;将处理结果保存到中心位置对应的像素处&…

新活动平台建设历程与架构演进

01 前言 历时近两年的重新设计和迭代重构&#xff0c;用户技术中心的新活动平台建设bilibili活动中台终于落地完成&#xff01;并迎来了里程碑时刻 —— 接过新老迭代的历史交接棒&#xff0c;从内到外、从开发到搭建实现全面升级&#xff0c;开启了活动生产工业化新时代&#…

一个好用的C++数据库操作库:OTL

目录 1.简介 2.OTL库的核心类 3.OTL使用 4.使用OTL时注意事项 4.1.多线程初始化 4.2.OTL支持连接池 4.3.大字段的读取方式 4.4.指定数据库类型 4.5.异常处理 5.下载地址 6.总结 1.简介 OTL&#xff08;Oracle, ODBC and DB2-CLI Template Library&#xff09;是一个…

高级生化大纲

一&#xff0c;蛋白质化学&#xff1a; 蛋白质分离是生物化学和分子生物学研究中的一项基本技术&#xff0c;用于根据蛋白质的物理和化学特性将其从混合物中分离出来。 1. 离心分离法 离心分离法利用离心力来分离不同质量或密度的颗粒和分子。 差速离心&#xff1a;通过逐…

linux网络 | http结尾、理解长连接短链接与cookie

前言&#xff1a;本节是http章节的最后一部分&#xff0c;主要解释一些小概念。讲解到了HTTP的方法&#xff0c;表单&#xff0c; 重定向等等。 现在废话不多说&#xff0c; 开始我们的学习吧。 ps&#xff1a;本节内容都是概念&#xff0c; 知道就行&#xff0c; 友友们放心观…

金融项目实战 03|JMeter脚本实现手工接口测试

目录 一、环境说明 1、项目环境搭建 2、Mock说明 二、构造测试数据 1、通过系统页面构造 2、通过接口构造 3、通过数据库构造【推荐】 4、案例&#xff1a;构造借款业务数据 三、JMeter执行接口测试用例 1、获取图片验证码、获取短信验证码 2、注册脚本 3、登录脚本…

点赞系统设计(微服务)

点赞业务是一个常见的社交功能&#xff0c;它允许用户对其他用户的内容&#xff08;如帖子、评论、图片等&#xff09;表示喜欢或支持。在设计点赞业务时&#xff0c;需要考虑以下几个方面&#xff1a; 一、业务需求 点赞业务需要满足以下特性&#xff1a; 通用&#xff1a;…

网络原理一>UDP协议详解

UDP和TCP都是应用层中的重要协议&#xff0c;如果做基础架构开发&#xff0c;会用得多一些。 这一篇我们先简单聊一下的UDP TCP格式呈现&#xff1a; 我们知道UDP是一种无连接&#xff0c;面向数据报&#xff0c;全双工&#xff0c;不可靠传输特性的网络协议。 基本格式如图…

时空笔记:CBEngine(微观交通模拟引擎)

CBEngine 是一个微观交通模拟引擎&#xff0c;可以支持城市规模的道路网络交通模拟。CBEngine 能够快速模拟拥有数千个交叉路口和数十万辆车辆的道路网络交通。 以下内容基本翻译自CBEngine — CBLab 1.0.0 documentation 1 模拟演示 1.0 模拟演示结构 config.cfg 定义了 roa…

金融项目实战 04|JMeter实现自动化脚本接口测试及持续集成

目录 一、⾃动化测试理论 二、自动化脚本 1、添加断言 1️⃣注册、登录 2️⃣认证、充值、开户、投资 2、可重复执行&#xff1a;清除测试数据脚本按指定顺序执行 1️⃣如何可以做到可重复执⾏&#xff1f; 2️⃣清除测试数据&#xff1a;连接数据库setup线程组 ①明确…

20250112面试鸭特训营第20天

更多特训营笔记详见个人主页【面试鸭特训营】专栏 250112 1. TCP 和 UDP 有什么区别&#xff1f; 特性TCPUDP连接方式面向连接&#xff08;需要建立连接&#xff09;无连接&#xff08;无需建立连接&#xff09;可靠性可靠的&#xff0c;提供确认、重传机制不可靠&#xff0c…

导出文件,能够导出但是文件打不开

背景&#xff1a; 在项目开发中&#xff0c;对于列表的查询&#xff0c;而后会有导出功能&#xff0c;这里导出的是一个excell表格。实现了两种&#xff0c;1.导出的文件&#xff0c;命名是前端传输过去的&#xff1b;2.导出的文件&#xff0c;命名是根据后端返回的文件名获取的…

马斯克的Grok-2 Beta APP在苹果应用商店上限了,Grok-2安装尝鲜使用教程

马斯克的Grok-2 Beta APP 已经上线苹果商城了&#xff0c;移动端的Grok挺好用的&#xff01;无需登录即可使用&#xff01; &#xff08;文末有安装教程&#xff09; 实测之后&#xff0c;Grok-2 绘画方面个人感觉比GPT-4的绘画还要强一些。而且速度还挺快&#xff0c;可以多次…

《机器学习》——sklearn库中CountVectorizer方法(词频矩阵)

CountVectorizer方法介绍 CountVectorizer 是 scikit-learn 库中的一个工具&#xff0c;它主要用于将文本数据转换为词频矩阵&#xff0c;而不是传统意义上的词向量转换&#xff0c;但可以作为词向量转换的一种基础形式。用于将文本数据转换为词频矩阵&#xff0c;它是文本特征…

CV 图像处理基础笔记大全(超全版哦~)!!!

一、图像的数字化表示 像素 数字图像由众多像素组成&#xff0c;是图像的基本构成单位。在灰度图像中&#xff0c;一个像素用一个数值表示其亮度&#xff0c;通常 8 位存储&#xff0c;取值范围 0 - 255&#xff0c;0 为纯黑&#xff0c;255 为纯白。例如&#xff0c;一幅简单的…

支持向量回归(SVR:Support Vector Regression)用于A股数据分析、预测

简单说明 支持向量回归是一种用来做预测的数学方法,属于「机器学习」的一种。 它的目标是找到一条「最合适的线」,能够大致描述数据点的趋势,并允许数据点离这条线有一定的误差(不要求所有点都完全落在这条线上)。 可以把它想象成:找到一条「宽带」或「隧道」,大部分…

ollama教程(window系统)

前言 在《本地大模型工具哪家强&#xff1f;对比Ollama、LocalLLM、LM Studio》一文中对比了三个常用的大模型聚合工具优缺点&#xff0c;本文将详细介绍在window操作系统下ollama的安装和使用。要在 Windows 上安装并使用 Ollama&#xff0c;需要依赖 NVIDIA 显卡&#xff0c…

Flink系统知识讲解之:容错与State状态管理

Flink系统知识之&#xff1a;容错与State状态管理 状态在Flink中叫作State&#xff0c;用来保存中间计算结果或者缓存数据。根据是否需要保存中间结果&#xff0c;分为无状态计算和有状态计算。对于流计算而言&#xff0c;事件持续不断地产生&#xff0c;如果每次计算都是相互…