算法与数据结构学习笔记
认识时间复杂度
什么是常数操作?
一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
我们常见的常数操作,如加减乘除和位(^)运算等,比如我们在写代码时,常常会写 int a = arr[i]
,因为这个操作和arr
的数据量没什么关系,只是把i
位置的元素取出,是一个固定的事件。
如果有一个链表,我们想要拿到它 i
位置的值,int b = list。get(i)
,虽然链表在逻辑概念是一个从左到右的线性结构,但在计算机底层实际结构中,我们不知道它下面的next
指向哪里,所以要找到 i
的位置,就必须通过遍历才能得到,即此操作不是常数操作。
时间复杂度
时间复杂度就是算法流程中,来估计常数操作数量的一个指标。常用 O(读作bigO) 来表示。
具体来说,先要对一个算法流程非常熟悉,然后去写出这个算法流程中,发生了多少常数操作,进而总结出常数操作数量的表达式。
如何确定算法流程的时间复杂度?
当确定了算法表达式,只保留高阶项,去除低阶项,高阶项的系数也去掉,剩下的部分如果为f(N),那么时间复杂度为O(N)
。例如一个选择排序,我们计算出它的常数操作数量的表达式为:a * n² + b * n +c
,时间复杂度我们计算为O(N²)
,评价一个算法流程的好坏,先看时间复杂度的指标
,当时间复杂度相同时,可以比较实际运行时间,也就是“常数项时间”。
额外空间复杂度:算法运行所耗费的储存空间。
排序算法
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 基于比较 |
---|---|---|---|---|
选择排序 | O(N²) | O(1) | × | 是 |
冒泡排序 | O(N²) | O(1) | √ | 是 |
插入排序 | O(N²) | O(1) | √ | 是 |
归并排序 | O(N*logN) | O(N) | √ | 是 |
快排 | O(N*logN) | O(logN) | × | 是 |
堆排 | O(N*logN) | O(1) | × | 是 |
计数排序 | O(N) | √ | 否 | |
基数排序 | O(N) | √ | 否 |
稳定性
:数组中值相同的元素,使用排序算法排序后,这些相同的元素相对次序位置不发生改变(桶排序思想),该排序算法就具备稳定性。- 一般情况下选择一种排序算法的话,选择快速排序 (虽然快排和堆排时间复杂度是一样,但是快排的常数时间是最快的),只有储存空间有限制的时候才选择堆排序。
- 基于比较的排序,没有一种算法可以将时间复杂度做到
O(N*logN)
以下* - 基于比较的排序,在时间复杂度为O(N*logN)情况下,空间复杂度在O(N)以下,没有一种算法可以做到稳定
常见的坑(非常难,不要求掌握,会破坏):
- 归并排序额外空间复杂度可以通过 “归并排序内部缓存法” 降为O(1),但会丢失其稳定性。
原地归并排序
可以将额外空间复杂度变为O(1),但是时间复杂度会变成O(N^2)。- 快速排序可以通过论文"01 stable sort"将稳定性变为稳定,但是于此同时,其额外空间复杂度会变成O(N)