算法基础
算法的复杂度
算法复杂度(算法复杂性)是用来衡量算法运行所需要的计算机资源(时间、空间)的量。通常我们利用渐进性态来描述算法的复杂度。
用n表示问题的规模,T(n)表示某个给定算法的复杂度。所谓渐进性态就是令n→∞时,T(n)中增长最快的那部分。
比如:T(n) = 2 * n ^ 2 + n log n + 3,那么显然它的渐进性态是 2 * n ^ 2,因为当n→∞时,后两项的增长速度要慢的多,可以忽略掉。
在算法复杂度分析中,log通常表示以2为底的对数。
引入渐进性态是为了简化算法复杂度的表达式,只考虑其中的主要因素。
当比较两个算法复杂度的时候,如果他们的渐进复杂度的阶不相同,那只需要比较彼此的阶(忽略常数系数)就可以了。
总之,分析算法复杂度的时候,并不用严格演算出一个具体的公式,而是只需要分析当问题规模充分大的时候,复杂度在渐进意义下的阶。
记号O给出了函数f(n)在渐进意义下的上界(但不一定是最小的)。
认识时间复杂度
- 常数操作
- 时间复杂度
- 一个简单的理解时间复杂度的例子
常数时间的操作:
- 一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度:
- 为一个算法流程中,常数操作数量的指标。常用O(读作big O)来表示。
- 具体来说:在常数操作数量的表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果记为f(N),那么时间复杂度为O(f(N))。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间。
一个简单的理解时间复杂度的例子:
一个有序数组A, 另一个无序数组B, 请打印B中的所有不在A中的数, A数组长度为N, B数组长度为M。
算法流程1:
- 对于数组B中的每一个数, 都在A中通过遍历的方式找一下;
- 算法时间复杂度分析:时间复杂度为O(N * M);
算法流程2:
- 对于数组B中的每一个数, 都在A中通过二分的方式找一下;
- 算法时间复杂度分析:时间复杂度为O(logN * M);
- 有M个数,每个数进行二分查找时间复杂度为O(logN);
算法流程3:
- 先把数组B排序, 然后用类似外排的方式打印所有在A中出现的数;
- 算法时间复杂度分析:时间复杂度为O((M * logM) + (M + N));
- 无序数组B排序的时间复杂度假定为O(M * logM)
- 外排时间复杂度为两个数组元素的长度和O(M + N) ;
对于同一个问题,比较以上3种不同的算法实现的时间复杂度情况:
- 算法流程1:时间复杂度为 O(N*M) , 性能最差;
- 算法流程2:时间复杂度为 O(logN * M) ;
- 算法流程3:时间度杂度为 O(M * logM) + O(M+N) ;
- 比较算法流程2和算法流程3,因为存在两个变量,M和N,需要根据不同的数据样本量具体分析,采用渐进性分析:
- 当 M 远大于 N 时, N 可看作常数项忽略:
- 算法流程2: O(logN * M) ,得到时间复杂度为: O(M)
- 算法流程3: O(M * logM) + O(M + N) ,得到时间复杂度为: O(M * logM)
- 得出算法流程2 优于 算法流程3;
- 当 N 远大于 M 时:
- 算法流程2: O(logN * M)
- 算法流程3: O(M * logM) + O(M + N)
- 需要根据 M 和 N 的具体的样本量才能分析
- 当 M 远大于 N 时, N 可看作常数项忽略:
对数器
对数器的概念和使用:
- 有一个你想要测的方法a;
- 实现一个绝对正确但是复杂度不好的方法b;
- 实现一个随机样本产生器;
- 实现比对的方法;
- 把方法a和方法b比对很多次(100000+)来验证方法a是否正确。
- 如果有一个样本使得比对出错, 打印样本分析是哪个方法出错;
- 当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
注意要点:
- 要测试的算法a是时间复杂度比较低的算法,而算法b唯一要求就是保证正确,而不用管复杂度的高低
- 随机产生的样本大小要小,这里说的是样本的大小而不是样本的个数。因为出错时,小样本方便分析。
- 随机产生的样本个数要多,100000+ - 只要大量随机产生的样本才可能覆盖所有的情况。
- 如果算法b也无法保证完全的正确,在不断出错调试的过程中,也可以不断完善b,最终达到a和b都正确的完美结果。
对数器的使用:
- 在算法竞赛或代码笔试中,要提前准备好常用算法的对数器,快速编写完算法实现后,使用相应的算法对数器快速验证算法的正确性,减少扣分或扣时;
- 平常在手写算法实现时,验证算法的正确性非常有效;
AlgorithmComparator:
- int[] generateRandomArray(int maxSize, int maxValue) 随机样本产生器
- void comparator(int[] arr) 实现绝对正确的方法b
- boolean isEqual(int[] arr1, int[] arr2) 实现比对方法a和方法b的处理结果的方法
- void printArray(int[] arr) 比对失败时打印样本分析
Master定理
有些算法在处理一个较大规模的问题时,往往会把问题拆分成几个子问题,对其中的一个或多个问题递归地处理,并在分治之前或之后进行一些预处理、汇总处理。
这时候我们可以得到关于这个算法复杂度的一个递推方程,通过 Master公式 求解此方程便能得到算法的复杂度。
Master公式:T(N) = a * T(N/b) + O(N^d)
- 说明:
- T(N):样本量为N的情况下的时间复杂度
- a*T(N/b) : 子过程调用的时间复杂度,N/b指子过程对数据的拆分情况,如二分为 N/2;
- O(N^d) :除掉子过程调用外,剩余操作的时间复杂度
- log(b,a) > d ,时间复杂度为:O(N^log(b,a))
- log(b,a) = d ,时间复杂度为:O(N^d * logN)
- log(b,a) < d ,时间复杂度为:O(N^d)
Master公式适用范围:划分为子过程的样本量规模是一致的。
当划分的子过程的样本量不一致时,不能使用Master公式,例:T(N) = T(2 * N / 5) + T(3 * N / 5) + O(N^2)
递归案例:在一个数组中找最大值
- 使用递归方式:
- 将数组切分成两部分,分别找到左边部分的最大值和右边部分的最大值,返回两数中的最大值;
- 递归终止条件:当L == R,当左边界等于右边界时,只有一个数,直接返回;
- 递归实现时间复杂度分析:T(N) = 2 * T(N/2) + O(1)
- 样本量等分为两部分,每部分的时间复杂度为T(N/2)
- 比较大小操作的时间复杂度为:O(1)
- 代入Master公式得:T(N) = 2 * T(N/2) + O(N^0), a=2,b=2,d=0
- 符合公式 log(2,2) = 1 > 0 , 得时间复杂度为:O(N^log(2,2)) -> O(N)
public static int findMax(int[] arr,int L, int R){
if(L == R){
return arr[L];
}
int mid = (L+R)/2;
int leftMax = findMax(arr, L, mid);
int rightMax = findMax(arr, mid+1 , R);
return leftMax > rightMax ? leftMax : rightMax;
}
案例2:如果某个算法时间复杂度公式为:T(N) = 2 * T(N/2) + O(N):
- 将样本量为N等分为两部分,每部分时间复杂度为:T(N/2)
- 计算结果操作的时间复杂度为:O(N)
- 代入Master公式得:T(N) = 2 * T(N/2) + O(N^1), a=2,b=2,d=1
- 符合公式 log(2,2) = 1 等于 1 , 得时间复杂度为:O(N^1 * logN) -> O(N * logN)
相关链接
gitee地址:https://gitee.com/chentian114/chen_algorithm_study
github地址:https://github.com/chentian114/chen_algorithm_study
CSDN地址:https://blog.csdn.net/chentian114/category_10828595.html
公众号
参考
Leetcode
左程云 牛客网 算法初级班课程