算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。
那么我们应该如何去衡量不同算法之间的优劣呢?
衡量代码好坏有两个非常重要的标准:
- 运行时间 (时间复杂度),是指执行当前算法所消耗的时间变化趋势;
- 占用内存 (空间复杂度),是指执行当前算法需要占用的内存空间的变化趋势;
因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。然而,有的时候时间和空间却又是「鱼和熊掌」,不可兼得的,那么我们就需要从中去取一个平衡点。
同时,掌握好这两个标准,也是学好算法的一个重要基石。
1. 时间复杂度
所占用的确切内存或运行的确切时间是算不出来的,而且同一段代码在不同性能的机器上执行的时间也不一样,可是代码的基本执行次数,我们是可以算得出来的,这就是我们要说到时间复杂度。
如何得到一个算法的 执行耗时呢?大家可能想到 将它运行一遍,那么它所消耗的时间就自然而然知道了。这种方式当然可以,但是这种方式存在一定的弊端:
- 运行环境影响,性能不同的机器上跑出来的想过会有一定差距;
- 我们在写算法的时候还没有办法完成的去运行呢
因此一种通用的统计方法就诞生了: 「 大O符号表示法 」,即 T(n) = O(f(n)),其中 f(n) 表示每行代码执行次数之和,而 O 表示正比例关系,这个公式的全称是:算法的渐进时间复杂度。
举个例子 1:
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
通过 「 大O符号表示法 」得出,上面这段代码的时间复杂度为:O(n)。接下来我们具体分析一下:假设每行代码的执行时间都是一样的,我们用 1颗粒时间 来表示,那么这个例子的第一行耗时是1个颗粒时间,第三行的执行时间是 n个颗粒时间,第四行的执行时间也是 n个颗粒时间(第二行和第五行是符号,暂时忽略),那么总时间就是 1颗粒时间 + n颗粒时间 + n颗粒时间 ,即 (1+2n)个颗粒时间,即: T(n) = (1+2n)颗粒时间,从这个结果可以看出,这个算法的耗时是随着n的变化而变化,因此,我们可以简化的将这个算法的时间复杂度表示为:T(n) = O(n)。
上面提到简化,那么为什么要将 T(n) = (1+2n) 简化为 T(n) = O(n)呢?因为「大O符号表示法 」并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的。所以如果n 无限大的时候, T(n) = (1+2n) 中的常量 1 就没有了意义,并且背倍数 2 的意义也就不大了。因此就直接简化成了 T(n) = O(n)。
常用的时间复杂度量级如下表中所示:
复杂度 | 名称 |
---|---|
O(1) | 常数阶 |
O(logn) | 对数阶 |
O(n) | 线性阶 |
O(nlogn) | 线性对数阶 |
O(n²) | 平方阶 |
O(n³) | 立方阶 |
O(nk) | K次方阶 |
O(2 n ) | 指数阶,一点数据量就卡的不行 |
O(n!) | 阶乘,就更慢了 |
上面表中从上至下,依次的时间复杂度越来越大,即执行的效率越来越低。
接下来我们选取一些较为常用的时间复杂度量级进行讲解:
1.1 常数阶O(1)
function foo(){
let n = 1
let b = n * 100
if(b === 100){
console.log("开始吃糖")
}
console.log("我吃了1颗糖")
console.log("我吃了2颗糖")
......
console.log("我吃了10000颗糖")
}
一般情况下,只要算法里没有循环和递归等复杂结构,就算有上万行代码,时间复杂度也都是 O(1)
,因为它的执行次数不会随着任何一个变量的增长而增长。
1.2 对数阶 O(logn)
// 16不断除以2,除几次之后等于1?
function foo1(n){
let day = 0
while(n > 1){
n = n/2
day++
}
return day
}
console.log( foo1(16) ) // 4
上面的例子中,循环次数的影响因素,主要来源于 n/2,这个时间复杂度就 O(logn)。
// 入参是16,在 i=i*2 的步阶条件下,循环几次?
function foo2(n){
for(let i = 0; i < n; i *= 2){
console.log("一天")
}
}
foo2( 16 )
上面的例子会打印4次,循环次数的主要因素来源于 i *= 2,这个时间复杂度也是 O(logn)。
进一步思考,这个 O(logn) 是怎么来的?我们先来看一下下面的这张图:
真数:就是入参的值,也就是上面的例子中的 16。
底数:就是步阶的变化规律,比如每次循环都是以 i*=2 为步阶,这个乘以 2 就是规律。比如1,2,3,4,5...... 这样的值的话,底就是 1,每个数变化的规律就是 +1 。
对数:可以理解成 上面的例子中 *2 的次数。
对数的表达公式:
把公式转换一下就是:
用时间复杂度表示就是 O(log2n),由于时间复杂度需要去掉常数和系数,而log的底数跟系数是一样的,所以也需要去掉,所以最后这个正确的时间复杂度就是 O(logn)
1.3 线性阶 O(n)
function foo1(n){
for( let i = 0; i < n; i++){
console.log("我吃了一颗糖")
}
}
function foo2(n){
while( --n > 0){
console.log("我吃了一颗糖")
}
}
function foo3(n){
console.log("我吃了一颗糖")
--n > 0 && foo3(n)
}
总的来说,只有一层循环或者递归的算法,时间复杂度就是 O(n)。
1.3 线性对数阶
O(nlogN)
for(m=1; m<n; m++){
i = 1;
while(i<n){
i = i * 2;
}
}
线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。
1.4 平方阶 O(n²)
function foo1(n){
for( let i = 0; i < n; i++){
for( let j = 0; j < n; j++){
console.log("生活虐我千百遍")
}
}
}
平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²) 了。比如上面的例子。
// 总执行次数为 n + n²,属于多项式的形式。
function foo2(n){
for( let k = 0; k < n; k++){
console.log("我吃了一颗糖")
}
for( let i = 0; i < n; i++){
for( let j = 0; j < n; j++){
console.log("我吃了一颗糖")
}
}
}
如果
「 大O符号表示法 」是一个多项式,比如上面的例子为 T(n) = O(n + n²),就取最高次项,所以这个时间复杂度也是 O(n²)。
function foo3(n){
if( n > 100){
for( let k = 0; k < n; k++){
console.log("我吃了一颗糖")
}
}else{
for( let i = 0; i < n; i++){
for( let j = 0; j < n; j++){
console.log("我吃了一颗糖")
}
}
}
}
如果向上面这样的算法,我们就以运行时间最长的作为时间复杂度的依据,所以下面的时间复杂度就是 O(n²)
2.空间复杂度
既然时间复杂度不是用来计算程序具体耗时的,那么我也应该明白,空间复杂度也不是用来计算程序实际占用的空间的。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个度量,同样反映的是一个趋势,我们用 S(n) 来定义。
空间复杂度比较常用的有:
- O(1)
- O(n)
- O(n²)
2.1 空间复杂度 O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 S(n) = O(1):
function foo(){
let n = 1
let b = n * 100
if(b === 100){
console.log("开始吃糖")
}
console.log("我吃了1颗糖")
console.log("我吃了2颗糖")
......
console.log("我吃了10000颗糖")
}
2.2 空间复杂度 O(n) --- (1)
下面的一段代码中,第一行new了一个数组出来,这个数据占用的内存空间大小为n,这段代码的2-5行,虽然有循环,但没有再分配新的空间。因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)。
int[] m = new int[n]
for(i=1; i<=n; ++i){
j = i;
j++;
}
2.2 空间复杂度 O(n) --- (2)
比如下面这样,n 的数值越大,算法需要分配的空间需要的就越多,用来存储数组里的值,所以它的空间复杂度就是 O(n)
,时间复杂度也是 O(n)。
function foo(n){
let arr = []
for( let i = 1; i < n; i++ ) {
arr[i] = i
}
}
以上,就是对算法的时间复杂度与空间复杂度基础的分析,结束!