努力是为了不平庸~
算法学习有些时候是枯燥的,这一次,让我们先人一步,趣学算法!

程序的复杂性

  • 时间的复杂度
  • 空间的复杂度
  • 爆炸级增量
  • 一个好的算法的产生过程
  • 总结


这是我在力扣网上找的一个题,题目中有这样的限制条件,你知道什么是时间的复杂的吗?你知道该怎样计算时间的复杂度吗?

1.时间的复杂度

好的算法的判断方法是:高效率、低存储。那么计算运行的速度,学习时间的复杂性是十分有必要的。

算法的时间复杂度就是算法运行所需的时间。由于相同配置的计算机进行一次基本运算的时间是一定的,可以用算法基本运算的执行次数来衡量算法的效率,因此我们将算法基本运算的执行次数作为时间复杂度的衡量标准。

观察算法1-1并分析算法的时间复杂度//算法1-1int sum = 0;//运行1次int total = 0;//运行1次for (int i = 1; i <= n; i++)//运行n+1次{sum = sum + i;//运行n次for (int j = 1; j <= n; j++)//运行n*(n+1)次{total = total + i * j;//运行n*n次}}

把算法1-1中所有语句运行的次数加起来:1+1+n+1+n+n*(n+1)+n*n。这可以用一个函数T(n)来表示:

  • T(n)=2n^2+3n+3

当n足够大时,例如n=105,T(n)=2*10^10+3*105^+3。可以看出,算法的运行时间主要取决于最高项。其实时间的复杂度也与最高项息息相关,接下来让我们化简求时间的复杂度。
化简得规则为:

  • 忽略常数项 //T(n)=2n^2+3n
  • 忽略低阶项 //T(n)=2n^2
  • 忽略最高阶的系数 //T(n)=n^2

所以f(n)=n^2,时间复杂度的表示为 O(f(n)),
所以则时间复杂度的表示为 O(n^2)

这个过程叫做大O渐近法:当n趋近于无穷大时,可以把一些影响较小的值省去,留下与时间复杂度相关性最大的值。这就好比你是一家上市公司的老板,年收入几十亿,身为老板的你丢了一百块钱,你这一年的收入也基本不会有什么影响!

以上就是时间复杂度的求法,让我们来几道题练练手吧:

  • 第一题
//算法1-2int i = 1;        //运行1次while (i <= n)    //假设运行x次{i = i * 2;    //假设运行x次}

对于算法1-2我们无法立即确定while以及i=i*2语句运行了多少次。这时可假设运行了x次,每次运行后 i的值为2、22、…、2x,当2x>n结束,此时x>log_2n,所以while运行的次数为log_2n+1, i=i*2运行次数为log2n,所以算法1-2的运行次数为1+2log_2n,时间的复杂度为O(f(n))=O(log_2n)。

在计算时间复杂度时,可以只考虑对算法运行时间贡献大的语句,而忽略那些运行次数少的语句。循环语句中处在循环内层的语句往往运行次数最多,它们是对运行时间贡献最大的语句。例如,在1-1中,total=total+i*j是对算法贡献最大的语句,只计算该语句的运行次数即可。

不是所有的算法都能直接计算运行次数
例如算法1-3,在数组a[n]中顺序查找x并返回其下标i。如果没有找到,则返回-1。

 //算法1-3int  findx(int x){int i = 0;for(i = 0; i<n; i++)if(a[i] == x)return i;return -1;}

对于该算法,很难计算到底执行了多少次,因为运算次数依赖于数组中x的位置。如果第一个元素是x,则执行一次(最好的情况);如果最后一个是x,则执行x次(最坏的情况);如果分布概率均等,则平均运行次数为(n+1)/2。
//
有些算法,如排序、查找、插入算法等,可以分为最好、最坏和平均情况分别求算法渐进复杂度。但考察一个算法时通常考查最坏的情况,而不是考察最好的情况,最坏的情况对衡量算法的好坏具有实际意义。

计算运行的速度:

开发环境是以毫秒单位来计算时间的。使用函数clock(),从进入main()函数开始计时到循环结束计时结束!!!让我们来观测把!!!

  • 时间复杂度O(1)

  • 第一次

#include<stdio.h>
#include<time.h>int main()
{int begin = clock();int n = 0;for (int i = 0; i < 100000; i++){n++;}int end = clock();printf("%d毫秒\n", end - begin);return 0;
}

现在的电脑硬件的运行速度已经非常快了,运行约10万次,连一毫秒都没有。

  • 第二次(1000万次)
#include<stdio.h>
#include<time.h>int main()
{int begin = clock();int n = 0;for (int i = 0; i < 10000000; i++){n++;}int end = clock();printf("%d毫秒\n", end - begin);return 0;
}

