【数据结构】高效排序算法精髓解析

1.快速排序

1.1Hoare 版本

快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中

的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

下面我们先理解快速排序排一趟的过程:

如果选择最右边的值让右边的 end 先走

选择最右边的值作为 key 需要 begin 先走,不然排序会出问题

找大是找一个比基准值 key 大的数,第一个数是 6 比 5 大,end 找到比基准值小的数 4 时,将 4 和 6 交换

到达中间位置时将中间数据和 key 交换

一趟我们就可以把一个数组分成三个部分比 key 小的子数组,key,比 key 大的子数组

如果要将一个数组排成升序,我们可以利用递归的思想,不断进行单趟,直到 key 的左右只有一个数据的时候,整个数组就都有序了;(下图是理想的划分情况,key 处于正中间的位置,实际情况 key 可能不在正中间位置,但是不影响我们递归实现,只要 key 左边的数都比 key 小,右边的都比 key 大就行)

加上三数取中后可以避免快速排序的缺陷(即 key 取到最大或最小)

// 快速排序递归实现
// 快速排序hoare版本
//时间复杂度:O(N*logN-N^2)
// 三数取中:确定中间值索引后只做一次交换
void GetMid(int* a, int begin, int end)
{
int mid = begin + (end - begin) / 2; // 计算中间索引
// 比较三个值,确定中间值的索引,无需实际排序
if ((a[begin] = a[begin])
{
begin++;
}
while (begin = right)
return;
int div = PartSort1(a, left, right);
QuickSort1(a, left, div - 1);
QuickSort1(a, div + 1, right);
}

1.2快速排序(挖坑法)

性能上和 Hoare 版本的快速排序没什么区别,只是实现方法有所差异

实现的步骤

Step 1:选基准值,挖第一个坑

  用三数取中法(GetMid)从 a[begin]a[mid]a[end] 中选中间值,交换到 end 位置(作为基准值)。

  记录基准值 key = a[end],此时 end 位置成为第一个 “坑”(需要被填充)。

Step 2:左指针找大值,填右坑

  左指针 begin 从左向右遍历,找到第一个 大于 key 的元素(a[begin] > key)。

  将该元素填入当前的 “坑”(初始是 end 位置),即 a[end] = a[begin]

  此时 begin 位置变成新的 “坑”(因为元素被移走了)。

Step 3:右指针找小值,填左坑

  右指针 end 从右向左遍历,找到第一个 小于 key 的元素(a[end] < key)。

  将该元素填入当前的 “坑”(上一步的 begin 位置),即 a[begin] = a[end]

  此时 end 位置变成新的 “坑”。

Step 4:重复填坑,直到指针相遇

  循环执行 Step 2 和 Step 3,不断移动 beginend,直到 begin == end(两指针相遇)。

  此时相遇的位置就是基准值 key 的最终位置,将 key 填入该位置(a[begin] = key)。

//挖坑法
int PartSort2(int* a, int begin, int end)
{
GetMid(a, begin, end);
int key = a[end];
while (begin = key)
{
end--;
}
a[begin] = a[end];
}
a[begin] = key;
return begin;
}

1.3快速排序(前后指针法)

这里我key选end下标的数据,cur指针开始指向待排序列的头,prev指向cur的前一个位置

当cur遇到比key小的数,prev++,交换cur和prev的数据

当cur遇到比key大的数时cur++,prev++和prev进行交换

cur遇到比key小的数,prev++,交换cur和prev的数据

最后prev指针的数据左边比它小,右边比它大

1.初始化指针:

prev = begin - 1:初始时 “小于基准值区域” 为空,指向区域外。

cur = begin:从数组起始位置开始遍历。

keyindex = end:选最右元素作为基准值(建议先用三数取中优化)。

2.遍历与分区:

      当cur < end(未遍历到基准值):

            若a[cur] < 基准值prev++(扩大 “小于区域”),交换a[prev]a[cur](将当前元素              加入小于区域)。    无论是否交换,cur++(继续遍历下一个元素)。

3.放置基准值:

        遍历结束后,prev指向 “小于区域” 的最后一个元素,prev + 1是基准值的正确位置。

         交换prev + 1keyindex(基准值位置),此时基准值左边全小于它,右边全大于等于               它。

4.返回基准值位置:prev + 1(即prev自增后的值)。

// 前后指针法分区函数
int PartSort3(int* a, int begin, int end)
{
// 三数取中优化(可选,建议加上)
GetMid(a, begin, end);
int keyindex = end;  // 基准值索引(选end位置元素)
int prev = begin - 1;  // 前指针:指向小于基准值区域的最后一个元素
int cur = begin;      // 后指针:遍历数组找小于基准值的元素
// 遍历到基准值前一个位置即可
while (cur < end)
{
// 若当前元素小于基准值,扩展小于区域
if (a[cur] < a[keyindex])
{
prev++;  // 前指针右移(扩大小于区域)
swap(&a[prev], &a[cur]);  // 交换到小于区域
}
// 无论是否交换,后指针都要右移(继续遍历)
cur++;
}
// 最后将基准值放到小于区域的后面(prev+1)
prev++;
swap(&a[prev], &a[keyindex]);
return prev;  // 返回基准值最终位置
}

2.快速排序非递归实现

递归最大的缺陷是如果栈帧的深度太深,可能会导致栈溢出

创建栈并初始化(StackInit)。

将整个数组的左右边界[left, right]入栈(注意栈是 “后进先出”,先推右边界,再推左边界)。

循环处理:栈非空时持续分割区间

关键:每次从栈中取出一个区间,用PartSort3(前后指针法)分区,确定基准值的最终位置div,将区间一分为二。

子区间入栈:处理未排序的部分

分区后,需判断左右子区间是否需要继续排序(长度 > 1 时才需要),并将有效子区间入栈

逻辑:子区间长度≤1 时(div+1 >= endbegin >= div-1),已天然有序,无需入栈。

当栈为空时,所有区间均已处理(均为有序的小区间),排序完成。

最后销毁栈(StackDestroy),释放资源。

“栈存区间→出栈分区→有效子区间入栈→循环至栈空”,通过栈模拟递归的 “先分右、再分左” 的处理顺序,最终实现整个数组有序。

// 快速排序非递归实现
void QuickSortNonR(int* a, int left,int right)
{
ST st;
StackInit(&st);
StackPush(&st, right);
StackPush(&st, left);
while (!StackEmpty(&st)) {
// 出栈获取当前待处理区间[begin, end]
int begin = StackTop(&st); StackPop(&st);  // 先出左边界
int end = StackTop(&st); StackPop(&st);    // 再出右边界
// 核心:分区操作(前后指针法)
int div = PartSort3(a, begin, end);  // 得到基准值位置div
// 此时数组分为:[begin, div-1](≤基准值) 和 [div+1, end](≥基准值)
// 右子区间[div+1, end]入栈(若长度>1)
if (div + 1 1)
if (begin < div - 1) {
StackPush(&st, div - 1);   // 先推右边界
StackPush(&st, begin);     // 再推左边界
}
StackDestroy(&st);
}

快速排序的特性总结:

快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

时间复杂度:O(N*logN)

posted @ 2025-08-21 14:46  yfceshi  阅读(13)  评论(0)    收藏  举报