我的编程空间,编程开发者的网络收藏夹
学习永远不晚

快速排序详解(递归实现与非递归实现)

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

快速排序详解(递归实现与非递归实现)

目录

一、快速排序的基本思想

二、将序列划分成左右区间的常见方法

2.1hoare版本(动图+解释+代码实现)

2.2挖坑法

2.3前后指针法

三、快速排序的初步实现

四、快速排序的优化实现

4.1快排的特殊情况

4.2对区间划分代码的优化

4.3小区间优化

五、快速排序的非递归实现

六、总结


一、快速排序的基本思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

// 假设按照升序对array数组中[left, right)区间中的元素进行排序void QuickSort(int array[], int left, int right){ if(right - left <= 1) return;  // 按照基准值对array数组的 [left, right)区间中的元素进行划分 int div = partion(array, left, right);  // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right) // 递归排[left, div) QuickSort(array, left, div);  // 递归排[div+1, right) QuickSort(array, div+1, right);}
上述为快速排序递归实现的主框架,会发现与二叉树前序遍历规则非常像,先取中间,递归左区间,再递归右区间。

二、将序列划分成左右区间的常见方法

从上面的主框架就可以看出,我们需要一个partion函数,将序列分为左区间和右区间。下面我将介绍三种划分左右区间的方法。我通常是选取最左边的数作为基准值,将比它小的数都换到它的左边,比它大的数都换到它的右边。

2.1hoare版本(动图+解释+代码实现)

hoare版本的思想就是选取最左边的数作为基准值,一个左标志一个右标志,左标志指向序列的第一个元素,右标志指向序列的最后一个元素。让右标志先开始往左走,找比基准值小的数,找到了就停下来;再让左标志开始往右走,找比基准值大的数,找到了就停下来,然后交换左右标志所指向的值然后重复红色字体的过程,直到左标志和右标志相遇,最后交换基准值和相遇标志所指向的值。基准值就出现在了它最后应该出现的地方,它的左区间都是小于等于它的值,右区间都是大于等于它的值。这时可能有人就会问了,相遇标志换到最前面了,一定能保证相遇标志对应的值比基准值小吗?答案是一定的。因为如果是左标志停下来了右标志走来跟左标志相遇,左标志对应的值一定是比基准值要小的(交换完以后左标志停下来右标志继续走,此时左标志对应的值一定是比基准值要小的);如果是右标志停下来左标志走来跟右标志相遇,那右标志一定是找到比基准值小的数才会停下来,同样可以保证相遇标志对应的值比基准值要小。

