快速排序的基本思想是通过一趟排序将一个数组分割成两个部分,随意取数组中的一个元素作为分割两个部分的标准,使左部分的元素均大于(小于)该元素,右部分的元素均小于(大于)该元素
1.快排的实现步骤
现在我们利用快排使元素从小到大排序
为了形成两个部分,我们使用到两个变量模拟指针i、j,分别指向数组的开头和数组的结尾。利用循环使i不断向下遍历数组,直到遇到大于分割元素的数组元素时i停止变化;同样地使j向上遍历数组,直到遇到小于分割元素的数组元素时则停止。此时i和j就分别记录了与分割的两部分的标准不相同的元素,我们将记录的这两个数字互换。直到i和j不断靠近至重合于一个元素或者二者交叉
我们再在快排函数中使用递归,使原数组不断地分割变成越来越多的两个部分直到每个部分均是单个元素,此时完成排序
2.快排的代码实现
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int q[N];
void quick_sort(int q[], int left, int right) {
if (left >= right) return;//如果左边界等于右边界或大于右边界表示已经分割到一个元素一个部分了,此时排序结束
int middle = (left + right) >> 1;//取位置较为中间的元素
int x = q[middle];
int i = left-1, j = right+1;
while (i < j) {
do i++; while (q[i] < x);//如果i指向的元素小于x就一直遍历直到i指向的元素大于x,并且此时保留i的指向
do j--; while (q[j] > x);//如果j指向的元素大于x就一直遍历直到j指向的元素小于x,并且此时保留j的指向
if (i < j) swap(q[i], q[j]);//如果i、j不重复且不交叉,交换i、j指向的元素
}
//把数组分割后的每个部分再度进行分割,直至left>=right
quick_sort(q, left, j);
quick_sort(q, j + 1, right);
}
int main() {
int n, l, r;
cin >> n;
for (int i = 0; i < n; i++) cin >> q[i];
l = 0, r = n - 1;
quick_sort(q, l, r);
for (int i = 0; i < n; i++) cout << q[i] << " ";
return 0;
}
3.关于递归时分界点的确立
再度分割时的表达式可以具有两种表达方式
quick_sort(q, left, j);
quick_sort(q, j + 1, right);quick_sort(q, left, i-1);
quick_sort(q,i, right);
学到这里我对i和j点的分割很不理解,理解第一种方法时想到j是属于大于分割元素的那一部分,所以不理解为什么会把j当作左边部分的边界点,我觉得应该是把j-1当作边界点的。想了很久终于想通,经过do…while循环后j指向大于分割元素的数组元素,但是在swap交换后,j所指向的元素立刻变成了小于分割元素的数组元素,所以边界点应该为j
i作为边界点时同样可以依据上述的理解推断得到
4.i、j变换的条件
while (i < j) {
do i++; while (q[i] < x);
do j–; while (q[j] > x);
q[i]/q[j]与x的比较也是实现上述代码容易出现问题的一个易错点
(1)i、j增加方面可能出现的问题:
我自己写这个代码时由于对while循环的代码更加熟悉,于是没有使用do…while,只使用了while,所以写出了这样的代码
while(q[i]<x) i++;
while(q[j]>x) j–;
当时一直很疑惑为什么输出不了结果,后面发现如果q[i]和q[j]都同时等于x时,i和j均不会更新,此时代码陷入死循环
但是由于do…while结构的特殊性,无论是否满足条件,i、j均会自增,所以不会出现上述问题
(2)数组越界问题
正是由于do…while结构的特殊性,无论是否满足条件i、j均会自增,所以我们就要开始考虑数组越界问题了
我认为如果我分割两个部分的时候把x也包含进去的话是否仍能实现快排的功能,我写下了q[i]<=x和q[j]>=x
程序报段错误,假设所有元素都相等,由于do…while先自增再判断的特性,i会自增至数组后一位,使数组越界
以上我用于记录我在学习快排时无法Accept遇到的各种各样的问题,用于记录我学习快排的过程中的收获以及踩到的各种坑