算法

算法是解决特定问题求解步骤的描述,取零个或多个值作为输入,经过一系列的计算步骤,产生一个或多个值作为输出,这一系列的计算步骤,就叫做算法

只看定义也许并不是很容易理解,那接下来我将举一个简单的例子来说明什么是算法

问题:编写程序计算 1+2+3+...+n

这种难度的问题大家一定都能计算出来,而且可能还都会想出很多种思路,其实这些解决问题的不同的思路,就是很多种不同的算法

那么上面的问题该怎么解决呢,最简单的做法就是将1-n之间的数依次相加起来

算法1:

void Sum1(int n)
{int i = 0;int sum = 0;for (i = 1; i <= n; i++){sum += i;}printf("%d\n", sum);
}

除此之外,我们还可以用等差数列求和公式:(首项 + 末项) × 项数 ÷ 2 = 和

算法2:

void Sum2(int n)
{int sum = 0;sum = (1 + n) * n / 2;printf("%d\n", sum);
}

可以看到,虽然只是一个简单的问题,却可以写出两种不同的算法,虽然每一种都可以得到正确的结果,但是它们的效率其实是不同的。算法1种语句执行了1+(n+1)+n+1=2n+3次,算法2中语句执行了1+1+1=3次,显然算法1的效率是比算法2慢的,而当输入量n的数值越来越大时,两个算法之间效率的差距也会越来越大,而这个输入量n的多少,也被称为问题输入规模


算法优劣的衡量

既然算法有好坏之分,那么我们应该用什么方式判断算法的好坏呢?

我最初的想法是根据程序的运行时间来判断,我认为应该不只是我一个人在第一时间有这种想法吧,但是很遗憾,这种想法是有很大缺陷的

  • 首先,如果想计算程序的运行时间,那么我们首先就先要将代码正确完整的编辑好,如果有好几种算法,而每个算法都要编辑好一个程序,当我们最后一个一个将所有程序的运行时间都统计好,再从中挑出运行时间最短的程序,并认为它的算法就是最好的(先不讨论这种用时间判断算法好坏是否正确),那当我们将“最优算法”找到后,其它算法编写出来的程序就都没有用了,而如果我们绞尽脑汁编辑一个算法的程序花费了大量的时间,当我们最终编辑好后,测试出它是一个差得不行的算法,那岂不是整个人都要疯掉了
  • 而且不同的计算机运行相同代码所耗费的时间也是不同的,不知道各位有没有过这样一个经历,同一款游戏,同时启动,别人可能都玩了一局了,你用几年前的计算机可能连游戏都没登陆上。代码也是如此,相同的一段代码受到CPU,操作系统,编译器等等影响,在不同设备上的运行时间是不同的,而就算是同一台机器,在不同情况下,运行时间也不可能完全相同
  • 除了上述原因,问题的输入规模也是一个影响运行时间的因素,因为现在的计算机它的计算效率是很高的,像上面的算法1和算法2,当n比较小时运行时间几乎相等,只有当n特别特别大时,它们的差距才能显现出来,而问题规模明显影响运行时间的例子,我之前写过一个函数的博客,分别用递归与迭代编写求斐波那契数的程序,当问题规模n为1,2,3这种小的数时,运行时间基本没有差别,当问题规模是50时,用迭代实现的程序很快就能求出结果,而递归实现的快十分钟都没求出结果(等不下去了,直接关了),很显然,用递归求斐波那契的算法特别差

显然,我们不应该用程序运行时间来判断算法的好坏,那么接下来就介绍两种判断算法优劣的方式

1.时间复杂度

算法中的基本操作的执行次数,为算法的时间复杂度。

分析一个算法的时间复杂度(推导大O阶方法)

  1. 用常数1取代运行时间中的所有加法常数
  2. 在修改后的运行次数函数中,只保留最高阶项
  3. 如果最高阶项存在且其系数不是1,就去除系数

最后得到的结果就是大O阶

了解了计算时间复杂度的方法,下面我举几个例子来说明

例一

void Sum2(int n)
{int sum = 0;sum = (1 + n) * n / 2;printf("%d\n", sum);
}

此代码是上述求和的算法2,可以看到执行了3次,根据推导规则,要将常数3改为1,修改后符合要求,因此这个算法的时间复杂度是O(1)。在这个算法中,无论输入规模是多少,程序语句的执行次数是不变的,具有这种时间复杂度的算法,又叫常数阶

这些语句在程序运行时都是一条一条执行的,这样的语句的执行次数并不会被输入值n所影响,因此无论有多少条这样的语句,到最后都会根据规则一被常数1取代,而如果函数中除了常数1以外还有其它高次项,那么这个常数1也会被去除。这样来看,若算法中有以输入规模n作为判断条件的循环语句,那么在计算时间复杂度时关键分析循环的执行情况即可

