1 基本思路
快排的思想是基于分治法,其思路是:
- 确定分界点:给定要排序的数组
q
,先在数组中找一个数作为分界点值x
,分界点可以取左边界值x = q[l]
,可以取右边界值x = q[r]
,可以取数组中间的数x = q[l + r >> 1]
,也可以随机取一个。 - 调整区间:将数组划分成两个区间,使得左半部分所有的数都
<= x
,右半部分所有的数都>= x
。 - 递归处理左右两个区间。
快排有多种实现方法,在y总的模板里,分界点的位置不一定是x
,因为x
参与交换之后仍然会被留在左右区间中的一个里。
2 指针移动的判断不带等号
使用两个指针i
和j
分别指向要处理的区间的左右两侧,每次向中间移动。只要q[i] < x
成立就说明i
位置的数在左侧区间是合理的,所以i ++
,直到q[i] >= x
停下来。接下来去移动j
,只要q[j] > x
成立就说明j
位置的数在右侧区间是合理的,所以j --
,直到q[j] <= x
停下来。
这里考虑一个边界问题,为什么移动i
和j
指针的条件是q[i] < x
和q[j] > x
,而不是q[i] <= x
和q[j] >= x
?因为如果选取的x
是数组里最大的数,那么一直都满足q[i] <= x
,所以i
会一直++
发生越界都不会停下来。同理,如果选取的x
是数组里最小的数,那么一直都满足q[j] >= x
,所以j
会一直--
发生越界都不会停下来。
反例数据:
3
1 3 2
说明:
第一轮时选取的3
是数组里最大的,所以在第一轮时i
就会一直++
发生越界。
3 在交换前检查指针相对位置
当两个指针都停下来之后,这一对数都是错位的,所以把它们交换一下,交换完成之后q[i] < x
并且q[j] > x
,下一轮就可以让i
和j
(只要满足i < j
)继续对向移动了。
这里考虑一个边界问题,试想q[i]
和q[j]
在i == j - 1
时停下来做交换的场景,交换完成之后i
和j
会各自前进(i ++, j --
)一步,形成i > j
(具体是i == j + 1
)的不合法局面,这时候就不应该做交换了,所以在swap
之前需要再判断一次i < j
。
反例数据:
3
3 1 2
说明:
交换完3
和1
后形成1 3 2
的局面,i ++
变成1
指向元素3
,j --
变成0
指向元素1
,没有做检查指针相对位置的判断,又发生了一次交换,恢复成3 1 2
的局面。
4 使用do-while而不是while循环
指针i
和j
初始化为数组两侧外一个元素,即i = l - 1
,j = r + 1
,然后在数组中使用do-while循环每次先进行一次指针的移动,再去看循环条件。
这里考虑一个边界问题,为什么不能让i = l
和j = r
然后使用while循环代替do-while循环?因为如果数组中存在重复的数字,那么某一轮可能存在i
和j
都指向重复的数字,并且划分数字x
也是这个数字,那么while (q[i] < x)
和while (q[j] > x)
判断不成立不会进入,又因为q[i] = q[j] = x
,交换它们之后这个局面仍然不会改变,所以要使用do-while循环,确保每次两个指针都至少会移动一步,以保证上一次交换的结果能被走掉。
反例数据:
3
2 1 2
说明:
在i
指向2
、j
指向2
并且x = 2
的一次处理中,一直重复发生这两个2
的交换。
5 选取数组中间的数字作为划分值
与其选取左右两端的数作为划分值,不妨选取数组中间的数字。
这里考虑一个边界问题,如果数组已经是有序的了,那么选取数组开头或者结尾的数字就意味着每次将长度为 n n n的数组划分成了长度为 1 1 1和 n − 1 n-1 n−1的两段,这时候快速排序递归处理的子问题规模还是这么大,总的时间复杂度就达到了 O ( n 2 ) O(n^2) O(n2),为了避免这种情况,要选取数组中间的数字作为划分值。
反例数据:
任意已排序的数组
说明:
只影响时间,不影响正确性。
6 使用[l, j]
作为区间左半边而不是[l, i]
在快排一轮的处理结束后,递归处理的两个子区间应该是[l, j]
、[j + 1, r]
而不是[l, i]
、[i + 1, r]
。
这里考虑一个边界问题,试想q[i]
和q[j]
在i == j - 1
时停下来做交换的场景,交换完成之后i
和j
会各自前进(i ++, j --
)一步,形成i > j
(具体是i == j + 1
)的不合法局面。在这个局面下,满足性质<= x
的区间是[l, j]
而不是[l, i]
,因此划分的两个区间是[l, j]
、[j + 1, r]
。
反例数据:
3
3 1 2
说明:
第一轮时选取的x = 1
,在3
和1
发生交换后,i ++
变成1
,j --
变成0
,满足性质<= 1
的区间应当是[0, j = 0]
,而不是[0, i = 1]
。
7 模板题
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int q[N];
void quick_sort(int q[], int l, int r) {
if (l >= r)
return ;
int x = q[l + r >> 1];
int i = l - 1, j = r + 1;
while (i < j) {
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j)
swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
int main() {
cin >> n;
for (int i = 0; i < n; i ++ ) cin >> q[i];
quick_sort(q, 0, n - 1);
for (int i = 0; i < n; i ++ ) cout << q[i] << ' ';
return 0;
}
AcWing 786 第k个数(快速选择,k缩减版)
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int quick_sort(int q[], int l, int r, int k) {
if (l >= r) return q[l];
int i = l - 1, j = r + 1;
int x = q[i + j >> 1];
while (i < j) {
do i ++; while (q[i] < x);
do j --; while (q[j] > x);
if (i < j)
swap(q[i], q[j]);
}
if (j - l + 1 >= k)
return quick_sort(q, l, j, k);
else
return quick_sort(q, j + 1, r, k - (j - l + 1));
}
int main() {
int n, k;
cin >> n >> k;
for (int i = 0; i < n; i ++)
cin >> a[i];
cout << quick_sort(a, 0, n - 1, k);
return 0;
}
LeetCode 215 数组中的第k个最大元素(快速选择,k不变版)
class Solution {
public:
int quick_sort(vector<int>& nums, int l, int r, int k) {
if (l >= r) return nums[k]; // 因为这种写法k在子问题里不变所以这里还是nums[k]
int x = nums[l + r >> 1];
int i = l - 1, j = r + 1;
while (i < j) {
do i ++ ; while (nums[i] > x);
do j -- ; while (nums[j] < x);
if (i < j) swap(nums[i], nums[j]);
}
// 注意因为这种写法k在子问题里不变,这里的k要直接和j比较,与l无关
if (k <= j) return quick_sort(nums, l, j, k);
else return quick_sort(nums, j + 1, r, k);
}
int findKthLargest(vector<int>& nums, int k) {
return quick_sort(nums, 0, nums.size() - 1, k - 1); // 第k个=>有序时下标为k-1
}
};
注意,k不变版和k缩减版的选择,与使用的是数组还是vector
无关,只和在子问题里是否选择去改变k有关系,这个完全看个人喜好了,我喜欢k缩减版。
8 Java实现
import java.util.Scanner;
public class Main {
static int[] a;
public static void quick_sort(int[] q, int l, int r) {
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j) {
do i++; while (q[i] < x);
do j--; while (q[j] > x);
if (i < j) {
// 注意Java没法实现swap函数
int t = q[i];
q[i] = q[j];
q[j] = t;
}
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
a = new int[n];
for (int i = 0; i < n; i++)
a[i] = scanner.nextInt();
quick_sort(a, 0, n - 1);
for (int i = 0; i < n; i++)
System.out.print(a[i] + " ");
System.out.println();
}
}