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

快速排序(Quick Sort)算法详解(递归与非递归)

引言

在计算机科学中,排序算法是最基础且重要的算法之一。快速排序(Quick Sort)作为一种高效的排序算法,在实际应用中被广泛使用。平均时间复杂度为 (O(n log n)),最坏情况下为 (O(n^2))。本文将详细介绍快速排序算法的原理,结合具体代码进行分析,给出两种递归方法与一种非递归方法,并给出两种优化方案。

快速排序原理

快速排序的基本思想是通过选择一个基准元素(key),将数组分为两部分,使得左边部分的所有元素都小于等于基准元素,右边部分的所有元素都大于等于基准元素。然后递归地对左右两部分进行排序,最终得到一个有序的数组。

具体步骤如下:

  1. 选择基准元素:从数组中选择一个元素作为基准元素。
  2. 分区操作:将数组中的元素重新排列,使得所有小于等于基准元素的元素都在基准元素的左边,所有大于等于基准元素的元素都在基准元素的右边。这个过程称为分区(partition)。
  3. 递归排序:对左右两部分分别递归地应用快速排序算法。

代码实现

1.hoare法(递归)

/交换
void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
//快速排序
void QuickSort(int* a, int left, int right)
{if(left >= right)return;int key = left;int begin = left, end = right;while(begin < end){while(begin < end && a[end] > a[key]){end--;}while(begin < end && a[begin] <= a[key]){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);key = begin;QuickSort(a, left, key - 1);QuickSort(a, key + 1, right);}
}

 2.双指针法(递归)

//快排递归:双指针
int PartSort2(int* a, int left, int right)
{//三数取中(优化)int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);int key = left;int prev = left;int cur = prev + 1;while(cur <= right){if(a[cur] < a[key] && ++prev != cur){Swap(&a[cur], &a[prev]);}cur++;}Swap(&a[prev], &a[key]);return prev;
}
//快速排序(递归)
void QuickSort(int* a, int left, int right)
{if(left >= right)return;//小区间优化if((right - left + 1) < 10){InsertSort(a + left, right - left + 1);}else{// int key = PartSort1(a, left, right);int key = PartSort2(a, left, right);QuickSort(a, left, key - 1);QuickSort(a, key + 1, right);}
}

代码解释:
  1. Swap 函数:用于交换两个整数的值。
  2. QuickSort 函数
    • 递归终止条件:当 left >= right 时,说明数组已经有序,直接返回。
    • 分区操作:使用双指针法将数组分为两部分,左边部分的元素都小于等于基准元素,右边部分的元素都大于等于基准元素。
    • 递归排序:对左右两部分分别递归地调用 QuickSort 函数。

3.非递归法

递归方法来实现排序终归有栈溢出的风险,所以这里提供一种非递归方式实现快速排序

//快速排序(非递归)
void QuickSortNonR(int* a, int left, int right)
{ST st;STInit(&st);STPush(&st, right);STPush(&st, left);while(!STEmpty(&st)){int begin = STTop(&st);STPop(&st);int end = STTop(&st);STPop(&st);int key = PartSort2(a, begin, end);if(end > key + 1){STPush(&st, end);STPush(&st, key + 1);}if(begin < key - 1){STPush(&st, key - 1);STPush(&st, begin);}}STDestory(&st);
}

这里使用栈来存储指针begin与end ,栈在堆中存储,能通过malloc不断申请内存空间,有效规避了栈溢出的风险。

测试代码

下面对快速排序做一个简单的测试:

void TestOP()
{srand((unsigned int)time(NULL));const int N = 100000;int* a1 = (int*)malloc(sizeof(int) * N);for(int i = 0; i < N; i++){a1[i] = rand();}int begin1 = clock();// 此处可调用 QuickSort 进行测试QuickSort(a1, 0, N - 1);int end1 = clock();printf("QuickSort:%d\n", end1 - begin1);
}int main()
{TestOP();return 0;
}

在这个测试文件中,我们生成了一个包含 100000 个随机整数的数组,并调用 QuickSort 函数对其进行排序,最后输出排序所需的时间。

