1.时间复杂度(上)
1.时间复杂度
- 时间复杂度是衡量算法执行时间随输入规模增长的增长率。
- 通过分析算法中基本操作的执行次数来确定时间复杂度。
- 常见的时间复杂度包含:常数时间 O ( 1 ) O(1) O(1)、线性时间 O ( n ) O(n) O(n)、对数时间 O ( l o g n ) O(logn) O(logn)、平方时间 O ( n 2 ) O(n^{2}) O(n2)等
- 在计算的时间我们关注的是复杂度的数量级,并不要求严格的表达式。
一般我们关注的是最坏时间复杂度,用 O ( f ( n ) ) O(f(n)) O(f(n))表示,大多数时候我们仅需估算即可。
一般来说,评测机1秒大约可以 1 e 8 ( 1 × 1 0 8 ) 1e8(1×10^{8}) 1e8(1×108)次运算,我们要尽可能地让我们的程序运算规模的数量级控制在
1 e 8 ( 1 × 1 0 8 ) 1e8(1×10^8) 1e8(1×108)以内。(尽量控制在1亿次以内)
假设此算法的时间复杂度为 O ( n 2 ) O(n^{2}) O(n2),为了控制在 1 e 8 ( 1 × 1 0 8 ) 1e8(1×10^{8}) 1e8(1×108)次内,执行次数应 ≤ 1 × 1 0 4 ≤1×10^{4} ≤1×104。如果执行次数大于次数,就应该换其他更小时间复杂度度算法。
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) O(1) < O(log n) < O(n) < O(n log n) < O(n^2) < O(n^3) < O(2^n) < O(n!) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)
2.空间复杂度
- 空间复杂度是衡量算法执行过程中所需的存储空间随输入规模增长的增长率。
- 通过分析算法中所使用的额外存储空间的大小来确定空间复杂度。
- 常见的空间复杂度包括:常数空间 O ( 1 ) O(1) O(1)、线性空间 O ( n ) O(n) O(n)、对数空间 O ( l o g n ) O(logn) O(logn)、平方空间 O ( n 2 ) O(n^{2}) O(n2)等。
一般我们关注的是最坏空间复杂度,用 O ( f ( n ) ) O(f(n)) O(f(n))表示,大多数时候程序占用的空间一般可以根据开的数组大小精确算出,但也存在需要估算的情况。题目一般不会卡空间,一般是卡时间。举个例子,例如题目限制128MB,1int~32bit~4Bytes,128MB~32 * 2 ^ 20 int ~ 3e7 int
- 1 字节( b y t e ) = 8 比特( b i t ) 1 字节(byte) = 8 比特(bit) 1字节(byte)=8比特(bit)
- 1 千字节( K B ) = 1024 ( 2 10 ) b y t e 1 千字节(KB) = 1024 (2^{10})byte 1千字节(KB)=1024(210)byte
- 1 兆字节( M B ) = 1024 ( 2 10 ) K B 1 兆字节(MB) = 1024(2^{10}) KB 1兆字节(MB)=1024(210)KB
- 1 千兆字节( G B ) = 1024 ( 2 10 ) M B 1 千兆字节(GB) = 1024(2^{10}) MB 1千兆字节(GB)=1024(210)MB
- 1 太字节( T B ) = 1024 ( 2 10 ) G B 1 太字节(TB) = 1024(2^{10}) GB 1太字节(TB)=1024(210)GB
注意:为了避免栈空间的溢出,开辟空间规模较大的数组都应该在int main()的外面变成全局数组。将大数组放在全局空间而不是局部空间(如 int main()
函数内部)可以避免栈空间溢出的问题,因为全局变量存储在静态存储区域,而不是栈上。
#include...
const int N = 1e8;
int a[N];
int main(){
...
}
3.分析技巧
1.理解基本操作:基本操作可以是算法运算(加法、乘法、位运算等)、比较操作、赋值操作等。基本操作也称为原子操作,意思为不可再分割的操作。
2.关注循环结构:循环是算法中常见的结构,它的执行次数对于时间复杂度的分析至关重要。
3.递归算法:递归算法的时间和空间复杂度分析相对复杂。需要确定递归的深度以及每个递归调用的时间和空间开销。
4.最坏情况分析:对于时间复杂度的分析,通常考虑最坏情况下的执行时间。要考虑输入数据使得算法执行时间达到最大值的情况。
5.善用结论:某些常见算法的时间和空间复杂度已经被广泛研究和证明。可以利用这些已知结果来分析算法那的复杂度。
4.代码示例
#include <iostream>
#include <vector>
using namespace std;
// 遍历, 时间复杂度为O(n)
int calculateSum(vector<int>& nums) {
int sum = 0;
for(int num : nums) {
sum += num;
}
return sum;
}
int main() {
int n;
cout << "Enter the number of elements: ";
cin >> n;
// 空间复杂度:O(n)
vector<int> nums(n);
cout << "Enter the elements: ";
// 时间复杂度:O(n)
for(int i = 0; i < n; i ++) {
cin >> nums[i];
}
int sum = calculateSum(nums);
cout << "Sum of the elements: " << sum << endl;
}
时间复杂度: O ( n ) O(n) O(n)
该算法通过循环遍历数组中的每个元素,并对它们求和,因此时间复杂度与数组中的元素数量成正比。
空间复杂度: O ( n ) O(n) O(n)
该算法使用一个大小为n的向量来存储输入元素,所以空间复杂度与输入元素的数量成正比。
#include <iostream>
using namespace std;
int fibonacci(int n) {
if(n <= 1)
return n;
int prev1 = 0;
int prev2 = 1;
int fib = 0;
// 时间复杂度为O(n)
for(int i = 2; i <= n; i ++) {
fib = prev1 + prev2;
prev1 = prev2;
prev2 = fib;
}
return fib;
}
int main() {
int n;
cout << "Enter the position: ";
cin >> n;
int result = fibonacci(n);
cout << "Fibonacci number at position " << n << " : " << result << endl;
return 0;
}
时间复杂度: O ( n ) O(n) O(n)
该算法使用迭代的方式计算斐波那契数列的第n个数,循环遍历n次,因此时间复杂度与 n n n成正比。
空间复杂度: O ( 1 ) O(1) O(1)
算法只使用了常数级别的额外空间来存储变量,不随输入规模变化。
#include <iostream>
using namespace std;
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for(int i = 1; i <= n; i ++) { // 1 -> n
for(int j = i; j <= n; j += i){ // j -> n/i
//....
}
}
return 0;
}
时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
这份代码中的第一重循环执行了 n n n次,但是第二重循环执行的次数与 i i i有关,具体来说,内部的基本操作被执行的总次数为:
∑ i = 1 n n / i = n ∑ i = 1 n 1 i ≈ n l o g n \sum_{i=1}^{n}n/i = n\sum_{i=1}^{n}\frac{1}{i}≈nlogn ∑i=1nn/i=n∑i=1ni1≈nlogn
空间复杂度: O ( 1 ) O(1) O(1)
算法只使用了常数级别的额外空间来存储变量,不随输入规模变化。当然这份代码并不完整,所以分析其空间复杂度意义不大。
#include <iostream>
using namespace std;
int fibonacci(int n) {
if(n <= 1) {
return n;
}
return fibonacci(n-1) + fibonacci(n-2);
}
int main() {
int n;
cout << "Enter the value of n: ";
cin >> n;
int result = fibonacci(n);
cout << "Fibonacci number at position " << n << " : " << result << endl;
return 0;
}
时间复杂度: O ( 2 n ) O(2^{n}) O(2n)
每个递归调用会产生两个额外的递归调用,因此递归深度为 2 n 2^{n} 2n,其中 n n n是斐波那契数列的位置。
空间复杂度: O ( n ) O(n) O(n)
在斐波那契数列的递归算法中,递归深度为 n n n,因此需要栈堆空间为 O ( n ) O(n) O(n)。
注意,一般来说堆栈空间只给8 M B MB MB,需要注意递归深度不宜过深,一般不宜超过 1 e 6 1e6 1e6层。