持续创作,加速成长!这是我参与「掘金日新计划 · 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 - 1;
let min = 0;
while (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呈线性对数级增⻓。
这其中典型代表就是归并排序,快速排序,堆排序,希尔排序。
以堆排序为例:
堆排序中有两个步骤:
- 创建堆:
- 调整堆
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为例)
- n = 10 log10n
- log2n = log10n * log210
- log2n => log10n
从上面可以知道,其实底数无论以2,或3为底数,其实这些都是常量,并不影响我们用logn来表述算法的时间复杂度。至此,我相信大家对算法的时间复杂度有了一定的了解。