常用排序算法总结

我们通常所说的排序算法往往指的是内部排序算法,即数据记录在内存中进行排序。

  排序算法大体可分为两种:

    一种是比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序选择排序插入排序归并排序堆排序快速排序等。

    另一种是非比较排序,时间复杂度可以达到O(n),主要有:计数排序基数排序桶排序等。

下表给出了常见比较排序算法的性能:

一点我们很容易忽略的是排序算法的稳定性

  排序算法稳定性的简单形式化定义为:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的。通俗地讲就是保证排序前后两个相等的数的相对顺序不变。

  对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。

  例如,对于冒泡排序,原本是稳定的排序算法,如果将记录交换的条件改成A[i] >= A[i + 1],则两个相等的记录就会交换位置,从而变成不稳定的排序算法。

  其次,说一下排序算法稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。

1、冒泡排序(Bubble Sort)

​ 冒泡排序是一种极其简单的排序算法,也是我所学的第一个排序算法。它重复地走访过要排序的元素,依次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。

​ 冒泡排序算法的运作如下:

  1. 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

  由于它的简洁,冒泡排序通常被用来对于程序设计入门的学生介绍算法的概念。冒泡排序的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package sort;

/**
* 冒泡排序
* 从小到大排列
* 分类 -------------- 内部比较排序
* 数据结构 ---------- 数组
* 最差时间复杂度 ---- O(n^2)
* 最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
* 平均时间复杂度 ---- O(n^2)
* 所需辅助空间 ------ O(1)
* 稳定性 ------------ 稳定
*/
public class BubbleSort {
//打印数组
public static void printArr(int[] arr) {
for (int temp : arr) {
System.out.print(temp + " ");
}
System.out.println();
}

//交换数组元素
public static void swapArr(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

//冒泡排序
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swapArr(arr, j, j + 1);
}
}
}
}

//主方法
public static void main(String[] args) {
int[] arr = {6, 5, 3, 1, 8, 7, 2, 4}; //数组初始化
System.out.print("原数组元素:");
printArr(arr); //打印数组
System.out.print("排序后数组元素:");
bubbleSort(arr); //冒泡排序
printArr(arr); //打印数组
}
}

上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行冒泡排序的实现过程如下

  尽管冒泡排序是最容易了解和实现的排序算法之一,但它对于少数元素之外的数列排序是很没有效率的。

2、鸡尾酒排序

  鸡尾酒排序,也叫定向冒泡排序,是冒泡排序的一种改进。此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能。

  鸡尾酒排序的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package sort;

/**
* 鸡尾酒排序:冒泡排序的改进
* 从小到大排列
* 分类 -------------- 内部比较排序
* 数据结构 ---------- 数组
* 最差时间复杂度 ---- O(n^2)
* 最优时间复杂度 ---- 如果序列在一开始已经大部分排序过的话,会接近O(n)
* 平均时间复杂度 ---- O(n^2)
* 所需辅助空间 ------ O(1)
* 稳定性 ------------ 稳定
*/
public class CocktailSort {
//打印数组
public static void printArr(int[] arr) {
for (int temp : arr) {
System.out.print(temp + " ");
}
System.out.println();
}

//交换数组元素
public static void swapArr(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

//鸡尾酒排序
public static void CocktailSort(int[] arr) {
//初始化边界
int left = 0;
int right = arr.length - 1;
while (left < right) {
//前半部分
for (int i = left; i < right; i++) {
if (arr[i] > arr[i + 1]) { //将最大元素放到后边
swapArr(arr, i, i + 1);
}
}
right--;
//后半部分
for (int j = right; j > left; j--) {
if (arr[j] < arr[j - 1]) { //将最小元素放到前边
swapArr(arr, j, j - 1);
}
}
left++;
}
}

//主方法
public static void main(String[] args) {
int[] arr = {6, 5, 3, 1, 8, 7, 2, 4}; //数组初始化
System.out.print("原数组元素:");
printArr(arr); //打印数组
System.out.print("排序后数组元素:");
CocktailSort(arr); //鸡尾酒排序
printArr(arr); //打印数组
}
}

以序列(2,3,4,5,1)为例,鸡尾酒排序只需要访问一次序列就可以完成排序,但如果使用冒泡排序则需要四次。但是在乱数序列的状态下,鸡尾酒排序与冒泡排序的效率都很差劲。

3、选择排序(Selection Sort)

