八大排序算法之快速排序(下篇)(快排的优化+非递归快排的实现)
创始人
2025-06-01 10:59:05
0

目录

一.前言

1.快速排序的实现:

快速排序的单趟排序(排升序)(快慢指针法实现):​编辑

2.未经优化的快排的缺陷

二.快速排序的优化

1.三数取中优化

优化思路:

2. 小区间插入排序优化

小区间插排优化的递归快排:

三.非递归快速排序的实现

1.快排一个无法避免的缺陷

2.非递归快排的实现思路

数据结构栈模拟系统栈算法思想:​编辑

非递归快排代码实现:


一.前言

1.快速排序的实现:

🤪快排的详细实现原理参见青菜的博客🤪:http://t.csdn.cn/0bf1ghttp://t.csdn.cn/0bf1g下面简单回顾一下快排的核心思想:

快速排序的单趟排序(排升序)(快慢指针法实现):

int PartionV3(int* arr, int left, int right)//完成1个元素的排序,同时分割数组
{assert(arr);int key = left;										//选取数组左端元素作为key值int slow = left;								    //slow从left开始  int fast = left + 1;							    //fast从left+1开始遍历数组while (fast <= right){if (arr[fast] < arr[key])			//fast找到比key小的值{++slow;				   			//slow指向下一个位置if (slow != fast)				//fast和slow相等没必要交换{swap(&arr[slow], &arr[fast]);//交换slow和fast所指向的值}}++fast;							 //fast遍历数组}swap(&arr[key], &arr[slow]);         //最后交换key和slow所指向的变量return slow;  						 //返回slow位置下标
}

  • 🤪假设乱序数组有N个元素,则需要进行N趟单趟排序(每次单趟排序可以完成一个key元素在有序序列中的归位)
  • 🤪直接循环N-1次单趟排序的时间复杂度为O(N^2)
  • 🤪为了降低排序的时间复杂度的数量级,我们采用分治递归思想来完成这N次单趟排序
  • 🤪基本思想是:每次单趟排序完成后,以被归位的key元素为划分点,将数组划分为在排序意义上互不关联的两个子数组(左子数组中每个元素都比key小,右子数组中每个元素都比key大),从而使得后续每次单趟排序需要遍历的元素个数呈指数式递减,总体排序的时间复杂度也因此降阶,递归过程中数组被逐步划分过程的图示:(划分结构逻辑上类似于二叉树)
  • 上图中每个数组拆分层次中(一个拆分层次中有多个子数组),单趟排序所需遍历的总的元素个数的数量级为O(N),拆分层次的数量级为O(logN),因此上述情形下,快排的总体时间复杂度为O(NlogN).

  • 😜这里我们假设所处理的数组都是逆序数极大的乱序序列:这种情况下,每次单趟排序所确定的数组分割点有极大的可能是在数组的中间位置。

  • 😜对应上图完成排序的分治递归代码: 

    //每完成一次数组分割,就进行左右子数组分治递归完成每一个元素的排序
    void QuickSort2(int* arr, int left, int right)
    {assert(arr);if (left>=right)							    //子数组只剩1个元素时(或left和right错开时)停止递归{return;}int breakpoint = PartionV3(arr, left, right);	//找到数组分割点(同时也完成了一个元素的排序)//左右子数组分治递归QuickSort(arr, left, breakpoint - 1);     //处理左子数组QuickSort(arr, breakpoint + 1, right);    //处理右子数组
    }
  • 😜注意递归结束条件的控制😜

2.未经优化的快排的缺陷

  • 单趟排序时,我们将子数组的左端元素作为key,因此在处理有序或接近有序以及倒序或完全倒序的序列时,大多数情况下经过单趟排序后,数组的分割点(key元素的最终位置)会停留在数组左端(倒序的情况则停留在数组右端),于是整个排序过程中,数组被逐步划分过程的图示:
  • 此时划分层次数量级为O(N),每个划分层次中单趟排序所需遍历的元素个数呈等差数列递减,此时分治递归的时间复杂度计算公式为等差数列求和式,数量级升阶为O(N^2);
  • 为了避免这种情况出现,我们需要对key元素的选取方式进行优化

二.快速排序的优化

1.三数取中优化

