1. 算法效率
如何去衡量一个算法的好坏?
通常我们从时间效率和空间效率两个方面去分析算法的好坏。时间效率即时间复杂度,空间效率被称为空间复杂度。时间复杂度主要是衡量一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间。
常见的复杂度大小比较:O(2^N) > O(N^2) > O(N*logN) > O(N) > O(logN) > O(1)
在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。因为现在的内存不像以前那么贵,所以经常听到过牺牲空间来换取时间的说法
事后统计法
**这种方法可行,但不是一个好的方法。**该方法有两个缺陷:一是要想对设计的算法的运行性能进行评测,必须先依据算法编制相应的程序并实际运行;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优势。
事前分析估算的方法
因事后统计方法更多的依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优劣。因此人们常常采用事前分析估算的方法。
在编写程序前,依据统计方法对算法进行估算。一个程序在计算机上运行时所消耗的时间取决于下列因素:
(1) 算法采用的策略、方法;(2). 编译产生的代码质量;(3) 问题的输入规模;(4) 机器执行指令的速度。
2. 时间复杂度
定义:在计算机科学中,算法的时间复杂度是一个数学函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间与其中语句的执行次数成正比例,算法中基本的执行次数,即算法的时间复杂度。
通常在计算算法的时间复杂度时,并不一定要计算精确的执行次数,而只需要计算大概执行次数,我们通常使用大 O 渐进法来表示。
1. 用常数 1 来取代运行时间中所有的加法常数;
2. 只保留最高的阶项;
3. 如果最高项存在且不是 1,则去除与这个项目相乘的常数,得到的结果就是大 O 阶.
代码演示如下:
//请计算一下func1基本操作运行了多少次
void func1(int N){
int count = 0;
for (int i = 0; i < N ; i++) {
for (int j = 0; j < N ; j++) {
count++;
}
}//这个for循环运行 N * N 次
for (int k = 0; k < 2 * N ; k++) {
count++;
}//这个for循环运行 N 次
int M = 10;
while ((M--) > 0) {
count++;
}//这个操作运行 10 次
System.out.println(count);
}
对于上述代码进行时间复杂度分析:
第一个嵌套 for 循环的执行次数为 N^2,第二个 for 循环的执行次数为 2N,第三个 while 循环的执行次数是 10, 则 F(N)=F(N^2) + F(2N)+10,根据大 O 渐进表示法,该算法的时间复杂度为 O(N^2).
算法的时间复杂度存在最好、平均、最坏情况:
最好情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最坏情况:任意输入规模的最小运行次数(下界)
注意: O(1) 表示基本语句的执行次数是一个常数,一般来说,只要算法中不存在循环语句,时间复杂度就为 O(1)。
下面选取一些常见的进行讲解
常数阶 O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是 O(1),如:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用 O(1) 来表示它的时间复杂度。
线性阶 O(n)
这个在最开始的代码示例中就讲解过了,如:
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码,for 循环里面的代码会执行 n 遍,因此它消耗的时间是随着 n 的变化而变化的,因此这类代码都可以用 O(n) 来表示它的时间复杂度。
对数阶 O(logN)
int i = 1;
while(i<n)
{
i = i * 2;
}
从上面代码可以看到,在 while 循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环 x 次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2^n
也就是说当循环 log2^n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)
线性对数阶 O(nlogN)
线性对数阶 O(nlogN) 其实非常容易理解,将时间复杂度为 O(logn) 的代码循环 N 遍的话,那么它的时间复杂度就是 n * O(logN),也就是了 O(nlogN)。
就拿上面的代码加一点修改来举例:
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}
平方阶 O(n2)
平方阶 O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²) 了。
举例:
for(x=1; i<=n; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}
这段代码其实就是嵌套了 2 层 n 循环,它的时间复杂度就是 O(n*n),即 O(n²)
如果将其中一层循环的 n 改成 m,即:
for(x=1; i<=m; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}
那它的时间复杂度就变成了 O(m*n)
立方阶 O(n³)、K 次方阶 O(n^k)
参考上面的 O(n²) 去理解就好了,O(n³) 相当于三层 n 循环,其它的类似。
指数阶 O(2^N)
递归的时间复杂度计算: 递归的次数 * 每次递归后代码的执行次数 (也是用大 O 渐进法表示)
int Fibonacci(int N)
{
return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}
上面的代码是一个计算斐波那契数列的方法,使用的是递归的方法,我们知道递归的方法来计算斐波那契数列是非常低效的,最好还是使用循环的方法,但是递归的时间复杂度是多少呢?
我们知道每一次调用这个方法时,我们的时间复杂度是一个常数,那么这个递归的时间复杂度就是我们一共调用了多少次方法,现在我们来分析一下我们到底调用了多少次这个方法。
其调用结构如图所示
当我们输入 N 时,方法会进行两调用,然后不断地调用,其调用的结果如图所示,在右下角是有一个空缺区域的,但是我们可以把这一块空缺的区域看作一个常数,假设它是满的,那么我们执行调用的总次数为 F(N)=2⁰+2¹+2²+…+2N-1=2N-1(使用等比数列得出结果),所以该算法的时间按复杂度为 O(2^N)。
由以上的计算我们就可以发现用递归来算斐波那契数的算法的时间复杂度太高了,也就说明了这个算法的低效。
除此之外,其实还有 平均时间复杂度、均摊时间复杂度、最坏时间复杂度、最好时间复杂度 的分析方法,有点复杂,这里就不展开了。
3. 空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少 bytes 的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数,其计算规则与时间复杂度类似,也采用大 O 渐进表示法.
注意:函数运行时所需要的栈空间 (存储参数、局部变量、一些寄存器信息等) 在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
1. 一个算法在计算机上占用的内存包括:程序代码所占用的空间,输入输出数据所占用的空间,辅助变量所占用的空间这三个方面,程序代码所占用的空间取决于算法本身的长短,输入输出数据所占用的空间取决于要解决的问题,是通过参数表调用函数传递而来,只有辅助变量是算法运行过程中临时占用的存储空间,与空间复杂度相关;
2. 通常来说,只要算法不涉及到动态分配的空间,以及递归、栈所需的空间,空间复杂度通常为 O(1);
代码演示如下:
void bubbleSort(int[] array) {
for (int end = array.length; end > 0; end--) {
boolean sorted = true;
for (int i = 1; i < end; i++) {
if (array[i - 1] > array[i]) {
Swap(array, i - 1, i);
sorted = false;
}
}
if (sorted == true) {
break;
}
空间复杂度:O(1)
这里需要理解:算法在运行过程中临时占用存储空间(额外)大小的量度,这里这开辟 end,sorted ,i 三个零时变量,因为大 O 渐进表示法,3 为常数所以为 1 。
空间复杂度 O(n)
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码中,第一行 new 了一个数组出来,这个数据占用的大小为 n,这段代码的 2-6 行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 O(n)