  选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

  注意选择排序与冒泡排序的区别:冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。

  选择排序的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package sort;

/**
* 选择排序
* 从小到大排列
* // 分类 -------------- 内部比较排序
* 数据结构 ---------- 数组
* 最差时间复杂度 ---- O(n^2)
* 最优时间复杂度 ---- O(n^2)
* 平均时间复杂度 ---- O(n^2)
* 所需辅助空间 ------ O(1)
* 稳定性 ------------ 不稳定
*/
public class SelectionSort {
//打印数组
public static void printArr(int[] arr) {
for (int temp : arr) {
System.out.print(temp + " ");
}
System.out.println();
}

//交换数组元素
public static void swapArr(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

//选择排序
public static void SelectionSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int minValue = i; //定义一个最小下标,假设i为最小值的下标
for (int j = i + 1; j < arr.length; j++) { //注意:j<arr.length
if (arr[minValue] > arr[j]) //如果有有比arr[minValue]小的,则将minValue = j
minValue = j;
}
//判断最小下标是否改变,如果改变则将最小的放到i的位置上
if (minValue != i) {
swapArr(arr, minValue, i); //数组元素交换
}
}
}

//主方法
public static void main(String[] args) {
int[] arr = {6, 5, 3, 1, 8, 7, 2, 4}; //数组初始化
System.out.print("原数组元素:");
printArr(arr); //打印数组
System.out.print("排序后数组元素:");
SelectionSort(arr); //选择排序
printArr(arr); //打印数组
}
}

  上述代码对序列{ 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }进行选择排序的实现过程如右图  

  使用选择排序为一列数字进行排序的宏观过程:  

  选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。

  比如序列:{ 5, 8, 5, 2, 9 },一次选择的最小元素是2,然后把2和第一个5进行交换,从而改变了两个元素5的相对次序。

4、插入排序(Insertion Sort)

插入排序是一种简单直观的排序算法。它的工作原理非常类似于我们抓扑克牌

  对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。

  插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

  具体算法描述如下:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5

  插入排序的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package sort;

/**
* 插入排序
*/
public class InserttionSort {
//打印数组
public static void printArr(int[] arr) {
for (int temp : arr) {
System.out.print(temp + " ");
}
System.out.println();
}

//交换数组元素
public static void swapArr(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

//插入排序
public static void InsertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) { //假设第一个元素为已排序序列,从第二个元素开始起插入
int j = i - 1; //已排序序列元素个数,下标从0开始,0代表一个
int get = arr[i];
while (j >= 0 && arr[j] > get) { //将要插入元素与已排序的序列从右向左进行比较
arr[j + 1] = arr[j]; //如果该元素大于要插入元素,则将其后移
j--;
}
arr[j + 1] = get; //直到该元素小于或等于要插入元素,则将其插入到该元素的后边
}
}

//主方法
public static void main(String[] args) {
int[] arr = {6, 5, 3, 1, 8, 7, 2, 4}; //数组初始化
System.out.print("原数组元素:");
printArr(arr); //打印数组
System.out.print("排序后数组元素:");
InsertionSort(arr); //直接插入排序
printArr(arr); //打印数组
}
}

上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行插入排序的实现过程如下

      

      

  使用插入排序为一列数字进行排序的宏观过程:  

  插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

5、二分插入排序

插入排序的改进

对于插入排序,如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的次数,我们称为二分插入排序,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package sort;

