浅谈算法时间复杂度

471 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

时间复杂度

什么是算法的时间复杂度呢?简单的说,就是算法执行在时间消耗上的度量。当问题规模即要处理的数据增长时,基本操作要重复执行的次数必定也会增长,那么我们关心地是这个执行次数以什么样的数量级增长。

算法时间复杂度是一个函数,它定性描述该算法的运行时间(也就是所花费时间的消耗)。我们要知道讨论的时间复杂度就是指一般情况下的时间复杂度。

表示法

我们用大O表示法表示以下常见的时间复杂度量级:

常数阶O(1), 线性阶O(n) ,对数阶O(logn) ,线性对数阶O(nlogn), 平⽅阶O(n²)。

排序算法中各个算法时间复杂度为:O(n2), O(nlogn), O(n)。 这些我们之前在大学的时候都学习过,可以先从排序算法来理解时间复杂度。这里所说的都是指的一般情况下的时间复杂度(或者平均时间复杂度)。

当然还有指数阶和阶乘阶这种非常极端的复杂度量级,我们就不讨论了,本篇文章只是介绍一些常见且简单的复杂度。

O(1)

传说中的常数阶的复杂度,这种复杂度无论数据规模n如何增长,计算时间是不变的。不管n如何增长,都不会影响到这个函数的计算时间,因此这个代码的时间复杂度都是O(1)。

O(n)

线性复杂度,随着数据规模n的增长,计算时间也会随着n线性增长。典型的O(n)的例⼦就是线性查找。

const linearSearch = (arr, target) => {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) {
      return i;
    }
  }
  return -1;
}

线性查找的时间消化与输⼊的数组数量n成⼀个线性比例,随着n规模的增⼤,时间也会线性增长。

注意: 下面代码的时间复杂度也是O(n)。

O(n + n) = O(2n) = O(n)。

const search = (arr) => {
  for (let i = 0; i < arr.length; i++) {
    ...
  }
  
  for (let i = 0; i < arr.length; i++) {
    ...
  }
    
  return -1;
}

O(logn)

对数复杂度,随着问题规模n的增⻓,计算时间也会随着n对数级增⻓。

典型的例⼦是⼆分查找法(有序序列)。

functions binarySearch(arr, target) {
    let max = arr.length - 1let min = 0while (min <= max) {
        let mid = Math.floor((max + min) / 2);
        if (target < arr[mid]) {
        		max = mid - 1;
        } else if (target > arr[mid]) {
        		min = mid + 1;
        } else {
        	return mid;
    		}
    }

    return -1
}

在⼆分查找法的代码中,通过while循环,成 2 倍数的缩减搜索范围,也就是说需要经过 log2n 次即可跳出循环。

事实上在实际项目中, O(logn) 是⼀个非常好的时间复杂度,比如当 n=100 的数据规模时,⼆分查找只需要7次,线性查找需要100次,这对于计算机而言差距不大,但是当有10亿的数据规模的时候,二分查找依然只需要30次,而线性查找需要惊⼈的10亿次, O(logn) 时间复杂度的算法随着数据规模的增大,它的优势就越明显。

再比如,我们都知道,堆的应用场景是适合维护集合的最值,如果我们想获得从一个集合里获取它的最值,最简单的办法就是遍历一遍,这种算法的时间复杂度是O(n),好像也不大,还可以接受,但是如果集合里元素很多时候,就可以看到把剩余集合里元素在求最值,这样的算法时间复杂度就是O(n2)了。但是如果用堆的话,求一次最值的时间复杂度是O(logn),比n小多了,这个从第一张图上是可以看出来的。

O(nlogn)

线性对数复杂度,随着数据规模n的增长,计算时间也会随着n呈线性对数级增⻓。

这其中典型代表就是归并排序,快速排序,堆排序,希尔排序。

以堆排序为例:

堆排序中有两个步骤:

  1. 创建堆:
  2. 调整堆

O(n²)

平方级复杂度,典型情况是当存在双重循环的时候,即把 O(n) 的代码再嵌套循环⼀遍,它的时间复杂度就是 O(n²)了,代表应用是冒泡排序,插入排序,简单选择排序。

注意: 要理解时间复杂度本质,不能单纯的记忆两个for循环的时间复杂度是O(n2),三个for循环的时间复杂度是O(n3),还要看for循环的数量级。例如希尔排序,是三个for循环,但是它的平均时间复杂度是O(nlogn)。

关于logn思考

logn指的是n的对数,在数学中,n的对数必须有底数,也就是像log2n,log3n...。

那时间复杂度中的logn是以几为底数呢?

有人说logn的底数为2,这种理解是不对的,说明还没有理解时间复杂度所要表达的本质是什么。看了一些文档,但是并没有看到一篇好的文章去说明,或者探讨清楚这个问题。实际上无论以2,或3为底数,其实这些都是常量,并不影响我们用logn来表述算法的时间复杂度,就像两个n数量级的for循环一样,并不是O(2n),而是O(n)。所以,我们统一说是O(logn),也就是忽略了底数的描述。

推导一下:(以2,和10为例)

  1. n = 10 log10n
  2. log2n = log10n * log210
  3. log2n => log10n

从上面可以知道,其实底数无论以2,或3为底数,其实这些都是常量,并不影响我们用logn来表述算法的时间复杂度。至此,我相信大家对算法的时间复杂度有了一定的了解。