一、思想
1 总体思想
对数组进行 分区,选定一个元素作为 基准元素,然后将 小于基准元素的元素 放在 基准元素的左边,将 大于基准元素的元素 放在 基准元素的右边,分区完成之后,再对 基准元素左边的元素 和 其右边的元素 分别进行分区,直到不可再分。该算法采用了 分治 的思想,使排序速度加快。
2 分区 ( 分治 )
- 首先,选取 区间中的某个元素 作为 基准元素,并将其放到 区间的第一个元素的位置。
- 然后,使用两个指针 (此处的指针其实是数组元素的索引) 寻找不同元素,在寻找的过程中,左指针不能超过右指针:
- 右指针 从区间尾部向区间头部(逆序) 寻找第一个 小于 基准元素 的元素
- 左指针 从区间头部向区间尾部(顺序) 寻找第一个 大于 基准元素 的元素
- 接着,交换 左、右指针 指向的元素。
- 重复步骤二和步骤三,直到左指针超过右指针,此时就完成了 将小于基准元素的元素放到一起、将大于基准元素的元素放到一起 的操作,区间按这种形式排序:
[基准元素, 小于基准的元素们, 大于基准的元素们]
。 - 最后,交换 基准元素 与 最后一个小于基准元素的元素 的位置。从而将区间按这种形式排序:
[小于基准的元素们, 基准元素, 大于基准的元素们]
。
3 选取基准元素
实际上,选取基准元素 也不是一个简单的事,有以下两种策略:
策略一
直接将 待排序区间的第一个元素 作为基准。但这样简单的实现有一个缺点:如果待排序区间的值从大到小排列,则每次分区的左区间都为空,右区间是 除已有的基准元素们外的 所有剩余元素,这样就会使快速排序降级为冒泡排序,极大地降低了排序速度。
策略二
找 待排序区间中 第一个元素、最后一个元素 和 中间元素 的中位数。这样就避免了 基准元素极度不均衡 的问题,就算这三个数分别是 区间的最大值、第二大值、第三大值,也能选出一个第二大值来,从而左区间就不可能为空了。寻找中位数可以使用 异或 这个逻辑运算符,可以看看这篇文章:寻找中位数。
二、举例
由于快速排序比较难理解,所以这里使用 第一种选取基准元素的策略 举一个例子,对于待排序数组 arr = [3, 4, 2, 9, 8, 7, 6]
,快速排序的流程如下:
第一轮分区:待排序区间为
0~6
(0
和6
都是arr
的下标),即[3, 4, 2, 9, 8, 7, 6]
- 第一步:选取
3
为基准元素- 第四步完成后,区间为
[3, 2, 4, 9, 8, 7, 6]
- 第五步交换 基准元素
3
与 元素2
的位置,区间为[2, 3, 4, 9, 8, 7, 6]
此时
arr = [2, 3, 4, 9, 8, 7, 6]
第二轮分区:待排序区间为
0~0
和2~6
,即[2]
和[4, 9, 8, 7, 6]
- 对于区间
0~0
,即[2]
- 第一步:选取
2
为基准元素- 第四步完成后,区间为
[2]
- 第五步交换 基准元素
2
与 元素2
的位置,区间为[2]
- 对于区间
2~6
,即[4, 9, 8, 7, 6]
- 第一步:选取
4
为基准元素- 第四步完成后,区间为
[4, 9, 8, 7, 6]
- 第五步交换 基准元素
4
与 元素4
的位置,区间为[4, 9, 8, 7, 6]
此时
arr = [2, 3, 4, 9, 8, 7, 6]
第三轮分区:待排序区间为
0~-1
和1~0
、2~1
和3~6
,前三个区间都为空,只有最后一个区间有元素,元素为[9, 8, 7, 6]
- 第一步:选取
9
为基准元素- 第四步完成后,区间为
[9, 8, 7, 6]
- 第五步交换 基准元素
9
与 元素6
的位置,区间为[6, 8, 7, 9]
此时
arr = [2, 3, 4, 6, 8, 7, 9]
第四轮分区:待排序区间为
3~5
和7~6
,第二个区间为空,第一个区间的元素为[6, 8, 7]
- 第一步:选取
6
为基准元素- 第四步完成后,区间为
[6, 8, 7]
- 第五步交换 基准元素
6
与 元素6
的位置,区间为[6, 8, 7]
此时
arr = [2, 3, 4, 6, 8, 7, 9]
第五轮分区:待排序区间为
3~2
和4~5
,第一个区间为空,第二个区间的元素为[8, 7]
- 第一步:选取
8
为基准元素- 第四步完成后,区间为
[8, 7]
- 第五步交换 基准元素
8
与 元素7
的位置,区间为[7, 8]
此时
arr = [2, 3, 4, 6, 7, 8, 9]
第六轮分区:待排序区间为
4~4
和6~5
,第二个区间为空,第一个区间的元素为[7]
- 第一步:选取
7
为基准元素- 第四步完成后,区间为
[7]
- 第五步交换 基准元素
7
与 元素7
的位置,区间为[7]
此时
arr = [2, 3, 4, 6, 7, 8, 9]
第七轮分区:待排序区间为
4~3
和5~4
,两个区间都为空,排序完成,最终arr = [2, 3, 4, 6, 7, 8, 9]
。
三、代码实现
1 使用 C 语言实现
// 寻找中位数
int medianThree(int arr[], int left, int mid, int right) {
int a = arr[left], b = arr[mid], c = arr[right];
if ((a < b) ^ (a < c)) {
return left;
} else if ((b < a) ^ (b < c)) {
return mid;
} else {
return right;
}
}
// 交换元素
void swap(int arr[], int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
// 分区
void partition(int arr[], int left, int right) {
if (left >= right) { // 如果子区间长度为0或为负数,则退出递归
return;
}
int p = medianThree(arr, left, (left + right) >> 1, right); // 寻找基准的索引
swap(arr, left, p); // 让基准成为区间左端点
int i = left + 1, j = right;
while (i < j) {
while (arr[j] >= arr[left] && i < j) { // 寻找小于基准的元素
j--;
}
while (arr[i] <= arr[left] && i < j) { // 寻找大于基准的元素
i++;
}
swap(arr, i, j); // 交换这两个元素
}
swap(arr, left, i); // 让基准成为区间的分界线,i指向最后一个小于基准的元素
partition(arr, left, i - 1); // 对左子区间进行分区
partition(arr, i + 1, right); // 对右子区间进行分区
}
// 快速排序
void quickSort(int arr[], int len) {
partition(arr, 0, len - 1);
}
2 使用 Java 语言实现
class Sort {
public static void sort(int[] arr) {
partition(arr, 0, arr.length - 1);
}
private static void partition(int[] arr, int left, int right) {
if (left >= right) { // 如果子区间长度为0或为负数,则退出递归
return;
}
int p = medianThree(arr, left, (left + right) >> 1, right); // 寻找基准的索引
swap(arr, left, p); // 让基准成为区间左端点
int i = left + 1, j = right;
while (i < j) {
while (arr[j] >= arr[left] && i < j) { // 寻找小于基准的元素
j--;
}
while (arr[i] <= arr[left] && i < j) { // 寻找大于基准的元素
i++;
}
swap(arr, i, j); // 交换这两个元素
}
swap(arr, left, i); // 让基准成为区间的分界线,i指向最后一个小于基准的元素
partition(arr, left, i - 1); // 对左子区间进行分区
partition(arr, i + 1, right); // 对右子区间进行分区
}
private static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
private static int medianThree(int[] arr, int left, int mid, int right) {
int a = arr[left], b = arr[mid], c = arr[right];
if ((a < b) ^ (a < c)) {
return left;
} else if ((b < a) ^ (b < c)) {
return mid;
} else {
return right;
}
}
}