算法效率
如何衡量一个算法的好坏:
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
随着科技的发展,计算机的存储容量已经达到了很高的程度,现在我们更多关心的是时间复杂度
时间复杂度T(n)
表示方法:T(n)=O(f(n)),大O表示法
f(n)表示算法中每条基本语句执行的总次数
基本语句是指程序中最小的可执行单元,通常是一条语句或一个语句块
作用:衡量算法的运行快慢
大O表示法(极限估算):
作用:去除对结果影响不大的项
使用方法:
- 用常数1取代运行时间中所有加法常数
- 只保留最高阶项
- 如果最高阶项存在且不为1,则去除这个项相乘的常数,得到的结果就是大O阶
另外有些算法的时间复杂度存在最好、平均和最坏情况:
- 最坏情况:任意输入规模的最大运行次数(上界)
- 平均情况:任意输入规模的期望运行次数
- 最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中,一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
关于非递归的时间复杂度求解:
非递归的时间复杂度求解包含:单层循环求解和多层循环求解
其中,单层循环的求解结果一般包含以下四种:
- O(1)
- O(N)
- O(N^1/m)------因为根号不会打所以用分数形式替代了
- O(logm N)------以m为底N的对数
而多层循环的结果有以下两种求解方式:
- 直观法 ------ 外层变量i与内层变量的取值无关时使用
- 计算法 ------ 外层变量i与内层变量的取值有关时使用
此时总时间复杂度是内层语句执行次数对应的时间复杂度与外层循环所对应的时间复杂度的最大值
单层循环求解时间复杂度:
单层循环(一):
void func(int N) { int num = 0; for(int i = 0;i<100;i++) ++num; }
++num的执行次数为100次是常数值,所以时间复杂度为O(1)
注意执行次数不等于复杂度
结果:O(1)
单层循环(二):
//情况一 void func(int N,int m) { int i = 1; while(pow(i,m)<=N) } //情况二 void func(int N,int m) { int i = 1; for(int i = 1;;pow(i,m)<=N;i++) }
情况一的i++执行了N^1/m次,所以时间复杂度为O(N^1/m),情况二与情况一类似
pow函数是用来计算某个数的次方,这里计算的是i的m次方
结果:O(N^1/m)
单层循环(三):
void func(int N,int m) { int i = 1; while(i<=N) i*=m; }
while循环等价于for(int i = 1;i<=N;i*=m),由于i起始值为1,故当满足循环条件时m已经乘了x次,即m^x = N,所以时间复杂度为O(logm N),若m=2则可以直接写成O(log N)
i *= m =》i = i * m,由于i初始值为1,故该式子=>m*m,假设循环x次那么总执行次数为m^x次
结果:O(logm N)或O(log N)
单层循环(四):
void func(int N,int M) { int num = 0; for(int i = 0;i<N;i++) ++num; while(M--) --num; }
for循环中++num的执行次数为N次,while循环中--num的执行次数为M次,所以时间复杂度为O(N)+ O(M)或者O(max(M,N))
若提及m远大于n或n远大于m则时间复杂度分别为O(M)和O(N)
结果:O(max(M,N))或O(N)+ O(M)
单层循环(五):
void func(int N,int M) { int num = 0; for(int i = 0;i<N;i++) ++num; int M = 5; while(M--) --num; }
for循环的执行次数仍为N次,而while循环的执行次数为5次,所以时间复杂度为O(N)
while循环执行次数为5次为常数值,上次的O(M)变为O(1) 而O(1)< O(N)
结果:O(N)
多层循环求解时间复杂度:
多层循环一般为两层循环
每一块多层for循环的求解方式均为:外层时间复杂度 * 内层时间复杂度
若函数有多块多层循环,分别计算每块的时间复杂度后相加,并利用大O表示法得到最终结果
直观法:
多层循环(一):
void func(int N) { int num = 0; for(int i = 0;i<N;i++) for(int j = 0;j<N;j++) ++num; int M = 5; for(int i = 0;i<N;i++) while(M--) --num; int k = 10; while(--k) num++; }
第一个双层for循环中内外两层的时间复杂度均为O(N),故时间复杂度为O(N^2)
第二个for与while循环中内外两层的时间复杂度分别为O(N)和O(1),故时间复杂度为O(N)
第三个while循环中num++执行次数为5次为常数值,所以时间复杂度为O(1)
故总时间复杂度为O(N^2)+ O(N)+ O(1) = O(N^2)
后续双层循环的例子不再说明执行次数直接算出某层的时间复杂度
结果:O(N^2)
多层循环(二):
void func(int N) { int count = 0; for(int i = 1;i<=N;i*=2) for(int j = 1;j<=N;j++) count++; }
由单层循环案例可知外层for循环的时间复杂度为O(log N),内层for循环为O(N),
故总时间复杂度为:O(log N)* O(N) = O(N*log N)
由于外层循环中是i *= 2而不是之前的i *= m,所以时间复杂度是以2为底N的对数
结果:O(N*log N)
计算法:
多层循环(一):
void func(int N) { int count = 0; for(int i = 1;i<=N;i*=2) for(int j = 1;j<i;j++) count++; }
外层for循环的时间复杂度为O(log N),对于内存for循环就已经不能直观的判断出它的时间复杂度了,我们需要经过计算count++的执行次数来得到它的时间复杂度:
i的值 | 1 | 2 | 4 |
...... | N |
count++的执行次数 | 0 | 1 | 3 | ...... | N-1 |
总执行次数为:0+1+2+......+N-1 = N^2
故时间复杂度为 O(N^2)------ [ O(N^2)> O(log N)]
结果:O(N^2)
多层循环(二):
void func(int N) { int m = 0; for(int i = 1;i<=N;i++) for(int j = 1;j<2*i;j++) m++; }
i的值 | 1 | 2 | 3 |
...... | N |
count++的执行次数 | 0 | 3 | 5 | ...... | 2N-1 |
总执行次数为:0+3+5......+2N-1 = N(1 + 2N - 1)/ 2 = N^2
故时间复杂度为O(N^2)------ [ O(N^2) > O(N)]
结果:O(N^2)
多层循环(三):
void func(int N) { int m = 0; for(int i = 1;i<=N;i*=2) for(int j = 1;j<i;j++) m++; }
i的值 | 2^0 | 2^1 | 2^2 |
...... | 2^k |
count++的执行次数 | 2^0-1 | 2^1-1 | 2^2-1 | ...... | 2^k-1 |
总执行次数为:1 * (1 - 2^k+1)/ (1 - 2) = 2 * 2^k - k - 2
故时间复杂度为O(N)
当外层循环结束时i的取值应该是2^k,故2^k = N,而k = log N,所以总执行次数为2*N-log N - 2
结果:O(N)
多层循环(四):
int func(int N) { int num = 0; for(int i = 0;i<N;++i) for(int j = 0;j<i;++j) for(int k = 0;k<j;++k) num++; }
这里我们采用画图的方法来解决,对于外面两层的for循环可以画出一个二维直角坐标系:
此时三角形的面积就是这两个循环的时间复杂度O(N^2)
如果加上最内层的k的话就可以画出一个三维直角坐标系:
此时该立方体的体积为N^3/6
故总时间复杂度为:O(N^3)
结果:O(N^3)
多层循环(五):
int func(int N) { int i = 0,sum = 0; while(sum<N) sum += ++i; return 1; }
i的值 | 0 | 1 | 2 |
...... | k |
sum的值 | 0 | 1 | 3 | ...... | (1+k)*k/2 |
当循环结束时,(1+k)*k/2 > N =》 (k^2 + k) / 2 <N
故时间复杂度为O(N^1/2)------ [k^2 < N] ------ N^1/2表示根号N
结果:O(N^1/2)
关于递归的时间复杂度求解:
关于递归的时间复杂度的求解方式一般有两种:master定理、递归树
master定理(主定理):
主定理适用于求解如下递归形式算法的时间复杂度:
前提条件:a >= 1,b > 1,f(n) > 0
适用情况:
- n为问题规模大小
- a为原问题的子问题个数
- n/b说每个子问题的大小(假设每个子问题的规模基本一样)
- f(n)是将原问题分解成子问题和将子问题的解合并成原问题的解的时间
主定理包含三种规则:
规则一:T(N)= 5T(n/2)+ n^2,则a=5 b=2,所以n的以b为底a的对数的平方的结果大致为n^2.5,而f(n) = n^2 < n^2.5,故T(n) = O(n^logb a)
规则二:T(N)= 4T(n/2)+ n^2,则a=4 b=2,所以n的以b为底a的对数的平方的结果一定为n^2,而f(n) = n^2 = n^2,故T(n) = O(n^logb a * log n) = O(n^2 * logn)
规则三:T(N)= 3T(n/2)+ n^2,则a=3 b=2,所以n的以b为底a的对数的平方的结果大致为n^1.6,而f(n) = n^2 > n^1.6,故T(n) = O(f(n)) = O(n^2)
结论:判断规则带入计算
递归树:
使用步骤:列出表达式 -> 画出递归树 -> 求解时间复杂度
计算方法:叶子数 + 层数 * f(n)
递归树(一):
当表达式为:T(n)= T(n-1) + 1
由于叶子数=1,层数=n,f(n) = 1,故时间复杂度=1+n*1 = O(N)
结果:O(N)
递归树(二):
当表达式为:T(n)= 2T(n-1) + 1
由于叶子数=2^n,层数=n,f(n) = 1,故时间复杂度=2^n+n*1 = O(2^n)
结果:O(2^n)
递归树(三):
//求斐波那契数列 int func(int N) { if(N<3) return 1; return func(N-1)+func(N-2); }
斐波那契数列的表达式为T(N) = T(N-1) + T(n-2)
由于叶子数约等于2^n,没有层数与f(n),故时间复杂度为O(2^n)
结论:O(2^n)
时间复杂度OJ题:
面试题 17.04. 消失的数字 - 力扣(LeetCode)
思路一:求0~n个数字之和然后减去数组中的元素,最后结果就是那个消失的数字
int missingNumber(int* nums, int numsSize) { //0-n求和,等差数列求和公式:Sn = n*(A1+An)/2 int sum = numsSize*(numsSize+1)/2;//在这里由于是0到n个数所以数列个数为n+1个 for(int i = 0; i < numsSize; ++i){ //总和减去数组元素 sum -= nums[i]; } return sum; }
思路二:类似于之前解决找单身狗问题时用到的异或办法,因为异或的规则是相同为0不同为1,且在这两数组中除了消失的数以外其他数都出现了两次,两数组异或(两两抵消)后剩下的那个没有被抵消的数字就是缺失的数字......
//两次异或的顺序是可以改变的,num的作用 int missingNumber(int* nums, int numsSize) { //异或的办法是相同为0不同为1 int num = 0; //这里要异或n次,表示与不完整数组进行异或 for(int i = 0;i<numsSize;++i) { num^=nums[i]; } //这里要异或n+1次,表示与完整数组进行异或 for(int i = 0;i<=numsSize;i++) { num^=i; } return num; }
常见复杂度对比:
O(1) < O(logN) < O(N) < O(NlogN) < O(N^2) < O(2^N) < O(n!)
~over~