目录
作为一个即将独当一面的程序员,不知道算法的时间复杂度与空间复杂度怎么行?作为一个计算机小白,每次看到屏幕上冰冷的“超出时间限制”时,都感觉备受打击。今天就来总结一下关于他们的知识点吧。
时间复杂度
定义:在计算机科学中,算法的时间复杂度是一个函数,定量描述了算法的运行时间。算法中基本操作的执行次数,即算法的时间复杂度。
(基本操作:指算法中最频繁执行且耗时相对固定的操作)
大O表示法(Big O notation):
1、用1取代所有加法常数
2、只保留最高阶项
3、去除该项的乘法常数
这个过程得到的结果称为“大O阶”,目的在于去掉影响较小的项,通常用字母N来表示输入规模。
以下为常见的大O阶,按效率从高到低排序。
时间复杂度 | 名称 | 特点 |
O(1) | 常数时间 | 运行时间(T)不随输入规模(N)变化。 |
O(log n) | 对数时间 | (T)随(N)对数增长。如二分查找。 |
O(n) | 线性时间 | (T)随(N)正比增长。 |
O(n*log n) | 线性对数时间 | (T)随(N)的变化呈现n与log n的乘积。 |
O(n*n) | 平方时间 | (T)与(N)的平方成正比。如冒泡排序。 |
O(n*n*n) | 立方时间 | (T)与(N)的立方成正比。 |
| 指数时间 | (T)随(N)指数增长。 |
O(n!) | 阶乘时间 | (T)随(N)阶乘增长。 |
一些算法的时间复杂度存在最好情况、平均情况和最坏情况。如遍历数组寻找某个元素的算法。理想状况下遍历的第一个元素就满足了目标,从而终止算法。但在实际中一般关注算法的最坏运行情况,时间复杂度就用最坏情况表示。
当算法的输入规模有两个或两个以上参数时,如果他们的关系相互独立,那么时间复杂度就是他们的和或积。如果参数之间存在关系,则根据关系简化,比如N远大于M时,可以将M简化掉。
递归算法的大O阶:O( 每次递归调用数量 ^ 递归次数 ) 如斐波那契数列,每次递归调用两次,复杂度为O(2^n)。
(如果每次递归只调用一次,则为O(N)。)
这里常常会用到等比数列与等差数列的计算:
空间复杂度
定义:算法的空间复杂度用于度量在运行过程中额外占用的储存空间大小。因为输入数据本身占用的空间是固定的,与算法的优劣无关,所以只讨论额外占用空间。
(可以看出时间复杂度讨论对CPU处理速度的影响,空间复杂度讨论对内存栈区的占用。)
也采用大O表示法。算法运行时的额外空间通常来自以下四个方面:
1、临时变量,例如以下代码段:
for (int i = 0; i < n; i++) {
int sum = 0;
cout << "Good" << endl;
}
对于变量“i”和“sum”,在循环中创建再销毁,每次占用的都是同一个空间。所以这段代码的空间复杂度为O(1)。所以不同于时间复杂度,空间可以重复利用的,一些空间可能在某些时机下销毁后再次创建。
2、数据结构,比如定义了一个大小为N的数组,大O阶就是O(N)了。
3、动态分配的内存,比如new或者malloc分配的。
4、递归调用栈
我们需要知道,当一个函数被调用时,系统会创建一个栈帧,保存该函数的返回地址、参数、局部变量等信息,然后压入栈中。在函数执行完毕后,栈帧会从栈中弹出,恢复到调用函数时的状态。
(栈帧(Stack Frame)是函数调用栈中的一个重要概念,它是函数调用过程中用于保存函数上下文信息的一块内存区域。每次函数调用时,系统都会为其创建一个栈帧,并将其压入栈中;函数返回时,系统会从栈中弹出该栈帧。)
例如斐波那契数列:
int fibonacci(int n) {
if (n <= 1)
return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
先开始调用fibonacci(n - 1),栈中压入一个新的栈帧,栈深度增加1。依此类推,栈的深度到达n-1时,递归开始返回,此时共创建了n-1个栈帧,在不断返回的过程中逐一销毁。一直返回到起点,紧接着现在开始调用fibonacci(n - 2),同理这边会创建n-2个栈帧,但是这里所用的空间仍然是上次调用时用过的空间,属于销毁后再利用,所以从整个算法的角度来看,最深的递归深度即为算法的空间复杂度,即O(N)。可以看出,递归算法的空间复杂度取决于递归深度,而不是递归调用的总数。
练手题
来瞅瞅下面这段看起来很愚蠢的代码,算算它的时间复杂度和空间复杂度是多少。(没错这就是我写的超出时间限制的算法)
int maximumProduct(vector<int>& nums) {
int mul;
int max;
bool first = true;
int num1, num2, num3;
for(int i = 0; i < nums.size(); i++){
num1 = nums[i];
for(int j = i + 1; j < nums.size(); j++){
num2 = nums[j];
for(int k = j + 1; k < nums.size(); k++){
num3 = nums[k];
if(first){
first = false;
mul = num1 * num2 * num3;
max = mul;
}else{
mul = num1 * num2 * num3;
if(max < mul){
max = mul;
}
}
}
}
}
return max;
}
这段代码使用了三重嵌套,计算时间复杂度时如果仍然像二重嵌套一样使用等差数列求解,可能会出现一些难以处理的计算。但是这段算法的本质是把数组中不相同的3个数的每个组合遍历了一遍,所以就用到了概率论中的组合数,C(n,3),从n个数里面随机挑3个组合,得到时间复杂度为O(N^3),由于临时变量都是常数级别的,空间复杂度为O(1)。
呵呵,这么大,怪不得会超出限制。
小结
做个总结,要想衡量一个算法的优劣是绕不开时间复杂度和空间复杂度的。时间复杂度往往讨论最坏情况,递归调用函数的空间复杂度只与递归深度有关。
如有补充纠正欢迎留言。