1000万次的时候,可以显示时间了!!!

  • 第三次(1亿次)
#include<stdio.h>
#include<time.h>int main()
{int begin = clock();int n = 0;for (int i = 0; i < 100000000; i++){n++;}int end = clock();printf("%d毫秒\n", end - begin);return 0;
}

从10万次到1000万次再到1亿次,时间分别从0变为9再到86毫秒,时间有明显的增长。(不同的计算机计算的时间不同,但时间增长幅度大致相同)

所以说不同程序员设计的相同的程序,运行时间上会有很大的差异,有的运行了已经十万次了,而有的在相同的时间内却一次还没有运行,所以说学习时间的复杂度是十分有必要的。就好比老板在相同的时间内挣一个亿都是小目标,而有的老板在相同的时间内挣一万都是个问题。

2.爆炸级增量

一盘棋的麦子

有个古老的传说,一位国王的女儿不幸落水,水中有很多鳄鱼,国王情急之下下令:“谁能把公主救上来,就把女儿嫁给他。”很多人纷纷退让,一个勇敢的小伙子挺身而出,冒着生命危险把公主救了上来,国王一看是个穷小子,想要反悔,说:“除了女儿,你要什么都可以。”小伙子说:“好吧,我只要一盘棋的麦子。你再第一个格子里放一粒麦子,第二个格子里放2粒麦子,在第三个格子里,放4粒麦子,在第四个格子,放8粒麦子,以此类推,每一个格子里麦子的粒数都是前一个格子里的麦子粒数的两倍。把这64个格子放满了就行,我就想要这么多。”国王听后哈哈大笑,觉得小伙子的要求很容易满足,满口答应。结果方向,把全国的麦子都拿来,也填不完这64个格子…国王无奈,只好把女儿嫁给了这个小伙子。

我们把这个故事当作数学题,试着用你那聪明的大脑求需要的麦粒数目是多少?

解:
S=1+21+22+23+…+263
S=264-1
一粒麦子的重量大约是41.9毫克,这些麦粒的总重量约等于7729000亿千克
全世界人口按77亿计算,每个人差不多可以分得100 000千克(即100吨)
我们称这样的函数为爆炸增量函数,想一想,如果算法的时间复杂度是O(2n)会怎样?随着n的增长,算法会不会”爆掉“?我们经常见到有些算法调试没问题,运行一段时间也没问题,但在关键的时候宕机(即死机)。例如在我们高考完过后,每个考生在那一天都要查自己的成绩,查成绩的网站就那一个,1000个人查成绩没问题,10000个也没问题,可一旦涉及到几十万几百万的时候网站就容易宕机,登陆不上。

常见的算法复杂度

常见的算法时间复杂度有以下几类。
(1)常数阶O(1)。

常数阶算法的运行次数是一个常数,如5、20、100。常数阶算法的时间复杂度通常用O(1)表示。

(2)多项式阶。

很多算法的时间复杂度是多项式,通常用O(n)、O(n2)、O(n3)等表示。

(3)指数阶。

指数阶算法的运行效率极差,程序员往往像躲“恶魔”一样避开这类算法。指数阶算法的时间复杂度通常用 O(2n)、O(n!)、O(nn)等表示。

(4)对数阶。

对数阶算法的运行效率较高,通常用O(logn)、O(nlogn)等表示。

指数阶增量随着x的增加而急剧增加,而对数阶增长缓慢。它们之间的关系如下:

O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(nn)<O(n!)<O(nn)

注意:在设计算法时,我们要注意算法复杂度增量的问题,尽量避免爆炸级增量。

3.空间的复杂度

空间的复杂度:算法占用空间的大小。
算法复杂度的本意是指算法在运行过程中占用了多少存储空间。算法占用的存储空间包括:

  • 输入/输出数据

  • 算法本身

  • 额外需要的辅助空间

    输入/输出数据占用的空间时必需的,算法本身占用的空间可以通过精简算法来缩减,但缩减的量是很小的,可以忽略不计。算法在运行时所使用的辅助变量占用的空间(即辅助空间)才是衡量算法空间复杂度的关键因素。

所以空间复杂度又称为:临时占用存储空间的大小。(寄存器什么的不算)

我们可以类比时间复杂度,当n趋近于无限大时,常量和低阶项都可以忽略不记。在这里,输入/输出、算法本身可以忽略不记。

请分析算法1-4的空间复杂度

  void swap(int* x, int* y)      //交换x,y的值{int temp;temp = *x;         //temp为辅助空间*x = *y;*y = temp;}

