题目
代码实现快速排序,将线性表a按照从小到大的顺序进行排序。
(*注意:此算法中用到了递归,关于实现递归的细节思想先不在此讨论)
初步分析实现
首先梳理快速排序算法的基本思想:
- 每趟选择一个元素作为枢轴(pivot)
- 每趟扫描的目标是将数组元素整理为 {<枢轴}{枢轴}{>枢轴} 这样的三部分,这样枢轴元素的位置就调整为了排序完成时的最终位置
- 对 {<枢轴} 和 {>枢轴} 这两部分递归进行同样的扫描
- 随着扫描的进行,每部分的元素个数不断减小,当元素个数减小为1时,表示已经到达递归的终点
本文中枢轴的选择策略——每次选择范围内最左侧元素。
另外,为了维持算法的稳定性,将=pivot
的元素看作>pivot
的元素,保持其一直待在枢轴元素的右侧,这样至少大小为pivot
的元素是稳定的。
理想中一趟扫描前后对比:
[灰色格子为枢轴元素,此处元素的ab下标是为了区分相同大小的元素,用于检测算法的稳定性]
扫描具体过程(用双指针实现)
初始化:
双指针开始执行扫描操作:
双指针继续执行:
以上就是当前策略下一趟排序的过程,具体实现如下:
测试版实现
void quickSort(vector<int>& a, int left, int right){
// 元素范围为[left, right],left>=right时无需调整
if(left >= right)
return ;
// [left, right]区间内元素个数大于1时,进行调整
int i = left, j = right;
// 选择最左边的元素作为pivot
int pivot = a[left];
while (i < j){
// 尝试尽量保持算法稳定性,使>pivot与=pivot等价
while (i < j && a[j] >= pivot)
j--;
a[i++] = a[j];
// <pivot作为第二种情况
while (i < j && a[i] < pivot)
i++;
a[j--] = a[i];
}
// 将pivot归位
a[i] = pivot;
// 递归扫描区间[left, i) (元素值<=pivot)
quickSort(a, left, i - 1);
// 递归扫描区间(i, right] (元素值>=pivot)
quickSort(a, i + 1, right);
}
分析改进
- 从上面的例子可以看到算法依旧是不稳定的(数组
⟸
\Longleftarrow
⟸调整时类似于数组逆置),因此可以放弃一开始为了保持算法稳定性的操作,追求尽可能快地进行一趟扫描——两指针都不对
=pivot
的元素进行移动操作 - 上面例子中出现了当
i==j
时对两元素进行交换的情况,可以添加条件避开(挖个坑,条件判断的时间消耗是不是真的<i==j
时数组交换的时间消耗)
最终版
void quickSort(vector<int>& a, int left, int right){
// 元素范围为[left, right],left>=right时无需调整
if(left >= right)
return ;
// [left, right]区间内元素个数大于1时,进行调整
int i = left, j = right;
// 选择最左边的元素作为pivot
int pivot = a[left];
while (i < j){
// 追求更快地完成一趟扫描,略过=pivot
while (i < j && a[j] >= pivot)
j--;
// 对数组赋值添加前提条件
if(i < j)
a[i++] = a[j];
// 同样略过=pivot
while (i < j && a[i] <= pivot)
i++;
if(i < j)
a[j--] = a[i];
}
a[i] = pivot;
quickSort(a, left, i - 1);
quickSort(a, i + 1, right);
}
总结分析
以上就是经典的快速排序,下面对比一下优化前后一趟扫描的过程(右侧为优化后):
再看一下排序的整体过程(优化版):
[红色代表元素已经处在最终位置。灰色表示此趟扫描选择的枢轴]