1.程序算法效率
1.1什么是算法?
算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。
1.2衡量算法好坏的标准
这是一个通过函数递归算法实现斐波那契数列的代码
long long Fib(int N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
你认为这样的算法是否好呢?让我们带着疑惑来阅读本篇文章,相信你读完后一定会对此算法有所理解,也希望你能有所收获。
当我们写完的算法代码被编译成指令后运行程序,这期间必定消耗了时间和空间(内存空间)。通常衡量一个算法的好坏是以时间维度和空间维度这两个标准来衡量的。也就是今天要介绍的主题算法的时间复杂度和空间复杂度。
2.时间复杂度
2.1什么是时间复杂度?
时间复杂度的定义:时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
2.2时间复杂度的计算
// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N ; ++ i)
{
for (int j = 0; j < N ; ++ j)
{
++count;
}
}
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
当然在实际的算法时间复杂度的计算中,我们并不会去计算一个精确的执行次数,而是去计算一个大概的执行次数。使用的方法是大O的渐进表示法。
2.3大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
大O的渐进表示法的推导方法:
1、用常数1来表示常数次的算法执行次数。
2、通常只保留算法函数式的最高阶。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O的渐进表示法以后,Func1的时间复杂度为:O(N^2)。
通过上面的例子不难发现,大O的渐进表示法实际上是去除了一些对结果影响不大的一些项。
2.4算法时间复杂度的考虑情况
在算法世界里时间复杂度的考量都是以最坏情况来进行考虑的。
假设有一个查找算法用来查找数组中的某个数x,那么将会有以下几种情况:
1、最好情况:O(1),此时x位于数组的第一位。
2、平均情况: O(N/2) ,此时x位于数组的中间位置。
3、最坏情况: O(N),此时x位于数组的最后一位。
我们应该从那种情况来考虑算法的复杂度呢?即不是最好情况,因为这有些许的不切实际。也不是平均情况,因为这个不太符合我们程序员的预期阈值。在算法世界里时间复杂度的考量都是以最坏情况来进行考虑的。
2.5时间复杂度试题讲解
// 计算Func2的时间复杂度?
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
解析:从上往下阅读代码可知,第一趟for循环一个需要执行2n次,下面的while程序需要执行10次,printf语句执行1次。其实本题的时间复杂度为F(n) = 2n+10+1。采用大O的渐进法表示,即O(N)。
// 计算Func3的时间复杂度?
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);
}
解析:从上往下阅读代码可知,第一趟for循环执行M次,第二趟for循环执行了N次,printf语句执行1次。程序运行的次数为:F(n) = N*M+1次。大O的渐进表示法为:O(N+M)。
// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
解析:从上往下阅读代码可知,这里for循环执行100次,printf语句执行一次。一共执行101次。采用大O的渐进表示法为:O(1)。这里可能很多刚接触的读者不理解为什么不是O(101)而是O(1)。这是因为Func4的复杂度量级是常数阶,常数阶的大O的渐进法表示形式就是O(1)。
// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
解析:从上往下阅读代码可知,上面代码是一个经典的冒泡排序算法,该算法最坏的情况是需要执行(n*n+1)/2次,它的时间复杂度为:O(N^2)。这里我用 ^符号来表示次方。
// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
// [begin, end]:begin和end是左闭右闭区间,因此有=号
while (begin <= end)
{
int mid = begin + ((end-begin)>>1);
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid-1;
else
return mid;
}
return -1;
}
解析:上述代码为一个经典的二分查找算法。每次查找都是会砍掉一半的可找范围。上图例为10个有序数的二分查找算法图。可以分析出10个有序数的二分查找算法最坏的情况是执行4次。当10亿个有序数进行二分查找仅需30次左右便可以找到该数。这是一个理论上很猛的算法就是前提条件要有序。所以该代码的时间复杂度是O(logN)。即log以2为底N的对数。这里是由于编辑器对于底数优化表示格式不支持,故对2进行了省略。
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
解析:通过上图可以看出其实fib函数递归的次数大概是2的n次方次数(2^n)次数。时间复杂度为O(2 ^ n)。
3空间复杂度
3.1什么是空间复杂度?
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
3.2空间复杂度计算
这里要说明一个概念,程序的空间复杂度并不是程序所占空间的大小,而是计算的是程序内开辟的空间(如变量的个数,函数的调用等等)。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
3.3空间复杂度试题
// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
解析:从上述代码可知,在函数执行是只额外开辟了两个临时变量end和exchange。故空间复杂度O(1)。
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
解析:上图一所示,其实函数栈帧销毁后,并不代表销毁后还给操作系统的空间不可以二次利用。这里我想引入一个经典的名言,时间一去不复还,空间可以重复用。如上图二所示,当函数在递归时,会开辟新的栈帧。当n=3时,会开辟3次栈帧。故空间复杂度为:O(n)。
4.常见复杂度的对比
运行(开辟空间)次数 | 大O的渐进表示法 | 量级 |
---|---|---|
250 | O(1) | 常数阶 |
3logn | O(logn) | 对数阶 |
2n+666 | O(n) | 线性阶 |
5n^2+5 | O(N^2) | 平方阶 |
n^3*n ^2 | O(N^3) | 立方阶 |
2^5+2*n+55 | O(2^n) | 质数阶 |