1. 概念
快速排序的核心是分治思想,且还利用到了递归思想,每次确定原数组中的一个中间数,使得其左边数全小于右边数,依次再对左右两边进行递归,直到原数组完全有序为止。
后续介绍均采用递增排序
2. 步骤
- 确定分界点:确定一个中间数(记为
X),这个数可以是数组首元素,也可以是数组末元素,以及数组中的任意一个元素都可以。 - 调整区间:对数组进行局部排序,即将所有小于
X的元素放在X的左边,所有大于等于X的元素放在元素右边。那么此时也使得X位于最终位置,并且X左边的元素全部小于X,X右边的元素全部大于等于X。 - 递归处理:对
X左边的子数组和X右边的子数组分别重复上述操作。 - 将所有子数组进行排序后以及回归,即可确定原数组整体有序。
3. 重难点
不难发现,上述步骤中的步骤一、步骤三、步骤四其实都非常简单,主要重难点是步骤二,如何将比X小的数放到X的左边以及将比X大的数放在X的右边。
这里介绍两种方法:
3.1. 暴力法
利用两个额外数组,分别记为a数组和b数组,对原数组进行遍历,将小于X的数存入a数组,将大于等于X的数存入b数组。从原数组首地址位置开始,对a数组进行遍历,开始依次存入原数组,再将X存入原数组,最后再将b数组进行遍历依次填入原数组。此时便符合步骤二的最终结果。
但是这个方法也很容易发现,在每一趟子排序中,需要利用到O(n)的空间复杂度,并且需要对原数组进行两次遍历,即O(2n)的时间复杂度,效率极其之低。
3.2. 双指针
令X为原数组首元素,利用两个额外指针,头指针i指向原数组首元素,尾指针j指向数组尾元素,从原数组的尾部开始遍历,找到第一个小于X的元素,将该元素存入i所指的位置,然后i依次向后遍历,找到第一个大于等于X的元素;重复此操作,直到i等于j为止,此时将X存入i或j所占的位置即可。
在每一趟子排序中,利用双指针仅需要O(1)的空间复杂度,并且只需要O(n)的时间复杂度,相比于暴力法效率快了很多。
4. 代码模板
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) swap(q[i], q[j]);
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}
5. 例题
5.1. 785. 快速排序 - AcWing题库
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int q[N];
void quick_sort(int q[], int l, int r)
{
if(l >= r) return;
int x = q[l];
// 或 int x = q[(l + r) / 2];
int i = l - 1;
int 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]); // swap是交换两个数的函数
}
quick_sort(q, l, j);
// 或 quick_sort(q, l, i - 1);
quick_sort(q, j + 1, r);
// 或 quick_sort(q, i, r);
}
int main()
{
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", &q[i]);
quick_sort(q, 0, n - 1);
for(int i = 0; i < n; i++) printf("%d", q[i]);
return 0;
}
5.2. 786. 第k个数 - AcWing题库
#include <iostream>
const int N = 100010;
int k;
int n;
int q[N];
int quick_sort(int q[], int l, int r)
{
if(l >= r) return q[l];
int x = q[l + r + 1 >> 1];
int i = l - 1;
int j = r + 1;
while(i < j)
{
do i++; while(q[i] < x);
do j--; while(q[j] > x);
if(i < j)
{
int tmp = q[i];
q[i] = q[j];
q[j] = tmp;
}
}
if(i >= k) return quick_sort(q, l, i-1);
return quick_sort(q, i, r);
}
int main()
{
scanf("%d", &n);
scanf("%d", &k);
for(int i = 0; i < n; i++) scanf("%d", &q[i]);
printf("%d", quick_sort(q, 0, n - 1));
return 0;
}
6. 注意点
当使用quick_sort(q, l, j); quick_sort(q, j + 1, r);时,x不能取int x = q[r];,否则会导致边界问题,使得递归一直重复下去;
同理,当使用quick_sort(q, l, i - 1); quick_sort(q, i, r);时,x不能取int x = q[l];,否则会导致边界问题,也会使得递归一直重复下去。
例如:区间为0-1时,如果x取0(即最左端点).那么后续每次右递归都是每次0-1,因此会陷入无限的死循环。
7. 复杂度分析
元素组长度为N的情况下,当利用到了递归操作,需要考虑由长度为N的元素,所产生的二叉树的高度即lognN,空间复杂度为O(logN),而时间复杂度为O(NlogN),共进行logN趟排序,而每趟排序的时间复杂度为O(N)。