例二

void Func1(int N)
{int count = 0;for (int k = 0; k < 2 * N; ++k){++count;}int M = 10;while (M--){++count;}printf("%d\n", count);
}

在上述分析中,初识数据结构的人会不会对while循环的分析有疑问呢,为什么是执行10次,而不是执行M次? 事实上,在这段代码中传过来的参数只有N,而M是在这个函数中定义的变量,有一个值为10的变量是算法本身的一部分,因此无论输入规模N是多少,while循环体执行的次数是不会改变的。

根据分析,此算法执行次数为2N+10次,根据上述规则先将其改为2N+1,再改为2N,最后去掉系数2,化为N,因此此算法的时间复杂度为O(N),也叫做线性阶

例三

在例二中,初学者可能对M有疑问,那么接下来我们来看例三

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

根据分析,此算法执行次数为M+N次,和例二不同的是,这个函数传过来的参数为N和M,输入规模也就是N和M,它们两个任意一个大小的改变都会影响到对应循环体的执行次数,因此,此算法的时间复杂度为O(M+N)

在继续接下来的例题前,这介绍要下一个知识点

有些算法的时间复杂度存在最好、平均和最坏三种情况:

  • 最好情况:任意输入规模的最小运行次数
  • 平均情况:任意输入规模的期望运行次数
  • 最坏情况:任意输入规模的最大运行次数

而一般在没有特殊说明的情况下,都取最坏情况的时间复杂度、

各位可能会有些疑惑这三种情况在什么时候才会发生,又是怎么判断的,那么接下来就对例四进行复杂度的分析

例四

这里再介绍一个库函数strchr,它的原型如下:

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

它在一个字符串中查找一个由使用者传过去的字符,如果找到了就返回其在字符串中第一次出现的地址,如果没找到就返回一个空指针

函数模拟实现如下:

const char* my_strchr(const char* str, int c)
{while (*str){if (*str == c){return str;}else{str++;}}return NULL;
}

我们不能确定字符串的长度会是多少,因此将字符串长度假设为N,当我们要找的字符刚好就在字符串的第一个位置,那么就是最好情况,时间复杂度是O(1),由于要查找的字符在每一个位置的概率都是相同的,因此平均时间复杂度就是O(N/2),如果要找的字符在字符串最后一个位置(不算'\0'),那么就是最坏情况,时间复杂度是O(N)。而这种算法的时间复杂度我们取最坏时间复杂度为O(N)

例五

在之前的博客我写过一次冒泡排序,例五就来判断冒泡排序的时间复杂度

void bubble_sort(int arr[], int n)
{int i = 0;for (i = 0; i < n-1; i++){int flag = 1;int j = 0;for (j = 0; j < n - 1 - i; j++){if (arr[j] < arr[j + i]){int tmp = arr[j];arr[j] = arr[j + i];arr[j + i] = tmp;flag = 0;}}if (flag == 1){break;}}
}

按照冒泡排序的思想,每进行一轮排序,下轮排序就少了一个数,因此下一轮内循环执行的次数要比上一轮排序少一次,如果有不懂的可以看一下我之前关于冒泡排序的博客,下面附上链接

C语言 - 冒泡排序_ImpEvday_Wang的博客-CSDN博客

或者我们也可以根据代码找出规律,当i=0时,内循环执行了n-1次,当i=1时,内循环执行了n-2次,......,当i=n-2时,内循环执行了1次,循环结束。可以看出执行的次数是个等差数列,那么根据等差数列求和公式,总执行次数为 ((n-1)+1)×(n-1)÷2 = n^2/2-n/2,根据推导规则,保留最高项后为n^2/2,再去掉其系数后得到n^2。因此冒泡排序的时间复杂度为O(n^2),也叫做平方阶

例六

接下来我们分析二分查找的时间复杂度

int BinarySearch(int* arr, int n, int k)
{int left = 0;int right = n - 1;while (left <= right){int mid = left + (right - left) / 2;if (arr[mid] < k){left = mid + 1;}else if (arr[mid] > k){right = mid - 1;}else{return mid;}}return -1;
}

根据二分查找的思想,每次查找,范围将缩小一倍。若原数组有n个数,第一次查找后,范围缩小到n/2,第二次查找后,范围又缩小了一倍,变为n/2^2,这样一直找下去,直到范围中只剩1个数了,该数就是我们要查找的数,这样就可以得到一个公式,若我们查找的次数为x,n/2^x=1,2^x=n,那么查找的次数(也就是执行的次数)就是logn(其实是log以2为底n的对数,但电脑中打不出来)。因此二分查找的时间复杂度为O(logn),又被称为对数阶

下面附上我二分查找的博客链接,感兴趣的朋友可以阅读一下

C语言 - 二分查找_ImpEvday_Wang的博客-CSDN博客