复杂度分析

  • 时间复杂度:平均情况下为 (O(n log n)),最坏情况下为 (O(n^2))。
  • 空间复杂度:递归调用栈的深度为 (O(log n)),因此空间复杂度为 (O(log n))。

优化方案 

1.三数取中

若数组排序前就有序,时间复杂度为O(n^2),那么此时三数取中就是消除这种接近有序带来的大量重复计算的优化方法

代码实现

//三数取中
int GetMid(int* a, int left, int right)
{int mid = (left + right) / 2;// left midi rightif (a[left] < a[mid]){if (a[mid] < a[right]){return mid;}else if (a[left] < a[right]){return right;}else{return left;}}else // a[left] > a[mid]{if (a[mid] > a[right]){return mid;}else if (a[left] < a[right]){return left;}else{return right;}}
}

紧接着再把返回的mid值与left处值交换即可

//三数取中(优化)int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);

2.小区间优化

我们通过前面所学内容可以得知快排的递归是类似与二叉树递归的一种算法,那么高度越高所消耗的空间越大,每个递归函数的调用会建立一个函数栈帧,为了避免出现栈溢出的情况,我们可以采取小区间优化来规避风险并高效实现排序。

代码实现

(可任选一种时间复杂度为O(n^2)的排序,这里选择更为高效的插入排序。)

//插入排序 最好O(N) 最坏O(N ^ 2)
void InsertSort(int* a, int n)
{for(int i = 0; i < n - 1; i++){int end = i;int tmp = a[end + 1];while(end >= 0){if(tmp < a[end]){a[end + 1] = a[end];end--;}else{break;}}a[end + 1] = tmp;}
}
//小区间优化if((right - left + 1) < 10){InsertSort(a + left, right - left + 1);}

3.其他

除了这些优化方案,可能有人会觉得快速排序难以理解,这里还有一种较为通俗的挖坑法(并没有改变排序效率,只是更为通俗易懂),可以自行查阅资料。

总结

快速排序是一种高效的排序算法,通过分治法和分区操作,将数组不断地分为两部分进行排序。在实际应用中,通过三数取中和小区间优化,可以提高算法的性能。希望本文对你理解快速排序算法有所帮助。

http://www.lqws.cn/news/101827.html

相关文章:

  • 吃透 Golang 基础:数据结构之 Map
  • 【Kotlin】高阶函数Lambda内联函数
  • PyTorch 入门学习笔记(数字识别实战)
  • 【Prompt实战】国际翻译小组
  • 为什么 uni-app 开发的 App 没有明显出现屏幕适配问题Flutter 开发的 App 出现了屏幕适配问题
  • Android 中的 DataBinding 详解
  • 如何轻松删除 Android 上的文件(3 种方法)
  • 从 Docker 到 Containerd:Kubernetes 容器运行时迁移实战指南
  • STM32H562----------ADC外设详解
  • 【亲测有效 | Cursor Pro每月500次快速请求扩5倍】(Windows版)Cursor中集成interactive-feedback-mcp
  • Python训练第四十三天
  • 定时线程池失效问题引发的思考
  • 智启未来:AI重构制造业供应链的五大革命性突破
  • 阿里云为何,一个邮箱绑定了两个账号
  • 冷雨泉教授团队:新型视觉驱动智能假肢手,拟人化抓握技术突破,助力截肢者重获生活自信
  • LINUX63 硬链接、软链接;FTP默认配置
  • 基于蝙蝠算法的路径优化
  • 基于大模型的短暂性脑缺血发作(TIA)全流程预测与干预系统技术方案
  • istringstream
  • ArrayList和LinkedList(深入源码加扩展)
  • 如何在PowerBI中使用Analyze in Excel
  • 基于springboot的图书管理系统的设计与实现
  • React 项目初始化与搭建指南
  • windows可视化粘贴使用剪贴板
  • 湖北理元理律师事务所:法律视角下的债务优化与生活平衡之道
  • 小体积涵盖日常办公等多功能的软件
  • NLP学习路线图(二十一): 词向量可视化与分析
  • unity UI Rect Transform“高”性能写法
  • 第1章_数据分析认知_知识点笔记
  • 强化学习鱼书(10)——更多深度强化学习的算法