为什么需要知道复杂度
所有数据结构与算法都是为了优化复杂度与稳定性存在的,对于同一个问题,使用不同的数据结构与算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。
一般我们从两个角度来评判一段算法的优秀程度:
- 时间:算法运行所需时间,通常被称为时间复杂度。
- 空间:算法运行所需额外空间,通常被称为空间复杂度。
因此,评价一个算法的优秀程度主要是看它的时间复杂度和空间复杂度情况。但有的时候时间和空间是鱼和熊掌不可兼得的,那么我们权衡业务需求然后从中去取一个平衡点。
复杂度计算方式
事后计算法
算法运行所需时间,大家第一反应是不是通过在程序的开头和结尾计时来计算出运行时间,这样行吗?肯定行啊,但它有很大的缺陷。
- 非常容易受运行环境的影响,在性能高的机器上跑出来的结果与在性能低的机器上跑的结果相差会很大。
- 不是所有的算法都是方便测试的代码,有些程序可能要跑好久才能得出答案,有些可能和其他的算法有耦合,那测出来的是当前的算法复杂度还是其他方法的复杂度呢?
所以我们就会想到通过预测来判断时间复杂度。
事前计算法
使用事前计算法的话,由于人无法判断一次操作的准确用时,只能评判大约运行了多少次,所以一般使用 “大O表示法”。
什么是大O表示法呢,让我们观察下面这段代码。
while(i<N) //N为正整数
{
i=i+1;
}
执行第三行代码的时间用Time(1)来表示,即1*1次操作时间(Time),而我们需要执行N次操作,所以用Time(N)来表示。
那有人就会问了,你咋没赋值呢?而且还有其他操作的耗时呢?那我就补充下代码,彻底模拟业务场景。
i=1;
N=10;
while(i<N) //为了方便讲解忽略判断用时
{
i=i+1;
}
执行第一行用了Time(1)的时间,第二行用了Time(1)的时间,第5行用了Time(N)。所以,这段算法的用时加起来,再用公式转化,就是大O表示法了。
这段代码用大O表示法表示就是 : O(N) = Time(1)+Time(1)+Time(N)
公式如下:
- 用常数1,来代表所有一次操作,无论这一次耗费时间。
- 只保留最高阶。
- 如果最高阶存在且不是1,就去除常数。比如Time(5),就去除其他的时间为O(1)
常见复杂度(升序依次排序):O(1) -> O(logN) -> O(N) -> O(NlogN) -> O(N^2) -> N(^3)……
N代表操作次数,logN则每次操作逐渐减少的次数。如下代码时间复杂度就是O(logN)
while(i<N) //N为正整数
{
i=i*2; //i为 2 -> 4 -> 8 ……
}
O(NlogN),将O(logN)的程序重复N次
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}
以及 O(N^2) 平方阶
for(j=1; i<=n; x++)
{
for(i=1; i<=n; i++)
{
i = i * 2;
}
}
最坏情况
从心理学角度讲,每个人对发生的事情都会有一个预期,比如看到半杯水,有人会说:哇哦,还有半杯水哦!但也 有人会说:天哪,只有半杯水了。一般人处于一种对未来失败的担忧,而在预期的时候趋向做最坏的打算,这样即 使最糟糕的结果出现,当事人也有了心理准备,比较容易接受结果。假如最糟糕的结果并没有出现,当事人会很快 乐。
而在程序中,最坏情况是一种保证,在应用中,这是一种最基本的保障,即使在最坏情况下,也能够正常提供服务,所以,除非特别指定,我们提到的运行时间都指的是最坏情况下的,如下排序算法:
public int search(int num) {
int[] arr = {11, 10, 8, 9, 7, 22, 23, 0};
for (int i = 0; i < arr.length; i++) {
if (num == arr[i]) {
return i;
}
}
return -1;
}
最好情况是第一个就是期望元素一次找到,最差情况就是N次才找到,平均情况是 N/2 ,一般我们取最差情况。
空间复杂度
空间复杂度则简单的多,如果算法执行所需要的临时空间不随着n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
int[] m = new int[n] //O(N)
int[] m = new int[n*n] //O(N^2)
排序稳定性
定义
数组arr中有若干元素,其中A元素和B元素相等,并且A元素在B元素前面,如果使用某种排序算法排序后,能够保证A元素依然在B元素的前面,可以说这个该算法是稳定的。
意义
如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。例如要排序的内容是一组商品对象,第一次排序按照价格由低到高排序,第二次排序按照销量由高到低排序,如果第二次排序使用稳定性算法,就可以使得相同销量的对象依旧保持着价格高低的顺序展现,只有销量不同的对象才需要重新排序。这样既可以保持第一次排序的原有意义,而且可以减少系统开销。
排序
归并排序,插入排序,冒泡排序是稳定的。
选择排序,希尔排序,快速排序是不稳定的。