C语言入门(快速排序之三路划分)

目录

一、传统快速排序回顾

(一)核心思想

(二)完整代码实现(带详细注释)

(三)传统快速排序的痛点

二、三路划分快速排序原理

(一)核心思想

(二)荷兰国旗问题类比

(三)详细执行步骤

(四)可视化执行过程

三、三路划分快速排序完整实现

四、三路快排为什么能解决重复元素问题?

五、三路快排与传统快排对比

六、适用场景

七、总结


快速排序是 C.A.R Hoare 于 1960 年提出的经典排序算法,核心优势是原地排序平均时间复杂度低(O(nlogn))。但传统快速排序在处理大量重复元素的数组时,划分效率会显著下降(最坏时间复杂度退化为O(n2))。三路划分快速排序(3-Way QuickSort)正是为解决这一问题而生,它将数组划分为 “小于基准、等于基准、大于基准” 三部分,避免对重复元素的重复处理,大幅提升含重复元素数组的排序效率。

一、传统快速排序回顾

(一)核心思想

传统快速排序基于分治法,核心步骤:

  1. 选基准:从数组中选择一个元素作为基准(pivot);
  2. 划分:将数组分为两部分,左部分≤基准,右部分≥基准,基准归位;
  3. 递归:分别对左右两部分递归执行快速排序。

(二)完整代码实现(带详细注释)

#include <stdio.h>

// 交换两个整数(地址传递,实现值交换)
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

/**
 * 划分函数(霍尔划分法)
 * @param arr 待排序数组
 * @param low 左边界索引
 * @param high 右边界索引
 * @return 基准元素的最终位置
 */
int partition(int arr[], int low, int high) {
    // 选择最右侧元素作为基准(简单但易退化,优化方案:三数取中法)
    int pivot = arr[high];
    // i:小于基准区域的右边界(初始为左边界左侧)
    int i = (low - 1);

    // 遍历[low, high-1]区间,将小于基准的元素移到左侧
    for (int j = low; j <= high - 1; j++) {
        if (arr[j] < pivot) {
            i++; // 扩大小于基准区域
            swap(&arr[i], &arr[j]); // 交换到小于基准区域
        }
    }
    // 基准元素归位(i+1是基准的最终位置)
    swap(&arr[i + 1], &arr[high]);
    return (i + 1);
}

/**
 * 传统快速排序函数
 * @param arr 待排序数组
 * @param low 左边界索引
 * @param high 右边界索引
 */
void quickSort(int arr[], int low, int high) {
    // 递归终止条件:区间长度≤1
    if (low < high) {
        // 划分:获取基准位置
        int pi = partition(arr, low, high);

        // 递归排序左半部分(小于基准)
        quickSort(arr, low, pi - 1);
        // 递归排序右半部分(大于基准)
        quickSort(arr, pi + 1, high);
    }
}

// 打印数组(辅助函数,便于验证结果)
void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++)
        printf("%d ", arr[i]);
    printf("\n");
}

// 测试示例
int main() {
    int arr[] = {10, 7, 8, 9, 1, 5};
    int n = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
    printf("初始数组: ");
    printArray(arr, n);

    quickSort(arr, 0, n - 1); // 排序:左边界0,右边界n-1

    printf("排序后的数组: ");
    printArray(arr, n);
    return 0;
}

(三)传统快速排序的痛点

  • 重复元素导致效率下降:当数组中存在大量重复元素时,划分结果会极度不平衡(如全部元素相同,每次划分仅能确定一个元素位置);
  • 递归深度增加:不平衡的划分会导致递归深度从logn变为n,甚至触发栈溢出;
  • 时间复杂度退化:最坏情况下时间复杂度从O(nlogn)退化为O(n2)。

二、三路划分快速排序原理

(一)核心思想

三路划分快速排序(也称为 “荷兰国旗问题解法”)将数组划分为三个区间

  • [low,lt−1]:小于基准(pivot)的元素;
  • [lt,gt]:等于基准的元素;
  • [gt+1,high]:大于基准的元素。

通过一次遍历完成三路划分,递归时只需处理 “小于基准” 和 “大于基准” 的区间,等于基准的区间无需递归,大幅减少重复元素的处理次数。

(二)荷兰国旗问题类比

荷兰国旗由红、白、蓝三色组成,要求将随机排列的三色球按 “红→白→蓝” 顺序排列,对应三路划分:

  • 红色球 → 小于基准的元素;
  • 白色球 → 等于基准的元素;
  • 蓝色球 → 大于基准的元素。

(三)详细执行步骤

  1. 初始化指针
    • lt(less than):小于基准区域的右边界,初始 =low
    • gt(greater than):大于基准区域的左边界,初始 =high
    • i:遍历指针,初始 =low
  2. 遍历数组(i≤gt):
    • arr[i] < pivot:交换arr[i]arr[lt]lt++i++
    • arr[i] == pivoti++(直接归入等于区域);
    • arr[i] > pivot:交换arr[i]arr[gt]gt--i不移动,交换后的元素需重新判断)。
  3. 递归排序:仅递归排序[low, lt-1](小于基准)和[gt+1, high](大于基准)区间。

(四)可视化执行过程

