Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

时间复杂度和空间复杂度 #1

Open
chris-paul opened this issue Jan 6, 2021 · 0 comments
Open

时间复杂度和空间复杂度 #1

chris-paul opened this issue Jan 6, 2021 · 0 comments
Labels
dataStructure dataStructure

Comments

@chris-paul
Copy link
Owner

chris-paul commented Jan 6, 2021

如何表示复杂度

如何表示算法复杂度,具体来讲就是代码执行的时间、执行消耗的存储空间。例如:

function cal(n) {
    let sum = 0; // 1 unit_time
    let i = 0; // 1 unit_time
    for(; i <= n; i++) { // n unit_time
        sum += i; // n unit_time
    }
    return sum
}

从 CPU 的角度看,每段代码不过是读写数据或操作数据,尽管每次操作 CPU 执行的个数、执行的时间都不同,但我们粗咯把每次执行的时间都一致,称为 unit_time

所以上述代码总共执行 (2n+2)*unit_time 时间,即:T(n)=(2n+2)*unit_time ,所以,我们可以写成:

T(n) = O(f(n))

其中:

  • n:表示数据规模的大小

  • f(n):表示每行代码执行的次数总和,也就是2n + 2

  • O:表示代码的执行时间 T(n) 与 f(n) 表达式成正比

当 n 很大时,例如 10000,甚至更大,T(n) = O(f(n)) 可以表示为 T(n) = O(n) 。

这就是大 O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。

时间复杂度

当 n 无限大时,时间复杂度 T(n) 受 n 的最高数量级影响最大,与f(n) 中的常量、低阶、系数关系就不那么大了。所以我们分析代码的时间复杂度时,仅仅关注代码执行次数最多的那段就可以了

function fun1(n) {
    let sum = 0,i = 0; 
    for(; i <= n; i++) {
        sum += i; 
    }
    return sum
}
function fun2(n) {
    let sum = 0, sum1 = 0, i = 0, j = 0; 
    for(; i <= n; i++) { // 循环1
        sum += i; 
    }
    for(i = 0; i <= n; i++) { // 循环2
        for(j = 0; j <= n; j++) { 
            sum += i * j; 
        }
    }
    return sum
}
function fun3(n) {
    let sum = 0, i = 0; 
    for(; i <= n; i++) { 
        sum += fun(i); 
    }
    return sum
}

fun1 中第1行都是常量,对 n 的影响不大,所以总的时间复杂度要看第2、3行的循环,即时间复杂度为: O(n)

fun2 中循环1的时间复杂度为 O(n) ,循环2的时间复杂度为 O(n2),当 n 趋于无穷大时,总体的时间复杂度要趋于 O(n2) ,即上面代码的时间复杂度是 O(n2)

fun3 的时间复杂度是 O(n * T(fun)) = O(n*n) ,即 O(n2)

所以:T(c+n)=O(n),T(m+n)=O(max(m, n)),T(n) = T1(n) T2(m) = O(nm),其中 c 为常量

常见复杂度(按数量阶递增)

多项式量级:

  • 常量阶: O(1):当算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)

  • 对数阶:O(logn): 每次循环 i 都乘以 2 ,直至 i > n ,即执行过程是:20、21、22、…、2k、…、2x、 n
    所以总执行次数 x ,可以写成 2x = n ,则时间复杂度为 O(log2n) 。这里是 2 ,也可以是其他常量 k ,时间复杂度也是: O(log3n) = O(log32 * log2n) = O(log2n)

    let i=1;
    while (i <= n)  {
        i = i * 2;
    }
  • 线性阶:O(n)

  • 线性对数阶:O(nlogn),归并排序、快排的时间复杂度

  • 平方阶、立方阶、….、k次方阶:O(n2)、O(n3)、…、O(nk),循环里嵌套一层循环的复杂度,冒泡排序、插入排序等排序的复杂度O(n2)

非多项式量阶

  • 指数阶:O(2k),已经是很难接受的时间效率,如未优化的斐波拉契数列的求值

  • 阶乘阶:O(n!)

如果a的x次方等于N(a>0,且a不等于1),那么数x叫做以a为底N的对数,记作x=logaN。其中,a叫做对数的底数,N叫做真数

递归函数的时间复杂度

如果一个递归函数再每一次调用自身时,只是调用自己一次,那么它的时间复杂度就是这段递归调用栈的最大深度

使用递归对一段字符串进行翻转,因为每次调用都会截取字符串的最后一位,所以这段程序的递归调用次数就是递归的深度,也就是字符串自身的长度,也就是O(n),这也是递归最简单的调用,每一次只调用自身一次

function reversetStr (s) {
	if (s === '') {
    	return ''
    }
	return s[s.length - 1] + reversetStr(s.slice(0, -1))
}

接下来我们使用递归求解斐波拉契数列,也就是这样的一堆数,后面一个数等于前面两个之和

1 1 2 3 5 8 13 21 34 55 89 ...

function fib (n) {
	if (n === 1 || n === 2) {
    	return n
    }
    return fib(n - 1) + fib(n - 2)
}

这个递归函数在调用自身的时,又调用了两次自身,我们可以看到,当要计算7时,需要计算出6和5;当要计算6和5时,又要分别计算出5和4以及4和3;每次这颗递归树展开的叶子节点都是上一层的两倍,也就说这是一个指数级的算法,时间复杂度为O(2ⁿ)

其他几种复杂度分析

以上说的时间复杂度指的是一段程序的平均时间复杂度,其实还会有最坏时间复杂度,最好时间复杂度以及均摊时间复杂度

  • 最好时间复杂度:例如我们要从数据里找到一个数字,数组的第一项就符合要求,这个时候就表示数组取值最好的时间复杂度是O(1),当然了这种概率是极低的,所以并不能作为算法复杂度的指导值

  • 最坏时间复杂度:数组取值直到最后一个才找到符合要求的,那就是需要O(n)的复杂度;又例如快排的平均时间复杂度是O(nlogn),但一个没经过优化的快排去处理一个已经排好序的数组,会退化成O(n²)

  • 均摊时间复杂度:表示的是一段程序出现最坏和最好的频次不同,这个时候复杂度的分析就是将它们的操作进行均摊,取频次的多操作,然后得出最终的复杂度

空间复杂度

时间复杂度表示算法的执行时间与数据规模之间的增长关系。类比一下,空间复杂度表示算法的存储空间与数据规模之间的增长关系。例如

function test(arr) {
    const a = 1
    const b = 2
    let res = 0
    for (let i = 0; i < arr.length; i++) {
    	res += arr[i]
    }
    return res
}

我们定义了三个变量,空间复杂度是O(3),又是常数级别的,所以这段程序的空间复杂度又可以表示为O(1)。只用记住是另外开辟的额外空间,例如额外开辟了同等数组大小的空间,数组的长度可以表示为n,所以空间复杂度就是O(n),如果开辟的是二维数组的矩阵,那就是O(n²),因为空间度基本也就是以上几种情况,计算会相对容易

function fun(n) {
    let a = [];
    for (let i = 0; i < n; i++) {
        a.push(i);
    }
    return a;
}

以上代码我们可以清晰的看出代码执行的空间为 O(1+n) = O(n),即为 i 及数组 a 占用的储存空间。

参考链接

@chris-paul chris-paul added the dataStructure dataStructure label Apr 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dataStructure dataStructure
Projects
None yet
Development

No branches or pull requests

1 participant