算法的时间复杂度与空间复杂度
反爬链接
1. 算法效率
引:如何衡量一个算法的好坏呢?
算法在编译链接成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。 时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。
在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度(摩尔定律:硬件每18个月翻倍)。所以我们如今已经不需要再特别关注一个算法的空间复杂度,重点关注时间复杂度。
2. 时间复杂度
2.1 时间复杂度的概念
在计算机科学中,算法的时间复杂度是一个函数(是含未知数的数学表达式啦hh),它定量描述了该算法的运行时间。
计算时间复杂度不是计算时间。一个算法执行所耗费的时间,理论上是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们如果每个算法都上机测试,就很麻烦;且运行环境不同,花费时间也不同。
一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
2.2 大O渐进表示法
🔑大O渐进表示法
-
用常数1表示运行次数中所有加法常数 -
在修改后的运行次数的函数中,只保留最高阶项。 -
如果最高阶存在且不为1,则去除与此项的系数。
例 ——
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
F(N) = N^2 + 2N + 10
F(N)=N2+2N+10
N | F(N) = N^2 + 2*N + 10 | N^2 |
---|
N = 10 | 130 | 100 | N = 100 | 1,0210 | 1,0000 | N = 1000 | 100,2010 | 100,0000 |
由上表可以看出,N越大,后两项对于结果的影响越小。因此,保留对结果影响最大的项,即可代表大概的执行次数。
计算时间复杂度时,无需计算精确次数,只需要计算大概的执行次数,那么这里我们使用大O的渐进表示法(Big O notation:用来描述函数渐进行为的数学符号)。—— 估算
💛因此,用大O渐进表示法后,Func1的时间复杂度为:
O
(
N
2
)
O(N^2)
O(N2)
2.3 常见时间复杂度计算实例
我们在练习中感受 ——
实例1 ——
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
)
=
2
?
N
+
10
F(N) = 2*N+10
F(N)=2?N+10 保留最高项,并去掉最高项系数后得
💛时间复杂度 ——
O
(
N
)
O(N)
O(N)
实例2 ——
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);
}
若题目有条件, 💛这里没有说明,因此时间复杂度 ——
O
(
M
+
N
)
O(M+N)
O(M+N)
实例3 ——
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
基本执行次数 ——
F
(
N
)
=
100
F(N) = 100
F(N)=100 常数项用1代替后得
💛时间复杂度 ——
O
(
1
)
O(1)
O(1)
当题目中要求,将时间复杂度优化至O(1),不是代表运算一次,而是常数次。
实例4 —— 时间复杂度是做悲观预期
const char* strchr ( const char* str, int character );
const char* strchr ( const char* str, int character )
{
while(*str != '\0')
{
if(*str == character)
{
return str;
}
str++;
}
return NULL;
}
当随着代码的输入不同,时间复杂度不同 ——
(例如:在一个长度为N数组中搜索一个数据x) hello world\0
- 最坏情况(查找d):任意输入规模的最大运行次数(上界) —— [F(N) = N次找到]
- 平均情况(查找w):任意输入规模的期望运行次数 —— [F(N) = N/2次找到]
- 最好情况(查找h):任意输入规模的最小运行次数(下界) —— [F(N) = 1次找到]
要明白时间复杂度是做悲观预期,看最坏情况。
💛因此,这段代码的时间复杂度为 ——
O
(
N
)
O(N)
O(N)
🔑计算时间复杂度,要会用思想计算
接下来再来看两段代码 ——
实例5 —— 冒泡排序·思想计算
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int flag = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
flag = 1;
}
}
if (flag == 0)
break;
}
}
这里不能因为嵌套了两层循环,就简单的认为时间复杂度为O(N)
思想分析 ——
N
N
N个数字,共排
N
?
1
N-1
N?1趟,每一趟进行
N
?
i
?
1
N-i-1
N?i?1次比较(第
i
i
i趟,
i
=
0
,
1
,
2...
i= 0,1,2...
i=0,1,2...),这过程太熟悉了,就不赘述了
最坏的情况(由等差数列公式):
F
(
N
)
=
(
N
?
1
)
+
(
N
?
2
)
+
.
.
.
+
2
+
1
=
(
N
?
1
+
1
)
(
N
?
1
)
/
2
F(N) = (N-1)+(N-2)+...+2+1 = (N-1+1)(N-1)/2
F(N)=(N?1)+(N?2)+...+2+1=(N?1+1)(N?1)/2 最好的情况(已经有序):
F
(
N
)
=
N
?
1
F(N) = N-1
F(N)=N?1 因此冒泡排序的时间复杂度为
O
(
N
2
)
O(N^2)
O(N2)
实例6 —— 二分查找·思想计算
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;
else
return mid - 1;
}
return -1;
}
因此,二分查找的时间复杂度为
O
(
l
o
g
N
)
O(logN)
O(logN)
如果我要在14亿人口中查找一个人,用二分查找要查找几次?
N个数中查找 | 能查找的人数 | 查找次数 |
---|
2^10 | 1024 | 10 | 2^20 | 100w | 20 | 2^30 | 10亿 | 30(变化很不明显) |
只需要查找31次。
这里我们要认识到二分查找还算是很nb的算法,当然了,排成有序也是有消耗的。
实例7、8-—— 递归·时间复杂度计算
🔑递归算法计算时间复杂度:
O
(
N
)
=
递
归
次
数
?
每
次
递
归
调
用
中
的
次
数
O(N) = 递归次数*每次递归调用中的次数
O(N)=递归次数?每次递归调用中的次数
每次递归调用中的次数是指 —— 一次递归调用中,基本操作的次数。
long long Fac(size_t N)
{
if (0 == N)
return 1;
return Fac(N - 1)*N;
}
下一个 ——
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
因此,时间复杂度为 ——
O
(
2
N
)
O(2^N)
O(2N) 实际上,斐波那契额数列的递归写法我们基本不用,因为太慢了,采用迭代就okk
3. 空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时额外占用存储空间大小的量度 。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
实例1 ——
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int flag = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
flag = 1;
}
}
if (flag == 0)
break;
}
}
💛因此空间复杂度为 ——
O
(
1
)
O(1)
O(1)
实例2 ——
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
)
O(N)
O(N) 时间复杂度为 ——
O
(
N
)
O(N)
O(N) 当然,如果只是要求斐波那契数列中的第n个数,那就不需要额外申请n个空间,空间复杂度即为 ——
O
(
1
)
O(1)
O(1)
实例3 ——
如何计算阶乘递归的空间复杂度?—— 递归深度
{
if(N == 0)
return 1;
return Fac(N-1)*N; }
💛因此空间复杂度为 ——
O
(
N
)
O(N)
O(N)
4. 常见复杂度的对比
🔑时间复杂度不计算时间,计算大概运算次数
🔑空间复杂度不计算空间,计算大概定义变量的个数
这里要对各种时间复杂度的好坏有认知。
|