复杂度
1.背景
当我们在讨论代码的好坏时,什么因素可以衡量?
——算法所耗费的资源和时间
我们把这类因素称为复杂度,显而易见,复杂度是从空间与时间两个维度进行讨论与计算的
2.分类
1)时间复杂度
一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为
T(n)
。
假设算法的问题规模为n,那么操作单元数量便用函数f(n)
来表示
算法执行时间的增长率和f(n)
的增长率相同,称为 算法的渐近时间复杂度,简称时间复杂度, 记为 T(n) = O(f(n))
怎样计算时间复杂度呢?又怎样去表示时间复杂度呢?
先来了解时间复杂度的分类
![OIP-C](https://i-blog.csdnimg.cn/blog_migrate/eb4727b13465860eb2bbb7c9cd94db6e.jpeg)
int n = 0;
int m = 0;
m = n + 2;
m++;
//可以看到,这里的几行代码计算了数次,但是时间复杂度却是 O(1)
//这类代码的运算次数并不会随着某个变量的改变而改变,结构并不“复杂”
//总结:常数阶的时间复杂度都为 O(1)
for(int i=0;i<n;i++)
{
sum += i;
}
//这里的代码进行了 n 个循环,很容易理解时间复杂度为 O(n)
//但如果是没有嵌套的两个 for循环呢?再加上几句独立的运算呢?
//打个比方,程序执行了2n+3次,那么时间复杂度会变成O(2n)吗?答案是否定的,依旧是O(n)
不妨这样理解:在计算时间复杂度时,只保留语句频度的数学表达式中最高阶的变量部分,因为在真正严苛的运算下,
n
的取值是极大的。在取值极大时,变量的阶越高,其所决定执行次数的权重便更大,所以我们保留最高阶的变量部分,也造就了所谓的渐进表示法
for(i=0;i<n;i++)
{
for(j=0;j<n;j++)
m++;
}
//随着循环的逐层嵌套,时间复杂度也会呈指数级别地上升
int i = 1;
while(i<n)
{
i = i * 2;
}
//在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2^n
//也就是说当循环 log2^n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)
其余的类别与已经介绍过的有异曲同工之处,这里就不一一赘述
2)空间复杂度
与时间复杂度也相类似,只不过空间复杂度计算的是临时占用的变量个数
与时间复杂度不同的是,空间复杂度的分类明了,只有两种:
- 没有变量决定占用的临时空间大小
- 临时空间根据变量分配
3.例题以及讲解
1 客观题
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
a[i][j]=i*j;
//计算时间复杂度: O(n^2)
//嵌套的循环,简单易懂
int f ( unsigned int n )
{
if (n == 0 || n==1)
return 1;
else
return n * f(n-1);
}
//计算时间复杂度: O(n)
//一看是个递归,第一反应很难,实际并不是,仔细看看,递归调用n - 1次,每次操作都是一次,所以时间复杂度为n
给定一个整数sum,从有N个有序元素的数组中寻找元素a,b,使得a+b的结果最接近sum,最快的平均时间复杂度是( ? )
//数组元素有序,所以a,b两个数可以分别从开始和结尾处开始搜,根据首尾元素的和是否大于sum,决定搜索的移动,整个数组被搜索一遍,就可以得到结果,所以最好时间复杂度为n
如果一个函数的内部中只定义了一个二维数组a[3][6],请问这个函数的空间复杂度为( )
//函数内部数组的大小是固定的,不论函数运行多少次,所需空间都是固定大小的,因此空间复杂度为O(1)
观察上面的几道客观题,不难发现他们几乎都是计算时间复杂度,而这些计算也都是关于基础的循环嵌套以及定义上的知识
2 编写代码
####旋转数组问题
给你一个数组,将数组中的元素向右轮转
k
个位置,其中k
是非负数。
看到题目之后,我们能反应出什么思路?
- 使用另建的数组来装重新排序过的数组元素
简单粗暴,来看看代码的实现:
void rotate(int* nums, int numsSize, int k)
{
int newArr[numsSize];
for (int i = 0; i < numsSize; ++i)
{
newArr[(i + k) % numsSize] = nums[i];
}
for (int i = 0; i < numsSize; ++i)
{
nums[i] = newArr[i];
}
}
要注意的点就是正确计算后续元素所放置位置的下标
虽然简单粗暴,但是复杂度方面呢?
时间复杂度: O(n)
空间复杂度: O(n)
- 反转数组
这是个挺妙的方法,规避了移位的固定思维,将这题的思路变成两次倒置
![IMG_0619](https://i-blog.csdnimg.cn/blog_migrate/f10e0ddcda74ac74b84d0488b33b9d5f.png)
看懂了思路之后代码该如何写就很清晰了
void swap(int* a, int* b)
{
int t = *a;
*a = *b, *b = t;
}
void reverse(int* nums, int start, int end)
{
while (start < end)
{
swap(&nums[start], &nums[end]);
start += 1;
end -= 1;
}
}
void rotate(int* nums, int numsSize, int k)
{
k %= numsSize;
reverse(nums, 0, numsSize - 1);//倒置整个数组
reverse(nums, 0, k - 1);//倒置规定的前半部分数组
reverse(nums, k, numsSize - 1);//倒置后半部分数组
}
这次的复杂度情况又是如何呢?
时间复杂度:O(n)
空间复杂度:O(1)
遗失数字问题
数组
nums
包含从0
到n
的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?
提供两种颇为巧妙的思路
-
异或运算
明确
^
运算的特殊性质:n ^ n = 0
n ^ 0 = n
按照题干,可以想到大概的方法:在原先数组中填充0 ~ n
,然后在数组中两两不遗漏地进行异或运算,按照上述特殊性质,最后得到的结果就是缺失的数字,代码如下:
public class Solution
{
public int MissingNumber(int[] nums)
{
int xor = 0;
int n = nums.Length;
for (int i = 0; i < n; i++)
{
xor ^= nums[i];
}
for (int i = 0; i <= n; i++)
{
xor ^= i;
}
return xor;
}
}
来看复杂度:
时间复杂度:O(n)
空间复杂度:O(1)
- 求和作差
根据数学知识,我们可以知道0 ~ n
的总和为 ( n * ( n + 1 ) ) / 2
,我们也可以利用循环来求得数组中所有数字之和,再作差求得缺失的数字,这个方法很是巧妙
int missingNumber(int* nums, int numsSize)
{
int n = numsSize;
int sum = 0;
int total = (n*(n + 1))/2;
for(int i=0;i<numsSize;i++)
{
sum += *(nums + i);
}
return total - sum;
}
时间复杂度:O(n)
空间复杂度:O(1)
经过优化的代码,其复杂度也会随之下降,从而提高运行效率,题目虽小,但要牢记思想意义,在每道做过的题目中发现新的、巧妙的方法,这也是一种自我水平的提升。所以我们在做题时应该多加思考,多多推敲细节才行。