算法基础:数据结构与算法的探索

发表时间: 2021-03-16 06:07

算法的特征

一个算法应该具有以下五个重要的特征:

有穷性

(Finiteness)

算法的有穷性是指算法必须能在执行有限个步骤之后终止;一个算法总是需要(输入合法的情况下)在有限的步骤结束,即每个算法需要在有穷的时间内完成。

确切性

(Definiteness)

算法的每一步骤必须有确切的定义;任何条件下,算法只有唯一的一条执行路径,即对于相同的输入只能得到相同的输出。

输入项

(Input)

一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;

输出项

(Output)

一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;

可行性

(Effectiveness)

算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,即每个计算步骤都可以在有限时间内完成(也称之为有效性)。

算法设计的要求

解决一个问题的方法可能有很多,但能称得上算法的,首先它必须能彻底解决这个问题(称为准确性),且根据其编写出的程序在任何情况下都不能崩溃(称为健壮性)。

在满足准确性和健壮性的基础上,还有一个重要的筛选条件,即通过算法所编写出的程序的运行效率。程序的运行效率具体可以从 2 个方面衡量,分别为:

  • 程序的运行时间。
  • 程序运行所需内存空间的大小。

算法涉及的要求主要有以下几个方面

1) 正确性

正确性(Correctness)指的是该算法能够满足预先指定的功能与性能的需求,即能够得到正确答案。

2)健壮性

健壮性(Robustness)指的是当输入数据不合法时,算法也能做出相关的处理,而不是产生不可预计的效果。

3)可读性

可读性(Readability)指的是算法是可以阅读,理解和交流的。

4)耗时低,占用空间少

运行时间(Running time)与占用空间(Storage space)概念,在设计算法时,我们总是希望能够更少地使用时间和空间达成我们的目标。

一个算法的评价主要从时间复杂度和空间复杂度来考虑。

时间复杂度

算法的时间复杂度是指执行算法所需要的计算工作量。

一般来说,计算机算法是问题规模n的函数f(n),算法的时间复杂度也因此记做:t=o(f(n))

因此,问题的规模n越大,算法执行的时间的增长率与f(n)的增长率正相关,称作渐进时间复杂度

实际场景中,我们更喜欢用一个估值来表示算法所编程序的运行时间。

也就是说,表示一个算法所编程序运行时间的多少,用的并不是准确值(事实上也无法得出),而是根据合理方法得到的预估值。

那么,如何预估一个算法所编程序的运行时间呢?很简单,先分别计算程序中每条语句的执行次数,然后用总的执行次数间接表示程序的运行时间。

以一段简单的 C 语言程序为例,预估出此段程序的运行时间:

for(int i = 0 ; i < n ; i++)     //<- 从 0 到 n,执行 n+1 次{    a++;                         //<- 从 0 到 n-1,执行 n 次}

可以看到,这段程序中仅有 2 行代码,其中:

  • for 循环从 i 的值为 0 一直逐增至 n(注意,循环退出的时候 i 值为 n),因此 for 循环语句执行了 n+1 次;
  • 而循环内部仅有一条语句,a++ 从 i 的值为 0 就开始执行,i 的值每增 1 该语句就执行一次,一直到 i 的值为 n-1,因此,a++ 语句一共执行了 n 次。

因此,整段代码中所有语句共执行了 (n+1)+n 次,即 2n+1 次。数据结构中,每条语句的执行次数,又被称为该语句的频度。整段代码的总执行次数,即整段代码的频度。

再举一个例子:

for(int i = 0 ; i < n ; i++)           // n+1{     for(int j = 0 ; j < m ; j++)       // n*(m+1)    {        num++;                         // n*m    }}

计算此段程序的频度为:(n+1)+n*(m+1)+n*m,简化后 2*n*m+2*n+1

值得一提的是,不同程序的运行时间,更多场景中比较的是在最坏条件下程序的运行时间。以上面这段程序为例,最坏条件即指的是当 n、m 都为无限大时此段程序的运行时间。

要知道,当 n、m 都无限大时,我们完全就可以认为 n==m。在此基础上,
2*n*m+2*n+1 又可以简化为 2*n2+2*n+1,这就是此段程序在最坏情况下的运行时间,也就是此段程序的频度。

如果比较以上 2 段程序的运行时间,即比较 2n+1 和 2*n2+2*n+1 的大小,显然当 n 无限大时,前者要远远小于后者,如上图所示

事实上,对于一个算法(或者一段程序)来说,其最简频度往往就是最深层次的循环结构中某一条语句的执行次数。例如 2n+1 最简为 n,实际上就是 a++ 语句的执行次数;同样 2n2+2n+1 简化为 n2,实际上就是最内层循环中 num++ 语句的执行次数。

数据结构推出了大 O 记法(注意,是大写的字母 O,不是数字 0)来表示算法(程序)的运行时间。发展至今,此方法已为大多数人所采纳。

用大 O 记法表示上面 2 段程序的运行时间,则上面第一段程序的时间复杂度为 O(n),第二段程序的时间复杂度为 O(n2)。

常用的几种时间复杂度,以及它们之间的大小关系:

O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(n2)平方阶 < O(n3)(立方阶) < O(2n) (指数阶)

空间复杂度

算法的空间复杂度是指算法需要消耗的内存空间。其计算和表示方法与时间复杂度类似,一般都用复杂度的渐近性来表示。同时间复杂度相比,空间复杂度的分析要简单得多。

和时间复杂度类似,一个算法的空间复杂度,也常用大 O 记法表示。

要知道每一个算法所编写的程序,运行过程中都需要占用大小不等的存储空间,例如:

  • 程序代码本身所占用的存储空间;
  • 程序中如果需要输入输出数据,也会占用一定的存储空间;
  • 程序在运行过程中,可能还需要临时申请更多的存储空间。


首先,程序自身所占用的存储空间取决于其包含的代码量,如果要压缩这部分存储空间,就要求我们在实现功能的同时,尽可能编写足够短的代码。

程序运行过程中输入输出的数据,往往由要解决的问题而定,即便所用算法不同,程序输入输出所占用的存储空间也是相近的。

事实上,对算法的空间复杂度影响最大的,往往是程序运行过程中所申请的临时存储空间。不同的算法所编写出的程序,其运行时申请的临时存储空间通常会有较大不同。

举个例子:

int n;scanf("%d", &n);int a[10];

通过分析不难看出,这段程序在运行时所申请的临时空间,并不随 n 的值而变化。而如果将第 3 行代码改为:

int a[n];

此时,程序运行所申请的临时空间,和 n 值有直接的关联。

所以,如果程序所占用的存储空间和输入值无关,则该程序的空间复杂度就为 O(1);反之,如果有关,则需要进一步判断它们之间的关系:

  • 如果随着输入值 n 的增大,程序申请的临时空间成线性增长,则程序的空间复杂度用 O(n) 表示;
  • 如果随着输入值 n 的增大,程序申请的临时空间成 n2 关系增长,则程序的空间复杂度用 O(n2) 表示;
  • 如果随着输入值 n 的增大,程序申请的临时空间成 n3 关系增长,则程序的空间复杂度用 O(n3) 表示;
  • 等等。

在多数场景中,一个好的算法往往更注重的是时间复杂度的比较,而空间复杂度只要在一个合理的范围内就可以。