概述
排序算法:是将一组数据,按照指定的顺序进行排列的过程。
排序的分类:
1、内部排序:将需要处理的所有数据加载到内存储存器中排序
2、外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。
常见的排序算法分类如下所示,外部排序先略过。
算法的时间复杂度
衡量一个程序(算法)执行时间的两种方法
1、事后统计法
这种方法可行,但是有两个问题,1)如果想要对设计的算法进行运行性能评测,需要实际运行程序。2) 运算的所得的时间的统计量依赖于计算机的硬件、软件等环境因素,这种方式需要在同等条件、同等状态下运行,才能比较算法的速度。
2、事前估算法:通过分析某个算法的时间复杂度来判断哪个算法更好。
时间频度
一个算法花费的时间与算法中语句的执行次数成正比,哪个算法语句执行次数越多,它消耗时间越多。一个算法中的语句执行次数称为语句频度或者时间频度,记为T(n)。
时间频度简要说明
@Test
// 计算 1 + 2 + 3 …… + 99 + 100
public void method1() {
// 使用for循环
// 时间复杂度 T(n) = n + 1
// n + 1 的原因是最后一次还需要进行判断
for (int i = 1; i <= end; i++) {
total += i;
}
System.out.println(total);
}
@Test
// 计算 1 + 2 + 3 …… + 99 + 100
public void method2() {
// 直接计算 时间复杂度 T(n) = 1
total = (end + 1) * end / 2;
System.out.println(total);
}
算法的时间复杂度可以忽略常数项。
2n+5 随着n逐渐增加,执行曲线无限接近
3n+10 随着n逐渐增加,执行曲线无限接近
同理:可以忽略低次项 、忽略系数。比如 T(n) = 2n2 与 T(n) = 2n2 +3n,可以忽略低次项。比如 T(n) = 3n2 与 T(n) = 2n2 当n越来越大,系数也可以忽略。
时间复杂度简要说明:
1、一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,使用T(n)表示。若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于0的常数,则称f(n)是T(n)的同数量级函数,记作T(n)= O(f(n)) ,称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
2、T(n)不同,但是时间复杂度可能相同,比如 T(n) = n2+7n+6与T(n) = 2n2+3n+1,他们的T(n)不同,但是时间复杂度相同,都是O(n2)
3、计算时间复杂的方法
3.1、使用常数1代替运行时间中的所有加法常数。
3.2、修改后得到运行次数函数中,只保留最高阶项。
3.3、去除高阶项的系数。
常见的时间复杂度
1、常数阶O(1)
2、对数阶O(
log
2
n
\log_2 n
log2n)
3、线性阶O(n)
4、常数对数阶O(n
log
2
n
\log_2 n
log2n)
5、平方阶O(n2)
6、立方阶O(n3)
7、k次方阶O(nk)
8、指数阶O(2n)
常见的算法时间复杂度,由小到大依次为:O(1) < O(
log
2
n
\log_2 n
log2n) < O(n) < O(n
log
2
n
\log_2 n
log2n) < O(n2) < O(n3) < O(nk) < O(2n),随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低,应该尽可能避免使用指数阶的算法。简要示意图如下:
常见的时间复杂度举例说明:
常数阶O(1)
// 常数阶 无论代码执行了多少行,只要没有循环等复杂结构,那么这个代码的时间复杂度都是O(1)
public void m1() {
// 这段代码在执行的事后,它的消耗并不随着某个变量的增长而增长,那么无论这类代码有多长
// 都可以用O(1)来表示它的时间复杂度
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
System.out.println(m);
}
2、对数阶O( log 2 n \log_2 n log2n)
// 对数阶说明
public void m2(int n) {
int i = 1;
while (i < n) {
// 每次都将 i * 2 ,i 与 n 的距离越来越小
i = i * 2;
}
}
对数阶简要说明:
在while循环中,每次都将i乘以2,i的值与n越来越接近。假设循环k次,i就大于n了,此时这个循环就退出了,也就是说2k >= n,那么 k =
log
2
n
\log_2 n
log2n,当循环
log
2
n
\log_2 n
log2n次以后,代码就结束向下执行。所以这段代码的时间复杂度为O(
log
2
n
\log_2 n
log2n)。假设,代码修改为i = i * 3;
此时的时间复杂度为O(
log
3
n
\log_3 n
log3n)。
如果 N = ax (a > 0,a
≠
\neq
= 1),即a的x次方等于N(a > 0,a
≠
\neq
= 1),那么数x就是以a为底N的对数,记作 x =
log
a
N
\log_a N
logaN,a是对数的底数,N是真数,x是“以a为底N的对数”。
线性阶
// 线性阶说明
public void m3(int n) {
// 这段代码for循环会执行n遍,它消耗的时间随着n的变化而变化,
// 这种类型的代码的时间复杂度,是O(n)
for (int i = 0; i < n; i++) {
//do something
}
}
线性对数阶
// 线性对数阶说明
// 线性对数阶其实就是将对数阶代码循环了n遍
public void m4(int m, int n) {
for (int i = 0; i < m; i++) { // 线性阶
int j = 1;
while (j < n) { // 对数阶
j = j * 2;
}
}
}
平方阶
// 平方阶
public void m5(int n) {
// 可以使用双层for循环来理解平方阶
for (int i = 0; i < n; i++) { // 线性阶
for (int j = 0; j < n; j++) {
//do something
}
}
}
平均时间复杂度和最坏时间复杂度
1、平均时间复杂度是指所有可能的输入实例以等概率出现的情况下,该算法的运行时间。
2、最坏情况下的时间复杂度是最坏时间复杂度,一般讨论的时间复杂度是最坏情况下的时间复杂度,这样做的原因是:最坏情况下的时间复杂度,是算法在任何实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
3、排序算法的时间复杂度如下图所示。
排序算法 | 平均时间 | 最坏情况 | 稳定度 | 额外空间 | 备注 |
---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n2) | 稳定 | O(1) | n小时较好 |
交换排序 | O(n2) | O(n2) | 不稳定 | O(1) | n小时较好 |
选择排序 | O(n2) | O(n2) | 不稳定 | O(1) | n小时较好 |
插入排序 | O(n2) | O(n2) | 稳定 | O(1) | 大部分已经排序好时较好 |
基数排序 | O( log R B \log_R B logRB) | O( log R B \log_R B logRB) | 稳定 | O(1) | B是真数,R是基数 |
希尔排序 | O(nlogn) | O(ns) 1<s<2 | 不稳定 | O(1) | S是所选分组 |
快速排序 | O(nlogn) | O(n2) | 不稳定 | Ologn | n大时较好 |
归并排序 | O(nlogn) | O(nlogn) | 稳定 | O(n) | n大时较好 |
堆排序 | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大时较好 |