一、为什么要引入时间空间复杂度
算法不具有唯一性,而算法好坏之分,时间空间复杂度的出现是用来评判算法好坏之分的。一个算法运行所消耗的时间越短、空间越小,则说明这个算法相对来说更优。时间复杂度是从时间维度来判定算法的好坏,指的是执行当前算法所消耗的时间。空间复杂度是指从空间维度来判断算法的好坏,指的是执行当前算法需要占用多少内存空间。
二、时间复杂度
1、什么叫基本运算
我们引入一个基本运算的概念:因为每个操作在具体执行的过程中,所消耗的时间会有差异,为了方便计算统计,我们把比较、加减法、乘法等叫认为每执行一次基本运算。
我们一般用大O符号表示法表示,即 T(n) = O(f(n)),我们把执行次数表示成一个关于n的函数。需要注意的是,我们认为时间复杂度表示的是一种趋势,我们计算出基本运算的次数后,只保留其最高阶,且将最高阶系数置成1。
举个栗子,比如下面这段算法,我们计算出算分的计算次数:f(n)=2n^2+1。但由于时间复杂度表示的一种趋势,这段算法的时间复杂度实际上是O(n^2)。
for(x=1; i<=n; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}
j=j+2;
2、常见的时间复杂度
常见的时间复杂度有:常数阶O(1)、对数阶O(logn)、线性阶O(n)、线性对数阶O(nlogn)、平方阶O(n²)、立方阶O(n³)、K次方阶O(n^k)、指数阶O(2^n)、O(m*n)
我们举几个例子:
对数阶O(logn):
int i = 1;
while(i<n)
{
i = i * 3;
}
这里粗略估计,基本运算次数为x=log3(n),所以为对数阶。
线性对数阶O(nlogn):
只需要在对数阶的基础上加上n次循环即可,不多赘述。
O(m*n):
将上面O(n^2)的一个n换成m,就可以了
for(x=1; i<=n; x++)
{
for(i=1; i<=m; i++)
{
j = i;
j++;
}
}
j=j+2;
由于m和n不一样,且都会反映基本运算次数的趋势,所以不能省略m或n
3、什么叫输入规模
输入规模是指算法要求解问题的规模,比如排序算法的规模就是数组中元素的个数,图的遍历算法的规模就是图的顶点数n和边数m
4、平均时间复杂度和最坏时间复杂度
平均复杂度A(n):是指算法求解输入规模为n的输入实例的概率分布下,算法求解这些实例所需要的平均时间
最坏时间复杂度W(n):是求解输入规模为n的实例所需要的最长时间
比如最简单的一个检索算法:
for(int j=0;j<n;j++){
if(Sort[j]==key){
return j;
}
}
return -1;
最坏的情况就是没有检索到,或者要检索的key是最后一个数,这个时候最坏时间复杂度W(n)=n
而平均时间复杂度需要我们进行计算:
假设key在数组的概率为p,且key在每个位置的概率相等,则就有:
当p的值确定时,A(n)也就确定了
三、空间复杂度
1、程序的空间复杂度
程序的空间复杂度一般指的是完成一段程序所需要的内存。一般空间复杂性由以下组成:
1)指令空间:指的是存储经过编译后的程序指令所需要的空间,不同的编译方式产生的汇编代码的长度肯定有所不同。这和编译器,编译时采用的编译器选项以及所用的目标计算器有关
2)数据空间:包括程序里的常量及变量,包括动态数组和动态类实例等动态对象所需要的空间
3)栈空间:主要是函数调用开辟的栈空间
2、算法的空间复杂度
而对于算法来说,算法的空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,我们用 S(n) 来定义。需要注意的是,算法的空间复杂度反应的并不是程序运行过程中实际所占内存,反应的是一种趋势,一种相对的关系。类似地。我们用大S符号表示法表示空间复杂度,空间复杂度比较常用的有:S(1)、S(n)、S(n²),我们来举例说明:
int i = 1;
int j = 1;
i++;
j++;
int sum = i + j;
这段算法中产生了i,j,sum这三个变量,开辟了三个临时的存储空间,为常数阶,即S(1),这里的‘1’指的是i,j,sum所分配的空间并不随着参数变化而变化。
接着我们来看下一段算法:
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j = j + 5;
}
注意到,这段代码中新建了一个数组,这个数组的大小会随着n变化,这也就是说这个算法分配的空间由n决定,空间复杂度为线性阶,也就是S(n)。
四、时间空间复杂度的应用
我们用一个经典的百鸡问题来说明:
公元5世纪末,我国古代数学家张丘建在撰写的《算经》中提出了这样一个问题:“鸡翁一,值钱五;鸡母一,值钱三;鸡雏三,值钱一,百元买百鸡,问鸡翁、母、雏各几个?”假设:a为公鸡,b为母鸡,c为小鸡
很显然我们可以用枚举法求解:
void Chick(int n) {
for (int i = 0;i <= n;i++) {
for (int j = 0;j <= n;j++) {
for (int k = 0;k <= n;k++) {
if ((i + j + k == n) && (5 * i + 3 * j + k / 3 == n) && (k % 3 == 0)) {
printf("%d %d %d", i, j, k);
}
}
}
}
}
这个算法的时间复杂度为O(n^3),那有没有可能简化算法呢?
因为总数确定,我们可以把k设成n-i-k,这样极大简化了算法:
void Chick(int n) {
for (int i = 0;i < n;i++) {
for (int j = 0;j < n;j++) {
k = n - i - j;
if ((5 * i + 3 * j + k / 3 == n) && (k % 3 == 0)) {
printf("%d %d %d", i, j, k);
}
}
}
}
而这个算法的时间复杂度仅为O(n^2)!
而如果算出这两个算法运行的具体时间,在n=100时,未优化的代码需要运行11秒,优化后的代码需要0.007秒。当n=10000时,未优化的代码需要3058个小时,优化后的代码仅需要70秒!这也可以说明,为什么算法的复杂度表示的是一种趋势,n^3的增长趋势肯定比n^2的增长趋势快很多。
所以,我们有了时间复杂度,空间复杂度,就可以更好的衡量算法的优劣,简单判断就可以得到更优的解法,而不需要真的跑3000个小时,然后得出结论:这个算法不好^_^