快速排序的深入优化探讨

一. 快速排序

快速排序(quick sort)是一种基于分治策略的排序算法,运行高效,应用广泛。

快速排序的核心操作是"哨兵划分",其目标是:选择数组中的某个元素作为"基准数",将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。

  1. 选取数组最左端元素作为基准数,初始化两个指针 i 和 j 分别指向数组的两端。
  2. 设置一个循环,在每轮中使用 i(j)分别寻找第一个比基准数大(小)的元素,然后交换这两个元素。
  3. 循环执行步骤 2. ,直到 i 和 j 相遇时停止,最后将基准数交换至两个子数组的分界线。
    哨兵划分完成后,原数组被划分成三部分:左子数组、基准数、右子数组,且满足"左子数组任意元素 < 基准数 < 右子数组任意元素"。因此,我们接下来只需对这两个子数组进行排序。
cpp 复制代码
/* 哨兵划分 */
int partition(vector<int> &nums, int left, int right) {
    // 以 nums[left] 为基准数
    int i = left, j = right;
    while (i < j) {
        while (i < j && nums[j] >= nums[left])
            j--;                // 从右向左找首个小于基准数的元素
        while (i < j && nums[i] <= nums[left])
            i++;                // 从左向右找首个大于基准数的元素
        swap(nums[i], nums[j]); // 交换这两个元素
    }
    swap(nums[i], nums[left]);  // 将基准数交换至两子数组的分界线
    return i;                   // 返回基准数的索引
}

这是一次划分,然后怎么将整个数组进行排序?

解决方法:持续递归,直至子数组长度为 1 时终止,从而完成整个数组的排序

cpp 复制代码
/* 快速排序 */
void quickSort(vector<int> &nums, int left, int right) {
    // 子数组长度为 1 时终止递归
    if (left >= right)
        return;
    // 哨兵划分
    int keyi = partition(nums, left, right);
    // 递归左子数组、右子数组
    quickSort(nums, left, keyi - 1);
    quickSort(nums, keyi + 1, right);
}

二. 快排性能的关键点分析:

决定快排性能的关键点是每次单趟排序后,key对数组的分割,如果每次选key基本⼆分居中,那么快排的递归树就是颗均匀的满⼆叉树,性能最佳。但是实践中虽然不可能每次都是⼆分居中,但是性能也还是可控的。但是如果出现每次选到最⼩值/最⼤值,划分为0个和N-1的⼦问题时,时间复杂度为O(N^2),数组序列有序时就会出现这样的问题,我们前⾯已经⽤三数取中或者随机选key解决了这个问题,也就是说我们解决了绝⼤多数的问题,但是现在还是有⼀些场景没解决(数组中有⼤量重复数据时),类似⼀下代码。

cpp 复制代码
1 // 数组中有多个跟key相等的值
2 int a[] = { 6,1,7,6,6,6,4,9 };
3 int a[] = { 3,2,3,3,3,3,2,3 };
4
5 // 数组中全是相同的值
6 int a[] = { 2,2,2,2,2,2,2,2 };

以下是《算法导论》书籍中给出的hoare和lomuto给出的快排的单趟排序的伪代码

三路划分算法思想讲解:

当⾯对有⼤量跟key相同的值时,三路划分的核⼼思想有点类似hoare的左右指针和lomuto的前后指针的结合。核⼼思想是把数组中的数据分为三段【⽐key⼩的值】 【跟key相等的值】【⽐key⼤的值】,所以叫做三路划分算法。结合下图,理解⼀下实现思想:

  1. key默认取left位置的值。
  2. left指向区间最左边,right指向区间最后边,cur指向left+1位置。
  3. cur遇到⽐key⼩的值后跟left位置交换,换到左边,left++,cur++。
  4. cur遇到⽐key⼤的值后跟right位置交换,换到右边,right--。
  5. cur遇到跟key相等的值后,cur++。
  6. 直到cur > right结束




hoare和lomuto和三路划分单趟排序代码分析:

数组中有⼤量重复数据时,快排单趟选key划分效果对象:

cpp 复制代码
 #include<stdio.h>
 #include<stdlib.h>
 #include<time.h>
 #include<string.h>

