一:为什么要有复杂度
*提到复杂度,首先我们要知道什么是算法,我们知道,任何事情都有一套解决办法,算法是输入到输出的一个方法,而对于我们程序员来说,能够找到一个合理高效的算法是最好的。可是我们怎样衡量一个算法的好坏,换句话说,都能解决问题的算法有什么差异嘛?我们先来看力扣上这道题的例子:
题目描述:
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
提示:
==1 <= nums.length <= 105
-231 <= nums[i] <= 231 - 1
0 <= k <= 105 ==
**我们分三步完成,第一步,保存数组最后的一个元素;
第二步,将数组前n-1元素向后移动,腾出第一个位置,第三步,将第一个元素插入到数组的第一个位置, **这个过程循环K次即可完成右旋。我们通过这个思路实现:
代码:
void rotate(int* nums, int numsSize, int k) {
int tmp;
while (k--) {
// 保留最后一个元素的位置
tmp = nums[numsSize - 1];
//剩余元素后移一个位置
for (int i = numsSize - 1; i > 0; i--) {
nums[i] = nums[i - 1];
}
//保留元素插入
nums[0] = tmp;
}
}
我们发现用这种方法确实理论上可行,可是随着右旋K的次数增加和数组的元素增加,这种方法好像行不通了,那我们不就白写了嘛?如果我们能事先分析这种思路的可行性,是不是就可以大大提高了我们的效率,所以,下面,小编给大家介绍时间复杂度。当我们学习了时间复杂度,我们都是预言家,那么这种方法不可行,我们排除就可以了。
时间复杂度
时间复杂度是一个函数表达式T(N),定量的描述了衡量了算法的运行时间。为什么不是精确地算出时间呢,而是定量的估计,因此:我们可以通过执行次数来估计是时间复杂度。
原因有以下几点
1.对于同一个算法,由于编译器不同,编译器旧一点或新一点,处理器不同,有的计算机处理器比较好,有的稍慢,也因环境各种因素影响,所以每一次算法的运行时间都是不同的,我们不能因为一个效率慢的算法在一个处理器非常优的计算机上和一个效率快的算法在一个处理器非常慢的计算机来比较,所以,衡量一个算法的好坏,我们引入了时间复杂度,我们不能定量准确计算,定性分析即可。
下面我们来分析一下下面代码的时间复杂度:
#define n 1000
int main()
{
int i;
int count = 0;
for (i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
count++;
}
}
for (i = 0; i < n; i++)
{
count++;
}
for (i = 0; i < 10; i++)
{
count++;
}
printf("%d", count);
return 0;
}
第一次循环是一个嵌套循环,外循是n次,内循环也是n次,总共执行n^2次,
第二次循环执行n次,第三个循环执行10次,
时间复杂度"T(N) = n^2+n+10
当n = 10 T(N) =100+10+10
当n = 100 T(N) =10000 +100 +10
当n = 1000 T(N) =1000000 + 1000 +10
我们发现,随着n的增加,T(N)的影响程度受高次幂影响更大,我们计算时间复杂度只是想比较算法程序的增长量级,也就是当N不断变大时T(N)的差别,上面我们已经看到了当N不断变大时常数和低阶项对结果的影响很小,所以我们只需要计算程序能代表增长量级的大概执行次数,复杂度的表式通常使用大O的渐进表示法。
上述算法的时间复杂度由原本的T(N) = n^2+n+10 --> O(n^2)
只保留高阶,忽略低阶,当N->无穷,低阶就可以忽略不计了。
我们再看下面代码的时间复杂度:
int main()
{
int i;
int count = 0;
for (i = 0; i < 2*n; i++)
{
count++;
}
for (i = 0; i < 10; i++)
{
count++;
}
printf("%d", count);
return 0;
}
渐进表示是O(2*n)还是O(n);其实,我们要从时间复杂度的本质来理解,时间复杂度是输入对增长趋势的影响,无轮这个常数有多大,都没有影响。就算是O(1000000000n)和O(n)对时间增长趋势的影响都是一样的,接下来,我们介绍空间复杂度:
空间复杂度
空间复杂度和时间复杂度类似,也是一个数学表达式,指的是算法需要开辟的额外的空间,而不是在编译阶段已经分配的空间,比如在编译阶段函数栈帧分配的空间等等,(存储参数、局部变量、⼀些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。可以看我上篇文章编译和链接: link
空间复杂度跟时间复杂度表示方法相同,也是通过O的渐进表示法实现的。
好,知道了这些,我们来看看经典的一些算法的时间复杂度和空间复杂度:
冒泡排序
void bubble_sort(int* arr, int sz)
{
int i, j;
for (i = 0; i < sz-1; i++)
{
int flag = 1;//假设每一趟已经有序
for (j = 0; j < sz - 1 - i; j++)
{
//升序
if (arr[j] > arr[j + 1])
{
flag = 0;//表示无序
//交换
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
//默认有序,直接退出
if (flag == 1)
{
return;
}
}
}
该算法时间复杂度计算为: n-1+n-2+n-3+…3+2+1;由等差数列求和公式得:
T(N) = N*(N-1) / 2 所以时间复杂度为O(N^2),空间复杂度只额外申请了常数量个变量,所以空间复杂度为O(1);
二分搜索
//找到返回下标,没找到返回-1
int binary_search(int* arr, int sz,int k)
{
int left = 0;//左下标
int right = sz - 1;//右下标
while (left<=right)
{
int middle = left + (right - left) / 2;//每次都要更新middle的值
if (arr[middle] > k)
{
right = middle-1;
}
else if(arr[middle] < k)
{
left = middle + 1;
}
else
{
return middle;//返回目标元素下标
}
}
return -1;//表示没找到
}
该算法时间复杂度计算:第一次查找剩余元素n/2,第二次查找剩余元素n/2/2,第三次:n/2/2/2,以此类推,假设查找了x次,(n/2)^x = 1;->x = log2 N.或者这样理解
:T(N) = 22222 …2= N;x = log2 N,查找x次,每次都把数据空间缩短一半,总之,时间复杂度:O(logN),空间复杂度只额外申请了常数量个变量,所以空间复杂度为O(1);
函数递归
我们再看一个函数递归的例子:
long long Fac(size_t N)
{
if (0 == N)
return 1;
return Fac(N - 1) * N;
}
该递归调用了n次,所以时间复杂度为O(n),每一次递归,都额外开辟了一个空间,共开辟了n个次,Fac递归调用了N次,额外开辟了N个函数栈帧,
每个栈帧使用了常数个空间,因此空间复杂度为O(N);
我们发现,递归的时间复杂度和空间复杂度相同, 但不是所有的递归都成立,具体分析要具体分析,如外层是循环,里层是递归,此时时空不相同,时间复杂度为O(n^2),空间复杂度为O(N)=
最后,小编给大家看一下常见时间复杂度对比函数图像: