一、算法效率
算法在编成可执行程序后,运行时需要耗费时间资源和空间(内存)资源,因此衡量一个算法的好坏,一般是由时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量算法运行的快慢,而空间复杂度主要衡量算法运行需要开辟的额外空间;如今计算机的存储空间已经很大了所以我们现在更加关心一个算法所需要的时间复杂度。
二、时间复杂度
1、时间复杂度的定义:
在计算机科学中,时间复杂度是一个函数,它定量地描述了一个算法运行所耗费的时间;从理论上来说,这是不能算出来的,只有把这个算法放在编译器里面运行去比较才能得知这个算法的运行时间长短;所以为了方便衡量,才有了时间复杂度的分析方式:一个算法的运行时间与语句的执行次数成正比,算法中基本操作的执行次数,成为算法的时间复杂度。
那么如何来确定算法的语句执行次数呢?通过下面的例子来看:
// 请计算一下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;}printf("%d\n", count);
}
执行次数为F(N)=N^2+2*N+10
2、大O的渐进表示法
但是在实际问题中不需要计算出准确的执行次数,只需要知道大概次数,这里用大O的渐进表示法
准确执行次数 | 大O渐进表示 | 阶数 |
5201314 | O(1) | 常熟阶 |
3n+4 | O(n) | 线性阶 |
3n^2+4n+5 | O(n^2) | 平方阶 |
3log(2)n + 4 | O(logn) | 对数阶 |
4n+3nlog(2)n + 14 | O(nlogn) | nlogn阶 |
n^3+4n^2+3n+6 | O(n^3) | 立方阶 |
2^n | O(2^n) | 指数阶 |
大O的渐进表示法只保留最大的一项并且去掉该项的系数,把最终得到的作为大O的渐进表示。
在计算时,有时执行次数会存在最多和最少的情况,这时按照最坏的情况计算。
3、时间复杂度的举例
例1
// 计算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);
}
2*N+10,所以是O(N);
例2
// 计算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)
}
M+N,含有两个未知数,所以是O(M+N);
例3
// 计算Func4的时间复杂度?
void Func4(int N)
{int count = 0;for (int k = 0; k < 100; ++ k){++count;}printf("%d\n", count);
}
100,常熟阶,所以是O(1);
例4
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
strstr函数是在一个字符串中查找字符,最坏情况是遍历完这个字符串,也就是n次,最好情况是1次。按照最坏情况来看,所以是O(n);
例5
// 计算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次,每次移动都要两两比较,两两比较时最多比较次数是n-1次,所以是O(n^2);
例6
// 计算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;elsereturn mid;}return -1;
}
二分查找假设有N个数据,每次查找数据就减半,最坏情况是最后找到只剩一个数据的情况,那么就执行了log(2)N 次,所以是O(logn);(logn在算法中表示底数为2的对数);
例7
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{if(0 == N)return 1;return Fac(N-1)*N;
}
从Fac(N)到Fac(0),总共是N+1,那么就是O(n);
例8
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{if(N < 3)return 1;return Fib(N-1) + Fib(N-2);
}
画出函数栈帧二叉树的图,把所有Fib的函数的执行次数加起来,最终是O(2^n);
三、空间复杂度
1、空间复杂度定义
空间复杂度是一个数学表达式,是对一个算法在运行过程中临时占用的空间大小的量度。空间复杂度不是计算临时占用多少个字节,而是看临时创建的变量的个数,这里也使用大O的渐进表示法;
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显示的额外空间来确定。
2、空间复杂度举例
// 计算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;
}
}
申请了常数个额外空间所以是O(1);
例2
// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{if(n==0)return NULL;long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));fibArray[0] = 0;fibArray[1] = 1;for (int i = 2; i <= n ; ++i){fibArray[i] = fibArray[i - 1] + fibArray [i - 2];}return fiibArray;
}
动态开辟了n+1个额外空间,所以是O(n);
例3
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{if(N == 0)return 1;return Fac(N-1)*N;
}
调用了N+1次函数,开辟了N+1个函数栈帧,所以是O(n)