void PrintArray(int* a, int n)
{
for (int i = 0; i < n; ++i)
{
 printf("%d ", a[i]);
 }
 printf("\n");
 }

 void Swap(int* p1, int* p2)
 {
 int tmp = *p1;
 *p1 = *p2;
 *p2 = tmp;
 }

 // hoare
 // [left, right]
 int PartSort1(int* a, int left, int right)
 {
 int keyi = left;
 ++left;

 while (left <= right)//left和right相遇的位置的值⽐基准值要⼤
 {
 //right找到⽐基准值⼩或等
 while (left <= right && a[right] > a[keyi])
 {
 right--;
 }

 //left找到⽐基准值⼤或等
 while (left <= right && a[left] < a[keyi])
 {
 left++;
 }

 //right left
 if (left <= right)
 {
 Swap(&a[left++], &a[right--]);
 }
 }
 //right keyi交换
 Swap(&a[keyi], &a[right]);

 return right;
 }

 // 前后指针
 int PartSort2(int* a, int left, int right)
{
 int prev = left;
 int cur = left + 1;
 int keyi = left;
 while (cur <= right)
 {
 if (a[cur] < a[keyi] && ++prev != cur)
 {
 Swap(&a[prev], &a[cur]);
 }

 ++cur;
 }

 Swap(&a[prev], &a[keyi]);
 keyi = prev;
 return keyi;
 }

 typedef struct
 {
 int leftKeyi;
 int rightKeyi;
 }KeyWayIndex;

 // 三路划分
 KeyWayIndex PartSort3Way(int* a, int left, int right)
 {
 int key = a[left];

 // left和right指向就是跟key相等的区间
 // [开始, left-1][left, right][right+1, 结束]
 int cur = left + 1;
 while (cur <= right)
 {
 // 1、cur遇到⽐key⼩,⼩的换到左边,同时把key换到中间位置
 // 2、cur遇到⽐key⼤,⼤的换到右边
 if (a[cur] < key)
 {
 Swap(&a[cur], &a[left]);
 ++cur;
 ++left;
 }
 else if (a[cur] > key)
{
 Swap(&a[cur], &a[right]);
 --right;
 }
 else
 {
 ++cur;
 }
 }

 KeyWayIndex kwi;
 kwi.leftKeyi = left;
 kwi.rightKeyi = right;
return kwi;
 }

 void TestPartSort1()
 {
 int a1[] = { 6,1,7,6,6,6,4,9 };
 int a2[] = { 3,2,3,3,3,3,2,3 };
 int a3[] = { 2,2,2,2,2,2,2,2 };

 PrintArray(a1, sizeof(a1) / sizeof(int));
 int keyi1 = PartSort1(a1, 0, sizeof(a1) / sizeof(int) - 1);
 PrintArray(a1, sizeof(a1) / sizeof(int));
 printf("hoare keyi:%d\n\n", keyi1);

 PrintArray(a2, sizeof(a2) / sizeof(int));
 int keyi2 = PartSort1(a2, 0, sizeof(a2) / sizeof(int) - 1);
 PrintArray(a2, sizeof(a2) / sizeof(int));
 printf("hoare keyi:%d\n\n", keyi2);

 PrintArray(a3, sizeof(a3) / sizeof(int));
 int keyi3 = PartSort1(a3, 0, sizeof(a3) / sizeof(int) - 1);
 PrintArray(a3, sizeof(a3) / sizeof(int));
 printf("hoare keyi:%d\n\n", keyi3);
 }
 void TestPartSort2()
 {
 int a1[] = { 6,1,7,6,6,6,4,9 };
 int a2[] = { 3,2,3,3,3,3,2,3 };
 int a3[] = { 2,2,2,2,2,2,2,2 };

 PrintArray(a1, sizeof(a1) / sizeof(int));
 int keyi1 = PartSort2(a1, 0, sizeof(a1) / sizeof(int) - 1);
 PrintArray(a1, sizeof(a1) / sizeof(int));
 printf("前后指针 keyi:%d\n\n", keyi1);

 PrintArray(a2, sizeof(a2) / sizeof(int));
 int keyi2 = PartSort2(a2, 0, sizeof(a2) / sizeof(int) - 1);
 PrintArray(a2, sizeof(a2) / sizeof(int));
 printf("前后指针 keyi:%d\n\n", keyi2);

 PrintArray(a3, sizeof(a3) / sizeof(int));
 int keyi3 = PartSort2(a3, 0, sizeof(a3) / sizeof(int) - 1);
 PrintArray(a3, sizeof(a3) / sizeof(int));
 printf("前后指针 keyi:%d\n\n", keyi3);
 }

 void TestPartSort3()
 {
 //int a0[] = { 6,1,2,7,9,3,4,5,10,4 };
 int a1[] = { 6,1,7,6,6,6,4,9 };
 int a2[] = { 3,2,3,3,3,3,2,3 };
 int a3[] = { 2,2,2,2,2,2,2,2 };

 PrintArray(a1, sizeof(a1) / sizeof(int));
 KeyWayIndex kwi1 = PartSort3Way(a1, 0, sizeof(a1) / sizeof(int) - 1);
 PrintArray(a1, sizeof(a1) / sizeof(int));
 printf("3Way keyi:%d,%d\n\n", kwi1.leftKeyi, kwi1.rightKeyi);

 PrintArray(a2, sizeof(a2) / sizeof(int));
 KeyWayIndex kwi2 = PartSort3Way(a2, 0, sizeof(a2) / sizeof(int) - 1);
 PrintArray(a2, sizeof(a2) / sizeof(int));
 printf("3Way keyi:%d,%d\n\n", kwi2.leftKeyi, kwi2.rightKeyi);

PrintArray(a3, sizeof(a3) / sizeof(int));
KeyWayIndex kwi3 = PartSort3Way(a3, 0, sizeof(a3) / sizeof(int) - 1);
PrintArray(a3, sizeof(a3) / sizeof(int));
 printf("3Way keyi:%d,%d\n\n", kwi3.leftKeyi, kwi3.rightKeyi);
 }

 int main()
 {
 TestPartSort1();
 TestPartSort2();
 TestPartSort3();

 return 0;
 }

