参考:https://blog.csdn.net/oneby1314/category_10231585.html
1、排序算法介绍
1.1、排序算法的简介
- 排序也称排序算法(Sort Algorithm), 排序是将一组数据, 依指定的顺序进行排列的过程。
1.2、排序算法的分类
-
内部排序:指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。
-
外部排序法:数据量过大, 无法全部加载到内存中, 需要借助外部存储(文件等)进行排序。
-
常见的排序算法分类
2、算法的复杂度
2.1、时间复杂度的度量方法
- 事后统计的方法:这种方法可行, 但是有两个问题:
- 一是要想对设计的算法的运行性能进行评测, 需要实际运行该程序;
- 二是所得时间的统计量依赖于计算机的硬件、 软件等环境因素, 这种方式, 要在同一台计算机的相同状态下运行, 才能比较哪个算法速度更快。
- 事前估算的方法:通过分析某个算法的时间复杂度来判断哪个算法更优
2.2、时间频度
-
基本介绍时间频度: 一个算法花费的时间与算法中语句的执行次数成正比例, 哪个算法中语句执行次数多, 它花费时间就多。 一个算法中的语句执行次数称为语句频度或时间频度。 记为 T(n)。 [举例说明]
-
举例说明-基本案例:比如计算 1-100 所有数字之和,我们设计两种算法:
举例说明-忽略常数项:
-
2n+20 和 2n 随着 n 变大, 执行曲线无限接近, 20 可以忽略
-
3n+10 和 3n 随着 n 变大, 执行曲线无限接近, 10 可以忽略
举例说明-忽略低次项:
-
2n^2+3n+10 和 2n^2 随着 n 变大, 执行曲线无限接近, 可以忽略 3n+10
-
n^2+5n+20 和 n^2 随着 n 变大,执行曲线无限接近, 可以忽略 5n+20
2.3、时间复杂度
- 一般情况下, 算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数, 用 T(n)表示, 若有某个辅助函数 f(n), 使得当 n 趋近于无穷大时, T(n) / f(n) 的极限值为不等于零的常数, 则称 f(n)是 T(n)的同数量级函数。记作 T(n)=O ( f(n) ), 称O ( f(n) ) 为算法的渐进时间复杂度, 简称时间复杂度。
- T(n) 不同, 但时间复杂度可能相同。 如: T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的 T(n) 不同, 但时间复杂度相同, 都为 O(n²)。
- 计算时间复杂度的方法:
- 用常数 1 代替运行时间中的所有加法常数 T(n)=n²+7n+6 => T(n)=n²+7n+1
- 修改后的运行次数函数中, 只保留最高阶项 T(n)=n²+7n+1 => T(n) = n²
- 去除最高阶项的系数 T(n) = n² => T(n) = n² => O(n²)
2.4、常见的时间复杂度
2.4.1、常见时间复杂度概述
-
常见时间复杂度
- 常数阶 O(1)
- 对数阶 O(log2n)
- 线性阶 O(n)
- 线性对数阶 O(nlog2n)
- 平方阶 O(n^2)
- 立方阶 O(n^3)
- k 次方阶 O(n^k)
- 指数阶 O(2^n)
-
结论:
-
常见的算法时间复杂度由小到大依次为: Ο (1)<Ο (log2n)<Ο (n)<Ο (nlog2n)<Ο (n2)<Ο (n3)< Ο (nk) < Ο (2n) , 随着问题规模 n 的不断增大, 上述时间复杂度不断增大, 算法的执行效率越低
-
从图中可见, 我们应该尽可能避免使用指数阶的算法
-
2.4.2、常数阶 O(1)
-
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)
-
代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
2.4.3、对数阶 O(log2n)
2.4.4、线性阶 O(n)
- 说明:这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度
2.4.5、线性对数阶 O(nlogN)
- 说明:线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)
2.4.6、平方阶 O(n²)
- 说明:平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(nn),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(mn)
2.4.7、其他阶
- 立方阶 O(n³)、 K 次方阶 O(n^k)
- 说明: 参考上面的 O(n²) 去理解就好了, O(n³)相当于三层 n 循环, 其它的类似
2.5、平均和最坏时间复杂度
-
平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下, 该算法的运行时间。
-
最坏情况下的时间复杂度称最坏时间复杂度。 一般讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是: 最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限, 这就保证了算法的运行时间不会比最坏情况更长。
-
平均时间复杂度和最坏时间复杂度是否一致, 和算法有关(如图)。
2.6、算法的空间复杂度
- 类似于时间复杂度的讨论, 一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间, 它也是问题规模 n 的函数。
- 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。 有的算法需要占用的临时工作单元数与解决问题的规模 n 有关, 它随着 n 的增大而增大, 当 n 较大时, 将占用较多的存储单元, 例如快速排序和归并排序算法, 基数排序就属于这种情况
- 在做算法分析时, 主要讨论的是时间复杂度。 从用户使用体验上看, 更看重的程序执行的速度。 一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间
3、冒泡排序
3.1、基本介绍
- 冒泡排序(Bubble Sorting) 的基本思想是: 通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值, 若发现逆序则交换, 使值较大的元素逐渐从前移向后部, 就象水底下的气泡一样逐渐向上冒。
- 优化:因为排序的过程中, 各元素不断接近自己的位置, 如果一趟比较下来没有进行过交换, 就说明序列有序, 因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。 从而减少不必要的比较。冒泡排序的优化
3.2、冒泡排序图解
- 一共比较n-1趟
- 每趟比较 n - i 次
3.3、代码实现
// 老师上课时候的版本
public static void bubbleSort(int[] arr) {
int temp = 0;
// 循环n-1趟
for (int i = 0; i < arr.length - 1; i++) {
// 优化冒泡排序算法
// 若在一趟排序中没有发生元素位置发生变化则提前结束循环
boolean flag = false; // 表示变量,表示是否进行过交换
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j+1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
//System.out.println("第" + (i+1) + "趟排序后的数组");
//System.out.println(Arrays.toString(arr));
if (!flag) {
break;
}
}
}
/**
* @Description: 我自己写的冒泡排序,i从1开始表示第一趟
* @Param: [arr]
* @Return: void
* @Author: Daniel
* @Date: 2020/11/20
*/
public static void bubbleSort_mine(int[] arr) {
int len = arr.length;
// i 代表循环趟数,共循环n-1趟
for (int i = 1; i < len; i++) {
for (int j = 0; j < len - i; j++) {
// 这里写的有冗余问题,因为这样写会新建 (n-1)*(n+1)/2 个temp对象
// 完全没有必要!直接把temp放在外面即可
// temp只是一个临时变量而已
int temp;
if (arr[j] > arr[j+1]) {
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
-
测试用例
使用Math.random( )生成80000个随机数,进行冒泡排序;
public static void main(String[] args) { int[] arr = new int[80000]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) (Math.random()*8000000); // 生成一个[0,800000)的数 } Date date1 = new Date(); SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String date1Str = sf.format(date1); System.out.println("排序前的时间是 = " + date1Str); bubbleSort(arr); Date date2 = new Date(); String date2Str = sf.format(date2); System.out.println("排序后的时间是= " + date2Str); }
-
试验结果
排序前的时间是 = 2020-11-20 10:18:11 排序后的时间是= 2020-11-20 10:18:21
4、选择排序
4.1、选择排序基本介绍
- 选择排序也属于内部排序法,是从欲排序的数组中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
4.2、选择排序思想
对n个数进行选择排序:
外循环n-1趟:
每一趟找到剩下的未排序的最小值,记录其下标,然后将 arr[i] 与 该最小值互换位置。
- 选择排序(select sorting) 也是一种简单的排序方法。 它的基本思想是(n 是数组大小):
- 第一次从 arr[0]~arr[n-1]选取最小值,与 arr[0] 交换
- 第二次从 arr[1]~arr[n-1]中选取最小值, 与 arr[1] 交换
- 第三次从 arr[2]~arr[n-1]中选取最小值, 与 arr[2] 交换, …,
- 第 i 次从 arr[i-1]~arr[n-1]中选取最小值, 与 arr[i-1] 交换, …,
- 第 n-1 次从 arr[n-2]~arr[n-1]中选取最小值,与 arr[n-2] 交换,
- 总共通过 n-1 次, 得到一个按排序码从小到大排列的有序序列。
4.3、选择排序图解
- 选择排序一共有数组大小减1轮排序
- 每1轮排序,又是一个循环,循环的规则
- 先假定当前这个数是最小的
- 然后和后面的每个数进行比较,如果发现有比当前更小的数,就重新确定最小数,得到下标
- 当遍历到数组最后时,就得到本轮最小数和下标
- 将第i个数和选出来的最小数互换位置
4.4、代码实现
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
int min = arr[i];
for (int j = i + 1; j < arr.length; j ++) {
if (min > arr[j]) { // 说明假定的最小值,并不是最小
min = arr[j]; //重置min
minIndex = j;
}
}
// 将最小值,放在arr[i],即交换
if (minIndex != i) {
arr[minIndex] = arr[i];
arr[i] = min;
}
}
}
4.5、总结
- 由于选择排序算法在最内层的 for 循环中,满足
if (min > arr[j]) {
条件后,只需要记录最小值和最小值在数组中的索引,无需像冒泡排序那样每次都要执行交换操作,所以选择排序算法的执行速度比冒泡排序算法快一些
5、插入排序
5.1、插入排序基本介绍
- 插入式排序属于内部排序法, 是对于欲排序的元素以插入的方式找寻该元素的适当位置, 以达到排序的目的。
5.2、插入排序思想
- 插入排序(Insertion Sorting)的基本思想是:把n个待排序的元素看成
一个有序表
和一个无序表
- 开始时,有序表中只含有一个元素,无序表中含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表
5.3、插入排序图解
-
插入排序逻辑:
- 首先,将数组分为两个数组,前部分有序数组,后部分是无序数组,我们的目的就是一点一点取出无序数组中的值,将其放到有序数组中区
- 第一趟:arr[0] 作为有序数组的元素,arr[1] 作为无序数组中第一个元素,将 arr[1] 与 arr[0] 比较,目标是将 arr[1] 插入到有序数组中
- 第一趟:arr[0] 和 arr[1] 作为有序数组的元素,arr[2] 作为无序数组中第一个元素,将 arr[2] 与 arr[0] 和 arr[1] 比较,目标是将 arr[2] 插入到有序数组中
- 第 i 趟:arr[0]~arr[i] 作为有序数组的元素,arr[i+1] 作为无序数组中第一个元素,将 arr[i+1] 与 arr[0]~arr[i] 比较,目标是将 arr[i+1] 插入到有序数组中
- 第 n-1 趟:此时有序数组为 arr[0]~arr[n-2] ,无序数组为 arr[n-1] ,将无序数组中最后一个元素插入到有序数组中即可
- 如何进行插入?
- 假设有个指针(index),指向无序数组中的第一个元素,即 arr[index] 是无序数组中的第一个元素,我们定义一个变量来存储该值:int insertVal = arr[index];,现在要将其插入到前面的有序数组中
- 将 index 前移一步,则指向有序数组最后一个元素,我们定义一个新的变量来存储该指针:insertIndex = index - 1; ,即 arr[insertIndex] 是有序数组最后一个元素
- 我们需要找到一个比 insertVal 小的值,并将 insertVal 插入在该值后面:
- 如果 insertVal > arr[insertIndex] ,执行插入
- 如果 insertVal < arr[insertIndex] ,将有序数组后移,腾出插入空间,insertIndex 指针前移,再看看前一个元素满不满足条件,直到找到插入位置
- 即循环终止条件为找到插入位置,又分为两种情况:
- 在有序数组中间找到插入位置
- insertVal 比有序数组中所有的数都小,插入在数组第一个位置(insertIndex = 0 的情况)
-
总结:两层循环
-
for 循环控制走多少趟:for(int i = 1; i < arr.length; i++) { ,从数组第一个元素开始到数组最后一个元素结束
-
while 循环不断将指针前移,在有序数组中寻找插入位置,并执行插入:
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
-
5.43、代码实现
// 插入排序
public static void insertSort(int[] arr) {
int insertVal = 0;
int insertIndex = 0;
// 使用for循环来把代码简化
for (int i = 1; i < arr.length; i++) {
// 定义待插入的数
insertVal = arr[i];
insertIndex = i - 1;
// 给insertValue找到插入的位置
// 说明
// 1. insertIndex >= 0 保证在给 insertVal 找插入位置,不越界
// 2. insertVal < arr[insertIndex] 待插入的数,还没有找到插入位置
// 3. 就需要将arr[insertIndex] 后移
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;
}
// 优化
if (insertIndex != i) {
arr[insertIndex + 1] = insertVal;
}
//System.out.println("第" + i + "轮插入");
//System.out.println(Arrays.toString(arr));
}
}
6、希尔排序
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
6.1、简单插入排序问题
-
我们看简单的插入排序可能存在的问题,数组 arr = { 2, 3, 4, 5, 6, 1 } 这时需要插入的数 1(最小),简单插入排序的过程如下
-
结论: 当需要插入的数是较小的数时, 后移的次数明显增多, 对效率有影响
{2,3,4,5,6,6} {2,3,4,5,5,6} {2,3,4,4,5,6} {2,3,3,4,5,6} {2,2,3,4,5,6} {1,2,3,4,5,6}
6.2、希尔排序基本介绍
- 希尔排序是希尔(Donald Shell) 于 1959 年提出的一种排序算法。 希尔排序也是一种插入排序, 它是简单插入排序经过改进之后的一个更高效的版本, 也称为缩小增量排序。
6.3、希尔排序基本思想
- 希尔排序按照增量将数组进行分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止
6.4、希尔排序图解(交换法)
第一次:gap = arr.length/5 = 5 , 将数组分为五组,每个数组元素的索引相差 5
-
如何完成第一次的排序?
- 仔细想想,我们需要用一次循环将每组中的元素排序
- 总共有五组,我们又需要一次循环
- 所以完成每次排序,需要两层循环
-
程序代码如下,把 i ,j 都看作是辅助指针:
-
i 与 j 配合使用,可以将指针从数组第一个元素,移动至最后一个元素,目的:把数组遍历一遍
-
j 与 i 配合使用,每次都从数组索引 i 处往前遍历,每次向前移动 gap 个位置,然后进行交换(冒泡排序的意思):看看前面的元素有没有比我的值大,如果前面的元素比我的值大,我就要和他交换位置,跑到前面去
// 希尔排序的第1轮排序 // 因为第1轮排序,是将10个数据分成了 5组 /* 为什么从5开始? 因为从5到10有5趟循环,5组会产生5个最小值; */ for (int i = 5; i < arr.length; i++) { // 遍历各组中所有的元素(共5组,每组有2个元素), 步长5 for (int j = i - 5; j >= 0; j -= 5) { // 如果当前元素大于加上步长后的那个元素,说明交换 if (arr[j] > arr[j + 5]) { temp = arr[j]; arr[j] = arr[j + 5]; arr[j + 5] = temp; } } }
-
第二次:gap = gap /2 = 2; , 将数组分为两组,每个数组元素的索引相差 2
- 第一组:
- i = 2 时,数组从索引 2 处往前遍历,间隔为 2 :将 arr[0]、arr[2] 排序
- i = 4 时,数组从索引 4 处往前遍历,间隔为 2 :将 arr[0]、arr[2]、arr[4] 排序
- i = 6 时,数组从索引 6 处往前遍历,间隔为 2 :将 arr[0]、arr[2]、arr[4]、arr[6] 排序
- i = 8 时,数组从索引 8 处往前遍历,间隔为 2 :将 arr[0]、arr[2]、arr[4]、arr[6]、arr[8] 排序
- 第二组:
- i = 3 时,数组从索引 3 处往前遍历,间隔为 2 :将 arr[1]、arr[3] 排序
- i = 5 时,数组从索引 5 处往前遍历,间隔为 2 :将 arr[1]、arr[3]、arr[5] 排序
- i = 7 时,数组从索引 7 处往前遍历,间隔为 2 :将 arr[1]、arr[3]、arr[5]、arr[7] 排序
- i = 9 时,数组从索引 9 处往前遍历,间隔为 2 :将 arr[1]、arr[3]、arr[5]、arr[7]、arr[9] 排序
// 希尔排序的第2轮排序 // 因为第2轮排序,是将10个数据分成了 5/2 = 2组 for (int i = 2; i < arr.length; i++) { // 遍历各组中所有的元素(共5组,每组有2个元素), 步长5 for (int j = i - 2; j >= 0; j -= 2) { // 如果当前元素大于加上步长后的那个元素,说明交换 if (arr[j] > arr[j + 2]) { temp = arr[j]; arr[j] = arr[j + 2]; arr[j + 2] = temp; } } } System.out.println("希尔排序2轮后=" + Arrays.toString(arr));
- 第一组:
-
第三次:gap = gap /2 = 1; , 将数组分为一组,每个数组元素的索引相差 1 ,对于交换法而言,这就是异常冒泡排序
- i = 1 时,数组从索引 1 处往前遍历,间隔为 1 :将 arr[0]、arr[1] 排序
- i = 2 时,数组从索引 2 处往前遍历,间隔为 1 :将 arr[0]、arr[1]、arr[2] 排序
- i = 3 时,数组从索引 3 处往前遍历,间隔为 1 :将 arr[0]、arr[1]、arr[2]、arr[3] 排序
- …
// 希尔排序的第3轮排序 // 因为第3轮排序,是将10个数据分成了 2/2 = 1组 for (int i = 1; i < arr.length; i++) { // 遍历各组中所有的元素(共5组,每组有2个元素), 步长5 for (int j = i - 1; j >= 0; j -= 1) { // 如果当前元素大于加上步长后的那个元素,说明交换 if (arr[j] > arr[j + 1]) { temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } System.out.println("希尔排序3轮后=" + Arrays.toString(arr));
-
**总结:**每次使用循环改变gap的值(初始值:数组大小/2,之后:gap=gap/2),然后在改变gap的循环中嵌套上面的双层for循环
-
改变gap:**for(int gap = arr.length/2;gap>0;gap/2) **
-
内存循环:实现对每组数组的排序
for(int i = gap; i < arr.length; i++) { // 遍历各组中所有的元素(共gap组,每组有?个元素),步长为gap for(int j = i - gap; j >=0 ; j-=gap){ }
-
-
希尔排序伪代码
for (int gap = arr.length / 2; gap > 0; gap /= 2) { for (int i = gap; i < arr.length; i++) { for (int j = i- gap ;j > 0; j -= gap) { // 对每组进行冒泡排序 } } }
-
6.5、代码实现
6.5.1、写希尔排序(交换法)
// 希尔排序时,对有序序列在插入时采用【交换法】
public static void shellSort(int[] arr) {
int temp = 0;
int count = 0;
// 使用循环处理
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
// 遍历各组中所有的元素(共gap组,每组有?个元素),步长gap
for (int j = i - gap; j >= 0; j -= gap) {
// 如果当前元素大于加上步长后的那个元素,就要交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
// System.out.println("希尔排序第"+ (++count) +"轮 =" + Arrays.toString(arr));
}
6.5.2、希尔排序(移位法)
-
编写基于插入法的希尔排序算法:
- 记录当前位置的元素值 int temp = arr[j];,从当前元素前一个位置开始,往前寻找,每次移动gap个距离
- 如果temp < arr [ j - gap]:
- 将数组元素往后移,腾出插入空间 : arr[j] = arr [j - gap];
- 然后继续往前找:j -= gap;
- 如果 temp > arr[j - gap],找到插入位置,执行插入 arr[j] = temp; , 因为在上一步已经腾出了空间,并且将指针 j 前移,所以可以直接插入
- 如果 找到数组最前面还是没有找到插入位置:j - gap < 0 ,则证明 temp 需要插入在数组最前面
- 如果temp < arr [ j - gap]:
- 仅仅就是将之前交换法的冒泡操作替换成了插入操作
// 增量gap,并逐步的缩小增量 for (int gap = arr.length / 2; gap > 0; gap /= 2) { // 从第gap个元素,逐个对其所在的组进行直接插入排序 for (int i = gap; i < arr.length; i++) { int j = i; int temp = arr[j]; if (arr[j] < arr[j - gap]) { while (j - gap >= 0 && temp < arr[j - gap]) { // 移动 arr[j] = arr[j - gap]; j -= gap; } // 当退出while后,就给temp找到插入的位置 arr[j] = temp; } } } // 我自己写的,根据前面插入排序算法,更好理解 public static void shellSort_2(int[] arr) { int len = arr.length; int insertVal; // 待插入的值 int insertIndex; // 插入位置 for (int gap = len / 2; gap > 0; gap /= 2) { for (int i = gap; i < len; i++) { insertVal = arr[i]; insertIndex = i - gap; while (insertIndex >= 0 && insertVal < arr[insertIndex]) { arr[insertIndex + gap] = arr[insertIndex]; // 将有序数组后移 insertIndex -= gap; // 插入指针前移 } arr[insertIndex + gap] = insertVal; } } }
- 记录当前位置的元素值 int temp = arr[j];,从当前元素前一个位置开始,往前寻找,每次移动gap个距离
移位法的排序速度非常快,80000个数据只需要1ms。
7、快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
7.1、快排简介
- 快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
- 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
- 快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
- 快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。
- 虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:
- 快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
7.2、代码思路
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
- 在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
快排流程分析
以 {25, 84, 21, 47, 15, 27, 68, 35, 20} 数列为例(下面的流程和上面的动图其实不太一样,不过大体思想是一样的)
- 第一趟:val = 25; 先取出来保存着
- {20, 84, 21, 47, 15, 27, 68, 35, 20}
- {20, 84, 21, 47, 15, 27, 68, 35, 84}
- {20, 15, 21, 47, 15, 27, 68, 35, 84}
- {20, 15, 21, 47, 47, 27, 68, 35, 84}
- {20, 15, 21, 25, 47, 27, 68, 35, 84}
- 第二趟:val = 20; 先取出来保存着
- {15, 15, 21}
- {15, 20, 21}
- 以此类推 …
7.3、代码实现
public static void quickSort(int[] arr, int left, int right) {
int l = left; // 左下标
int r = right; // 右下标
// pivot 中轴值
int pivot = arr[(left + right) / 2];
int temp = 0; // 临时变量,交换的时候使用
// while循环的目的是让比pivot值小的放到左边
// 比pivot值大的放到右边
while ( l < r ) {
// 在pivot的左边一直找,找到大于等于pivot值,才退出
while (arr[l] < pivot) {
l += 1;
}
// 在pivot的右边一直找,找到小于等于pivot值,才退出
while (arr[r] > pivot) {
r -= 1;
}
// 如果l >= r 说明pivot的左右两边的值,已经按照
// 左边全部是小于等于pivot值,右边全部是大于等于pivot值
if ( l >= r) {
break;
}
// 交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
// 如果交换完成后,发现这个arr[l] == pivot 值相等 r--, 前移
if (arr[l] == pivot) {
r -= 1;
}
// 如果交换完后,发现这个arr[r] == pivot 值相等 l--, 后移
if (arr[r] == pivot) {
l += 1;
}
}
// 如果 l == r, 必须 l++, r--,否则会出现栈溢出
if (l == r) {
l += 1;
r -= 1;
}
// 向左递归
if (left < r) {
quickSort(arr, left, r);
}
// 向右递归
if (right > l) {
quickSort(arr, l, right);
}
}
归并排序和基数排序没有看!