优化思路:

  • 为了避免序列有序或接近有序时,分治递归过程中不断在数组区间端点处对数组进行划分(从而导致数组划分总次数数量级为O(N)),我们采用三数取中的方式优化key的选取:
  1. 设计一个接口,确定数组的首元素,尾元素和中间位置元素三者中的中位数(中间大小的数),并返回其下标.接口首部:
    int GetMid(int* arr,int left,int right)

     

    接口实现: 

    int GetMid(int* arr,int left,int right)
    {int mid = left + ((right - left) >> 2);     //在arr[left],arr[mid],arr[right]三者中取中间值作为key,返回key的下标if (arr[left] < arr[right]){if (arr[left] < arr[mid] && arr[mid] < arr[right]){return mid;}else if (arr[mid] > arr[right]){return right;}else{return left;}}else{if (arr[left] > arr[mid] && arr[mid] > arr[right]){return mid;}else if (arr[mid] > arr[left]){return left;}else{return right;}}
    }
  2. 确定数组的首元素,尾元素和中间位置元素三者中的中位数后,将其交换到arr[left]的位置,后续的单趟排序过程我们依然选择arr[left]作为key元素

  3. 三数取中优化后的单趟排序:

     

    int PartionV3(int* arr, int left, int right)//完成1个元素的排序,同时分割数组
    {assert(arr);swap(&arr[left], &arr[GetMid(arr, left, right)]);int key = left;										//在arr[left],arr[mid],arr[right]三者中取中间值作为keyint slow = left;								    //slow从left开始  int fast = left + 1;							    //fast从left+1开始遍历数组while (fast <= right){if (arr[fast] < arr[key])			//fast找到比key小的值{++slow;				   			//slow指向下一个位置if (slow != fast)				//fast和slow相等没必要交换{swap(&arr[slow], &arr[fast]);//交换slow和fast所指向的值}}++fast;							 //fast遍历数组}swap(&arr[key], &arr[slow]);         //最后交换key和slow所指向的变量return slow;  						 //返回slow位置下标
    }
  4. 经过三数取中后的单趟排序,面对有序序列时,数组分割点(key元素的最终位置)会确定在数组的中间位置,因此分治递归时,函数栈帧的逻辑分布会呈现出满二叉树的结构(递归深度最小),也就意味着,在面对有序(或接近有序)的序列时,三数取中优化后的快排有着最高的排序效率(相比于排序其余乱序序列的情况).这一点大大增加了快排在各种实际场景中的适用性

     (每个子数组代表一个函数栈帧)

  5. 同时三数取中优化也让快排的单趟排序在处理各种乱序序列时,令数组的分割点(key元素的最终位置)尽可能地接近数组中间位置

2. 小区间插入排序优化

  • 分治递归过程中的函数栈帧逻辑分布(每一个子数组代表了一次函数递归调用):
  • 根据二叉树的结构特点(满二叉树最后一层结点个数占总结点个数的一半)可知,元素个数为1个(以及2个3个等等)的子数组个数最多(一个子数组代表一次函数递归调用),因此我们可以修改递归的结束条件:当子数组的元素个数小于10(可以在一定范围内任意设定)时,对子数组进行插入排序后结束递归
  • 插入排序接口:
    void InsertSort(DataType* arr, int size)
    {assert(arr);for (int end = 0; end < size; end++)    //用end来维护数组前end个元素构成的有序序列{int x = arr[end];					//x为待插入有序序列的数据int insert = end;					//用变量insert来确定x要插入的位置while (insert>0)					//通过元素比较确定x要插入的位置{if (x < arr[insert-1])			   //说明insert不是x要插入的位置{arr[insert] = arr[insert-1];   //往后挪动数据为x留出空位insert--;                      //令insert指向序列前一个元素}else{break;	    //有序序列中x>=arr[insert-1]说明insert是x要插入的位置}}					//最坏的情况下x会被插入到数组的首地址处(此时数据比较和挪动了end次)arr[insert] = x;    //完成元素x的插入(继续插入下一个元素)}
    }
    

    小区间插排优化的递归快排:

    //每完成一次数组分割,就进行左右子数组分治递归完成每一个元素的排序
    void QuickSort(int* arr, int left,int right)
    {assert(arr);if (right - left+1 <=10)							//子数组只剩10个元素时(小区间插排)停止递归{InsertSort(arr + left, right - left+1);return;}int breakpoint = PartionV3(arr, left, right);		//找到数组分割点(同时也完成了一个元素的排序)//左右子数组分治递归QuickSort(arr, left, breakpoint - 1);     //处理左子数组QuickSort(arr, breakpoint + 1, right);    //处理右子数组
    }

  • 经过上述小区间插入排序处理后的分治快排,函数的递归调用次数减少了一半以上,这对于递归优化比较落后的编译平台是非常友好的

附:由于现代编译器对于递归函数的时间效率会进行大幅程度的优化,因此快排的小区间插排优化实测效果并不明显

三.非递归快速排序的实现

1.快排一个无法避免的缺陷

  • 当序列为常序列(所有元素相同)或序列中有大量(绝大部分)元素相同时,快排递归中数组的划分过程也会出现下面类似的情况:
  • 其原因与前面未经优化的快排面对有序序列时时间复杂度升阶的原因是一致的(常序列是特殊的有序序列),并且面对常序列或大量(绝大部分)元素相同的序列时,三数取中优化是起不到任何作用的(三个元素都是相同的,取中位数没有任何意义)
  • 因此使用递归快排时,如果处理的序列为常序列或大量(绝大部分)元素相同的序列,会有一定的栈溢出风险(上图中的递归深度为O(N),即递归过程中会有O(N)数量级个函数栈帧同时存在)(此时快排的时间复杂度为O(NlogN),可见常序列(或大量(绝大部分)元素相同的序列)要避免使用快排进行处理)
  • 所以实际运用中,我们需要用数据结构栈来模拟系统栈,利用循环结构实现快排.