/**
* 二分插入排序
*/
public class DichotomyInsertionSort {
//打印数组
public static void printArr(int[] arr) {
for (int temp : arr) {
System.out.print(temp + " ");
}
System.out.println();
}

//交换数组元素
public static void swapArr(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

//二分插入排序
public static void DichotomyInsertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
//初始化已排序序列的边界
int left = 0;
int right = i - 1;
int get = arr[i];
//二分查找,找到已排序元素序列中小于或等于要插入元素的位置left
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] > get) {
right = mid - 1;
} else {
left = mid + 1;
}
}
for (int j = i - 1; j >= left; j--) {
arr[j + 1] = arr[j]; //>=left位置之后的元素整体后移
}
arr[left] = get; //将left位置赋值为get
}
}

//主方法
public static void main(String[] args) {
int[] arr = {6, 5, 3, 1, 8, 7, 2, 4}; //数组初始化
System.out.print("原数组元素:");
printArr(arr); //打印数组
System.out.print("排序后数组元素:");
DichotomyInsertionSort(arr); //二分插入排序
printArr(arr); //打印数组
}
}

当n较大时,二分插入排序的比较次数比直接插入排序的最差情况好得多,但比直接插入排序的最好情况要差,所当以元素初始序列已经接近升序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。

6、希尔排序(Shell Sort)