这里我们介绍下一个知识点

递归算法的总执行次数 = 递归次数 × 每次递归中执行的次数

例七

long long Factorial(int N)
{if (N == 0){return 1;}else{return Factorial(N - 1) * N;}
}

如图所示,阶乘的递归算法一共调用了N次,每次递归执行的都是常数次,为O(1),二者相乘,得到阶乘算法的时间复杂度为O(N)

例八

前面提到过,用递归实现的求第N个斐波那契数,当N等于50的时候就已经需要很长时间计算了,那么这种算法的时间复杂度是多少呢

int Fib(int n)
{if (n <= 2){return 1;}else{return Fib(n - 2) + Fib(n - 1);}
}

上图是算法的执行图,进行了大量的重复计算,随着层数的增加,它所需要进行的运算就越多,图中缺了一块的意思是当递归到N<=2就不再向下递归,由于从左至右函数的参数依次减小,因此提前结束递归的层数也会依次减小,因此会导致这些层并每没有完全填满,会缺少一部分。即便如此,缺少的这部分对于如此多的运算几乎是可以忽略不计的。

如果上图看着比较复杂,下面讲给出当N=5时的执行图

设缺的那块的执行次数是x次,那么此算法执行的次数是2^0+2^1+2^2+...+2^(N-3)+2^(N-2),是一个等比数列,根据等比数列求和公式 (2^0 - 2^(N-2)×2) / (1-2) = 2^(N-1) -1 -X = 2^N × 2^(-1) -1 -X ,根据推导规则,只保留最高次项后得2^N,因此用递归实现求斐波那契数的算法的时间复杂度是O(2^N),又叫指数阶。很显然,具有这种时间复杂度的算法十分可怕,我们应该避免写出这种算法

常用的时间复杂度耗费时间排序

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)

而根据求斐波那契数的递归算法的分析,时间复杂度是O(2^n)的算法运行耗费时间已经很长了,而实际上当时间复杂度到了O(n^2),那么当输入规模n逐渐变大时,它的算法耗费时间的上升速度也是极为恐怖的,因此在设计算法时要尽量将时间复杂度控制在O(n^2)之前

至此,时间复杂度的内容就全部分享结束了,接下来介绍衡量算法的第二种方式

2.空间复杂度

是一个算法在运行过程中临时额外占用存储空间大小的量度

分析空间复杂度依然是推导大O阶方式

接下来依然举例说明

例一 判断冒泡排序的空间复杂度

void bubble_sort(int arr[], int n)
{int i = 0;for (i = 0; i < n-1; i++){int flag = 1;int j = 0;for (j = 0; j < n - 1 - i; j++){if (arr[j] < arr[j + i]){int tmp = arr[j];arr[j] = arr[j + i];arr[j + i] = tmp;flag = 0;}}if (flag == 1){break;}}
}

下图是此算法中变量在内存中的存储

可以看到,变量i、变量flag、变量j都是算法本身需要开辟的空间,无论输入规模是多少,这三个变量都会这样开辟,而没有额外空间的使用,因此冒泡排序的空间复杂度是O(1)

这里介绍空间复杂度的第二个知识点

当分析递归算法的空间复杂度时,关键是递归的深度

例二 求N的阶乘的递归算法的空间复杂度

long long Factorial(int N)
{if (N == 0){return 1;}else{return Factorial(N - 1) * N;}
}

可以看到递归了N层,因此空间复杂度是O(N)

例三 求斐波那契数的递归算法的空间复杂度

int Fib(int n)
{if (n <= 2){return 1;}else{return Fib(n - 2) + Fib(n - 1);}
}

在分析这个算法的空间复杂度之前,我们要先想明白一个道理,时间不可以重复利用,而空间是可以重复利用的,而递归算法在执行时并不是一次性把所有递归分支运算需要的空间都开辟出来(容易栈溢出),它的执行过程时一条分支接着一条分支的执行下去,因此递归算法所需要额外开辟的空间就是它最深的层数,在这里递归最多为N-1层,根据推导规则,求斐波那契数的递归算法的空间复杂度为O(N)

至此,空间复杂度也全部介绍完毕