2.非递归快排的实现思路

  • 在快排递归的过程中,每个函数栈帧中存储的关键变量是待排序的子数组的左右端下标(left和right)
  • 函数栈帧进出系统栈的规则满足后进先出
  • 因此我们可以用数据结构中的整形栈来存储每个待排序的子数组的左右端点下标,借助数据结构栈我们就可以利用循环结构来模拟递归的过程

函数首部: 

void QuickSortNonR(int* arr, int begin, int end)

begin和end是待排序的数组左端下标和右端下标(两个下标都指向有效元素)(闭区间)

  • 为了方便我们借助C++STL的栈对象来完成模拟递归,快排递归(前序递归)遍历次序:

数据结构栈模拟系统栈算法思想:

  • 先将原数组的区间左右端下标压入栈中,接着开始执行循环:
  1. 首先,利用left和right变量来接收栈顶区间
  2. 对[left,right]子数组进行单趟排序,完成单个元素排序的同时确定数组的分割点下标breakpoint
  3. 将[left,right]数组的右子数组[breakpoint+1,right]区间和左子数组[left,breakpoint-1]区间压入栈中(注意右子数组区间要先入栈,左子数组区间要后入栈,保证后续左子数组先出栈被处理)(左右子数组区间入栈的前提是区间中的元素个数大于或等于2)
  • 重复上述步骤直到栈为空,则完成了分治递归的模拟过程(相当于模拟了前序遍历的递归过程)
  • 算法gif:

非递归快排代码实现:

void QuickSortNonR(int* arr, int left, int right)
{assert(arr);stack range;if (right > left)                //区间中有两个以上元素时,区间入栈{range.push(right);           //取的时候先取左边界,因此左边界后入栈(令左边界压在右边界上面)range.push(left);            //初始区间入栈;}while (!range.empty()){int left = range.top();	     //令栈顶区间出栈range.pop();int right = range.top();range.pop();int breakpoint = PartionV3(arr, left, right); //确定区间分割点,同时完成一个数组元素的排序//栈是后进先出,递归是先向左子数组递归,因此做左子数组区间后入栈if (right>breakpoint+1)					      //若左子区间元素个数大于一个则左区间入栈{range.push(right);range.push(breakpoint + 1);}if(breakpoint-1>left){range.push(breakpoint - 1);range.push(left);}}
}
  • 区间入栈时注意左右端下标入栈顺序

 

 

相关内容

热门资讯

南昌南斯友好路两家“海湾石油”... 今天(2日)上午 多名消费者向《都市现场》反映 他们在南昌南斯友好路上的两家“海湾石油” 预付充值了...
曝拜仁与莱奥密谈!AC米兰索要... 足坛转会市场再起波澜!德甲巨无霸拜仁慕尼黑,这次将目光投向了亚平宁半岛,盯上了AC米兰的王牌边锋——...
曼联2500万甩卖“铁腰”,那... 提起苏格兰中场斯科特·麦克托米奈,如今的足坛可谓是无人不知,无人不晓。但就在2024年夏天,当曼联以...
行驶中推送广告、恶意更新隐私政... 深蓝汽车法务部今日(6 月 2 日)发布声明提出,部分网络内容发布 “深蓝汽车在行驶过程中推送广告严...
德国银行高管:美政府政策频繁变... 根据德国《商报》6月2日刊发的专访文章,德国国家开发银行复兴信贷银行董事会主席斯特凡·温特尔斯指出,...
过程不到2分钟!深夜母子潜入建... 前几天, 曲靖会泽一加气站内的井盖被盗, 幸好工作人员发现及时 才没造成人员受伤, 民警随即展开调查...
李亚鹏否认丽江项目失败,称开盘... 近日,李亚鹏在社交平台发视频辟谣丽江项目失败:丽江项目做得挺好的,在2015年开盘,就是丽江销售的第...
宝信软件(安徽)股份申请基于高... 金融界2025年6月2日消息,国家知识产权局信息显示,宝信软件(安徽)股份有限公司;清华大学申请一项...
曼联50年最差赛季引援告急!“... 曼联跌下神坛!50年最差赛季敲响警钟,球员宁投蓝军不来投,“梦剧场”光环褪色,新帅阿莫林能否力挽狂澜...
又换一个?旺达勾搭上阿什拉夫,... 直播吧06月02日讯 据粉丝超过300万的阿根廷八卦记者Yani Latorre透露,旺达近期勾搭上...