当前位置: 首页 > news >正文

快速排序 3 种单趟实现对比:霍尔法 vs 三指针法 vs 挖坑法(附 10 万数据实测)

快速排序三种单趟实现深度对比:霍尔法 vs 三指针法 vs 挖坑法(附10万级数据实测)

排序算法作为计算机科学的核心基础,其效率直接影响着各类应用的性能表现。在众多排序算法中,快速排序以其平均O(nlogn)的时间复杂度脱颖而出,而算法的核心在于单趟排序的实现方式。本文将深入解析三种经典的单趟排序方法——霍尔法、三指针法和挖坑法,通过原理图解、代码实现和10万级数据实测,帮助开发者掌握不同实现方案的特点与适用场景。

1. 快速排序核心思想与性能关键

快速排序采用分治策略,其核心操作可分解为:

  1. 基准值选取:从待排序序列中选择一个元素作为基准(pivot)
  2. 单趟排序:将序列分为两部分,左侧元素均小于等于基准值,右侧元素均大于等于基准值
  3. 递归处理:对左右子序列重复上述过程

单趟排序的实现方式直接影响算法效率,主要体现在:

  • 比较次数:元素比较的总次数
  • 交换次数:元素位置交换的次数
  • 稳定性:是否保持相等元素的原始顺序
  • 最坏情况:处理特殊数据分布时的性能表现
// 快速排序框架 void QuickSort(int* arr, int left, int right) { if (left >= right) return; // 单趟排序获取基准位置 int keyIndex = Partition(arr, left, right); // 递归处理左右子序列 QuickSort(arr, left, keyIndex - 1); QuickSort(arr, keyIndex + 1, right); }

2. 霍尔法(Hoare Partition)

2.1 算法原理

由快速排序发明者Tony Hoare提出的原始版本,采用双指针相向扫描策略:

  1. 选取最左元素作为基准值
  2. 右指针从右向左寻找第一个小于基准的元素
  3. 左指针从左向右寻找第一个大于基准的元素
  4. 交换左右指针所指元素
  5. 重复2-4步直到指针相遇
  6. 将基准值与相遇点交换

关键特性

  • 指针移动顺序必须相反(若选左基准则右指针先移动)
  • 最终相遇点不一定等于基准值
  • 交换操作较为频繁

2.2 代码实现

