数据结构是程序的骨架,算法是程序的灵魂。
1.什么是算法?
算法是对特定问题求解步骤的一种描述。
2. 算法的特性
- 有穷性: 算法是由若干条指令组成的有穷序列,总是在执行若干次后结束,不可能永不停止。
- 确定性: 每条语句都有确定的含义,无歧义。
- 可行性: 算法在当前环境条件下可以通过有限次运算来实现。
- 输入/输出: 有零个或多个输入以及一个或者多个输出。
3. 衡量一个算法好坏的常用标准
-
基本标准
- 正确性: 算法能够满足解决问题的需求,能够通过典型的软件测试,并达到预期。
- 易读性: 算法遵循标识符命名规则,简单易懂,注释恰当适量。方便他人阅读,便于后期调试修改。
- 健壮性: 算法能够对非法数据及操作较好的反应和处理。
-
其他标准
-
高效性 :运行效率高,算法运行时间短。
衡量一个算法的高效性,通常我们从时间和空间两个维度去考量。
- 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
- 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。
-
低存储性:算法所需的存储空间小。
-
4. 时间复杂度和空间复杂度
-
时间复杂度
我们知道,对于一个算法的执行时间有:
算法执行的总时间 = 算法中的每条语句执行时间之和
通常情况下,我们可以假设算法的每条语句执行一次所需要的时间为单位时间1,那么算法的执行时间则与算法中需要执行语句的数量成正比,因此,我们可以通过算法中语句数量的总和作为算法时间复杂度的评判标准。
其对应的公式如下:
T [ n ] = O ( f ( n ) ) (1) T[n]=O(f(n))\tag{1} T[n]=O(f(n))(1)
其中 O O O代表的是代码执行时间随数据规模增长变化的趋势,即渐进时间复杂度(简称时间复杂度)。下面我们结合例子具体分析:
int sum = 0; //运行1次 int total = 0; //运行1次 for(int i = 1; i <= n; i++) //运行n+1次(其中1代表最后一次判断不满足循环条件) { sum = sum + i; //运行n次 for(int j = 1; j <= n; j++) //运行n(n+1)次 { total = total + i + j; //运行n*n次 } }
该例子中, T ( n ) = O ( 2 n 2 + 3 n + 3 ) T(n)=O(2n^2+3n+3) T(n)=O(2n2+3n+3),当n足够大的时候,低阶、常数项和系数对 T ( n ) T(n) T(n)的增长趋势起不到决定性的作用,即其并不能左右 T ( n ) T(n) T(n)的增长趋势,因此可以忽略不计,我们可将其简化为
T ( n ) = O ( n 2 ) (2) T(n)=O(n^2)\tag2 T(n)=O(n2)(2)
通常情况下,我们在求一个算法的时间复杂度的时候,我们都会将n看作一个很大的数(因为只有当n很大的时候探讨算法的效率才有意义)。因此我们只需要考虑分析它的高阶项即可。对于部分语句不太好计算运行次数的算法,我们可以对其进行简单的假设,然后进行求解,例子如下:
int = 1; //运行1次 while(i <= n) //假设运行x+1次 { i = i * 2; //运行了x次 }
对于
while
循环中内容我们无法立即确定其运行了多少次,我们可以先假设其运行了x次,每次循环后i的值为 2 2 2、 2 2 2^2 22、 2 3 2^3 23、 2 4 2^4 24、… 、 2 x 2^x 2x,当i=n的时候循环结束,即 x = l o g 2 n x=log_2{n} x=log2n,因此上例,运行次数为 2 l o g 2 n + 2 2log_2{n}+2 2log2n+2,因此其对应的复杂度为
T ( n ) = O ( l o g 2 n ) T(n)=O(log_2{n}) T(n)=O(log2n)
但是并不是所有的算法都能够直接计算其运算次数的。例如:int findx(int x) //在a[n]数组中顺序查找x { for(int i = 0; i < n; i++) { if(a[i] == x) //查找成功,返回其下标 { return i; } } return -1; //查找失败,返回-1 }
对于上例的算法,我们很难计算其到底执行多少次,因为运算次数强依赖于x在数组中的位置,且其位对于不同目标x,位置是不同的,如果第一个元素就是x,则其循环执行一次(最好的情况);如果是最后一个元素是x,则循环需要执行n次(最坏的情况)。如果x的位置概率均等,则循环平均运行次数为 ( n + 1 ) / 2 (n+1)/2 (n+1)/2。
因此,对于时间复杂度会随着输入数据的顺序、位置等不同而存在量级的差距的算法,我们在考量其时间复杂度的时候,需要从最好时间复杂度、最坏时间复杂度和平均时间复杂度等角度去分析,根据其具体的应用场景,做具体的衡量和评判。
-
空间复杂度
空间复杂度表示算法的存储空间与数据规模之间的增长关系,它的分析规则与时间复杂度的规则一致,也是只考虑高阶项,不要低阶项,也不要高阶项的系数。
算法在运行过程中占用的存储空间,主要包括:
-
输入/输出数据
-
算法本身
-
运行过程中额外需要的辅助空间
输入/输出数据占用的空间是必须的,算法本身的大小可以通过精简算法来缩减,但通常情况下,其缩减量对于算法缩减占用的存储空间的贡献量也比较小,因此算法在运行过程中需要的辅助变量占用的空间(即辅助空间)才是降低算法空间复杂度的关键。
void print(int n) { int[] a = new int[n]; for (int i = 0; i < n; ++i) { a[i] = i * i; } }
对于上述例子,忽略其低阶项和系数,其空间复杂度为 o ( n ) o(n) o(n)。
-