【排序】快速排序
对于C++,快速排序(quicksort)历史上一直是实践中已知最快的泛型排序算法,其平均运行时间是O(N log N)。
算法描述
像归并排序一样,快速排序也是一种分治的递归算法。
使用下面简单的排序算法将一个表进行排序作为开始。随意选取表中任一项,则表中元素此时形成3组:比所选项小的一组、等于所选项的一组以及大于所选项的元素的一组。递归地将第1组和第3组排序,然后再将这3组联结起来。递归的基本原则保证所得结果就是原始初表的有序排列。
上述描述构成了快速排序的基础,下面描述快速排序最普遍的实现——“经典快速排序”,此时的输入是一个数组,而且该算法并没有创建任何附加的数组。
将数组S排序的经典快速排序算法由下列简单的4步组成:
- 如果S中元素个数是0或1,则返回。
- 取S中任一元素v,称之为枢纽元(pivot)。
- 将S - {v}(即S中其余元素)划分成两个不相交的集合:S1={ x∈ S - {v} |x≤v}, S2={ x∈ S - {v} |x≥v}。
- 返回 { quicksort(S1),后跟v,继而再quicksort(S2) } 。
以图例说明快排的各步
枢纽元的选取
虽然上面描述的算法无论选择哪个元素作为枢纽元都能完成排序工作,但有些选择明显优于其他的选择。
一种安全的方针是随机选取枢纽元。
三数中值分割法(Median-of-Three Partitioning)
一组N个数的中值(median)是第⌈N/2⌉个最大的数。枢纽元的最好的选择是数组的中值。但这很难算出且明显减慢快速排序的速度。一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。例如,输入为8,1,4,9,6,3,7,5,2,0,它的左端元素为8,右端元素为0,中心位置(⌊(left+right)/2⌋)上的元素为6,于是枢纽元则是v=6。
分割策略
第一步是通过将枢纽元与最后的元素交换使得枢纽元离开要被分割的数据段。i 从第一个元素开始而 j 从倒数第二个元素开始。下面的图表表示了当前的状态。
暂时假设所有元素互异。分割阶段要做的就是把所有小元素移到数组的左边而把所有大元素移到数组的右边,当然,“小”和“大”是相对于枢纽元而言的。
当 i 在 j 的左边时,我们将 i 右移,移过那些小于枢纽元的元素,并将 j 左移,移过那些大于枢纽元的元素。当 i 和 j 停止时,i 指向一个大元素而 j 指向一个小元素。如果 i 在 j 的左边,那么将这两个元素互换,其效果是把一个大元素推向右边而把一个小元素推向左边。
在上面例子中,i 不移动,而 j 滑过一个位置,情况如下图。
然后我们交换由 i 和 j 指向的元素,重复该过程直到 i 和 j 彼此交错为止。
此时,i 和 j 已经交错,故不再交换。分割的最后一步是将枢纽元与 i 所指向的元素交换。
在最后一步当枢纽元与 i 所指向的元素交换时,我们知道在位置 p< i 的每一个元素都必然是小元素,这是因为或者位置 p 包含一个从它开始移动的小元素,或者位置 p 上原来的大元素在交换期间被置换了。
现在的一个重要细节就是如何处理那些等于枢纽元的元素。给出的方案是,如果 i 和 j 遇到等于枢纽元的关键字,那么我们就让 i 和 j 都停止。这会导致在相等的元素间将有很多次交换,但其正面效果是 i 和 j 将在中间交错,因此当枢纽元被替代时,这种分割建立了两个几乎相等的子数组。归并排序的分析告诉我们,此时总得运行时间为O(N log N)。
快速排序的分析
与归并排序一样,快速排序也是递归的,因此它的分析需要求解一个递推公式。假设有一个随机的枢纽元,快排的运行时间等于两个递归调用的运行时间加上花费在分割上的线性时间(枢纽元的选取仅花费常数时间),由此得基本的快速排序关系:
其中,i = |S1|是S1中元素的个数。
- 最坏情况:枢纽元始终是最小元素,在深度 d 上的递归调用中所有分割的总开销必然最大是 N。因为递归的深度最多到 N,则得到快速排序最坏情形的界O(N2)。
- 最好情形:枢纽元正好位于中间,两个子数组恰好各为原数组的一半,则最佳情形的界为O(N log N)。
- 平均情形:假设对于S1,每个大小都是等可能的,因此它们均有1/N的概率,得到平均情形界O(N log N)。
通常,对于小的数组不递归地使用快速排序,而代之以诸如插入排序这样的对小数组有效的排序算法。
实现代码
//简单递归排序算法 template<typename Comparable> void SORT(vector<Comparable>& items) { if (items.size() > 1) { vector<Comparable> smaller; vector<Comparable> same; vector<Comparable> larger; auto chosenItem = items[items.size() / 2]; for (auto& i : items) { if (i < chosenItem) smaller.push_back(std::move(i)); else if (chosenItem < i) larger.push_back(std::move(i)); else same.push_back(std::move(i)); } SORT(smaller); //递归调用 SORT(larger); //递归调用 std::move(begin(smaller), end(smaller), begin(items)); std::move(begin(same), end(same), begin(items) + smaller.size()); std::move(begin(larger), end(larger), end(items) - larger.size()); } } /** * 快速排序算法(驱动程序) */ template<typename Comparable> void quickSort(vector<Comparable>& a) { quickSort(a, 0, a.size() - 1); } /** * 返回left、center和right三项的中值 * 将它们排序并隐匿枢纽元 */ template<typename Comparable> const Comparable& median3(vector<Comparable>& a, int left, int right) { int center = (left + right) / 2; if (a[center] < a[left]) std::swap(a[left], a[center]); if (a[right] < a[left]) std::swap(a[left], a[right]); if (a[right] < a[center]) std::swap(a[center], a[right]); //将枢纽元置于right-1 处 std::swap(a[center], a[right - 1]); return a[right - 1]; } /** * 进行递归调用的内部快速排序方法 * 使用三数中值分割法,以及截止范围是10的截止技术 * a是Comparable项的数组 * left为子数组最左元素的下标 * right为子数组最右元素的下标 */ template<typename Comparable> void quickSort(vector<Comparable>& a, int left, int right) { if (left + 10 <= right) { const Comparable& pivot = median3(a, left, right); //开始分割 int i = left, j = right - 1; for (;;) { while (a[++i] < pivot) {} while (pivot < a[--j]) {} if (i < j) std::swap(a[i], a[j]); else break; } std::swap(a[i], a[right - 1]); //恢复枢纽元 quickSort(a, left, i - 1); //将小于等于枢纽元的元素排序 quickSort(a, i + 1, right); //将大于等于枢纽元的元素排序 } else //对子数组进行一次插入排序 insertionSort(a); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!