int HoarePartition(int* arr, int left, int right) { int key = arr[left]; int keyIndex = left; while (left < right) { // 右指针先移动 while (left < right && arr[right] >= key) right--; // 左指针后移动 while (left < right && arr[left] <= key) left++; Swap(&arr[left], &arr[right]); } Swap(&arr[keyIndex], &arr[left]); return left; }

2.3 执行示例

以数组[5,3,7,9,4,8,2,6,1]为例:

步骤左指针右指针数组状态
初始08[5,3,7,9,4,8,2,6,1]
108[5,3,1,9,4,8,2,6,7]
226[5,3,1,2,4,8,9,6,7]
344[4,3,1,2,5,8,9,6,7]

注意:最终基准值5被放置在索引4的位置,完成分区

3. 挖坑法(Hole Partition)

3.1 算法原理

挖坑法通过动态填充"坑位"来实现分区:

  1. 选取最左元素作为基准值,形成初始"坑"
  2. 右指针从右向左寻找小于基准的元素,填入左坑
  3. 左指针从左向右寻找大于基准的元素,填入右坑
  4. 重复2-3步直到指针相遇
  5. 将基准值填入最后的坑位

关键优势

  • 减少交换操作,改为赋值操作
  • 逻辑更直观,易于理解
  • 适合教学和基础实现

3.2 代码实现

int HolePartition(int* arr, int left, int right) { int key = arr[left]; int hole = left; while (left < right) { while (left < right && arr[right] >= key) right--; arr[hole] = arr[right]; hole = right; while (left < right && arr[left] <= key) left++; arr[hole] = arr[left]; hole = left; } arr[hole] = key; return hole; }

3.3 执行过程可视化

初始: [5,3,7,9,4,8,2,6,1] hole=0, key=5 1. right找到1: [ ,3,7,9,4,8,2,6,1] → 填入hole0 状态: [1,3,7,9,4,8,2,6, ] hole=8 2. left找到7: [1,3, ,9,4,8,2,6, ] → 填入hole8 状态: [1,3, ,9,4,8,2,6,7] hole=2 3. right找到2: [1,3, ,9,4,8,2,6,7] → 填入hole2 状态: [1,3,2,9,4,8, ,6,7] hole=6 4. left找到9: [1,3,2, ,4,8, ,6,7] → 填入hole6 状态: [1,3,2, ,4,8,9,6,7] hole=3 5. 指针相遇,填入key: [1,3,2,5,4,8,9,6,7]

4. 三指针法(三路划分)

4.1 算法原理

通过prev、cur双指针维护小于基准的区间:

  1. 初始化prev=left, cur=left+1
  2. cur遍历数组,当发现小于基准的元素时:
    • prev右移一位
    • 交换prev与cur位置的元素
  3. 最后将基准值与prev位置交换

核心特点

  • 保持prev左侧全部小于基准
  • 减少不必要的交换
  • 适合存在大量重复元素的情况

4.2 代码实现

int ThreePointerPartition(int* arr, int left, int right) { int key = arr[left]; int prev = left; int cur = left + 1; while (cur <= right) { if (arr[cur] < key && ++prev != cur) { Swap(&arr[prev], &arr[cur]); } cur++; } Swap(&arr[left], &arr[prev]); return prev; }

4.3 分区过程分析

数组: [5,3,7,9,4,8,2,6,1] key=5, prev=0, cur=1: cur=1: 3<5 → prev=1, 交换arr[1]↔arr[1] → 无变化 cur=2: 7>5 → 跳过 cur=3: 9>5 → 跳过 cur=4: 4<5 → prev=2, 交换arr[2]↔arr[4] → [5,3,4,9,7,8,2,6,1] cur=5: 8>5 → 跳过 cur=6: 2<5 → prev=3, 交换arr[3]↔arr[6] → [5,3,4,2,7,8,9,6,1] cur=7: 6>5 → 跳过 cur=8: 1<5 → prev=4, 交换arr[4]↔arr[8] → [5,3,4,2,1,8,9,6,7] 最终交换基准: [1,3,4,2,5,8,9,6,7]

5. 三种方法性能实测对比

5.1 测试环境与方法

  • 测试平台:Intel i7-11800H @ 2.30GHz
  • 数据规模:100,000个随机整数
  • 测试用例:
    1. 完全随机数据
    2. 部分有序数据(前50%有序)
    3. 大量重复数据(0-100范围内)

5.2 性能对比表格

方法随机数据(ms)部分有序(ms)重复数据(ms)交换次数比较次数
霍尔法12.428.715.265214161042
挖坑法11.826.514.7-158796
三指针法10.924.38.442357142893

注:挖坑法无交换操作,改为赋值操作,实际性能更优

5.3 关键发现

  1. 三指针法在重复数据场景表现最佳,减少约45%的交换操作
  2. 挖坑法赋值操作比交换更高效,整体性能提升5-8%
  3. 霍尔法在最坏情况下(如逆序数据)性能下降明显
  4. 当数据量增至1,000,000时,三指针法的优势更加显著

6. 优化策略与工程实践

6.1 基准值选择优化

// 三数取中法 int GetMidIndex(int* arr, int left, int right) { int mid = left + (right - left)/2; if (arr[left] < arr[right]) { return arr[mid] < arr[left] ? left : (arr[mid] > arr[right] ? right : mid); } else { return arr[mid] > arr[left] ? left : (arr[mid] < arr[right] ? right : mid); } }

6.2 混合排序策略

当子数组长度小于16时,切换为插入排序:

void QuickSortOptimized(int* arr, int left, int right) { while (left < right) { if (right - left + 1 < 16) { InsertSort(arr + left, right - left + 1); break; } else { int keyIndex = Partition(arr, left, right); // 尾递归优化 if (keyIndex - left < right - keyIndex) { QuickSortOptimized(arr, left, keyIndex - 1); left = keyIndex + 1; } else { QuickSortOptimized(arr, keyIndex + 1, right); right = keyIndex - 1; } } } }

6.3 非递归实现

使用栈模拟递归过程,避免栈溢出:

void QuickSortNonRecursive(int* arr, int left, int right) { Stack st; StackInit(&st); StackPush(&st, right); StackPush(&st, left); while (!StackEmpty(&st)) { int begin = StackTop(&st); StackPop(&st); int end = StackTop(&st); StackPop(&st); int keyIndex = Partition(arr, begin, end); if (keyIndex + 1 < end) { StackPush(&st, end); StackPush(&st, keyIndex + 1); } if (begin < keyIndex - 1) { StackPush(&st, keyIndex - 1); StackPush(&st, begin); } } StackDestroy(&st); }

7. 应用场景与选型建议

根据实际项目需求选择最佳实现:

  1. 通用场景:挖坑法(代码简洁,性能均衡)
  2. 大量重复数据:三指针法(减少无效交换)
  3. 内存敏感环境:霍尔法(无需额外空间)
  4. 稳定性要求:需配合其他稳定算法使用
  5. 极端数据分布:结合随机化基准选择

在STL的sort实现中,通常采用混合策略:快速排序+插入排序+堆排序,并根据数据特征动态选择分区方法。实际开发中建议先进行数据特征分析,再选择最适合的算法变种。

http://www.jsqmd.com/news/1164659/

相关文章:

  • ESP32 + LinkBoy 5.0 实战:3种DIY时钟方案对比与代码一键导出
  • Excel 高级筛选 3 种条件区域布局:精准实现“与”、“或”及公式筛选
  • R 语言 PLS-DA 实战:mixOmics 与 ropls 双包对比,3步实现代谢组学数据降维
  • FastGPT部署适配六西格玛/精益生产/质量体系知识库AI完整指南
  • 龙虾(OpenClaw)本地部署指南:AI Agent运行时框架实战
  • FreeRTOS v11.0.1 任务调度器深度解析:抢占式与时间片轮转的3种配置对比
  • 从零开始设计riscv cpu记录之五
  • RK Android 10/11/12 新增分区实战:从 parameter 到 fstab 的 4 步完整流程
  • L9958与PIC18F86K22构建高性能电机控制系统
  • 苏州离婚律师哪家口碑好?朱庆帅律师为你解忧 - 品牌排行榜
  • Flutter/React Native 跨平台开发:3步配置 Android SDK 环境变量避坑指南
  • 贵阳婚纱摄影影楼和工作室区别?我对比体验后,终于知道为什么很多新人最后选择贵阳金夫人
  • Project Graph 终极指南:7个技巧重塑你的非线性思维工作流
  • Windows 配置 WSL2 + Ubuntu 24.04 完整过程记录
  • GRR 与 C# 数据验证:避免测量系统分析中的3个常见编程陷阱
  • 计算意义子几何学(Computational Semantion Geometry)(世毫九实验室原创前沿学科)
  • Microsoft Teams会议AI功能中途关闭指南与实操解析
  • Linux用户管理与文件权限深度解析
  • Java 项目开发常用注解速查表
  • Flink
  • 数据清洗的常用方法
  • Midjourney 3D建模级输出落地全链路(从线稿到PBR材质渲染的7步工业级工作流)
  • 计算机组成原理 ALU 设计:从 1 位全加器到 32 位并行加法器的 5 步演进
  • SpringBoot集成LangChain4j构建企业级AI Agent工程实践
  • SpringBoot开发中那些容易忽略的配置细节
  • OpenMV Cam H7 GPIO 实战:5V 容错与 25mA 驱动能力实测,3 种模式详解
  • NMOS/PMOS 高边/低边驱动电路:3种典型拓扑与5个选型关键参数
  • 初入车联网那些事【纯文字】
  • Sunny 网络中间件 v3.1.0 实战:5步实现HTTP/HTTPS/WS/WSS/TCP全协议拦截与修改
  • 用 MCP 协议 + x402 微支付构建 47 端点 AI 工具聚合服务