以数组[5, 1, 7, 5, 8, 5, 9, 5]为例,选择基准5

步骤数组状态ltigt操作说明
初始[5, 1, 7, 5, 8, 5, 9, 5]007基准 = 5
1[5, 1, 7, 5, 8, 5, 9, 5]017arr [1]=1 < 5,交换 arr [0] 和 arr [1],lt=1,i=2
2[1, 5, 7, 5, 8, 5, 9, 5]127arr [2]=7 > 5,交换 arr [2] 和 arr [7],gt=6,i=2
3[1, 5, 5, 5, 8, 5, 9, 7]126arr[2]=5 == 5,i=3
4[1, 5, 5, 5, 8, 5, 9, 7]136arr[3]=5 == 5,i=4
5[1, 5, 5, 5, 8, 5, 9, 7]146arr [4]=8 > 5,交换 arr [4] 和 arr [6],gt=5,i=4
6[1, 5, 5, 5, 9, 5, 8, 7]145arr [4]=9 > 5,交换 arr [4] 和 arr [5],gt=4,i=4
7[1, 5, 5, 5, 5, 9, 8, 7]144arr[4]=5 == 5,i=5
结束[1, 5, 5, 5, 5, 9, 8, 7]154遍历终止(i>gt)

最终划分结果:

  • 小于基准:[1](索引 0);
  • 等于基准:[5,5,5,5](索引 1-4);
  • 大于基准:[9,8,7](索引 5-7)。

三、三路划分快速排序完整实现

#include <stdio.h>

// 交换两个整数
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

/**
 * 三路划分函数
 * 将数组 arr[low..high] 分成三部分:
 *  < pivot:arr[low  .. lt-1]
 *  == pivot:arr[lt   .. gt]
 *  > pivot:arr[gt+1 .. high]
 */
void partition3Way(int arr[], int low, int high, int *lt, int *gt) {
    // 这里选最左边作为基准,也可改用三数取中优化
    int pivot = arr[low];
    *lt = low;
    *gt = high;
    int i = low;

    while (i <= *gt) {
        if (arr[i] < pivot) {
            // 小于基准:放到左边区域
            swap(&arr[*lt], &arr[i]);
            (*lt)++;
            i++;
        } else if (arr[i] > pivot) {
            // 大于基准:放到右边区域
            swap(&arr[i], &arr[*gt]);
            (*gt)--;
            // i 不前进,因为换过来的是新数,还要再判断
        } else {
            // 等于基准:直接跳过
            i++;
        }
    }
}

/**
 * 三路快速排序主函数
 */
void quickSort3Way(int arr[], int low, int high) {
    if (low >= high) {
        return;
    }

    int lt, gt;
    partition3Way(arr, low, high, &lt, &gt);

    // 递归:只排 小于pivot 和 大于pivot 的部分
    quickSort3Way(arr, low, lt - 1);
    quickSort3Way(arr, gt + 1, high);
}

// 打印数组
void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++)
        printf("%d ", arr[i]);
    printf("\n");
}

int main() {
    // 大量重复元素的测试用例
    int arr[] = {5, 1, 7, 5, 8, 5, 9, 5, 3, 5, 2, 5};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("排序前:");
    printArray(arr, n);

    quickSort3Way(arr, 0, n - 1);

    printf("排序后:");
    printArray(arr, n);

    return 0;
}

运行结果:

排序前:5 1 7 5 8 5 9 5 3 5 2 5
排序后:1 2 3 5 5 5 5 5 5 7 8 9

四、三路快排为什么能解决重复元素问题?

传统快排:

  • 只分成 小于 / 大于等于 两部分
  • 大量重复时,划分极度不平衡,退化成接近 O (n²)

三路快排:

  • 分成 小于 / 等于 / 大于 三部分
  • 等于基准的元素一次性全部就位
  • 递归只处理两边,重复元素不再参与任何递归和交换

时间复杂度:

  • 最好 / 平均:O (n log n)
  • 大量重复时仍保持 O (n log n),不会退化

五、三路快排与传统快排对比

对比项传统快速排序三路划分快速排序
划分区域2 部分:< pivot、≥ pivot3 部分:<、==、> pivot
重复元素处理效率差,容易退化极高效,重复元素一次性搞定
指针数量2 个指针3 个指针(lt、i、gt)
适用场景元素重复少的随机数据含大量重复元素的数组(最擅长)
实际工程使用基础库常用(配合三数取中)Java、C++ STL 中 sort 的核心优化

六、适用场景

三路快速排序特别适合:

  1. 数组中有大量重复整数 / 浮点数
  2. 对成绩、年龄、分数、状态码等进行排序
  3. 嵌入式、数据处理、算法竞赛中追求稳定高效
  4. 需要避免传统快排最坏情况的场景

七、总结

  1. 传统快排:分治法,分成两部分,遇到大量重复元素效率下降。
  2. 三路快排
    • 一次遍历分成:小于、等于、大于 三部分
    • 等于基准的元素不再递归,大幅减少交换与比较
    • 对重复元素多的数组,性能远超传统快排
  3. 核心思想来自 荷兰国旗问题,是工程中最实用的快排优化之一。

掌握三路划分快速排序,你就掌握了应对含大量重复数据的最强排序方案之一。

评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值