如何衡量一个算法的好与坏?
答案是:用时间复杂度和空间复杂度来衡量算法的好与坏。
一、时间复杂度
时间复杂度是指当前问题的规模以某种单位从1增加到n时,解决这个问题的算法在执行时所耗费的时间也以某种单位由1增加到T(n),就称此算法的时间复杂度为T(n)。
1、分析方法:
(1)时间复杂度就是函数中基本操作所执行的次数。
(2)一般默认的是最坏时间复杂度,即分析最坏情况下所能执行的次数。
(3)忽略掉常数项及系数,保留对增长因子影响最大的一部分。
(4)关注运行时间的增长趋势,关注函数式中增长最快的表达式,忽略系数。
(5)计算时间复杂度是估算随着n的增长函数执行次数的增长趋势。
(6)递归算法的时间复杂度为:递归总次数 * 每次递归中基本操作所执行的次数。
注意:时间复杂度实际就是一个函数,该函数计算的是执行基本操作的次数。
2、算法的情况分类:
(1)最坏情况:任意输入规模的最大运行次数。(上界)
(2)平均情况:任意输入规模的期望运行次数。
(3)最好情况:任意输入规模的最小运行次数,通常最好情况不会出现。(下界)
注意:在实际中,通常考量的是最坏情况。理由如下:
1)一个算法的最坏情况的运行时间是在任意输入下运行时间的上界。
2. 对于某些算法,最坏的情况出现的较为频繁。
3. 大体上看,平均情况与最坏情况一样差。
二、空间复杂度
空间复杂度是指当前问题的规模以某种单位从1增加到n时,解决这个问题的算法在执行时所占用的存储空间也以某种单位由1增加到S(n),就称此算法的空间复杂度为S(n)。
也就是说:算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系。算法的空间复杂度S(n)定义为该算法所耗费空间的数量级。
(1)若算法执行时所需要的辅助空间相对于输入数据量n而言是一个常数(S(n)=O(f(n))),则称这个算法的辅助空间为O(1)。
(2)递归算法的空间复杂度计算:递归深度*每次递归所创建的对象个数(辅助空间)。如果每次递归所需的辅助空间是常数,则递归的空间复杂度是 O(N)。
三、算法复杂度的计算
1、递归算法的时间复杂度计算:
递归算法的时间复杂度=递归总次数*每次递归次数。
2、递归算法的空间复杂度计算:
递归算法的空间复杂度=递归深度*每次递归所创建的对象个数。
【例一】
(1)计算折半查找的复杂度(非递归)
# include<stdio.h>
# include<stdlib.h>
int BinerySearch(int *arr, int key, int len)
{
int left = 0;
int right = len - 1;
while (left <= right)
{
int mid = left + (right - left) >> 1;
if (key < arr[mid])
{
return right = mid - 1;
}
else if (key > arr[mid])
{
return left = mid + 1;
}
else
{
return mid;
}
}
return -1;
}
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int len = sizeof(arr) / sizeof(arr[0]);
int ret = BinerySearch(arr, 6, len);
if (ret)
{
printf("找到了!/n");
}
else
{
printf("没找到!/n");
}
system("pause");
return 0;
}
查找过程如下:
由上图分析可得出规律:
如果程序要执行M次,那么N/(2^M)=1,则M=log2N(2为底数,N为对数)。因为在最坏的情况下,二分查找需要查找的次数为log2N(2为底数,N为对数)次,即执行的次数,所以二分查找的时间复杂度为O(log2N)。因为在整个程序中,新创建变量个数为len是常数级的,所以二分查找的空间复杂度即就是O(1)。
(2)计算折半查找的复杂度(递归)
# include<stdio.h>
# include<stdlib.h>
int count = 0;
int BinarySearch(int arr[], int left, int right, int key)
{
int mid = left + ((right - left) >> 1);
if (left > right)//递归出口
{
return -1;
}
if (key < arr[mid])
{
right = mid - 1;
BinarySearch(arr, left, right, key);
}
else if (key > arr[mid])
{
left = mid + 1;
BinarySearch(arr, left, right, key);
}
else
{
return mid;
}
count++;
}
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int ret = BinarySearch(arr, 0, 8, 1);
if (ret == -1)
{
printf("没有找到!/n");
}
else
{
printf("找到了!/n");
}
printf("%d/n", ret);
system("pause");
return 0;
}
查找过程示意如下:
对上图分析可得:
1)假设递归的总次数为M,那么递归的深度也是M,则时间复杂度为O(log2 N)//2为底数,N为对数。
2)因为递归的空间复杂度=递归深度每次创建对象的个数,又因为每次创建对象所需要的辅助空间都是常数级别的,所以递归的空间复杂度=M常数=log2 N*常数,
则空间复杂度为O(log2 N )。
【例2】斐波那契数列的时间复杂度和空间复杂度
斐波那契数列的定义如下:
方法一:时间复杂度为:O(2^N);空间复杂度为:O(N)。
#include<stdio.h>
#include<stdlib.h>
long long Fibonacci1(int n)
{
return n < 2 ? n : Fibonacci1(n - 1) + Fibonacci1(n - 2);
}
int main()
{
long long ret = Fibonacci1(4);
printf("%ld/n", ret);
system("pause");
return 0;
}
运行结果为:
其图示如下:
1)从图中可看出,这是非满二叉树。因为时间复杂度的计算是在最坏情况下考量的。所以以满二叉树来计算时间复杂度,即递归的总次数为总节点的个数:2^h-1=2^(N-1)-1,其中,h为递归深度,N为层数(在本例中N为4),则时间复杂度为O(2^N)。
2)因为递归算法的空间复杂度=递归深度*每次递归所创建的对象个数,其中递归深度为h=N-1,每次递归所创建的对象个数为2,则递归算法的空间复杂度=(N-1)*2=O(N)。
方法二(非递归-升级):时间复杂度为:O(n);空间复杂度为:O(n)。
long long *Fibonacci2(int n)
{
long long *array = new long long [n + 1];
array[0] = 0;//注意
array[1] = 1;
for (int i = 2; i < n + 1; i++)
{
array[i] = array[i - 1] + array[i - 2];
}
return array;
}
释:因为循环的基本操作次数是:n+1-2=n+1,辅助空间是:n+1,所以:
1)非递归算法的时间复杂度为:O(n);
2)非递归算法的空间复杂度为:O(n)。
方法三(非递归-再升级):时间复杂度为:O(n);空间复杂度为:O(l)。
#include<stdio.h>
#include<stdlib.h>
long long Fibonacci2(int n)
{
if (n == 1 || n == 2)
{
return n;
}
int first = 1;
int second = 1;
long long ret = 0;
for (int i = 2; i < n; i++)
{
ret = first + second;
first = second;
second = ret;
}
return ret;
}
int main()
{
long long ret = Fibonacci2(16);
printf("%ld/n", ret);
system("pause");
return 0;
}
释:因为循环的基本次数是:n-2+1=n-1,所用的辅助空间是:常数级别的,所以:
1)非递归算法的时间复杂度为:O(n);
2)非递归算法的空间复杂度为:O(l)。
四、时间复杂度的种类
1、常用的时间复杂度有以下七种,如下图所示:
2、复杂度依次增加:
O(1)常数型 < O(log2 n)对数型 < O(n)线性型 < O(nlog2n)二维型 < O(n^2)平方型 < O(n^3)立方型 < O(2^n)指数型。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/18199.html