当前位置: 首页 > news >正文

算法复杂度

🔥承渊政道:个人主页

❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》

✨逆境不吐心中苦,顺境不忘来时路!

🎬 博主简介:
在这里插入图片描述

文章目录

  • 1.数据结构前言
    • 1.1数据结构
    • 1.2 算法
    • 1.3 数据结构和算法的重要性
    • 1.4数据结构的相关术语
    • 1.5数据的逻辑和存储结构
  • 2. 算法效率
    • 2.1时间复杂度的概念
    • 2.2复杂度的重要性
  • 3. 时间复杂度
    • 3.1 ⼤O的渐进表示法
    • 3.2 时间复杂度计算示例
      • 3.2.1 示例1
      • 3.2.2 示例2
      • 3.2.3 示例3
      • 3.2.4 示例4
      • 3.2.5 示例5
      • 3.2.6 示例6
      • 3.2.7 示例7
  • 4. 空间复杂度
    • 4.1 空间复杂度计算示例
      • 4.1.1 示例1
      • 4.1.2 示例2
  • 5. 常⻅复杂度对⽐
  • 6.重新思考轮转数组的实现

1.数据结构前言

数据结构是一门综合性的专业基础课,它主要研究非数值计算的程序设计问题中计算机的操作对象,以及这些对象之间的关系和运算等.

1.1数据结构

数据结构是计算机存储、组织数据的⽅式,指相互之间存在⼀种或多种特定关系的数据元素的集合.没有⼀种单⼀的数据结构对所有⽤途都有⽤.所以我们要学各式各样的数据结构,如:线性表、树、图、哈希等.

1.2 算法

算法:就是定义良好的计算过程,他取⼀个或⼀组的值为输⼊,并产⽣出⼀个或⼀组值作为输出.简单来说算法就是⼀系列的计算步骤,⽤来将输⼊数据转化成输出结果.

1.3 数据结构和算法的重要性

1️⃣校园招聘笔试必考 2️⃣校园招聘⾯试必考
如何学好数据结构和算法?
秘诀1:死磕代码!!!秘诀2:画图画图画图+思考

1.4数据结构的相关术语

1️⃣数据元素:是数据的基本单位.
2️⃣数据项:是组成数据元素不可再分的最小单位.
3️⃣数据对象:是性质相同的数据元素的集合.

1.5数据的逻辑和存储结构

1️⃣数据的逻辑结构:有集合、线性、树形、网状结构.
在这里插入图片描述
2️⃣数据的存储结构:有顺序存储结构和链式存储结构.
<1>顺序存储结构:指逻辑上相邻的数据元素,其结点的物理位置也相邻,通常借助于程序设计语言中的数组来实现.
<2>链式存储结构:结点间的逻辑关系是由附加的指针字段来指示的,通常借助于程序设计语言中的指针来实现.

2. 算法效率

如何衡量⼀个算法的好坏呢?

案例:轮转数组

