快速排序

快速排序

一、算法描述

1.1 核心思想

  • 二分,选取一个分区值,将数据分割成 2 部分:小于和大于
  • 递归,对小于大于两部分再排序,不断选点分割数据,直到无法分割为止
  • 整个二分过程类似于一棵二叉树,从上往下排序,先确定根节点位置,再处理子树排序

1.2 细节解释

比如说, 数组 [5, 7, 2, 4, 3, 6, 1]

首先选取一个分区值(随意选一个),比如说 5。

以 5 基准,将其他数据分割成 2 部分:一部分小于 5,一部分大于 5。

1
2
3
      小于5                     大于5
___ ___ ___ ___ ___ ___
| 2 | 4 | 3 | 1 | | 7 | 6 |

然后将 5 插入 2 部分的中间:

1
2
3
      小于5                     大于5
___ ___ ___ ___ ___ ___ ___
| 2 | 4 | 3 | 1 | | 5 | | 7 | 6 |

此时 5 就已经排好了。

下面接着对左右 2 部分进行递归排序。

(1) 先对左边小于 5 的部分排序

首先选取一个分区值 2,然后以 2 为基准分割成 2 部分:

1
2
3
小于2           大于2
___ ___ ___
| 1 | | 4 | 3 |

将 2 插入中间位置:

1
2
3
小于2           大于2
___ ___ ___ ___
| 1 | | 2 | | 4 | 3 |

此时 2 就排好了。

接着不断往下递归,直到无法分割为止,最后可以得到:

1
2
 ___    ___    ___    ___
| 1 | | 2 | | 3 | | 4 |

(2) 再对右边大于 5 的部分排序

同理,也是递归分割数据,最后可以得到:

1
2
 ___    ___
| 6 | | 7 |

(3) 最后完整的序列结果

1
2
 ___    ___    ___    ___    ___    ___    ___
| 1 | | 2 | | 3 | | 4 | | 5 | | 6 | | 7 |

至此排序就完成了,快速排序本质上是一种分治算法,将大排序变成小排序来做。

二、算法实现

2.1 递归二分排序

1
2
3
4
5
6
7
8
9
10
void partSort(int[] arr, int start, int end) {
if (start < end) {
// 分区
int m = partition(arr, start, end);
// 左边排序
partSort(arr, start, m);
// 右边排序
partSort(arr, m + 1, end);
}
}

2.2 对数据进行分区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int partition(int[] arr, int start, int end) {
// 选取分区点
int p = pivot(arr, start, end - 1);
// 将分区点放到最前面
swap(arr, start, p);
int ref = arr[start];
int lp = start;
// 将数据与分区点对比,分成小于和大于2部分
for (int i = lp + 1; i < end; i++) {
if (arr[i] <= ref) {
swap(arr, ++lp, i);
}
}
// 将分区点放到它最终的位置
swap(arr, start, lp);
return lp;
}

2.3 分区点的选择

1
2
3
4
5
6
7
8
9
// 采用左中右三点取中值的方式
int pivot(int[] arr, int i, int j) {
int mid = i + (j - i) / 2;
if (arr[i] < arr[j]) {
return arr[mid] > arr[i] ? mid : j;
} else {
return arr[mid] < arr[i] ? mid : i;
}
}

三、算法分析

3.1 时间复杂度

  • 最好时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(n^2)
  • 平均时间复杂度:O(nlogn)

3.2 空间复杂度

  • 空间复杂度:O(logn)
  • 原地算法

3.3 稳定性

  • 不稳定排序算法

四、适用场景

  • 数据量比较大
  • 数据有序性比较差

附录

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
82
83
84
85
86
/**
* 快速排序
* <p>
* 时间复杂度:最好 O(nlogn) 最差 O(n^2) 平均 O(nlogn)
* <p>
* 空间复杂度:O(logn)
* <p>
* 稳定性:不稳定
*
* @author weijiaduo
* @since 2022/7/16
*/
public class QuickSort implements Sort {

/**
* 排序
*
* @param arr 数组
*/
@Override
public void sort(int[] arr) {
partSort(arr, 0, arr.length);
}

/**
* 递归排序
*
* @param arr 数组
* @param start [start, end)
* @param end [start, end)
*/
private void partSort(int[] arr, int start, int end) {
if (start < end) {
// 分区
int m = partition(arr, start, end);
// 左边排序
partSort(arr, start, m);
// 右边排序
partSort(arr, m + 1, end);
}
}

/**
* 二分数组
*
* @param arr 数组
* @param start [start, end)
* @param end [start, end)
* @return 分隔点索引
*/
private int partition(int[] arr, int start, int end) {
// 选取分区点
int p = pivot(arr, start, end - 1);
// 将分区点放到最前面
swap(arr, start, p);
int ref = arr[start];
int lp = start;
// 将数据与分区点对比,分成小于和大于2部分
for (int i = lp + 1; i < end; i++) {
if (arr[i] <= ref) {
swap(arr, ++lp, i);
}
}
// 将分区点放到它最终的位置
swap(arr, start, lp);
return lp;
}

/**
* 选择分区点(选择三个点的中值位置)
*
* @param arr 数组
* @param i [i, j]
* @param j [i, j]
* @return 分区点索引
*/
private int pivot(int[] arr, int i, int j) {
int mid = i + (j - i) / 2;
if (arr[i] < arr[j]) {
return arr[mid] > arr[i] ? mid : j;
} else {
return arr[mid] < arr[i] ? mid : i;
}
}

}
作者

jiaduo

发布于

2022-07-16

更新于

2023-10-06

许可协议