算法效率的衡量方式 - 时间复杂度与空间复杂度相关推荐

  1. LeetCode0:学习算法必备知识:时间复杂度与空间复杂度的计算

    算法(Algorithm)是指用来操作数据.解决程序问题的一组方法.算法是大厂.外企面试的必备项,也是每个高级程序员的必备技能.针对同一问题,可以有很多种算法来解决,但不同的算法在效率和占用存储空间上 ...

  2. 排序算法之 归并排序 及其时间复杂度和空间复杂度

    在排序算法中快速排序的效率是非常高的,但是还有种排序算法的效率可以与之媲美,那就是归并排序:归并排序和快速排序有那么点异曲同工之妙,快速排序:是先把数组粗略的排序成两个子数组,然后递归再粗略分两个子数 ...

  3. 常见排序算法及其对应的时间复杂度、空间复杂度

    常见排序算法及其对应的时间复杂度.空间复杂度: 排序算法经过长时间演变,大体可以分为两类:内排序和外排序.在排序过程中,全部记录存放在内存,则成为内排序:如果排序过程中需要使用外存,则称为外排序,本文 ...

  4. 常见排序算法及对应的时间复杂度和空间复杂度

    排序算法经过了很长时间的演变,产生了很多种不同的方法.对于初学者来说,对它们进行整理便于理解记忆显得很重要.每种算法都有它特定的使用场合,很难通用.因此,我们很有必要对所有常见的排序算法进行归纳. 排 ...

  5. python【数据结构与算法】一种时间复杂度和空间复杂度的计算方法

    文章目录 1 算法的时间复杂度定义 2 推导大O阶方法 2.1 常数阶 2.2 线性阶 2.3 对数阶 2.4 平方阶 2.5 立方阶 3 常见的时间复杂度排序 4 算法空间复杂度 5 常用算法的时间 ...

  6. 深度优先算法(DFS)和广度优先算法(BFS)时间复杂度和空间复杂度计算精讲

    现在我们设定任务为到山东菏泽曹县买牛逼,需要利用深度优先算法(DFS)和广度优先算法(BFS)在中国.省会.市.区县这张大的树中搜索到曹县,那么这个任务Goal就是找到曹县. 假如图的最大路径长度m和 ...

  7. 堆排序重建堆的时间复杂度_排序算法之 堆排序 及其时间复杂度和空间复杂度-Go语言中文社区...

    堆排序是由1991年的计算机先驱奖获得者.斯坦福大学计算机科学系教授罗伯特.弗洛伊德(Robert W.Floyd)和威廉姆斯(J.Williams)在1964年共同发明了的一种排序算法( Heap ...

  8. 排序算法之 冒泡排序 及其时间复杂度和空间复杂度

    冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法.它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来.走访数列的工作是重复地进行直到没有再需要交 ...

  9. 算法的时间复杂度、空间复杂度、稳定性

    1.算法的概念: 算法 (Algorithm),是对特定问题求解步骤的一种描述. 解决一个问题往往有不止一种方法,算法也是如此.那么解决特定问题的多个算法之间如何衡量它们的优劣呢?有如下的指标: 2. ...

  10. 算法复杂度:算法时间复杂度和空间复杂度表示法

    文章地址:http://lzw.me/a/algorithm-complexity.html 算法复杂度分为时间复杂度和空间复杂度. 时间复杂度用于度量算法执行的时间长短:而空间复杂度则是用于度量算法 ...

最新文章

  1. Linux学习笔记-软件安装管理
  2. JSP页面中的pageEncoding和contentType两种属性
  3. JAVA连接Excel最好用的开源项目EasyExcel,官方使用文档及.jar包下载
  4. MySQL在Django框架下的基本操作(MySQL在Linux下配置)
  5. dos窗口mysql创建数据库指定字符集_MySQL数据库 dos 命令窗口命令集
  6. 【英语学习】【Daily English】U10 Education L01 Is this certificate a must?
  7. pandas拉长dataframe
  8. python 删除指定时间之前文件的脚本 包括下级目录
  9. 年薪50万的程序员_985程序员年薪50万,看似风光,但当事人却想转行
  10. 开发工具Charles for Mac(信息抓取) v4.6.3b1
  11. 【记录】帮同学做的一个函数拟合
  12. MySQL中的基本SQL语句
  13. win10简洁之道(有效去广告)
  14. 【Python】实现商品信息管理系统(界面版,附带数据库)
  15. 原来安卓手机安装谷歌服务框架这么简单!
  16. Linux触摸板设置
  17. c# 操作word光标
  18. 利用ipconfig命令查看IP及释放和重获IP
  19. loT行业生死竞速:Aqara绿米得用户得天下
  20. 人工智能实验二——prolog语言求解渡河问题(传教士和野人渡河,农夫渡河问题)实现详解

热门文章

  1. 大数据教学竞赛科研平台设计思路
  2. JAVA一维数组求和
  3. 飞机大战游戏python_《飞》字意思读音、组词解释及笔画数 - 新华字典 - 911查询...
  4. 【转】深度技术分析“为什么ios比android流畅”
  5. 科普硬解,软解,gpu,dsp等等的关系
  6. Mac的邮件客户端使用--登录GMail邮箱和QQ邮箱的解决方案
  7. latex 删除脚注的标号
  8. 邮件助手工具哪个好用?哪个企业群发邮件的软件好用?
  9. word制作多级标题目录
  10. wex5 页面跳转