前言
排序作用的重要性是不言而喻的,例如成绩的排名、预约时间的先后顺序、不同路程的消耗与利润等。快排可以实现O(n * logn)
的时间复杂度,O(logn)
的空间复杂度来实现排序【虽然结果是不稳定的】。
算法思想
快速排序实际上是采用分治的思想,每次迭代在当前区间中选取一个数作为哨兵,通过一系列的交换操作将当前区间分为左区间和右区间【使得左区间的值全部小于等于哨兵,右区间的值全部大于等于哨兵】。然后再对左区间、右区间执行这种划分区间的策略操作,当区间的长度为1时停止。等到所有分治的区间长度都为1时,此时的原数组就已经是一个排好序的数组了。
具体步骤
假设数组名称为q
,具体步骤如下:
- 如果区间长度小于等于1了,则结束循环。否则执行下一步。
- 先从本区间中取出第一个数作为哨兵
mid
,即令mid
等于本区间最左端元素的值。执行下一步。 - 令
i
等于本区间最左端元素在原数组中的下标 ,j
等于本区间最右端元素在原数组中的下标。执行下一步。 - 判断
i < j
是否成立,如果满足,则执行下一步。否则跳转到第9点
。 - 判断
q[j] >= mid && i < j
是否成立,如果满足,则j
向左移动一位【j--
】,再次执行本轮
【即本次步骤是循环】。否则执行下一步
。 - 令
q[i] = q[j]
。执行下一步。【目的:进行元素移动,保证右区间的值都是大于等于哨兵的值,此时j
右侧的值都不小于哨兵的值,且一定会有下一步来使得q[j]
的值大于等于哨兵的值】 - 判断
q[i] <= mid && i < j
是否成立,如果满足,则i
向右移动一位【i++
】,再次执行本轮
【即本次步骤是循环】。否则执行下一步
- 令
q[j] = q[i]
。跳转到第4点
。【目的:进行元素移动,保证左区间的值都是小于等于哨兵的值,此时i
左侧的值都不大于哨兵的值,且一定会有下一步来使得q[i]
的值小于等于哨兵的值】【第4点~第8点是一轮大循环】 - 令
q[i] = mid
。执行下一步。【循环结束后,i
的位置即是哨兵的位置,此时令q[i] = mid
即可。这一步操作保证了第6、8点
担忧的地方,即这里一定可以使得最终的q[i]、q[j]
等于哨兵的值。】 - 划分两个区间【
本区间左端点,i - 1
】,【i + 1, 本区间右端点
】,将这两个区间再次执行第一步
的操作。【整个步骤是快排的分治操作的循环】
图表演示
假设我们拥有一个数组:a
,长度为:5
,内容为:3 1 2 4 5
,需要对其进行从小到大排序。则流程为:
第一次递归:
此时数组为:3 1 2 4 5
基础数据:
l = 0
:本轮区间左边界在数组中的下标r = 4
:本轮区间右边界在数组中的下标mid = a[l] = 3
:哨兵的值i = l = 0
:左指针j = r = 4
:右指针
初始化数据:
3 | 1 | 2 | 4 | 5 |
---|---|---|---|---|
i、l | j、r | |||
哨兵【3】 |
- 此时
q[j] = 5 > 哨兵
,满足右指针移动条件,右指针左移。
3 | 1 | 2 | 4 | 5 |
---|---|---|---|---|
i、l | j | r | ||
哨兵【3】 |
- 此时
q[j] = 4 < 哨兵
,满足右指针移动条件,右指针左移。
3 | 1 | 2 | 4 | 5 |
---|---|---|---|---|
i、l | j | r | ||
哨兵【3】 |
- 此时
q[j] = 2 > 哨兵
,不满足右指针移动条件,进行元素移动【保证j
右侧的值都大于哨兵的值】,接下来进行左指针移动
2 | 1 | 2 | 4 | 5 |
---|---|---|---|---|
i、l | j | r | ||
哨兵【3】 |
此时 q[l]
的值不见了,但是!!!我们的哨兵存的就是 q[l]
的值,在最后 q[l]
的值会回到数组中,故一个元素的值都不会少。
- 此时
q[i] = 2 < 哨兵
,满足左指针移动条件,左指针右移。【第一次交换左右指针移动时左指针条件一定满足,因为此时q[i]
的值是刚才q[j]
的值,而刚才的q[j]
是一定小于哨兵的值】
2 | 1 | 2 | 4 | 5 |
---|---|---|---|---|
l | i | j | r | |
哨兵【3】 |
- 此时
i = j
,循环条件结束,此时左右指针都不会再移动了,则执行q[i] = q[j]
和q[j] = q[i]
是没有意义的,因为此时i = j
。
2 | 1 | 2 | 4 | 5 |
---|---|---|---|---|
l | i、j | r | ||
哨兵【3】 |
- 此时循环结束,令
q[i] = mid
。
2 | 1 | 3 | 4 | 5 |
---|---|---|---|---|
l | i、j | r | ||
哨兵【3】 |
即此时 q[l]
的值回到数组中了,故数组中的一个元素的值都没有少。
- 划分两个新的区间
l, i - 1
、i + 1, r
。对这两个新区间进行递归处理。
第二轮递归
本轮的数组为:2 1
【上一轮递归处理后得到的左区间】
基础数据:
l = 0
:本轮区间左边界在数组中的下标r = 1
:本轮区间右边界在数组中的下标mid = a[l] = 2
:哨兵的值i = l = 0
:左指针j = r = 1
:右指针
初始化数据:
2 | 1 |
---|---|
i、l | j、r |
哨兵【2】 |
- 此时
q[j] = 1 < 哨兵
,不满足右指针移动条件,进行元素移动【保证j
右侧的值都大于哨兵的值】,接下来进行左指针移动。
1 | 1 |
---|---|
i、l | j、r |
哨兵【2】 |
- 此时
q[i] = 1 < 哨兵
,满足左指针移动条件,左指针右移。【第一次交换左右指针移动时左指针条件一定满足,因为此时q[i]
的值是刚才q[j]
的值,而刚才的q[j]
是一定小于哨兵的值】
1 | 1 |
---|---|
l | i、j、r |
哨兵【2】 |
- 此时
i = j
,循环条件结束,此时左右指针都不会再移动了,则执行q[i] = q[j]
和q[j] = q[i]
是没有意义的,因为此时i = j
。
1 | 1 |
---|---|
l | i、j、r |
哨兵【2】 |
- 此时循环结束,令
q[i] = mid
。
1 | 2 |
---|---|
l | i、j、r |
哨兵【2】 |
- 划分两个新的区间
l, i - 1
、i + 1, r
。对这两个新区间进行递归处理。
接下来迭代的流程同上,不再演示。
实现代码:
cpp
#include<stdio.h>
// 定义一个常量N,用来修饰数组的长度
#define N 100007
// 定义一个数组a,用来接受输入的数据
int a[N];
// 进行快速排序
void quickSort(int q[], int l, int r)
{
// 当前区间的长度小于等于1时停止循环
if (l >= r) return;
// 创建哨兵 mid
int mid = q[l];
// 创建i,j指针进行移动
int i = l, j = r;
// 进行区间数字交换,使得左侧区间全小于等于mid,右侧区间全大于等于mid
while (i < j)
{
// j指针从右向左移动,至到遇到第一个小于哨兵的值
while (q[j] >= mid && i < j) j--;
// 将该值移动到左区间中
q[i] = q[j];
// i指针从左向右移动,至到遇到第大个小于哨兵的值
while (q[i] <= mid && i < j) i++;
// 将该值移动到右区间中
q[j] = q[i];
}
// 交换结束后此时i,j指针指向的同一个位置,即哨兵应该放的位置
// 而左区间已经是全部小于等于哨兵的值,右区间已经是全部大于等于哨兵的值了。
q[i] = mid;
// 对划分出来的左右区间的再一次进行快排
quickSort(q, l, i - 1);
quickSort(q, i + 1, r);
}
int main()
{
int n; //要排序的数据量个数
scanf("%d", &n);
// 按顺序输入每一个数字
for (int i = 0; i < n; i++)
{
scanf("%d", &a[i]);
}
// 进行快速排序
quickSort(a, 0, n - 1);
// 按顺序输入排序后的数组内容
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
return 0;
}
总结分析:
- 为什么是先移动
j
,而不是先移动i
:因为哨兵等于q[i]
,那么先移动i
,则此时q[r]
的数据是没人保存的,如果发生交换了q[j] = q[i]
之后,实际上q[r]
的值就不见了。但如果先移动j
,由于哨兵的值是mid
,那么就算发生了交换q[i] = q[j]
,而q[l]
的值还是存在的,即哨兵的值。 - 记住:哨兵存的数组中的值,而不是下标,他并不是一个抽象的内容,他实际上就是
q[某个下标]
,而这个元素也会发生移动。即最开始哨兵的位置是在q[l]
,而最后哨兵已经被移动到q[i]
了。 - 下一次迭代选中区间
l, i - 1
,i + 1, r
。不包含i
是因为i
这个位置已经是哨兵了,不需要再进行排序了,他的位置一定是这个地方。 - 初级版【或者说通用版本】的快速排序大部分情况下是可以使用的,但是效率并不能达到快速排序的预期值,比如该链接中的的题目是不能通过:活动 - AcWing 。需要将排序的步骤进行优化,才能达到真正快速排序的预期。【可参考下一篇链接】