//我们有思路会先想到循环k次将数组所有元素向后移的一位
void rotate(int* nums, int numsSize, int k) {
while(k--)
{int end =nums[numsSize-1];for(int i =numsSize-1;i>0;i--){nums[i] =nums[i-1];}nums[0] =end;
}


当我运行时会发现代码能够通过,但当我提交时却告诉我不能通过,超出时间限制!那该如何衡量其好与坏呢?接下来我就要讲一讲时间复杂度的概念.

2.1时间复杂度的概念

算法在编写成可执⾏程序后,运⾏时需要耗费时间资源和空间(内存)资源 .因此衡量⼀个算法的好
坏,⼀般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度.时间复杂度主要衡量⼀个算法的运⾏快慢,⽽空间复杂度主要衡量⼀个算法运⾏所需要的额外空间.在计算机发展的早期,计算机的存储容量很⼩.所以对空间复杂度很是在乎.但是经过计算机⾏业的迅速发展,计算机的存储容量已经达到了很⾼的程度.所以我们如今已经不需要再特别关注⼀个算法的空间复杂度.

2.2复杂度的重要性

1️⃣预判系统的 “承载能力”. 2️⃣降低开发与维护成本. 3️⃣确保系统的 “通用性” 与 “扩展性”.

3. 时间复杂度

定义:在计算机科学中,算法的时间复杂度是⼀个函数式T(N),它定量描述了该算法的运⾏时间.时间复杂度是衡量程序的时间效率,那么为什么不去计算程序的运⾏时间呢?
1️⃣因为程序运⾏时间和编译环境和运⾏机器的配置都有关系,⽐如同⼀个算法程序,⽤⼀个⽼编译
器进⾏编译和新编译器编译,在同样机器下运⾏时间不同。
2️⃣同⼀个算法程序,⽤⼀个⽼低配置机器和新⾼配置机器,运⾏时间也不同.
3️⃣并且时间只能程序写好后测试,不能写程序前通过理论思想计算评估.

那么算法的时间复杂度是⼀个函数式T(N)到底是什么呢?这个T(N)函数式计算了程序的执⾏次数.通过c语⾔编译链接学习,我们知道算法程序被编译后⽣成⼆进制指令,程序运⾏,就是cpu执⾏这些编译好的指令.那么我们通过程序代码或者理论思想计算出程序的执⾏次数的函数式T(N),假设每句指令执⾏时间基本⼀样(实际中有差别,但是微乎其微),那么执⾏次数和运⾏时间就是等⽐正相关,这样也脱离了具体的编译运⾏环境.执⾏次数就可以代表程序时间效率的优劣.⽐如解决⼀个问题的算法a程序T(N) = N,算法b程序T(N) = N2,那么算法a的效率⼀定优于算法b.
执行程序的时间=二进制指令运行时间*执行次数

// 请计算⼀下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;
}
}

Func1 执⾏的基本操作次数:
T (N) = N2 + 2∗N + 10
• N = 10 T(N) = 130
• N = 100 T(N) = 10210
• N = 1000 T(N) = 1002010
通过对N取值分析,对结果影响最⼤
的⼀项是 N2

实际中我们计算时间复杂度时,计算的也不是程序的精确的执⾏次数,精确执⾏次数计算起来还是很⿇烦的(不同的⼀句程序代码,编译出的指令条数都是不⼀样的),计算出精确的执⾏次数意义也不⼤,因为我们计算时间复杂度只是想⽐较算法程序的增⻓量级,也就是当N不断变⼤时T(N)的差别,上⾯我们已经看到了当N不断变⼤时常数和低阶项对结果的影响很⼩,所以我们只需要计算程序能代表增⻓量级的⼤概执⾏次数,复杂度的表⽰通常使⽤⼤O的渐进表⽰法.

3.1 ⼤O的渐进表示法

⼤O符号:是⽤于描述函数渐进⾏为的数学符号.
推导⼤O阶规则:
1️⃣时间复杂度函数式T(N)中,只保留最⾼阶项,去掉那些低阶项,因为当N不断变⼤时,低阶项对结果影响越来越⼩,当N⽆穷⼤时,就可以忽略不计了.
2️⃣如果最⾼阶项存在且不是1,则去除这个项⽬的常数系数,因为当N不断变⼤,这个系数对结果影响越来越⼩,当N⽆穷⼤时,就可以忽略不计了.
3️⃣T(N)中如果没有N相关的项⽬,只有常数项,⽤常数1取代所有加法常数.

通过以上⽅法,可以得到Func1的时间复杂度为:O(N2 )

3.2 时间复杂度计算示例

3.2.1 示例1

// 计算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);
}

Func2执⾏的基本操作次数:
T (N) = 2N + 10
根据推导规则第2条得出
Func2的时间复杂度为:O(N)

3.2.2 示例2

// 计算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);
}

