在学习数据结构和算法的时候,经常会碰到O(1),O(n)等等用来表示时间和空间复杂度,那这到底是什么意思。我们对于同一个问题经常有不同的解决方式,比如排序算法就有十种经典排序(快排,归并排序等),虽然对于排序的结果相同,但是在排序过程中消耗时间和资源却是不同。
对于不同排序算法之间的衡量方式就是通过程序执行所占用的
时间
和
空间
两个维度去考量。
设
A
、
B
是非空的数集,如果按照某个确定的对应关系
f
,使对于集合
A
中的任意一个数
x
,在集合
B
中都有唯一确定的数
f
(
x
)和它对应,那么就称
f
:
A
→
B
为从集合
A
到集合
B
的一个函数。记作:
y
=
f
(
x
),
x
∈
A
。其中,
x
叫做自变量,
x
的取值范围
A
叫做函数的定义域;与
x
的值相对应的
y
值叫做函数值,函数值的集合{
f
(
x
)|
x
∈
A
}叫做函数的值域。
例:已知
f
(
x
)的定义域为[3,5],求*f(2x-1)*的定义域。
函数
y
=
a
x
(
a
>
0
且
a
=
1
)
叫做指数函数,自变量叫做指数,
a
叫做底数。
如果
a
(
a
>
0
,
a
=
1
)
的
b
次幂等于
N
,
即
a
b
=
N
,
那么
b
叫做以
a
为底
N
的对数,记作
l
o
g
a
N
=
b
,
其中
a
叫做对数的底数,
N
叫做真数
时间复杂度
若存在函数 f(n),使得当n趋近于无穷大时,T(n)/ f(n))的极限值为不等于零的常数,则称 f(n)是T(n)的同数量级函数。记作 T(n)= O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
简单理解就是一个算法或是一个程序在运行时,所消耗的时间(或者代码被执行的总次数)。
在下面的程序中:
int sum(int n) {
① int value = 0;
② int i = 1;
③ while (i <= n) {
④ value = value + i;
⑤ i++;
⑥ return value;
假设n=100,该方法的执行次数为①(1次)、②(1次)、③(100次)、④(100次)、⑤(100次)、⑥(1次)
合计1+1+100+100+100+1 = 303次
上面的结果如果用函数来表示为:f(n) = 3n+3,那么在计算机算法中的表示方法如下。
大O表示法:算法的时间复杂度通常用大O来表示,定义为T(n) = O(f(n)),其中T表示时间。
即:T(n) = O(3n+3)
这里有个重要的点就是时间复杂度关心的是数量级,其原则是:
省略常数,如果运行时间是常数量级,用常数1表示
保留最高阶的项
变最高阶项的系数为1
如 2n 3 + 3n2 + 7,省略常数变为 O(2n 3 + 3n2),保留最高阶的项为 O(2n 3 ),变最高阶项的系数为1后变为O(n 3 ),即O(n 3 )为 2n 3 + 3n2 + 7的时间复杂度。
同理,在上面的程序中 T(n) = O(3n+3),其时间复杂度为O(n)。
注:只看最高复杂度的运算,也就是上面程序中的内层循环。
时间复杂度的阶
时间复杂度的阶主要分为以下几种
常数阶O(1)
int n = 100
System.out.println("常数阶:" + n)
不管n等于多少,程序始终只会执行一次,即 T(n) = O(1)
对数阶O(logn)
// n = 32 则 i=1,2,4,8,16,32
for (int i = 1
System.out.println("对数阶:" + n)
i 的值随着 n 成对数增长,读作2为底n的对数,即f(x) = log2n,T(n) = O( log2n),简写为O(logn)
则对数底数大于1的象限通用表示为:
线性阶O(n)
for (int i = 1
System.out.println("线性阶:" + n)
n的值为多少,程序就运行多少次,类似函数 y = f(x),即 T(n) = O(n)
线性对数阶O(nlogn)
for (int m = 1
int i = 1
while (i < n) {
i = i * 2
System.out.println("线性对数阶:" + i)
线性对数阶O(nlogn)其实非常容易理解,将对数阶O(logn)的代码循环n遍的话,那么它的时间复杂度就是 n * O(logn),也就是了O(nlogn),归并排序的复杂度就是O(nlogn)。
若n = 2 则程序执行2次,若n=4,则程序执行8次,依次类推
平方阶O(n2)
for (int i = 1
for (int j = 1
System.out.println("平方阶:" + n)
若 n = 2,则打印4次,若 n = 3,则打印9,即T(n) = O(n2)
同理,立方阶就为O(n3),如果3改为k,那就是k次方阶O(nk),相对而言就更复杂了。
以上5种时间复杂度关系为:
从上图可以得出结论,当x轴n的值越来越大时,y轴耗时的时长为:
O(1) < O(logn) < O(n) < O(nlogn) < O(n2)
在编程算法中远远不止上面4种,比如O(n3),O(2n),O(n!),O(nk)等等。
这些是怎么在数学的角度去证明的,感兴趣的可以去看看主定理。
注:以下数据来自于Big-O Cheat Sheet,常用的大O标记法列表以及它们与不同大小输入数据的性能比较。
大O标记法 | 计算10个元素 | 计算100个元素 | 计算1000个元素 |
---|
O(1) | 1 | 1 | 1 |
O(logN) | 3 | 6 | 9 |
O(N) | 10 | 100 | 1000 |
O(NlogN) | 30 | 600 | 9000 |
O(N2) | 100 | 10000 | 1000000 |
O(2N) | 1024 | 1.26e+29 | 1.07e+301 |
O(N!) | 3628800 | 9.3e+157 | 4.02e+2567 |
常见数据结构操作的复杂度
数据结构 | 连接 | 查找 | 插入 | 删除 |
---|
数组 | 1 | n | n | n |
栈 | n | n | 1 | 1 |
队列 | n | n | 1 | 1 |
链表 | n | n | 1 | 1 |
哈希表 | - | n | n | n |
二分查找树 | n | n | n | n |
B树 | log(n) | log(n) | log(n) | log(n) |
红黑树 | log(n) | log(n) | log(n) | log(n) |
AVL树 | log(n) | log(n) | log(n) | log(n) |
数组排序算法的复杂度
名称 | 最优 | 平均 | 最坏 | 内存 | 稳定 |
---|
冒泡排序 | n | n2 | n2 | 1 | 是 |
插入排序 | n | n2 | n2 | 1 | 是 |
选择排序 | n2 | n2 | n2 | 1 | 否 |
堆排序 | n log(n) | n log(n) | n log(n) | 1 | 否 |
归并排序 | n log(n) | n log(n) | n log(n) | n | 是 |
快速排序 | n log(n) | n log(n) | n2 | log(n) | 否 |
希尔排序 | n log(n) | 取决于差距序列 | n (log(n))2 | 1 | 否 |
空间复杂度
空间复杂度表示的是算法的存储空间和数据之间的关系,即一个算法在运行时,所消耗的空间。
空间复杂度的阶
空间复杂度相对于时间复杂度要简单很多,我们只需要掌握常见的O(1),O(n),O(n2)。
常数阶O(1)
int i;
线性阶O(n)
int[] arr
平方阶O(n2)
int[][] arr
写这篇文章的目的在于,我在更新栈和队列的时候,留下了一个问题栈的入栈和出栈操作与队列的插入和移除的时间复杂度是否相同,确实是相同的 ,都用了O(1)。由此我想到,对于刚开始或者说是不太了解复杂度的同学,碰到此类的问题,或者是我在跟后续数据结构和算法文章的时候不懂,十分的不友好,于是就写了一篇关于复杂 度的文章。
本文只对时间复杂度和空间复杂度做了简单介绍,有错误可以指正,不要硬杠,杠就是我输。
一套图搞懂时间复杂度
算法的时间复杂度和空间复杂度
都是自己人,点赞关注我就收下了🤞