算法1-4使用了辅助空间temp,空间复杂度为O(1)
空间复杂度可以表示常数类型个数的辅助变量的程序的空间复杂度
例如:10、100个辅助变量等等

请分析算法1-5(递归)空间复杂度

int fac(int n){ //计算n的阶乘if(n == 0&&n == 1)return 1;elsereturn n*fac(n-1);}

阶乘是典型的递归调用问题,递归包括递推和回归。递推是将原问题不断分解成子问题,直至满足结束条件,返回最近子问题的解;然后逆向逐一回归,最终到达递推开始的原问题,返回原问题的解。

请计算n=5的空间复杂度。

计算机使用一种称为“栈”的数据结构,“栈”类似于一个放盘子的容器,每次从顶端放进去一个,拿出来的时候只能从顶端拿一个,不允许从中间插入或抽取,因此,称为“先进后出”。

递归算法的空间复杂度需要计算递归使用的栈空间

           5的阶乘进栈的过程

        5的阶乘出栈的过程


则5的阶乘的辅助空间是5,空间复杂度为5
则n的阶乘的空间复杂度为n

4.一个好的算法的产生过程

题目:斐波那契数列如下:

  1,1,2,3,5,8,13,21,34,......求数列前n项和。

算法设计:

算法1-6:递归

int Fib1(int n)
{if (n == 1 || n == 2)return 1;elsereturn Fib1(n - 1) + Fib1(n - 2);
}

当写完一个算法时,需要考虑如下3个问题。

  • 算法是否正确?
  • 算法复杂度如何?
  • 算法能否改进?

算法1-6经过公式与运行实例,得出该算法的正确性没有问题
计算算法的时间复杂度:

这是一份递归树,每往下递归一行,函数Fib都要分裂为两个Fib函数,每个程序的函数都要进行计算,所以时间复杂度是指数阶的。算法1-6的时间复杂度属于爆炸增量函数。

难点:计算斐波那契数递归程序的空间复杂度

在这里借用斐波那契数列这个特例,我想让大家更加深刻的理解递归程序的空间复杂度的计算。

斐波那契数列递归程序运行的树状图:

在这里我首先要告诉大家的是空间复杂度的计算与时间复杂度的计算结果是完全不一样的(时间复杂度是2n),我们知道计算机使用一种称为“栈”的数据结构,“栈”类似于一个放盘子的容器,每次从顶端放进去一个,拿出来的时候只能从顶端拿一个,但过程中存在栈帧销毁的过程,因为有了栈帧销毁所以有了栈的空间是可以重复利用的,接下来用图给大家演示如何计算!


栈帧销毁,栈的空间重复利用的演示:

#include<stdio.h>
#include<stdlib.h>
void Add1()
{int a = 0;printf("%p\n", &a);
}
void Add2()
{int b = 0;printf("%p\n", &b);
}
int main()
{Add1();Add2();return 0;
}

当使用函数时,计算机会为其开辟"栈空间“,然后进行栈帧销毁,下次可以重复使用这个空间,所以两次的地址是一样的。

斐波那契数列的空间复杂度为n,表示为O(n).

算法1-7

int Fib2(int n)
{int arr[n + 1] = { 0 };arr[1] = 1;arr[2] = 1;for (int i = 3; i <= n; i++){arr[i] = arr[i - 1] + arr[i - 2];}return arr[n];
}

很明显,算法1-7的算法复杂度为O(n),因为算法1-7仍然遵从F(n)的定义,所以正确性没有问题,但时间复杂度却从算法1-6的指数阶降到了多项式阶,算法效率有了重大突破!使用了一整个数组的辅助变量,算法的空间复杂度为O(n);

算法1-7使用了一整个数组的辅助变量,其实我们仔细观察,我们只需要一个辅助变量即可,请看!

算法1-8

int Fib3(int n)
{if (n == 1 || n == 2)return 1;int s1 = 1;int s2 = 1;int temp = 0;for (int i = 3; i <= n; i++){temp = s1;s1 = s2;s2 = temp + s2;}return s2;
}

很明显,我们只用了一个辅助变量temp即可,空间复杂度降为O(1),时间复杂度为O(n);

5.总结

  • 将程序执行的次数作为时间复杂度的衡量标准
  • 时间复杂度通常用符号O(f(n))表示
  • 衡量算法的好坏通常考查算法的最坏情况
  • 空间复杂度只计算辅助空间
  • 递归算法的空间复杂度需要计算递归使用的栈空间
  • 设计算法应避免爆炸级增量复杂度

你程序的复杂性知道嘛?相关推荐

  1. P12- 软件复杂性 之度量、程序图、强连通图、McCabe度量法

    [软考-软件设计师-历年真题-2013年上半年上午基础知识] 软件的复杂性主要体现在程序的复杂性.(30)是度量软件复杂性的一个主要参数.若采用McCabe度量法计算环路复杂性,则对于下图所示的程序图 ...

  2. 如何构建虚拟护士应用程序?

    如何构建虚拟护士应用程序? How to build a virtual nurse app like Sensely? 传统上,技术的进步引发了企业的变革.由最先进的计算机软件提供的交互式工具意味着 ...

  3. 嵌入式C程序基础与编程结构

    嵌入式C程序基础与编程结构 Basics of Embedded C Program and Programming Structure 嵌入式C编程是处理器在我们日常生活中遇到的每一个嵌入式系统(如 ...

  4. 面向 Java 开发人员的 Ajax: 构建动态的 Java 应用程序

    面向 Java 开发人员的 Ajax: 构建动态的 Java 应用程序 Ajax 为更好的 Web 应用程序铺平了道路 在 Web 应用程序开发中,页面重载循环是最大的一个使用障碍,对于 Java™ ...

  5. 《高效程序员的45个习惯》-之三

    请您在阅读本文之前,先了解<高效程序员的45个习惯>-之二. 每一期都会涉及15个话题,用3期来列出这45个习惯,每次不贪多,贪精,大家如果有空,一定要细细品味这15个习惯. 注意:每一个 ...

  6. Oracle学习之三 程序控制结构

    1.条件控制 1.1 IF语句 if语句由于根据条件,执行两个代码块之一.其语法形式如下: IF 条件1 THEN ... ELSEIF 条件2 THEN ... ELSE ... END IF; 这 ...

  7. Java程序员从笨鸟到菜鸟之(六十七)细谈Spring(一)spring简介

    spring 是一个开源框架,是为了解决企业应用程序开发复杂性而创建的.框架的主要优势之一就是其分层架构,分层架构允许您选择使用哪一个组件,同时为 J2EE 应用程序开发提供集成的框架.  然而,Sp ...

  8. 【中级软考】什么是McCabe测量法(McCabe复杂性度量、环路度量。计算有向弧数、结点数、强连通分量个数)

    McCabe度量法是由托马斯·麦克凯提出的一种基于程序控制流的复杂性度量方法.McCabe复杂性度量又称环路度量.它认为程序的复杂性很大程度上取决于程序图的复杂性.单一的顺序结构最为简单,循环和选择所 ...

  9. vb设计一个由计算机,计算机VB程序的设计第一章.ppt

    Visual Basic程 序 设 计 ;1.初期的程序设计 高运行效率.少占用内存为目标2.结构化程序设计程序的可读性.可维护性为目标 程序=算法+数据结构 的面向过程的程序设计3.面向对象的程序设 ...

最新文章

  1. 【编译原理】让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 6.)(python/c/c++版)(笔记)
  2. nanflash编程的地址问题
  3. 微信小程序 fire_如何在Fire TV和Fire TV Stick上侧面加载应用程序
  4. YBTOJ洛谷P4869:出现位置(线性基)
  5. 华为内部面试题库---(6)
  6. pagePiling.js - 创建美丽的全屏滚动效果
  7. 数独题的生成与解决方法
  8. DDD Microservices
  9. matlab图像处理教学视频,MATLAB图像处理实例详解视频教程
  10. SPSS基础教程:SPSS菜单命令详解(三)
  11. python2.7下安装PyQt4
  12. 接入淘宝客+拼多多(多多客)+京东进行优惠券推广
  13. 设计师胡晓丹的创作历程
  14. 15款你可能不知道的精致Mac应用
  15. ACM训练了日记—12月10日
  16. 笔记本计算机作文,介绍笔记本电脑的作文600字
  17. 如何获得你的准确位置及iphon手机应用定位不准确原因分析
  18. (XJTLU)Methodology to MAV Auto-Polination
  19. 追忆第一次南下深圳有哪些重大抉择(上)?
  20. STM32输入捕获原理与配置

热门文章

  1. java做同学录管理系统_基于jsp的同学录管理系统-JavaEE实现同学录管理系统 - java项目源码...
  2. 项目经理的转型进阶重点:瞄准主线任务
  3. Android中arm64-v8a、armeabi-v7a、armeabi是什么?
  4. 对Android中arm64-v8a、armeabi-v7a、armeabi、x86认识
  5. 最新数据处理 之 最新DSM(AW3D30)数据批量拼接--文末附数据获取方式
  6. 产品8D报告是指哪8D步骤
  7. 咬文嚼字中医启蒙-归经
  8. js入门 运算符的入门到了解
  9. 【Joy of Cryptography 读书笔记】Chapter 8 分组密码的工作模式(Modes of Operation)
  10. docker的可视化管理