Func3执⾏的基本操作次数:
T (N) = M + N
因此:Func3的时间复杂度为:O(N+M);这里我们不清楚M和N之间的大小;
如果M=N,复杂度是:O(N)或者O(M);
如果M>>N,复杂度是:O(M);
如果M<<N,复杂度是:O(N);

3.2.3 示例3

// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}

Func4执⾏的基本操作次数:
T (N) = 100
根据推导规则第3条得出
Func4的时间复杂度为:O(1)

3.2.4 示例4

// 计算strchr的时间复杂度?
const char * strchr ( const char
* str, int character)
{
const char* p_begin = s;
while (*p_begin != character)
{
if (*p_begin == '\0')
return NULL;
p_begin++;
}
return p_begin;
}

strchr执⾏的基本操作次数:
若要查找的字符在字符串第⼀个位置,则:T(N) = 1
若要查找的字符在字符串最后的⼀个位置,则:T(N) = N
若要查找的字符在字符串中间位置,则:T(N) =2N
因此:strchr的时间复杂度分为:
最好情况:O(1)
最坏情况:O(N)
平均情况:O(N/2)

总结:
通过上⾯我们会发现,有些算法时间复杂度存在最好,平均和最坏情况.
最坏情况:任意输⼊规模的最⼤运⾏次数(上界)
平均情况:任意输⼊规模的期望运⾏次数
最好情况:任意输⼊规模的最⼩运⾏次数(下界)
⼤O的渐进表⽰法在实际中⼀般情况关注的是算法的上界,也就是最坏运⾏情况.

3.2.5 示例5

// 计算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执⾏的基本操作次数:
若数组有序,则:T(N) = N
若数组有序且为降序,则:
T(N) =N*(N-1)/2
若要查找的字符在字符串中间位置,则:
因此:BubbleSort的时间复杂度取最
差情况为:O(N2 )

3.2.6 示例6

//计算Func5的时间复杂度?
void Func5(int n)
{
int cnt = 1;
while (cnt < n)
{
cnt *= 2;
}
}

当n=2时,执⾏次数为1
当n=4时,执⾏次数为2
当n=16时,执⾏次数为4
假设执⾏次数为 x ,则 2x = n
因此执⾏次数:x = log n
因此:Func5的时间复杂度取最差情况为:
O(log⁡2n\log_{2}{n}log2n)

注意:log2 n 、 log n 、 lg n 的表示
当n接近⽆穷⼤时,底数的⼤⼩对结果影响不⼤.因此,⼀般情况下不管底数是多少都可以省略不写,即可以表⽰为log n
不同书籍的表⽰⽅式不同,以上写法差别不⼤,建议使⽤ log n.

3.2.7 示例7

// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}

递归算法的时间复杂度=单次递归的时间复杂度*递归次数
调⽤⼀次Fac函数的时间复杂度为O(1)
⽽在Fac函数中,存在n次递归调⽤Fac函数
因此:阶乘递归的时间复杂度为:O(n)

4. 空间复杂度

空间复杂度也是⼀个数学表达式,是对⼀个算法在运⾏过程中因为算法的需要额外临时开辟的空间.空间复杂度不是程序占⽤了多少bytes的空间,因为常规情况每个对象⼤⼩差异不会很⼤,所以空间复杂度算的是变量的个数.空间复杂度计算规则基本跟实践复杂度类似,也使⽤⼤O渐进表⽰法.
注意:函数运⾏时所需要的栈空间(存储参数、局部变量、⼀些寄存器信息等)在编译期间已经确定好
了,因此空间复杂度主要通过函数在运⾏时候显式申请的额外空间来确定.

4.1 空间复杂度计算示例

4.1.1 示例1

// 计算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额外申请的空间有exchange等有限个局部变量,使⽤了常数个额外空间, 因此空间复杂度为:O(1)

4.1.2 示例2

// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}