三种快排单趟排序运⾏结果分析:

从下⾯的运⾏结果分析,lomuto的前后指针法,⾯对key有⼤量重复时,lomuto划分不是很理想,性能退化,hoare相对还不错,但是⼤量重复时,没有三路划分快。三路划分算法,把跟key相等的值都划分到了中间,可以很好的解决这⾥的问题

cpp 复制代码
 6 1 7 6 6 6 4 9
 6 1 4 6 6 6 7 9
 hoare keyi:3

 3 2 3 3 3 3 2 3
 3 2 3 2 3 3 3 3
 hoare keyi:4

 2 2 2 2 2 2 2 2
 2 2 2 2 2 2 2 2
 hoare keyi:3

 6 1 7 6 6 6 4 9
 4 1 6 6 6 6 7 9
 前后指针 keyi:2

 3 2 3 3 3 3 2 3
 2 2 3 3 3 3 3 3
 前后指针 keyi:2

 2 2 2 2 2 2 2 2
 2 2 2 2 2 2 2 2
 前后指针 keyi:0

 6 1 7 6 6 6 4 9
 1 4 6 6 6 6 9 7
 3Way keyi:2,5

 3 2 3 3 3 3 2 3
 2 2 3 3 3 3 3 3
 3Way keyi:2,7

2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2
3Way keyi:0,7

三. 排序OJ

912. 排序数组 - ⼒扣(LeetCode)

题目链接

下⾯我们再来看看这个OJ题,这个OJ,当我们⽤快排的时候,lomuto的⽅法,过不了这个题⽬,hoare版本可以过这个题⽬。堆排序和归并和希尔是可以过的,其他⼏个O(N^2)也不过了,因为这个题的测试⽤例中不仅仅有数据很多的⼤数组,也有⼀些特殊数据的数组,如⼤量重复数据的数组。堆排序和归并和希尔不是很受数据样本的分布和形态的影响,但是快排会,因为快排要选key,每次key都当趟分割都很偏,就会出现效率退化问题。

• 前⾯我们分析了lomuto的前后指针⾯对⼤量重复数据时,效率会退化,hoare版本会好很多,所以hoare是可以过这个OJ的,但是OJ还是⼀个相对局限的测试,就像leetcode官⽅为啥开始写的答案是lomuto,说明那会lomuto是可以过的,后⾯加了⼤量重复数值的测试⽤例,所以就过不了,但是答案忘记改了,说明写答案讲解和测试⽤例补充的不是⼀个团队,协作出问题(当然后⾯看这个视频课,可能官⽅答案就修正)。那么hoare现在可以过,leetcode哪天增加⼀个特殊测试⽤例以后,就过不了,三路划分也类似,因为他们的思想还是在特殊场景下效率会退化,⽐如⼤多数选key都是接近最⼩或者最⼤的值,导致划分不均衡,效率退化。

  1. introsort是由David Musser在1997年设计的排序算法,C++ sgi STL sort中就是⽤的introspectivesort(内省排序)思想实现的。内省排序可以认为不受数据分布的影响,⽆论什么原因划分不均匀,导致递归深度太深,他就是转换堆排了,堆排不受数据分布影响,具体看下⾯代码细节。

  2. 其次三路划分针对有⼤量重复数据时,效率很好,其他场景就⼀般,但是三路划分思路还是很有价值的,有些快排思想变形体,要⽤划分去选数,他能保证跟key相等的数都排到中间去,三路划分的价值就体现出来了。

