作者: @情话0.0
专栏:《数据结构》
个人简介:一名双非研究生的编程菜鸟,在这里分享自己的编程学习笔记,欢迎大家的指正与点赞,谢谢!
在数据结构中,有着众多的算法,比如查找算法,排序算法等。在查找算法中有顺序查找、折半查找、分块查找等,排序算法中有冒泡排序、快速排序、希尔排序等,而面对这么多的算法,是怎样去衡量算法的执行效率呢?而这也就是此篇文章的重点:时间复杂度和空间复杂度。
算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。此外,一个算法还具有下列5个重要特性:
有穷性:一个算法必须总在执行有穷步之后结束,并且每一步都可在又穷时间内完成。
确定性:算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的结果。
可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
输入:一个算法有零个或多个输入,这些输入来自于某些特定对象的集合。
输出:一个算法有一个或多个输出,这些输出是与输入存在某种特定关系的量
而一个算法被认定为 “好” 算法应该考虑到以下目标:
正确性:算法能够正确地解决问题。
可读性:算法应具有良好的可读性,以帮助读者理解。
健壮性:输入非法数据时,算法能够及时地做出回应或进行处理,而不会产生莫名其妙的输出结果。
效率与敌存储空间:效率是指算法的执行时间,存储量需求是指算法执行过程中所需要的最大存储空间,这两个都与问题的规模有关。
算法效率的度量是通过时间复杂度与空间复杂度来描述的。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展初期,计算机的存储容量很小,所以对空间复杂度很是在乎。但是经过这几十年计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
时间复杂度的定义:一个语句的 频度 是指该语句在算法中被重复执行的次数,而算法中的语句频度之和表示了算法问题规模的函数。时间复杂度就是分析频度之和的数量级。在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。
对于一个算法执行所耗费的时间,从理论上说是不能算出来的。为什么呢?只有把你的程序放在机器上跑起来,才能知道改程序所耗费的时间,这只是其中一点,还有就是相同的程序放在不同的机器上,不同的编译器上都会有不同的执行时间。所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
算法的时间复杂度不仅依赖于问题的规模,也取决于待输入数据的性质。 例如,在数组 arr[0...n-1]
中,查找数值 k
的大概算法如下:
i = n - 1;
while(i >= 0 && (arr[i]) != k)
i--;
return i;
该算法中第三条语句 i--
的频度不仅与问题规模有关,而且与输入实例中 arr
中的个元素的取值及 k
的取值有关:
若
arr
中没有与k
相等的元素,则第三条语句的频度为 n。
若arr
的最后一个元素等于k
,则第三条语句的频度为常数 0。
对于这样的算法,出现了最坏和最优的情况。
最坏时间复杂度是指在需要执行最多的次数才能完成算法的执行而算出的时间复杂度。
平均时间复杂度是指所有可能输入实例在等概率出现的情况下,算法的期望运行时间。
最好时间复杂度是指执行最少的次数完成算法的执行而算出的时间复杂度。
一般总是考虑在最坏情况下的时间复杂度,以保证算法的执行时间不会比它更长。
// 请计算一下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);
}
Func1 执行的基本操作次数 : F(N) = N^2 + 2*N + 10
在计算时间复杂度时,我们不需要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
大O符号:是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O的渐进表示法以后,Func1的时间复杂度为:O(N^2)
常见的渐进时间复杂度为:
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(2^n) < O(n!) < O(n^n)
案例一
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-1 次,然后依次递减,最终的总次数为 n*(n-1) / 2,最终的时间复杂度为O(n^2)
案例二
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
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;
}
该算法为二分查找算法,在每执行完一次循环都会减去当前数组的一半,时间复杂度为O(logn)
案例三
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
从上图可以看到,关于斐波那契函数的递归调用,没递归一次都会是上次调用个数的二倍,是以2为底数的指数级增长,时间复杂度为O(2^n)
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
一个程序在执行时除需要存储空间来存放本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为实现计算机所需信息的辅助空间。
空间复杂度不是程序占用了多少bytes的空间,算的是变量的个数,为这些变量所开辟的空间多少。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
注意: 函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因
此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
案例一
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、i
,使用了常数个额外空间,所以空间复杂度为 O(1)
对于exchange
重复创建,计算结果只算一个
案例二
//返回斐波那契数列的前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 fibArray;
}
在斐波那契数列的算法当中,动态开辟了
n+1
个内存空间,创建了一个变量,所以空间复杂度为 O(N)
案例三
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
该算法使用递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N)
大家可以看看下面这几个算法的时间复杂度是多少?
例一:
count = 0;
for(k = 1;k <= n;k *= 2)
{
for(j=1;j<=n;j++)
{
count++;
}
}
例二:
int func(int n)
{
int i=0,sum=0;
while(sum<n)
{
sum += ++i;
}
return i;
}
例三:
void func(int n)
{
int i=0;
while(i*i*i<=n)
{
i++;
}
}