递归算法的空间复杂度=单次递归度空间复杂度*递归次数
Fac递归调⽤了N次,额外开辟了N个函数栈帧,每个栈帧使⽤了常数个空间
因此空间复杂度为:O(N)

5. 常⻅复杂度对⽐


6.重新思考轮转数组的实现


思路1:
时间复杂度O(n); 空间复杂度 O(n)
申请新数组空间,先将后k个数据放到新数组中,再将剩下的数据挪到新数组中.

void rotate(int* nums, int numsSize, int k){//创建新数组int newArr[numsSize];//向右轮转k次,将结果保存在临时数组中for (int i = 0; i < numsSize; ++i){newArr[(i + k) % numsSize] = nums[i];//防止数组元素越界}//将临时数组中的数据导入到原数组中for (int i = 0; i < numsSize; ++i){nums[i] = newArr[i];}}

思路2:
时间复杂度O(n); 空间复杂度 O(1)
前n-k个逆置:4 3 2 1 5 6 7
后k个逆置 : 4 3 2 1 7 6 5
整体逆置 : 5 6 7 1 2 3 4

void reverse(int* nums,int begin,int end)
{
while(begin<end){
int tmp = nums[begin];
nums[begin] = nums[end];
nums[end] = tmp;//实现个数交换
begin++;
end--;
}
}
void rotate(int* nums, int numsSize, int k)
{
k = k%numsSize;//防止k>=numsSize
reverse(nums,0,numsSize-k-1);//前numsSize-k数据逆置
reverse(nums,numsSize-k,numsSize-1);//后k个数据逆置
reverse(nums,0,numsSize-1);//整体逆置
}

以上就是今天的博客内容了,希望能够帮助到读者朋友们!
我们一起继续加油努力💪!一键三连🙏!!!
本篇完结,点赞收藏加关注,找到小编不迷路,感谢大家🙏🤝!

http://www.dtcms.com/a/541066.html

相关文章:

  • Quant4.0,基于AgentScope开发 | 年化316%,回撤14%的超级轮动策略,附python代码
  • 第三方检测机构如何选对LIMS?以“数治”破解效率与合规难题
  • 建设网站需要什么手续设计公司网站 唐山
  • 网站接入地查询织梦网站修改数据库表数据
  • 南昌企业做网站设计怎么设置微信公众号
  • esp32在vscode使用jtag下载调试openocd问题解决
  • Java奇幻漂流:从Spring秘境到微服务星辰的冒险指南
  • Ubuntu 20.04.6使用vscode从0开始搭建uniapp
  • 【PDF】PDF技术概述
  • 金仓KCSM认证攻略:经验分享
  • [Nginx] 3.由HTTP转发引出的重定向问题
  • 子网站如何做哪个网站有做兼职的
  • hive的SQL语句练习2
  • 做中学网站做课件的网站有哪些
  • 【Java +AI |基础篇day6、7、8 OOP高级 继承 多态 抽象 代码块 内部类 函数式编程】
  • 菲林式投影灯成像模糊?OAS 软件精准优化破瓶颈
  • 匹配最接近的行政区域sql 反向匹配
  • ROS2系列 (6) : 多功能包工作空间(Workspace)最佳实践
  • Nacos动态刷新实战:客户端集成与案例验证
  • 谷歌网站怎么做排名pc端手机网站 viewport 自适应
  • 建设银行衡阳市分行网站数字营销
  • 淄博网站建设卓迅科技有限公司属于什么企业类型
  • 梅州企业网站wap网站推荐
  • 14、Docker swarm-1-理论
  • Jenkins Share Library教程 —— 企业级 Jenkins Shared Library 实战示例
  • 做微新闻怎么发视频网站seo网站沙盒期
  • 中国建设信息港网站wordpress开源程序建站教程
  • Win11 跨设备同步的便笺内容突然丢失,如何恢复?
  • 三、cmake语法-提高篇
  • 仓颉编程(20)泛型