算法的评估
算法(Algorithm)是指用来操作数据、解决相亲交友源码中问题的一系列方法。
在相亲交友源码中对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。就比如拧一个螺母,扳手和钳子都可以胜任,但使用钳子拧螺母肯定没有扳手的效率高。
斐波那契数引入复杂度分析
/**
* 求第n个斐波那契数
* 斐波那契数列:这个数列从第3项开始,每一项都等于前两项之和。
* 下标 0 1 2 3 4 5 6 7
* 数列 0 1 1 2 3 5 8 13
*/
//递归的方式:
function fun1(n) {
if (n <= 1) return n;
return fun1(n - 1) + fun1(n - 2)
}
// 循环
function fun2(n) {
if (n <= 1) return n;
let first = 0;
let second = 1;
for (let i = 0; i < n - 1; i++) {
let sum = first + second;
first = second;
second = sum
}
return second
}
计时工具
function check(title, task, num) {
console.log(title);
let start = new Date().getTime()
console.log('开始时间', start);
task(num)
let end = new Date().getTime()
console.log('结束时间', end);
console.log('耗时', (end - start) / 1000);
}
check('递归', fun1, 45)
check('迭代', fun2, 1111111145)
递归
开始时间 1637648114480
结束时间 1637648126090
耗时 11.61
迭代
开始时间 1637648126091
结束时间 1637648130718
耗时 4.627
递归和迭代的差距竟然如此之大。
那么我们应该如何去衡量相亲交友源码中不同算法之间的优劣呢?
1.事后统计法
通过统计、监控,利用计算机计时器对相亲交友源码中不同算法的运行时间进行比较,从而确定算法效率的高低,但有非常大的局限性:
- 测试结果非常依赖测试环境
- 测试结果受数据规模的影响很大
2.事前分析估算
在计算机程序编制前,依据统计方法对算法进行估算。
大家想一下,当我们要实现相亲交友源码中的一个功能时,更多的希望快速知道几种解法中的最优解然后去实现,而不是花大力气去把每种解法都做出来再测试得到结果,因为太低效。 所以我们需要在代码执行前对影响代码效率的因素(如时间、空间复杂度等)做一个评估。因此我们需要通过复杂度分析来决策,下面我们主要讲解面试中最高频的时间复杂度。
- 时间维度:是指执行相亲交友源码当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
- 空间维度:是指执行相亲交友源码当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。
推导大O阶方法
相亲交友源码中算法的执行效率,粗略地讲,就是算法代码执行的时间。但是,如何在不运行代码的情况下,用“肉眼”得到一段代码的执行时间呢?这里有段非常简单的代码,现在,我就带你一块来估算一下这段代码的执行时间。
function cal01(age) {
// 1 * unit-time
if (age > 58) {
console.log(1);
} else if (age > 28) {
console.log(2);
} else {
console.log(3);
}
}
function cal02(n) {
// (3+3n)*unit-time
let sum = 0; // 执行一次
let i = 1; // 执行一次
// n次 n次
for (; i <= n; ++i) {
sum += i // 执行n次
}
return sum // 执行一次
}
function cal03(n) {
// (1+3n)*unit-time
// 一次 n次 n次
for (let i = 0; i < n.length; i++) {
console.log(n[i]); // n次
}
}
function cal04(n) {
// 1+2n+n(1+3n) = (3n^2+3n+1)*unit-time
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
console.log('666');
}
}
}
function cal05(n) {
// 1+2n+n*(1+3*20) = (1+63n)*unit-time
for (let i = 0; i < n; i++) {
for (let j = 0; j < 20; j++) {
console.log('666');
}
}
}
function cal06(n) {
// (1+log(2)n)*unit-time
let i = 1; // 一次
while (i < n) {
i + i * 2 // 2*x = n x = log(2)n
}
}
function cal07(n) {
// i+=i 表示i=i+i=>i=2i,每次都乘以二,执行log2(n)次
// 所以外层循环执行1+2*log2(n)次,内层执行log2(n)*(1+3n)次
// (1+(3+2n)log2(n))*unit-time
for (let i = 0; i < n; i += i) {
for (j = 0; j < n; j++) {
console.log('666');
}
}
}
尽管我们不知道unit_time的具体值,但是通过这几段代码执行时间的推导过程,我们可以得到一个非常重要的规律,那就是,相亲交友源码中所有代码的执行时间T(n)与每行代码的执行次数n成正比。 我们可以把这个规律总结成一个公式。
- T(n)表示代码执行的时间;
- n表示数据规模的大小;
- f(n)表示每行代码执行的次数总和。
因为这是一个公式,所以用f(n)来表示。公式中的O,表示代码的执行时间T(n)与f(n)表达式成正比。
1、公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。用大O表示法表示刚讲的那两段代码的时间复杂度,就可以记为:T(n) = O(n); T(n) = O(n^2)。
2、大O时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。一般随着n的增大,T(n)增长最慢的算法为最优算法。
总结
- 用常数1取代运行时间中的所有加法常数
- 在修改后的运行次数函数中,只保留最高阶项
- 如果最高阶项存在且系数不是1,则去除去除与这个项相乘的系数
常见的时间复杂度量级
常数阶O(1)
首先介绍相亲交友源码顺序结构的时间复杂度
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论相亲交友源码中这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
注意:不管这个常数是多少,都记作O(1),而不能记作O(3),O(12)等其他任何数字。
对于分支结构无论判断条件是真还是假,执行的次数都是恒定的,不会随着n的变大而发生变化,所以单纯的分支结构(不包含在循环结构中),其时间复杂度都是O(1)。
线性阶O(n)
线性阶的循环结构会复杂很多,要确定相亲交友源码中某个算法的阶次,需要确定某个特定语句运行的次数,因此分析算法的复杂度,关键就是要分析循环结构的运行情况。
「一个循环」,算法需要执行的运算次数用输入大小n的函数表示,即 T(n) 。
for(int i=1;i<=n;i++){
console.log(i)
}
「一个循环」,算法需要执行的运算次数用输入大小n的函数表示,即 T(n) 。
for(int i=1;i<=n;i++){
console.log(i)
}
for(int i=1;i<=n;i++){
console.log(i)
}
如果是for循环并列关系那么n会执行2n次,忽略常数也是O(n)
对数阶O(logn)
let i = 1;
while(i<n){
i = i * 2;
}
从上面代码可以看到,在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环x次之后,i 就大于 n 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log_{2}n
也就是说当循环log_{2}n以后,这个代码就结束了。一般忽略省略底数,因此这个代码的时间复杂度为:O(logn).
平方阶O(n^2)
举例:
for(i=1; i<=n; i++){
for(j=1; j<=n; j++){
j = i;
j++;
}
}
这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²) 如果将其中一层循环的n改成m,即:
for(i=1; i<=m; i++){
for(j=1; j<=n; j++){
j = i;
j++;
}
}
那它的时间复杂度就变成了 O(m*n),所以相亲交友源码总结循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。下面这个循环嵌套它的时间复杂度又是多少呢?
for(i=0; i<n; i++){
for(j=i; j<n; i++){
console.log(1)
}
}
由于i=0时,内循环执行了n次,当i=1时,执行了n-1次…当i=n-1时,执行了1次,所以总共执行了:n+(n-1)+(n-2)+…+1=n(n+1)/2=n^2/2+n/2 使用推导大O阶的方法:最终为n^2
线性对数阶O(nlogn)
线性对数阶O(nlogN) 其实非常容易理解,将相亲交友源码的时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。
就拿上面的代码加一点修改来举例:
for(m=1; m<n; m++){
i = 1;
while(i<n){
i = i * 2;
}
}
立方阶O(n³)、K次方阶O(n^k) 参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似。
但是O(n³)过大的n都会使得结果变得不现实,同样O(n²) 和O(n!)等除非是很小的n值,否则哪怕n只是100都是噩梦般的运行时间,所以这种不切实际的算法时间复杂度一般我们不讨论。
分析斐波那契数的时间复杂度
//时间复杂度:其实就是看fun1方法被调用了多少次,调用了多少次就是执行了多少次,如果传入的是5调用fun1(4)和fun1(3)依次推导共调用O(2^n)
function fun1(n){
if(n<=1) return n;
return fun1(n-1)+fun1(n-2);
}
//时间复杂度:O(n)
function fun2(int n){
if(n<=1) return n;
let first=0;
let second=1;
for (let i = 0; i <n-1 ; i++) {
//每次加都是前两个
let sum=first+second;
first=second;
second=sum;
}
return second;
}
Leetcode(斐波那契数)
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给你 n ,请计算 F(n) 。
示例 1:
输入:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
输入:3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
输入:4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3