前言:哈喽小伙伴们,从这篇文章开始,博主就要带领大家一起进入数据结构与算法的学习啦。
C语言掌握的还不扎实的小伙伴一定要记得先补习哦。
今天我们就先来学习——算法复杂度。
目录
一.算法效率
二.时间复杂度
1.时间复杂度的定义
2.大O的渐进表示法
三.空间复杂度
4.总结
小伙伴们从开始学习编程到现在,肯定已经做过了不少的编程题,那么我们解决这些题目的思路,就称之为算法。
我们还知道,每一道复杂的题目肯定都不止一种解法,那么我们如何判断一个算法的好坏呢?
算法在编写成可执行程序后,运行时肯定都需要消耗时间和空间。因此,衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,也就是时间复杂度和空间复杂度。
那么时间复杂度究竟是什么东西呢???
博主这里通俗的告诉大家,实际上就是我们数学上所学习的函数。
那么再简单一点来说,时间复杂度就是我们代码中,某些基本操作的执行次数。
下面我们就通过实际例子来讲解时间复杂度:
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);
}
我们来看这个函数,试问它的时间复杂度是多少???
能够看出,这个函数一共有三个循环,其中第一个还是嵌套循环。
那么这三个循环的循环次数分别是多少呢???
不难看出,第一个嵌套循环的循环次数为N*2次,第二个为2N次,第三个则为10次。
那么我们就能得出使用这个函数所需要执行循环的次数F(N) = N ^ 2 + 2N + 10。
这个函数式,就是我们所说的时间复杂度。但是这样的写法是不是有的复杂而且好老套。
但是我们学过数学就会知道,当N的值不断取大时,影响最大的会是幂数最高的项,比如:
N = 10 F(N) = 130
N = 100 F(N) = 10210
N = 1000 F(N) = 1002010
所以我们规定时间复杂度的简易写法为取影响最大的项:
O(N^2)
这种时间复杂度的取法,也叫做大O的渐进表示法。
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
了解过大O的渐进表示法时候, 我们再来看一个例子:
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);
}
这个函数的时间复杂度是多少呢???
能够得出F(N) = 2N + 10,保留最高阶2N,再去除与N相乘的常数2,得出时间复杂度为O(N)。
到这里小伙伴们是否已经了解到时间复杂度的求解方法了呢???
那么我们再来看一例子:
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);
}
这个函数的时间复杂度是多少呢???
F(N) = N + M,好像大O法的三条规则都不是很满足,那么到底该怎么表示呢???
当我们不知道N和M的大小关系时,时间复杂度表示为O(N+M);
当N远大于M时,时间复杂度表示为O(N);
当N远小于M时,时间复杂度表示为O(M);
当N和M差不多大时,时间复杂度表示为O(N)或者O(M)都可以。
那么我们再来看最后一个例子:
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
这个函数的时间复杂度是多少呢???
是O(100)吗?并不是,根据大O表示法的第一条规定,用常数1取代所有运行的常数次数,也就是说,如果你的循环是常数次,那么时间复杂度就为O(1)。
实际上,如果一个循环进行的次数为常数次,那么它的运行时间都是相同的,感兴趣的小伙伴们可以测试一下一个循环运行1次和运行1亿次的时间是否是相同的。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x:
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)。
空间复杂度和时间复杂度一样,也是通过一个数学表达式来表示,它是对一个算法在运行过程中额外的,临时创建的变量所占用的空间大小的量度。
此外,对于空间复杂度的运算规则,同样也是采用大O的渐进表示法。
因为函数运行时所需要的栈空间在编译期间已经确定好了,因此空间复杂度主要通过函数在运行的时候显示申请的额外空间来确定。
下面我们就来通过实际例子来讲解空间复杂度的计算方法:
void BubbleSort(int* a, int n)
{
assert(a);
for (int end = n; end > 0; --end)
{
int exchange = 0;
for (int 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)。
有的小伙伴可能发现,end和i变量不是一直在循环创建吗,不应该是O(N^2)吗???
要注意,空间和时间是不一样的。
时间过去了就没有了,再也找不回来了;而空间却是可以长期存在的,可以反复利用的,当我们循环使用end和i变量时,实际上一直在使用同一块空间。
long long* Fibonacci(int 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;
}
我们再来看这个斐波那契函数,这个就很直观了,直接就能看见用malloc函数开辟了n+1个空间大小,还有一个变量i,总共是n+2个空间,那么根据大O表示法,空间复杂度为O(N)。
那么对于数据结构的第一块知识——算法复杂度到这里就跟大家分享完啦,这一块知识并不难,就是一些个计算题,多多练习都是能够很轻松掌握的。
喜欢博主文章的小伙伴不要忘记一键三连呀!!!
我们下期再见啦!!!