int PartSort1(int* a, int left, int right){int keyi = left;//取最左边的数为基准值while (left < right){//找小while (left < right && a[right] >= a[keyi]){right--;}//找大while (left < right && a[left] <= a[keyi]){left++;}Swap(&a[left], &a[right]);}Swap(&a[keyi], &a[left]);return left;}

2.2挖坑法

挖坑法虽然与hoare的方法实现上有差异,但思想上其实差别不大。

坑位就是最终key要去的位置。

int PartSort2(int* a, int left, int right){int key = a[left];int hole = left;while (left < right){//找小while (left < right && a[right] >= key){right--;}a[hole] = a[right];hole = right;//找大while (left < right && a[left] <= key){left++;}a[hole] = a[left];hole = left;}//左右标志相遇在坑位上,相遇时坑位的左边都小于等于key,右边都大于等于key,//坑位就是key应该去的位置。a[hole] = key;return hole;}

2.3前后指针法

prev最终在的位置的值一定会比key的值要小,prev最终在的位置也就是key最终要去的位置。交换到最后prev位置以及它前面的值都是比key要小的,后面的值都是大于等于key的。

int PartSort3(int* a, int left, int right){int prev = left;int cur = prev + 1;int keyi = left;while (cur <= right){if ( a[keyi] > a[cur]  && ++prev != cur)//a[keyi] > a[cur]找小,但如果a[keyi] > a[cur]一直成立,//++prev 会一直== cur不会拉开差距。只有a[keyi] <= a[cur],++prev 才会和cur拉开差距。Swap(&a[prev], &a[cur]);cur++;}Swap(&a[keyi], &a[prev]);return prev;}

三、快速排序的初步实现

void QuickSort(int* a, int left, int right){if (left >= right)//区间只有一个值或区间不存在时就返回return;int keyi = PartSort1(a, left, right);    //这里用PartSort1/PartSort2/PartSort3都可以,时间效率都是一样的QuickSort(a, left, keyi-1);QuickSort(a, keyi+1, right);//不断递归左区间和右区间}

四、快速排序的优化实现

4.1快排的特殊情况

上面的写法面对绝大多数情况的排序已经可以实现时间复杂度接近O(n*logn),但面对某些特殊的情况,比如说你要将一个序列排成一个升序序列,然而这个序列本身就是一个升序序列,那就会造成keyi的左边没有数而其他数都在它的右边的情况,这样就会造成时间复杂度变成O(n^{2}),影响了快排的效率。看下图解释,红色矩形代表keyi的位置,如果是一个完全无序的序列进行排序,keyi所处的为值一般不会特别靠左也不会特别靠右,能保证排序的时间复杂度接近O(n*logn)

但如果是下面这种情况keyi一直位于最左侧,就会使时间复杂度为 O(n^{2})(每一次遍历时间复杂度为O(n),遍历n次)。

 

所以为了防止这种特殊情况的出现,可以对划分区间的代码进行一点优化。

4.2对区间划分代码的优化

我们还是可以取最左边的数作为key,只是要将最左边的数换成一个在序列中不是那么大也不是那么小的数,在此我们引进了一段三数取中代码。

int getMid(int* a, int left,int right){int mid = (left + right) / 2;if (a[left] > a[right]){if (a[mid] > a[left]){return left;}else if (a[mid] > a[right]){return mid;}else{return right;}}else{if (a[mid] > a[right])return right;else if (a[mid] > a[left])return mid;elsereturn left;}}

这段代码负责找到序列中最左边,最右边,中间三个数中第二大的那个数。区间划分代码加入三数取中后,就可以规避掉刚才所说的那种特殊情况了。

int PartSort1(int* a, int left, int right){int mid = getMid(a, left, right);Swap(&a[left], &a[mid]);int keyi = left;//取最左边的数为基准值while (left < right){//找小while (left < right && a[right] >= a[keyi]){right--;}//找大while (left < right && a[left] <= a[keyi]){left++;}Swap(&a[left], &a[right]);}Swap(&a[keyi], &a[left]);return left;}

这里以hoare版本为例,其它两种区间划分方法也可以加上三数取中

4.3小区间优化

因为在递归到后期时,有的小序列已经接近有序,使用直接插入排序效率就会很高。(直接插入排序在希尔排序那篇博客已有实现,在这里就不再赘述)

void QuickSort(int* a, int left, int right){if (left >= right)return;//小区间优化if ((right - left + 1) > 10)//当区间长度大于10时{int keyi = PartSort1(a, left, right);QuickSort(a, left, keyi-1);QuickSort(a, keyi+1, right);}else//区间长度小于10时{InsertSort(a + left, right - left + 1);}}

五、快速排序的非递归实现

快排使用到了递归的思想和方法,但是递归如果递归太深的话就会有爆栈的风险,所以在这里也介绍一下快速排序的非递归实现方法。因为快排是先处理左边的序列再处理右边的序列,这里就需要用到数据结构中的栈了,先入右再入左,进行排序。(栈的结构在之前的博客中已经实现过了,在这里同样不赘述)

void QuickSortNonR(int* a, 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 keyi = PartSort1(a, begin, end);        //不断把大区间化成小区间处理if (keyi + 1 < end){StackPush(&st, end);StackPush(&st, keyi+1);}if (begin < keyi - 1){StackPush(&st, keyi-1);StackPush(&st, begin);}}StackDestroy(&st);}

六、总结

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序 2. 时间复杂度:O(N*logN) 3. 空间复杂度:O(logN) 4. 稳定性:不稳定

来源地址:https://blog.csdn.net/m0_74265792/article/details/133612517

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

快速排序详解(递归实现与非递归实现)

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

c语言递归和非递归排序怎么实现

本篇内容主要讲解“c语言递归和非递归排序怎么实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“c语言递归和非递归排序怎么实现”吧!递归代码流程归并就是把两个或多个序列合并,这里只介绍二路归并,就
2023-06-30

C++ 函数的递归实现:递归与非递归算法的比较分析?

递归算法通过函数自调用解决结构化的问题,优点是简洁易懂,缺点是效率较低且可能发生堆栈溢出;非递归算法通过显式管理堆栈数据结构避免递归,优点是效率更高且避免堆栈溢出,缺点是代码可能更复杂。选择递归或非递归取决于问题和实现的具体限制。C++ 函
C++ 函数的递归实现:递归与非递归算法的比较分析?
2024-04-22

python非递归全排列实现方法

刚刚开始学习python,当前看到了函数这一节。结合数组操作,写了个非递归的全排列生成。原理是插入法,也就是在一个有n个元素的已有排列中,后加入的元素,依次在前,中,后的每一个位置插入,生成n+1个新的全排列。因为Python切割数组或者字
2022-06-04

原:八皇后问题的递归和非递归Java实现

原:八皇后问题的递归和非递归实现八皇后问题是一个古老而著名的问题,是回溯算法的典型例题。该问题是十九世纪著名 的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线
2023-06-03

Python3实现快速排序、归并排序、堆

# -*- coding: utf-8 -*-# @Time : 2019-03-26 16:46# @Author : Jayce Wong# @ProjectName : leetcode# @FileNa
2023-01-31

C++ 函数递归详解:递归调用的形式和实现

递归是函数自身调用的一种编程技术,在 c++++ 中有两种常见形式:直接递归和间接递归。要实现递归,函数必须满足基线条件和递归调用。实战案例中,利用递归计算阶乘,其基线条件是 n 为 0 时返回 1,递归调用是函数乘以 n 并调用自身,递减
C++ 函数递归详解:递归调用的形式和实现
2024-05-04

C++实现二叉树非递归遍历算法详解

在C++中,二叉树非递归遍历是一种常用的算法,可避免递归过程中的系统开销和栈溢出问题。非递归遍历算法利用栈数据结构实现,可以实现前序、中序和后序遍历,是C++程序员必备技能之一
2023-05-17

java中如何实现递归排列

递归排列递归,俗称“我 调 我 自 己”,如果从数据结构的角度来理解,其实就是栈。假如我们要求得到A、B、C的排列,流程大概如下:(0)初始状态,栈内无数据。此时栈外:A、B、C(1)将A放入栈底。此时栈外:B、C(2)将B放入栈中。此时栈外:C(3)将C放入
java中如何实现递归排列
2020-04-05

kotlin实现快递与号码归属地查询案例详解

时间轴时一个很炫酷的效果,一般作用在物流信息上,我们同样也可以作为一个学习对象去学习他的使用方法,同时呢,我们可以在线查询我们的电话号码归属地,巧用键盘的逻辑提升我们用户体验
2023-02-16

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录