本文是对《算法 第四版》中排序章节的总结,包括 选择排序,插入排序,希尔排序,归并排序,快速排序,堆排序和冒泡排序
各种排序算法的性能特点
有多种排序算法存在,就是因为各种算法拥有不同的性能特点,各有所长,适用于不同场合,下面是书中对各种排序算法的性能特点的总结:
算法 | 时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
选择排序 | 最差:N^2,平均:N^2,最优:N^2 | 1 | 不稳定 |
插入排序 | 最差:N^2,平均:N^2,最优:N | 1 | 稳定 |
希尔排序 | 最差:N*logN,平均:N*logN,最优:与递增序列有关 | 1 | 不稳定 |
快速排序 | 最差:N^2,平均:N*logN,最优:N*logN | lgN | 不稳定 |
归并排序 | 最差:N*logN,平均:N*logN,最优:N*logN | N | 稳定 |
堆排序 | 最差:N*logN,平均:N*logN,最优:N*logN | 1 | 不稳定 |
冒泡排序 | 最差:N^2,平均:N^2,最优:N | 1 | 稳定 |
本文使用 Java 实现以上几种排序算法,并对《算法 第四版》书中的代码稍有修改,作为演示,只针对 int[] 类型进行排序,因此文中排序算法的输入源都是 int[] 类型,并且将一些公共方法抽离出来,比如比较两个数大小的 less() 和交换两个数的 exchange() 方法,公共方法放在抽象类 SortModel.java 中,其他具体的排序方法只需要继承它,并实现自己特有的 sort() 方法即可
模版
将排序算法的公共方法放在一个抽象类中,具体的排序算法类只需要继承自这个抽象类,并实现自己的 sort() 方法即可,具体代码如下:
public abstract class SortModel {
//记录排序消耗的时间
protected long usedTime = 0;
//具体的排序方法,由子类实现
protected abstract void sort(int[] a);
//比较 a 和 b 的大小,如果 a 小于 b,则返回 true
protected boolean less(int a, int b) {
return a < b;
}
//交换数组中的两个数
protected void exchange(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
//打印数组
protected void show(int[] arr, int count) {
System.out.println("\n\n使用 " + getSortMethod() + " 对 " + count + " 个数排序用时: " + usedTime + "ms");
for (int i : arr) {
System.out.print(" " + i);
}
}
//获取当前使用的排序方法名称
protected abstract String getSortMethod();
}
接下来开始总结这些排序算法的具体实现
选择排序
选择排序是比较基础的排序算法,也是一种很容易想到的排序算法,具体描述是这样的:首先找到数组中最小的元素(这是一个遍历比较的过程),然后将它和数组中的第一个元素交换位置,接着在剩下的元素中找到最小的元素,将它与第二个元素交换位置,如此循环,直到将整个数组排序完成
在选择排序中一个主要的操作就是在数组中找到最小的元素,如何在一个给定的数组中找到值最小的那个元素呢?这个过程分为两步:
遍历:
遍历最简单的形式就是使用一个 for 循环,从开始索引,到结束索引,依次访问数组中的元素比较:
比较至少需要两个元素,在遍历的时候,每次只访问数组中的一个元素,因此为了能够比较,需要有一个临时索引指向的元素来和当前访问的元素进行比较,如果当前元素小于临时索引指向的元素,就把当前元素的索引赋值给临时索引
通过以上这两步,在遍历中比较,在满足当前元素小于临时索引指向的元素的条件时,就将当前元素的索引赋值给临时索引,如此循环,遍历结束后,临时索引指向的元素值便是最小元素值
假设有一组数:16,13,18,11,14,12
找最小元素的过程是:
默认临时索引为数组第一个元素,即索引为 0,开始遍历数组
当前元素是索引为 0 的元素 16,和临时索引为 0 的元素比较,相等,不赋值
当前元素是索引为 1 的元素 13,和临时索引为 0 的元素比较,小于,当前索引赋值给临时索引
当前元素是索引为 2 的元素 18,和临时索引为 1 的元素比较,大于,不赋值
当前元素是索引为 3 的元素 11,和临时索引为 1 的元素比较,小于,当前索引赋值给临时索引
当前元素是索引为 4 的元素 14,和临时索引为 3 的元素比较,大于,不赋值
当前元素是索引为 5 的元素 12,和临时索引为 3 的元素比较,大于,不赋值
遍历结束,临时索引为 3,因此索引为 3 的元素就是这个数组中的最小元素
以上是一次寻找最小元素的过程,需要执行的比较次数与遍历的数组长度成正比,如果遍历的数组长为 N,则查找最小元素需要 N 次比较
选择排序的排序过程是:
遍历索引[0-5],找到最小元素 11,对应的索引为 3,将它与索引为 0 的元素交换,交换后如下
遍历索引[1-5],找到最小元素 12,对应的索引为 5,将它与索引为 1 的元素交换,交换后如下
遍历索引[2-5],找到最小元素 13,对应的索引为 5,将它与索引为 2 的元素交换,交换后如下
遍历索引[3-5],找到最小元素 14,对应的索引为 4,将它与索引为 3 的元素交换,交换后如下
遍历索引[4-5],找到最小元素 16,对应的索引为 4,将它与索引为 4 的元素交换,交换后如下
遍历索引[5-5],找到最小元素 18,对应的索引为 5,将它与索引为 5 的元素交换,交换后如下
以上是选择排序的过程,从这个过程中可以分析到,在一次选择排序过程中需要* N+(N-1)+(N-2)+…+3+2+1 = N(N-1)/2 ~ N^2/2* 次比较和 N 次交换,需要的比较次数属于 N^2 级别。并且,在选择排序中不存在最优与最坏情况,无论输入的数据情况怎样,选择排序都需要固定次数的比较和交换,
选择排序有两个特点:
运行时间和输入无关
从上面的分析可以知道,选择排序中不存在最优情况和最坏情况,即使输入数据已经整体有序,选择排序所需要的比较和交换次数依然是固定的,只和输入数组的大小有关数据移动次数最少
选择排序所需的交换次数和输入数组的大小有关,如果输入数组大小为 N,则选择排序所需交换次数为 N
代码实现
public class SelectionSort extends SortModel {
@Override
protected String getSortMethod() {
return "选择排序";
}
@Override
protected void sort(int[] a) {
long startTime = System.currentTimeMillis();
int N = a.length;
for (int i = 0; i < N; i++) {
int min = i;
for (int j = i + 1; j < N; j++) {//循环找到剩余元素中最小的,赋值给 min
if (less(a[j], a[min])) {
min = j;
}
}
exchange(a, i, min);//交换 N 次
}
long endTime = System.currentTimeMillis();
usedTime = endTime - startTime;
}
}
插入排序
插入排序的思想如同玩纸牌时抽牌,每次抽一张,将其插入手中已经有序的牌组中,这样,每次插入前的牌组都是有序的,而要插入的这张牌就在已经有序的牌组中从高到低依次比较,在合适的位置插入。插入排序的具体实现是:在数组中遍历,索引每次加一,将新增索引对应的值插入当前索引之前的数组中(当前索引之前的数组已经有序),在当前索引位置开始往前两两比较,如果后者小于前者就交换,如此循环,直到全部有序
插入排序的主要操作是插入,插入的过程是从当前索引往前两两比较,因此插入过程分为两步:
逆向遍历:
使用 for 循环,从当前索引,到起始索引,依次访问数组中的元素
* 比较:*
将当前索引的元素和其前一位的元素进行比较,如果当前索引元素小于前一位元素值,则交换(如果要求升序排序,就是小于,如果是降序排序,则是大于)
假设有一组数:16,13,18,11,14,12
执行一次插入的操作是这样:
假设当前索引为 4,也就是说数组的[0-3]部分已经有序,接下来需要将索引为 4 的元素插入到数组的[0-4]部分中
插入排序的排序过程是:(注意初始索引是 1 而不是 0)
当前索引 1,逆向遍历[1-0],两两比较:
- 当前索引为 1 的元素值 13,前一位元素为 16,小于,交换
比较 1 次,交换 1 次,交换后的数组:
当前索引 2,逆向遍历[2-0],两两比较
当前索引为 2 的元素值 18,前一位元素为 16,大于,不交换
当前索引为 1 的元素值 16,前一位元素为 13,大于,不交换
比较 2 次,交换 0 次,交换后的数组:
当前索引 3,逆向遍历[3-0],两两比较:
当前索引为 3 的元素值 11,前一位元素为 18,小于,交换
当前索引为 2 的元素值 11,前一位元素为 16,小于,交换
当前索引为 1 的元素值 11,前一位元素为 13,小于,交换
比较 3 次,交换 3 次,交换后的数组:
当前索引 4,逆向遍历[4-0],两两比较:
当前索引为 4 的元素值 14,前一位元素为 18,小于,交换
当前索引为 3 的元素值 14,前一位元素为 16,小于,交换
当前索引为 2 的元素值 14,前一位元素为 13,大于,不交换
当前索引为 1 的元素值 13,前一位元素为 11,大于,不交换
比较 4 次,交换 2 次,交换后的数组:
当前索引 5,逆向遍历[5-0],两辆比较
当前索引为 5 的元素值 12,前一位元素为 18,小于,交换
当前索引为 4 的元素值 12,前一位元素为 16,小于,交换
当前索引为 3 的元素值 12,前一位元素为 14,小于,交换
当前索引为 2 的元素值 12,前一位元素为 13,小于,交换
当前索引为 1 的元素值 12,前一位元素为 11,大于,不交换
比较 5 次,交换 4 次,交换后的数组:
从上面的执行过程分析,在插入排序中,比较次数和交换次数都是和输入有关的,因此会存在最优情况和最坏情况,很好理解,最优情况是在输入数组已经基本有序的时候,最坏情况是在输入数组为逆序的时候,下面分别从最优情况,最坏情况和平均情况分析插入排序的性能
最优情况:
当输入数据已经基本有序时,比如输入数据为 11,12,13,14,16,18 ,则需要的比较次数为 1+1+1+1+1=5 次,交换次数为 0 次,延伸到长度为 N 的输入数据中,比较次数为 N,交换次数为 0。最坏情况:
当输入数据为逆序是,比如输入数据为 18,16,14,13,12,11 ,则需要的比较次数为 1+2+3+4+5=15 次,交换次数为 1+2+3+4+5=15 次,延伸到长度为 N 的输入数据中,比较次数为 1+2+3+…+(N-2)+(N-1)+N = N(N-1)/2 ~ N^2/2,交换次数为 1+2+3+…+(N-2)+(N-1)+N = N(N-1)/2 ~ N^2/2平均情况:
已经知道了最优情况和最坏情况下的比较次数和交换次数,平均情况就是 (最优情况 + 最坏情况) / 2,因此,平均情况下,插入排序需要的比较次数为 ~ N^2/4,交换次数为 ~ N^2/4
通过以上对插入排序的分析,可以总结几点:
插入排序对部分有序的数组十分高效,也很适合小规模数组
可以对插入排序进行优化,比如在内循环中将较大的元素向右移动而不总是交换两个相邻的元素
代码实现:
public class InsertionSort extends SortModel {
@Override
protected String getSortMethod() {
return "插入排序";
}
@Override
protected void sort(int[] a) {
long startTime = System.currentTimeMillis();
int N = a.length;
for (int i = 1; i < N; i++) {
//从当前索引位置往前遍历,如果找到满足"后者小于前者"条件的,就交换两者的位置
//这里有个改进的写法,之前的写法是这样的:
// for (int j = i; j > 0; j--) {
// if(less(a[j], a[j - 1])){
// exchange(a, j, j - 1);
// }
// }
//这种写法会导致每次的 for 循环都会一直进行到底,就会导致插入排序的比较次数是固定的
//而下面这种写法,在不满足条件的情况下,就会结束 for 循环,因此比较次数是跟输入有关的
//因而是可以存在最优情况的
for (int j = i; j > 0 && less(a[j], a[j - 1]); j--) {
exchange(a, j, j - 1);
}
}
long endTime = System.currentTimeMillis();
usedTime = endTime - startTime;
}
}
希尔排序
希尔排序是基于插入排序的,改进了插入排序对于大规模乱序数组排序很慢的缺点,比如,如果一个值最小的元素正好在数组的尽头,要将它移到正确的位置(数组起始位置),如果使用插入排序,需要移动 N-1 次,希尔排序改进的方法就是交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。希尔排序使数组中任意间隔为 h 的元素都是有序的,然后再逐渐缩小 h 的值,直到 h=1,整个数进行插入排序,最终使数组有序。书中给出的增量 h 的计算公式为 h=1/2(3^k-1),其中 k = 1,2,3,4,5…,这样得到的 h 值是 1,4,13,40,121,364…的序列,在 h 小于数组的三分之一时(即 h < N/3)h 值开始递减至 1。
假设有一组数:16,13,18,11,14,12
值 | 16 | 13 | 18 | 11 | 14 | 12 |
---|---|---|---|---|---|---|
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
希尔排序的排序过程是:
根据公式计算得到递增序列的最大值,计算得到 h = 4
h 值为 4,即递减量为 4,遍历 [4,5]
- 当前索引值为 4,逆向遍历[4,4],递减序列为 (4,0),比较 1 次,交换 1 次
值 | 14 | 13 | 18 | 11 | 16 | 12 |
---|---|---|---|---|---|---|
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
- 当前索引值为 5,逆向遍历[5,4],递减序列为 (5,1),比较 1 次,交换 1 次
值 | 14 | 12 | 18 | 11 | 16 | 13 |
---|---|---|---|---|---|---|
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
- h 值为 1,即递减量为 1,遍历 [1,5]
- 当前索引值为 1,逆向遍历[1,1],递减序列为 (1,0),比较 1 次,交换 1 次
值 | 12 | 14 | 18 | 11 | 16 | 13 |
---|---|---|---|---|---|---|
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
- 当前索引值为 2,逆向遍历[2,1],递减序列为 (2,1,0),比较 2 次,交换 0 次
值 | 12 | 14 | 18 | 11 | 16 | 13 |
---|---|---|---|---|---|---|
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
- 当前索引值为 3,逆向遍历[3,1],递减序列为 (3,2,1,0),比较 3 次,交换 3 次
值 | 11 | 12 | 14 | 18 | 16 | 13 |
---|---|---|---|---|---|---|
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
- 当前索引值为 4,逆向遍历[4,1],递减序列为 (4,3,2,1,0),比较 2 次,交换 1 次
值 | 11 | 12 | 14 | 16 | 18 | 13 |
---|---|---|---|---|---|---|
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
- 当前索引值为 5,逆向遍历[5,1],递减序列为 (5,4,3,2,1,0),比较 4 次,交换 3 次
值 | 11 | 12 | 13 | 14 | 16 | 18 |
---|---|---|---|---|---|---|
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
代码实现:
public class ShellSort extends SortModel {
@Override
protected String getSortMethod() {
return "希尔排序";
}
@Override
protected void sort(int[] a) {
long startTime = System.currentTimeMillis();
int N = a.length;
int h = 1;
while (h < N / 3) {//根据 N 计算递增序列中的最大值,1,4,13,40,121...
h = 3 * h + 1;
}
while (h >= 1) {//逐渐缩小递增序列进行排序,13,4,1
System.out.println("h: "+h);
for (int i = h; i < N; i++) {//在最后一个增量段中遍历 i
System.out.print(" i: "+i);
for (int j = i; j >= h; j -= h) {//从 i 还是逐增量,得到的一个相隔增量段的序列进行插入排序
System.out.print(" j: "+j);
if (less(a[j], a[j - h])) {
exchange(a, j, j - h);
}
}
System.out.println();
}
h = h / 3;//增量递减
}
long endTime = System.currentTimeMillis();
usedTime = endTime - startTime;
}
}
归并排序
归并排序的思想是:要将一个数组排序,可以先将它分成两个子数组分别排序,然后将结果归并起来。在归并排序中,排序的过程是,把一个数据分成两个子数组,每个子数组进行排序(递归的尽头,一个子数组中只有两个元素,这时的排序只需要简单的比较两个元素的大小),然后再把有序的子数组归并成一个整体有序的数组,总结起来就是,先分解成最小单元,再组合成一个整体。
假设有一组数:16,13,18,11,14,12
归并排序的排序过程是:(其中 lo 为数组最低位索引,mid 为数组中间位置索引,hi 为数组最高位索引)
- 对数组排序:[0,5], lo=0, mid=2, hi=5
- 对左半部分排序:[0,2], lo=0, mid=1, hi=2
- 对左半部分�92序:[0,1], lo=0, mid=0, hi=1
- 对左半部分排序:[0,0]
- 对右半部分排序:[1,1]
- 对数组归并: lo=0, mid=0, hi=1
- 对右半部分排序:[2,2]
- 对数组归并: lo=0, mid=1, hi=2
- 对右半部分排序:[3,5], lo=3, mid=4, hi=5
- 对左半部分排序:[3,4], lo=3, mid=3, hi=4
- 对左半部分排序:[3,3]
- 对右半部分排序:[4,4]
- 对数组归并:lo=3, mid=3, hi=4
- 对右半部分排序:[5,5]
- 对数组归并:lo=3, mid=4, hi=5
- 对数组归并:lo=0, mid=2, hi=5
代码实现:
public class MergeSort extends SortModel {
int[] temp;
@Override
protected String getSortMethod() {
return "归并排序";
}
@Override
protected void sort(int[] a) {
long startTime = System.currentTimeMillis();
temp = new int[a.length];
sortT2B(a, 0, a.length - 1);
long endTime = System.currentTimeMillis();
usedTime = endTime - startTime;
}
private void sortT2B(int[] a, int lo, int hi) {
if (hi > lo) {
int mid = lo + (hi - lo) / 2;//取数组中间位置索引
sortT2B(a, lo, mid);//递归排序左边的元素
sortT2B(a, mid + 1, hi);//递归排序右边的元素
merge(a, lo, mid, hi);//归并
}
}
/**
* 归并操作,将两个有序的数组归并成一个有序的数组
* <p>
* 首先将数组复制到临时数组 temp 中
* 将临时数组分为左右两部分,左边部分索引起始位置为 i,右边部分索引起始位置为 j
* 将左右两部分归并到原来的数组中
*
* @param a 数组
* @param lo 数组第一个元素
* @param mid 数组中间的元素
* @param hi 数组最后一个元素
*/
private void merge(int[] a, int lo, int mid, int hi) {
int i = lo, j = mid + 1;
for (int k = lo; k <= hi; k++) {//复制数组
temp[k] = a[k];
}
for (int k = lo; k <= hi; k++) {
if (i > mid) {//如果左边用尽,则取右边的元素
a[k] = temp[j++];
} else if (j > hi) {//如果右边用尽,则取左边的元素
a[k] = temp[i++];
} else if (less(temp[j], temp[i])) {//如果右边当前元素小于左边当前元素,则取右边元素
a[k] = temp[j++];
} else {//右边当前元素大于左边当前元素,则取左边元素
a[k] = temp[i++];
}
}
}
}
快速排序
快速排序是应用最广泛的排序算法,它将一个长度为 N 的数组排序所需要的时间和 N*lgN 成正比,快速排序的基本思路是:将一个数组分成两个子数组,将两部分独立的排序,当两个子数组都有序时,整个数组也就有序了,这与归并排序有所不同,归并排序中,将一个数组分成两个数组后,需要对两个子数组进行归并,在归并的过程中排序,而快速排序是在将一个数组分成两个数组的过程中进行排序,当分到尽头的时候,数组就已经有序了,不需要再进行其它任何操作
快速排序中重点是找到将一个数组分为两个数组的切分点,在归并排序中,其实也是有这样的切分点的,就是 mid,也就是说归并排序默认将一个数组等分,而在快速排序中,对一个数组的切分,并不一定是等分,需要根据具体的切分点的位置来进行切分,所以,找到合适的切分点的位置是很重要的,直接影响到整个排序的性能。
寻找切分点
切分点需要满足三个条件:(假设切分点索引为 k)
- 对于某个索引 k,数组中对应索引的值 a[k] 是确定的
- 数组索引[lo,k-1] (即切分点左边的所有元素) 中的所有元素的值都不大于切分点元素的值( <= )
- 数组索引[k+1,hi] (即切分点右边的所有元素) 中的所有元素的值都不小于切分点元素的值( >= )
假设有一组数:16,13,18,11,14,12
寻找切分点的过程是:(其中 lo 为数组最低位索引,hi 为数组最高位索引,从左往右遍历的指针为 i,从右往左遍历的指针为 j)
- 随意取 a[lo] 的值作为初始切分点元素的值,即切分点索引为 0,值为 16
- 从数组左端向右遍历[1,5],当遇到一个大于等于切分点的元素,即索引为 2 的元素,停止遍历,此时 i=2
- 从数组右端向左遍历[5,0],当遇到一个小于等于切分点的元素,即索引为 5 的元素,停止遍历,此时 j=5
- 交换 i 和 j 对应的值,交换后数组为:16,13,12,11,14,18
- 从数组左端向右遍历[3,5],当遇到一个大于等于切分点的元素,即索引为 5 的元素,停止遍历,此时 i=5
- 从数组右端向左遍历[4,0],当遇到一个小于等于切分点的元素,即索引为 4 的元素,停止遍历,此时 j=4
- 当 i>=j 时停止循环,不会执行 i 和 j 的交换,而是将切分点元素和 j 元素交换,交换后数组为:14,13,12,11,16,18
- 到此,寻找第一个切分点完成,切分点索引为 4,对应的值为 16
找到切分点,接下来将数组按照切分点分成两部分,从上面的执行结果可以知道,数组将被分为 [0,3] 和 [5,5],接下来就是对 [0,3] 部分重复寻找切分点的过程:
- 随意取 a[lo] 的值作为初始切分点元素的值,即切分点索引为 0,值为 14
- 从数组左端向右遍历[1,3],当遇到一个大于等于切分点的元素,没有找到,遍历到数组尽头,此时 i=3
- 从数组右端向左遍历[3,0],当遇到一个小于等于切分点的元素,即索引为 3 的元素,停止遍历,此时 j=3
- 当 i>=j 时停止循环,不会执行 i 和 j 的交换,而是将切分点元素和 j 元素交换,交换后数组为:11,13,12,14,16,18
- 到此,寻找第二个切分点完成,切分点索引为 3,对应的值为 14
同理,数组将按照切分点分为 [0,2] 和[3,3],接下来对 [0,2] 部分重复寻找切分点:
- 随意取 a[lo] 的值作为初始切分点元素的值,即切分点索引为 0,值为 11
- 从数组左端向右遍历[1,2],当遇到一个大于等于切分点的元素,即索引为 1 的元素,停止遍历,此时 i=1
- 从数组右端向左遍历[2,0],当遇到一个小于等于切分点的元素,没有找到,遍历到数组起始位置,此时 j=0
- 当 i>=j 时停止循环,不会执行 i 和 j 的交换,而是将切分点元素和 j 元素交换,交换后数组为:11,13,12,14,16,18
- 到此,寻找第三个切分点完成,切分点索引为 0,对应的值为 11
此时,由于切分点位置为 0,所以只能切分出一个子数组,即 [1,2],继续寻找切分点
- 随意取 a[lo] 的值作为初始切分点元素的值,即切分点索引为 1,值为 13
- 从数组左端向右遍历[2,2],当遇到一个大于等于切分点的元素,没有找到,遍历到数组尽头,此时 i=2
- 从数组右端向左遍历[2,1],当遇到一个小于等于切分点的元素,即索引为 2 的元素,此时 j=2
- 当 i>=j 时停止循环,不会执行 i 和 j 的交换,而是将切分点元素和 j 元素交换,交换后数组为:11,12,13,14,16,18
- 到此,寻找第四个切分点完成,切分点索引为 2,对应的值为 12
到此,数据已经有序了,整个过程中寻找了四次切分点
排序过程
下面整理完整的排序过程,将会忽略寻找切分点的过程,直接给出找到的切分点
假设有一组数:16,13,18,11,14,12
快速排序的过程是:
- 寻找切分点,索引为 4,值为16,进行了 2 次交换,第 1 次交换后的数组是:16,13,12,11,14,18,第二次交换后的数组是:14,13,12,11,16,18
- 对左半部分排序:[0,3]
- 寻找切分点,索引为 3,值为 14,进行了 1 次交换,交换后的数组是:11,13,12,14,16,18
- 对左半部分排序:[0,2]
- 寻找切分点,索引为 0,值为 11,进行了 1 次交换,交换后的数组是:11,13,12,14,16,18
- 对左半部分排序:[0,-1]
- 对右半部分排序:[1,2]
- 寻找切分点,索引为 2,值为 12,进行了 1 次交换,交换后的数组是:11,12,13,14,16,18
- 对左半部分排序:[1,1]
- 对右半部分排序:[3,2]
- 对右半部分排序:[4,3]
- 对右半部分排序:[5,5]
代码实现:
public class QuickSort extends SortModel {
@Override
protected String getSortMethod() {
return "快速排序";
}
@Override
protected void sort(int[] a) {
long startTime = System.currentTimeMillis();
sort(a, 0, a.length - 1);
long endTime = System.currentTimeMillis();
usedTime = endTime - startTime;
}
private void sort(int[] a, int lo, int hi) {
if (hi > lo) {
int j = partition(a, lo, hi);//找到切分点
sort(a, lo, j - 1);//对左半部分排序
sort(a, j + 1, hi);//对右半部分排序
}
}
/**
* 找到切分点,使得切分点左边所有元素都不大于切分点,右边有所有素都不小于切分点
*
* @param a 数组
* @param lo 数组起始位置
* @param hi 数组结束为止
* @return 切分点
*/
private int partition(int[] a, int lo, int hi) {
//这里 j 取 hi+1,是因为在循环中用的 --j,是先执行减操作再比较,因此在首次执行时如果直接取 j 为 hi,会忽略hi 这个元素,直接比较 hi-1
//而 i 取 lo,没有取 lo-1 是因为默认取第一个元素为切分点,正好需要略过第一个元素,所以这样第一个比较的元素其实是 lo+1
int i = lo, j = hi + 1;
int v = a[lo];
while (true) {
while (less(a[++i], v)) {//从起始位置向右扫描,直到找到一个大于等于切分点的元素
if (i == hi) {
break;
}
}
while (less(v, a[--j])) {//从右端向左扫描,直到找到一个小于等于切分点的元素
if (j == lo) {
break;
}
}
if (i >= j) {//当两个指针相遇,则中断循环
break;
}
exchange(a, i, j);//交换两个元素
}
exchange(a, lo, j);//交换切分点和左子数组最右端的元素交换,这样返回的就是最新的切分点元素值了
return j;
}
}
堆排序
在了解堆排序之前,需要先了解二叉堆这种数据结构,它的定义是:二叉堆是一组能够用堆有序(每个节点都大于等于它的两个字节点时,堆有序)的完全二叉树排序的元素,并在数组中按照层级储存(不使用数组的第一个位置,就是不使用索引 0 ,索引从 1 开始)
在二叉堆中存在这样一种关系:索引位置为 k 的元素,他的父节点索引位置为 k/2,而它的两个字节点索引位置分别为 2*k 和 2*k+1。这样就能很容易在二叉堆中上下移动,比如,从 a[k] 位置向上层移动就令 k=k/2,向下层移动就令 k=2*k 或者 k=2*k+1
在堆中有两个比较重要的操作,也是用来排序的操作,即由下至上的堆有序化(上浮)和由上至下的堆有序化(下沉)
- 上浮:如果某个节点的值比它的父节点还大,就需要通过上浮操作,使它和它的父节点交换,以此来达到从上至下的有序状态
- 下沉:如果某个节点的值比它的两个子节点中较大者还小,就需要通过下沉操作,使它和它的子节点中较大者交换,以此来达到从上至下的有序状态
堆排序正是利用了二叉堆的这个性质来完成排序的。堆排序的过程大致分为两步:初始构造堆,然后是下沉排序。
初始构造堆就是把要排序的数组构造成一个二叉堆,初始构造的堆只需要满足:当前节点大于它的两个字节点并且小于它的父节点即可,并不需要在初始构造阶段进行排序
下沉排序是从数组末尾开始逆序遍历将所有的数都放在数组起始位置(索引为 1),然后让他根据下沉规则找到合适的位置,这样,当遍历到数组起始位置时,数组就有序了
假设有一组数:16,13,18,11,14,12
堆排序分为两个过程:构造堆和下沉排序,其中构造堆就是将一个数组构造成具有堆的性质(当前节点大于其左右子节点的值),构造完成的堆满足从上至下的有序状态,即当前节点大于其子节点,但是在其两个子节点中并不存在有序状态,于是需要使用下沉操作将堆从下至上进行排序,使堆整体有序。
构造堆:
未完待续。。。