今天刚学习了一点点时间复杂度,有错误的地方希望各位大佬指出来,非常感谢!!!
1.程序时间复杂度
1.0官方的概念
在计算机科学中,算法时间复杂度是一个函数(这个函数不是我们程序中的函数之后再讲),它的定量描述了该算法的运行时间。一个算法所执行所消耗的时间是不能够精确表示的,(比如相同的算法在四核处理器上和八核处理器上的运算时间是不同的)。所以一个算法所花费的时间与其中语句的执行次数成正比例(比如我们计算count值,执行次数就是count值在这个算法中运算了多少次)算法的基本操作的执行次数为算法的时间复杂度。
以下面这个函数为例讲解什么叫算法时间复杂度是一个函数与算法时间复杂度的表示方法
void Func1(int N)
{
int count = 0;
for(int i = 0; i < N; i++)
{
for(int j = 0; i < N; j++)
{
++count;
}
}
for(int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while(M--)
{
++count;
}
printf("%d\n",count);
}
通过这个函数我们可以发现count运行的次数n等于N^2+2*N+M,函数表达式n=N^2+2*N+M
1.1大O的渐进表示法
在实际计算中我们并不能够准确的计算出算法时间复杂度,因此使用大O渐进表示法:
如何推到出大O渐进表示法?
1、用常熟1取代所有时间中所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶
我们引用上面的函数式n=N^2+2*N+M
第一步:用常熟1取代所有时间中所有加法常数->n=N^2+2*N+1
第二步:在修改后的运行次数函数中,只保留最高阶项->n=N^2
第三步:如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶-> 用不到了。
这样我们就得到了这个算法的时间复杂度N^2,用大O阶表示O(N^2)
因此O(1)并不是这个算法中求的值运行了一次,而是运行了常数次!
1.1.1大O阶表示法是一个悲观态度
我们以下面的冒泡排序代码为例:
void Bubblesort(int* a,int n)
{
assert(a);
for(int i = n; i > 0; --i)
{
int exchange ;
int flag = 0 ;
for(unsignedint j = 1; j < i; j++)
{
if(a[i-1]>a[i])
{
exchange = a[i-1];
a[i-1] = a[i];
a[i] = exchange;
flag = 1;
}
}
if(flag = 0 )
{
break;
}
}
}
通过上面的冒泡排序我们可以发现,最少执行1次,最多要执行(n-1)+(n-2)+(n-3)·····+1次才能够排完!
那我们如何去用大O阶法来计算这个不确定的时间复杂度呢?,很简单,举个例子老板叫我去做个算法,我发现这个算法最好的一次只需要2秒就出来了,我去给老板说我这个算法只需要两秒就可以计算,老板过来看的时候但是花了10秒,啪啪打脸!为了惊喜感我们往往用算法的最大时间复杂度去表示!
因此在计算时间复杂度时我们要找到这个算法耗时最多的函数表达式去转换成我们的大O阶表示法。计算次数M=(n-1)+(n-2)+(n-3)·····+1(等差数列求和)=(1+(n-1))/2->O(n),因此上面的冒泡排序的时间复杂度为O(n)
1.2练习计算
计算下面递归算法的时间复杂度
long long Fac(size_t N)
{
if(N < 3)
return 1;
return Fac(N-1)+Fac(N-2);
}
答案O(2^N)
假设N特别大N->无穷
不会旋转图片呜呜呜!
根据return语句我们发现递归会先执行完左边,再执行右边,左边执行的次数第一排忽略就等于2^0+2^1+2^2+······+2^N=(1*(1-2^N))/(1-2)=2^N,左边的时间复杂度为2^N右边也约为2^N2*2^N(N趋于无穷)约等于2^N
2.程序空间复杂度
首先需要了解O阶渐进表示法,不懂的可以在目录查找。
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时额外占用内存空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个没多大意义,所以空间复杂度算的是变量的个数,空间复杂度计算规则跟时间复杂度类似,都是使用O阶渐进表示法。
废话不多说我们直接上实际例子来讲!
2.1举例讲解
例一:
//判断下面冒泡排序的空间复杂度 O(1)
void sort_fun(int* str,int n)
{
assert(str);//断言
for (int i = 0; i < n - 1; i++)//n个数进行n-1次比较
{
int num;
int exchange = 0;
for (int j = 0; j <= n -1-1; j++)//移位排序
{
if (str[j] > str[j + 1])
{
num = str[j + 1];
str[j + 1] = str[j];
str[j] = num;
exchange = 1;
}
}
if(exchange == 0)
{
break ;
}
}
}
我们上面讲了空间复杂度算的是变量的个数,那这个变量个数由于循环每次都会创建变量那它的空间复杂度m=n*((n-2)+(n-3)+····+(1))(假设n->无穷,不知道为啥n->无穷可以看1.1.1)那么m=n^2,如果你觉得我说的有道理那就错了。因为在循环中创建的变量的生命周期只在本次循环开始到结束,所以i,j,num,exchange ,这些变量所占的空间最大也就是4个int(额外需要的变量个数),所以这个算法的空间复杂度为O(1)。
在之后的例子中我就不推导数学表达式,大家自己推一下,相信自己可以的!
例二:
//判断下面算法的空间复杂度
//求斐波那契数列存到数组
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 fibArray;
}
这个空间复杂度是O(n)
malloc((n+1)* sizeof(long long)这句代码是主要影响因素,它每次都会创建
(n+1)个long long的空间,这些空间能够存储n+1个大小为longlong的变量,所以它的空间复杂度为O(n)
例三:
//递归空间复杂度
long long Fac(size_t N)
{
if (N == 1)
return 1;
return Fac(N - 1) * N;
}
答案是多少呢?
N趋于无穷就有N个变量创建,O(N),我们一定要搞明白局部变量的生命周期!
最后一个例子:
//递归
long long Fac(size_t N)
{
if (N < 3)
return 1;
return Fac(N - 1) + Fac(N - 2);
}
答案也是O(N),为什么呢?创建多了为什么还是O(N)呢?,下面听我细细道来:
我们知道函数局部变量的生命周期只从这个函数开始到函数结束(排除静态变量),大家理解这么一句话,空间是可以重复利用的,时间是一去不复返的!
我不会搞这个图片大家将就一下吧。
大家可以根据return Fac(N - 1) + Fac(N - 2);画出这个图,左边这个Fac(N - 1)的递归深度肯定是远大于右边 Fac(N - 2);那根据调用关系,函数会先执行完左边的递归,再去执行右边的,当执行右边递归时,左边递归的变量所占空间已经被释放了,那右边的递归是可以重复使用之前左边递归的空间的!这样的话它所占用的空间是和例三是等阶的,所以它的空间复杂度就是O(N)。
好了,程序的时间空间复杂度就聊到这里,我们下次继续聊!