插入排序的更高效改进:

  希尔排序,也叫递减增量排序,是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。

  希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

  希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
  假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n^2)的排序(冒泡排序或直接插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。

  希尔排序的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package sort;

/**
* 希尔排序:插入排序的更高级改进
* 从小到大排列
*/
public class ShellSort {
// 打印数组
public static void printArr(int[] arr) {
for (int temp : arr) {
System.out.print(temp + " ");
}
System.out.println();
}

// 交换数组元素
public static void swapArr(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

// 希尔排序
public static void ShellSort(int[] arr) {
int h = 0;
while (h <= arr.length) { // 生成初始化增量
h = 3 * h + 1;
}
while (h >= 1) {
for (int i = h; i < arr.length; i++) {
int j = i - h;
int get = arr[i];
while (j >= 0 && arr[j] > get) {
arr[j + 1] = arr[j];
j = j - h;
}
arr[j + h] = get;
}
h = (h - 1) / 3; // 递减增量
}
}

//主方法
public static void main(String[] args) {
int[] arr = {6, 5, 3, 1, 8, 7, 2, 4}; // 数组初始化
System.out.print("原数组元素:");
printArr(arr); // 打印数组
System.out.print("希尔排序后数组元素:");
ShellSort(arr); // 希尔排序
printArr(arr); // 打印数组
}
}

  以23, 10, 4, 1的步长序列进行希尔排序:  

  希尔排序是不稳定的排序算法,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。

  比如序列:{ 3, 5, 10, 8, 7, 2, 8, 1, 20, 6 },h=2时分成两个子序列 { 3, 10, 7, 8, 20 } 和 { 5, 8, 2, 1, 6 } ,未排序之前第二个子序列中的8在前面,现在对两个子序列进行插入排序,得到 { 3, 7, 8, 10, 20 } 和 { 1, 2, 5, 6, 8 } ,即 { 3, 1, 7, 2, 8, 5, 10, 6, 20, 8 } ,两个8的相对次序发生了改变。

7、归并排序(Merge Sort)

  归并排序是创建在归并操作上的一种有效的排序算法,效率为O(nlogn),1945年由冯·诺伊曼首次提出。

  归并排序的实现分为递归实现非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。

  归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

  归并排序的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package sort;

/**
* 归并排序
* 从小到大排序
*/
public class MergeSort {
// 打印数组
public static void printArr(int[] arr) {
for (int temp : arr) {
System.out.print(temp + " ");
}
System.out.println();
}

// 合并两个已排好序的数组A[left...mid]和A[mid+1...right]
public static void merge(int A[], int left, int mid, int right) {
int len = right - left + 1;
int[] temp = new int[len];
int index = 0;
int i = left; //前一数组的起始元素
int j = mid + 1; //后一数组的起始元素
while (i <= mid && j <= right) {
temp[index++] = A[i] <= A[j] ? A[i++] : A[j++]; // 带等号保证归并排序的稳定性
}
while (i <= mid) {
temp[index++] = A[i++];
}
while (j <= right) {
temp[index++] = A[j++];
}
for (int k = 0; k < len; k++) {
A[left++] = temp[k];
}
}

// 递归实现的归并排序
public static void mergeSortRecursion(int A[], int left, int right) {
// 当待排序的序列长度为1时,递归开始回溯,进行merge操作
if (left == right) {
return;
}
int mid = (left + right) / 2;
mergeSortRecursion(A, left, mid);
mergeSortRecursion(A, mid + 1, right);
merge(A, left, mid, right);
}

// 非递归(迭代) 实现的归并排序(自底向上)
public static void mergeSortIteration(int A[], int len) {
// 子数组索引,前一个为A[left...mid],后一个子数组为A[mid+1...right]
int left, mid, right;
for (int i = 1; i < len; i *= 2) { // 子数组的大小i初始化为1,每轮翻倍
left = 0;
while (left + i < len) { // 后一个数组存在(需要归并)
mid = left + i - 1;
right = mid + i < len ? mid + i : len - 1; // 后一个子数组大小可能不够
merge(A, left, mid, right);
left = right + 1; // 前一个子数组向后移动
}
}
}

public static void main(String[] args) {
// 从小到大归并排序
int[] A1 = {6, 5, 3, 1, 8, 7, 2, 4};
int[] A2 = {6, 5, 3, 1, 8, 7, 2, 4};
int n1 = A1.length;
int n2 = A2.length;
System.out.print("A1原数组元素:");
printArr(A1); // 打印数组
System.out.print("递归实现的归并排序结果:");
mergeSortRecursion(A1, 0, n1 - 1); // 递归实现
printArr(A1);
System.out.print("A2原数组元素:");
printArr(A2); // 打印数组
System.out.print("非递归实现的归并排序结果:");
mergeSortIteration(A2, n2); // 非递归实现
printArr(A2);
}
}

上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行归并排序的实例如下 

     

  使用归并排序为一列数字进行排序的宏观过程:    

  归并排序除了可以对数组进行排序,还可以高效的求出数组小和(即单调和)以及数组中的逆序对。

8、堆排序(Heap Sort)

​ 堆排序是指利用堆这种数据结构所设计的一种选择排序算法。堆是一种近似完全二叉树的结构(通常堆是通过一维数组来实现的),并满足性质:以最大堆(也叫大根堆、大顶堆)为例,其中父结点的值总是大于它的孩子节点。

  我们可以很容易的定义堆排序的过程:

  1. 由输入的无序数组构造一个最大堆,作为初始的无序区
  2. 把堆顶元素(最大值)和堆尾元素互换
  3. 把堆(无序区)的尺寸缩小1,并调用heapify(A, 0)从新的堆顶元素开始进行堆调整
  4. 重复步骤2,直到堆的尺寸为1

  堆排序的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package sort;

/**
* 堆排序
* 从小到大排列
*/
public class HeapSort {
// 打印数组
public static void printArr(int[] arr) {
for (int temp : arr) {
System.out.print(temp + " ");
}
System.out.println();
}

// 交换数组元素
public static void swapArr(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

// 从A[i]向下进行堆调整
public static void heapify(int A[], int i, int size) {
int left_child = 2 * i + 1; // 左孩子索引
int right_child = 2 * i + 2; // 右孩子索引
int max = i; // 选出当前节点与其左右孩子三者之中的最大值
if (left_child < size && A[left_child] > A[max]) {
max = left_child;
}
if (right_child < size && A[right_child] > A[max]) {
max = right_child;
}
if (max != i) {
swapArr(A, i, max); // 把当前节点和它的最大(直接)子节点进行交换
heapify(A, max, size); // 递归调用,继续从当前节点向下进行堆调整
}
}

// 建堆
public static int buildHeap(int A[], int n) {
int heap_size = n;
for (int i = heap_size / 2 - 1; i >= 0; i--) // 从每一个非叶子结点开始向下进行调整
heapify(A, i, heap_size);
return heap_size;
}

// 堆排序
public static void heapSort(int A[], int n) {
int heap_size = buildHeap(A, n); // 建立一个最大堆
while (heap_size > 1) { // 堆(无序区)元素个数大于1,未完成排序
// 将堆顶元素与堆的最后一个元素交换,并从堆中去掉最后一个元素
// 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
swapArr(A, 0, --heap_size);
heapify(A, 0, heap_size); // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(log n)
}
}

// 主方法
public static void main(String[] args) {
int[] arr = {6, 5, 3, 1, 8, 7, 2, 4}; // 数组初始化
System.out.print("原数组元素:");
printArr(arr); // 打印数组
System.out.print("堆排序后数组元素:");
heapSort(arr, arr.length); // 堆排序
printArr(arr); // 打印数组
}
}

  堆排序算法的演示:  

  动画中在排序过程之前简单的表现了创建堆的过程以及堆的逻辑结构。

  堆排序是不稳定的排序算法,不稳定发生在堆顶元素与A[i]交换的时刻。

  比如序列:{ 9, 5, 7, 5 },堆顶元素是9,堆排序下一步将9和第二个5进行交换,得到序列 { 5, 5, 7, 9 },再进行堆调整得到{ 7, 5, 5, 9 },重复之前的操作最后得到{ 5, 5, 7, 9 }从而改变了两个5的相对次序。

9、快速排序(Quick Sort)

​ 快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。

  快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。步骤为:

  1. 从序列中挑出一个元素,作为”基准”(pivot).
  2. 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
  3. 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

  快速排序的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package sort;

/**
* 快速排序
* 从小到大排列
*/
public class QuickSort {
// 打印数组
public static void printArr(int[] arr) {
for (int temp : arr) {
System.out.print(temp + " ");
}
System.out.println();
}

// 交换数组元素
public static void swapArr(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

// 划分函数
public static int partition(int A[], int left, int right) {
int pivot = A[right]; // 这里每次都选择最后一个元素作为基准
int tail = left - 1; // tail为小于基准的子数组最后一个元素的索引
for (int i = left; i < right; i++) { //遍历基准以外的其他元素
if (A[i] <= pivot) { // 把小于等于基准的元素放到前一个子数组末尾
swapArr(A, ++tail, i);
}
}
// 最后把基准放到前一个子数组的后边,剩下的儿子数组既是大于基准的子数组
// 该操作很有可能把后面的稳定性打乱,所以快速排序是不稳定的排序算法
swapArr(A, tail + 1, right);
return tail + 1;
}

// 快速排序
public static void quickSort(int A[], int left, int right) {
if (left >= right) {
return;
}
int pivot_index = partition(A, left, right); // 基准的索引
quickSort(A, left, pivot_index - 1);
quickSort(A, pivot_index + 1, right);
}

// 主方法
public static void main(String[] args) {
int[] arr = {6, 5, 3, 1, 8, 7, 2, 4}; // 数组初始化
System.out.print("原数组元素:");
printArr(arr); // 打印数组
System.out.print("快速排序后数组元素:");
quickSort(arr, 0, arr.length - 1); // 快速排序
printArr(arr); // 打印数组
}
}

  使用快速排序法对一列数字进行排序的过程:  

  快速排序是不稳定的排序算法,不稳定发生在基准元素与A[tail+1]交换的时刻。

  比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。

  Java系统提供的Arrays.sort函数。对于基础类型,底层使用快速排序。对于非基础类型,底层使用归并排序。请问是为什么?

  答:这是考虑到排序算法的稳定性。对于基础类型,相同值是无差别的,排序前后相同值的相对位置并不重要,所以选择更为高效的快速排序,尽管它是不稳定的排序算法;而对于非基础类型,排序前后相等实例的相对位置不宜改变,所以选择稳定的归并排序。

坚持原创技术分享,您的支持将鼓励我继续创作!