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

目录

一、快速排序的基本思想

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

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. 稳定性:不稳定

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/132514.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

整理mongodb文档:副本集成员可以为偶数

个人博客 整理mongodb文档:副本集成员可以为偶数 想了下&#xff0c;仲裁节点还是不想直接说太多&#xff0c;怕有的同学想太多&#xff0c;且本身副本集就偏向运维的&#xff0c;新手基本也没什么权限操作&#xff0c;就不多废话了。 文章概叙 文章从MongoDB是否可以用偶数…

k8s - Flannel

1.Flannel概念剖析 Flannel是 CoreOS 团队针对 Kubernetes 设计的一个覆盖网络&#xff08;Overlay Network&#xff09;工具&#xff0c;其目的在于帮助每一个使用 Kuberentes 的 CoreOS 主机拥有一个完整的子网。这次的分享内容将从Flannel的介绍、工作原理及安装和配置三方…

20231012_python练习_服务端与客户端数据交互v2_增加xlsx表格数据批量导入数据库

服务端增加根据上传附件格式 xlsx 类型&#xff0c;将表格第一个sheet数据批量快速导入数据库 服务端 import socketserver import json import os #import pymysql import cx_Oracle #Oracle 数据库连接 import time import tqdm import pandas as pd import openpyxlclass …

vscode远程ssh服务器且更改服务器别名

目录 1、打开VS Code并确保已安装"Remote - SSH"扩展。如果尚未安装&#xff0c;请在扩展市场中搜索并安装它。 2、单击左下角的"Remote Explorer"图标&#xff0c;打开远程资源管理器。 3、在远程资源管理器中&#xff0c;单击右上角的齿轮图标&#x…

不用for循环,巧妙自动翻页获取所有数据

1、设置while循环的标记条件&#xff1b; 2、设置初始页码&#xff1b; 3、防止无限循环&#xff0c;设置最大循环次数为1000次&#xff1b; 4、当页码为第一次时&#xff0c;获取数据&#xff0c;并获取一共有多少页&#xff0c;并更新最大页码&#xff1b; 5、页码自动加…

小白网络安全学习手册

作为一个合格的网络安全工程师&#xff0c;应该做到攻守兼备&#xff0c;毕竟知己知彼&#xff0c;才能百战百胜。 谈起黑客&#xff0c;可能各位都会想到&#xff1a;盗号&#xff0c;其实不尽然&#xff1b;黑客是一群喜爱研究技术的群体&#xff0c;在黑客圈中&#xff0c;一…

Ubuntu 20.04使用源码安装nginx 1.14.0

nginx安装及使用&#xff08;详细版&#xff09;是一篇参考博文。 http://nginx.org/download/可以选择下载源码的版本。 sudo wget http://nginx.org/download/nginx-1.14.0.tar.gz下载源代码。 sudo tar xzf nginx-1.14.0.tar.gz进行解压。 cd nginx-1.14.0进入到源代码…

Python算法练习 10.12

leetcode 649 Dota2参议院 Dota2 的世界里有两个阵营&#xff1a;Radiant&#xff08;天辉&#xff09;和 Dire&#xff08;夜魇&#xff09; Dota2 参议院由来自两派的参议员组成。现在参议院希望对一个 Dota2 游戏里的改变作出决定。他们以一个基于轮为过程的投票进行。在每…

掌动智能:UI自动化测试工具几点优势

UI自动化测试工具在现代软件开发中扮演着重要的角色&#xff0c;它们能够有效地验证应用程序的用户界面&#xff0c;确保其在不同平台和设备上的正常运行。本文将介绍掌动智能UI自动化测试工具的优势有哪些! 掌动智能UI自动化测试工具优势 1、零成本入门 自然语言编写&#xff…

2023年中国划船机产量、销量及市场规模分析[图]

划船机是一种健身器材&#xff0c;它模拟了划船的运动&#xff0c;可以锻炼身体的肌肉力量和协调性。划船机通常由座椅、把手、脚踏板和传动装置组成&#xff0c;使用者可以通过拉动把手来模拟划船的动作&#xff0c;从而达到锻炼身体的目的。 划船机产业链 资料来源&#xff…

聊聊分布式架构05——[NIO基础]BIO到NIO的演进

目录 I/O I/O模型 BIO示例 BIO与NIO比较 NIO的三大核心 NIO核心之缓冲区 Buffer常用子类&#xff1a; Buffer常用API Buffer中的重要概念 NIO核心之通道 FileChannel 类 FileChannel常用方法 NIO核心之选择器 概述 应用 NIO非阻塞原理分析 服务端流程 客户端…

ESP32-S3上手开发

1、搭建开发环境 首先搭建开发环境&#xff0c;这里采用了windows下集成开发环境ide进行开发&#xff0c;具体的安装方法&#xff1a;ESP-IDF安装配置 这里使用的乐鑫的esp32s3&#xff0c;N16R8 2、esp32s3模块 从上面图中可以看到&#xff0c;N16R8这里使用了外扩16M的fl…