下⾯我们分别展⽰⼀下这⼏种思想去跑leetcode oj的思路和代码。

lomuto的快排跑排序OJ代码

cpp 复制代码
void Swap(int* x, int* y)
{
 int tmp = *x;
 *x = *y;
 *y = tmp;
}
void QuickSort(int* a, int left, int right)
{
 if (left >= right)
 return;
 int begin = left;
 int end = right;

 // 随机选key
 int randi = left + (rand() % (right-left + 1));
 // printf("%d\n", randi);
 Swap(&a[left], &a[randi]);

 int prev = left;
 int cur = prev + 1;

 int keyi = left;
 while (cur <= right)
 {
 if (a[cur] < a[keyi] && ++prev != cur)
 {
 Swap(&a[prev], &a[cur]);
 }

 ++cur;
 }

 Swap(&a[prev], &a[keyi]);
 keyi = prev;

 // [begin, keyi-1] keyi [keyi+1, end]
 QuickSort(a, begin, keyi - 1);
 QuickSort(a, keyi+1, end);
 }


 int* sortArray(int* nums, int numsSize, int* returnSize){
 srand(time(0));
 QuickSort(nums, 0, numsSize-1);

 *returnSize = numsSize;
 return nums;
 }

运⾏结果:

hoare的快排跑排序OJ代码

cpp 复制代码
void Swap(int* x, int* y)
 {
 int tmp = *x;
 *x = *y;
 *y = tmp;
 }

void QuickSort(int* a, int left, int right)
{
 if (left >= right)
 return;

 int begin = left, end = right;

 int randi = left + (rand() % (right-left+1));
 Swap(&a[left], &a[randi]);

 int keyi = left;
 ++left;

 while (left <= right)//left和right相遇的位置的值⽐基准值要⼤
 {
 //right找到⽐基准值⼩或等
 while (left <= right && a[right] > a[keyi])
 {
 right--;
 }

 //left找到⽐基准值⼤或等
 while (left <= right && a[left] < a[keyi])
 {
 left++;
 }
 
 if (left <= right)
 {
 Swap(&a[left++], &a[right--]);
 }
 }
 //right keyi交换
 Swap(&a[keyi], &a[right]);
 keyi = right;

 // [begin, keyi-1] keyi [keyi+1, end]
 QuickSort(a, begin, keyi - 1);
 QuickSort(a, keyi+1, end);
 }

 int* sortArray(int* nums, int numsSize, int* returnSize){
 srand(time(0));
 QuickSort(nums, 0, numsSize-1);

 *returnSize = numsSize;
 return nums;
 }

三路划分的快排跑排序OJ代码

cpp 复制代码
 void Swap(int* x, int* y)
 {
 int tmp = *x;
 *x = *y;
 *y = tmp;
 }

 void QuickSort(int* a, int left, int right)
 {
 if (left >= right)
 return;

 int begin = left;
 int end = right;

 // 随机选key
  int randi = left + (rand() % (right-left + 1));
 Swap(&a[left], &a[randi]);
 // 三路划分
 // left和right指向就是跟key相等的区间
// [begin, left-1] [left, right] right+1, end]
 int key = a[left];
 int cur = left+1;
 while(cur <= right)
 {
 // 1、cur遇到⽐key⼩,⼩的换到左边,同时把key换到中间位置
 // 2、cur遇到⽐key⼤,⼤的换到右边
 if(a[cur] < key)
{
 Swap(&a[cur], &a[left]);
 ++left;
 ++cur;
 }
 else if(a[cur] > key)
 {
 Swap(&a[cur], &a[right]);
 --right;
 }
 else
 {
 ++cur;
 }
 }

 // [begin, left-1] [left, right] right+1, end]
 QuickSort(a, begin, left - 1);
 QuickSort(a, right+1, end);
 }

 int* sortArray(int* nums, int numsSize, int* returnSize){
 srand(time(0));
 QuickSort(nums, 0, numsSize-1);

 *returnSize = numsSize;
 return nums;
 }

introsort的快排跑排序OJ代码

introsort是introspective sort采⽤了缩写,他的名字其实表达了他的实现思路,他的思路就是进⾏⾃我侦测和反省,快排递归深度太深(sgi stl中使⽤的是深度为2倍排序元素数量的对数值)那就说明在这种数据序列下,选key出现了问题,性能在快速退化,那么就不要再进⾏快排分割递归了,改换为堆排序进⾏排序。

