光阴易逝,人生无常,把握住你能把握住的点滴。
今天让我们一起来学习算法的时间复杂度和空间复杂度ヾ(≧▽≦*)o
一、算法效率
如何衡量一个算法的好坏?
算法的复杂度
算法在编写成可执行程序之后,运行时需要消耗时间资源和空间(内存)资源。因此,衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。
在计算机发展早期,计算机的存储容量很小,所以对空间复杂度很在乎,但是经过计算机行业的快速发展,计算机的存储容量已经达到了很高的程度,如今已不需要特别关注一个算法的空间复杂度。(摩尔定律:集成电路上可以容纳的晶体管数目大约每经过18个月便会增加一倍)
二、时间复杂度
2.1时间复杂度的定义
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量的描述该算法的运行时间。一个算法执行所耗费时间,从理论上说,是不能算出来的。只有你把你的程序放在机器上跑起来,才能知道,但是我们需要每个算法都上机测试吗?是可以,但很麻烦(并且,一个算法运行时间跟硬件配置有关系,所以一个算法是没有办法准确算出时间的),所以才有了时间复杂度这个分析方法。一个算法所花费的时间与其中语句执行的次数成正比例。算法中的基本操作的执行次数 ,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是该算法的时间复杂度。
一个执行次数,不一定是一条语句,也可以是多条语句,但肯定是常数条语句。
2.2 大O的渐进表示法
先看一个代码:计算一下程序中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 = 0;
while (M--)
{
count++;
}
}
其准确的时间复杂度函数式为:F(N) = N * N+2 * N+10。
当N =10时,F(10)= 130 100 (估算)
当N=100时,F(100) = 10210 10000 (估算)
当N = 1000时,F(1000) = 1002010 100w(估算)
通过观察发现:随着N的增大,后两项对结果的影响几乎可以忽略不记。(一方面)
(另一方面):准确的时间复杂度函数式,不方便在算法之间进行比较。
大O的渐进表示法的出现。 (大概估算,方便比较)
2.2大O的渐进表示法
大O符号:是用于描述函数渐进行为的数学符号。
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数,得到的结果就是大O阶。
所有上面题用大O阶表示为O(N^2)
//计算strchr的时间复杂度?(在字符串数组中查找一个字符)
const char* str(const char* str, int character)
{
while (*str)
{
if (*str == character)
{
return str;
}
else
{
str++;
}
}
return NULL;
}
在此次我们发现此算法有几种情况;
最坏情况:O(N)
最好情况:O (1)
平均情况: O (N/2)
在实际中一般情况关注的是算法的最坏运行情况(底线思维),所有为O(N)。
另外:值得大家注意的一点是,是不是计算时间复杂度就数循环就行了?答案是否定的。
时间复杂度不能去数循环,这个不一定准确,一定要看算法思想来进行计算。
比如:
冒泡排序:
void BubbleSort(int* a, int n)
{
for (int end = n; end > 0; end--)
{
int exchange = 0;
for (int i = 1; i < end; i++)
{
if (a[i - 1]>a[i])
{
int tmp = a[i - 1];
a[i - 1] = a[i];
a[i] = tmp;
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
}
通过冒泡排序的思想,最坏情况为:N-1 + N-2 + N-3 +…+ 1=N (N-1) / 2,即O (N^2)
最好情况为:遍历一遍发现都不需要交换,即O (N)
比如:二分查找
int BinarySearch(int* a, int n, int x)
{
int begin = 0;
int end = n - 1;
while (begin <= end)
{
int mid = (begin + end) / 2;
if (a[mid] > x)
{
end = mid - 1;
}
else if (a[mid] < x)
{
begin = mid + 1;
}
else
{
return mid;
}
}
return -1;
}
如果不看算法思想,通过数循坏,那它的时间复杂度为O(N),但是通过算法思想我们知道,其最坏情况的时间复杂度为O(log以2为底 N 的对数),最好情况的时间复杂度为O(1),所以大家要看一个算法的思想呀!
O(log以2为底 N 的对数) 为方便起见,通常写成O(log N),有些书也写成O(lg N),通过换底公式得到的,建议大家写成O(log N) 更规范一些。
2.3递归函数的时间复杂度
递归函数如何求其时间复杂度呢?
例一:
long long Fac(size_t n)
{
if (n == 0)
{
return 1;
}
return Fac(n - 1)*n;
}
分析清楚其递归调用的次数。
空间复杂度为:O(N)
例二:计算斐波那契的第n项
long long Fib(size_t n)
{
if (n < 3)
{
return 1;
}
return Fib(n - 1) + Fib(n - 2);
}
发现它的时间复杂度为O(2^n ),指数量级的,显然它不实用,递归该循坏后 变为O(N);
空间复杂度为:O(N)
求Fibonacci数列的前n项
代码如下:
long long* fibonacci(size_t n)
{
if (n == 0)
{
return NULL;
}
long long* FibArray = (long long*)malloc(sizeof(long long)*(n + 1));
FibArray[0] = 0;
FibArray[1] = 1;
for (int i = 2; i <= n; i++)
{
FibArray[i] = FibArray[i - 1] + FibArray[i - 2];
}
return FibArray;
}
三、空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没有太大意义,所以空间复杂度计算的是变量的个数。
它也用大O的渐进表示法。
注意:函数运行所需要的栈空间(存储参数,局部变量,一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过运行时显式申请的额外空间来确定。
例一:
例二:
3.1 注意
时间是可以累计的,空间是不累计的,可以重复使用。
举例:
void f1()
{
int a = 0;
printf("%p\n", &a);
}
void f2()
{
int a = 0;
printf("%p\n", &a);
}
int main()
{
f1();
f2();
return 0;
}
运行结果:
发现地址一样,所以空间是可以重复利用的。
3.2常见的复杂度的对比
最常见的七个示例,按照运行效率从高到低排序。
1.O(1) — 常数复杂度
2.O(log n) — 对数复杂度
3.O(n) — 线性复杂度
4.O(n log n) — 对数线性复杂度
5.O(nᵏ) — 多项式复杂度
6.O(kⁿ) — 指数复杂度
7.O(n!) — 阶乘复杂度
你可以看到,随着输入规模的增长,红色阴影区域中算法的运行时间急剧增长。另一方面,在黄色和绿色阴影区域中的算法,当输入规模增长时,运行时间在变化不是很大,因此它们更高效,处理大量数据时更游刃有余。
好啦,文章到此就结束了,如果对你有所帮助,可以给个赞吗?
哈哈,期待下次再见!O(∩_∩)O🧙♂️