cpp 复制代码
/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
 void Swap(int* x, int* y)
 {
 int tmp = *x;
 *x = *y;
 *y = tmp;
 }

 void AdjustDown(int* a, int n, int parent)
 {
 int child = parent * 2 + 1;
 while (child < n)
 {
 // 选出左右孩⼦中⼤的那⼀个
 if (child + 1 < n && a[child + 1] > a[child])
 {
 ++child;
 }

 if (a[child] > a[parent])
 {
 Swap(&a[child], &a[parent]);
 parent = child;
 child = parent * 2 + 1;
 }
 else
 {
 break;
 }
 }
 }

 void HeapSort(int* a, int n)
 {
 // 建堆 -- 向下调整建堆 -- O(N)
 for (int i = (n - 1 - 1) / 2; i >= 0; --i)
 {
 AdjustDown(a, n, i);
 }

 // ⾃⼰先实现 -- O(N*logN)
 int end = n - 1;
 while (end > 0)
 {
 Swap(&a[end], &a[0]);
 AdjustDown(a, end, 0);

 --end;
 }
 }

 void InsertSort(int* a, int n)
 {
 for (int i = 1; i < n; i++)
 {
 int end = i-1;
 int tmp = a[i];
 // 将tmp插⼊到[0,end]区间中,保持有序
  while (end >= 0)
 {
 if (tmp < a[end])
 {
 a[end + 1] = a[end];
 --end;
 }
 else
 	{
	 break;
	 }
	}

	 a[end + 1] = tmp;
	 }
 }

 void IntroSort(int* a, int left, int right, int depth, int defaultDepth)
 {
 	if (left >= right)
 	return;
 
 // 数组⻓度⼩于16的⼩数组,换为插⼊排序,简单递归次数
 if(right - left + 1 < 16)
 {
 	InsertSort(a+left, right-left+1);
 	return; 
 }

 // 当深度超过2*logN时改⽤堆排序
if(depth > defaultDepth)
 {
 HeapSort(a+left, right-left+1);
 return;
 }

 depth++;

 int begin = left;
 int end = right;

 // 随机选key
 int randi = left + (rand() % (right-left + 1));
 Swap(&a[left], &a[randi]);

 int prev = left;
 int cur = prev + 1;

 int keyi = left;
 while (cur <= right)
 {
 if (a[cur] < a[keyi] && ++prev != cur)
 {
 Swap(&a[prev], &a[cur]);
 }

 ++cur;
 }

 Swap(&a[prev], &a[keyi]);
 keyi = prev;

 // [begin, keyi-1] keyi [keyi+1, end]
 IntroSort(a, begin, keyi - 1, depth, defaultDepth);
 IntroSort(a, keyi+1, end, depth, defaultDepth);
 }

 void QuickSort(int* a, int left, int right)
 {
 int depth = 0;
 int logn = 0;

 int N = right-left+1;
 for(int i = 1; i < N; i *= 2)
 {
 logn++;
 }
 
 // introspective sort -- ⾃省排序
 IntroSort(a, left, right, depth, logn*2);
 }

 int* sortArray(int* nums, int numsSize, int* returnSize){
 srand(time(0));
 QuickSort(nums, 0, numsSize-1);

 *returnSize = numsSize;
 return nums;
 }

四. 竞赛中快速排序的写法

竞赛时我们需要快速的写出快排的代码,所以下面是快排的模板,当然思路是一样的。

cpp 复制代码
#include <iostream>
using namespace std;
const int N = 100010;
int q[N];
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);
}
int main()
{
int n;
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;
}
相关推荐
Miraitowa_cheems2 小时前
LeetCode算法日记 - Day 62: 黄金矿工、不同路径III
数据结构·算法·leetcode·决策树·职场和发展·深度优先·剪枝
ACEEE12223 小时前
解读DeepSeek-V3.2-Exp:基于MLA架构的Lightning Index如何重塑长上下文效率
人工智能·深度学习·算法·架构·deep
qq_437896433 小时前
unsigned 是等于 unsigned int
开发语言·c++·算法·c
佛系彭哥3 小时前
C语言笔记(2)
c语言·笔记
细节控菜鸡3 小时前
【2025最新】ArcGIS for JS 实现地图卷帘效果,动态修改参数(进阶版)
开发语言·javascript·arcgis
枫叶丹43 小时前
【Qt开发】输入类控件(四)-> QSpinBox
开发语言·qt
952364 小时前
数据结构—单链表
c语言·数据结构·学习
Learn Beyond Limits4 小时前
Using per-item Features|使用每项特征
人工智能·python·神经网络·算法·机器学习·ai·吴恩达
greentea_20134 小时前
Codeforces Round 863 A. Insert Digit (1811)
数据结构·算法