漫画:什么是快速排序?(完整版)

转载自   漫画:什么是快速排序?(完整版)

 

 

 

 

 

 

 

同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。

不同的是,冒泡排序在每一轮只把一个元素冒泡到数列的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。

 

这种思路就叫做分治法

每次把数列分成两部分,究竟有什么好处呢?

假如给定8个元素的数列,一般情况下冒泡排序需要比较8轮,每轮把一个元素移动到数列一端,时间复杂度是O(n^2)。

而快速排序的流程是什么样子呢?

 

如图所示,在分治法的思想下,原数列在每一轮被拆分成两部分,每一部分在下一轮又分别被拆分成两部分,直到不可再分为止。

这样一共需要多少轮呢?平均情况下需要logn轮,因此快速排序算法的平均时间复杂度是 O(nlogn)

 

 

基准元素的选择

基准元素,英文pivot,用于在分治过程中以此为中心,把其他元素移动到基准元素的左右两边。

那么基准元素如何选择呢?

最简单的方式是选择数列的第一个元素:

 

这种选择在绝大多数情况是没有问题的。但是,假如有一个原本逆序的数列,期望排序成顺序数列,那么会出现什么情况呢?

 

..........

 

 

我们该怎么避免这种情况发生呢?

其实很简单,我们可以不选择数列的第一个元素,而是随机选择一个元素作为基准元素

这样一来,即使在数列完全逆序的情况下,也可以有效地将数列分成两部分。

当然,即使是随机选择基准元素,每一次也有极小的几率选到数列的最大值或最小值,同样会影响到分治的效果。

所以,快速排序的平均时间复杂度是 O(nlogn),最坏情况下的时间复杂度是 O(n^2)

 

元素的移动

选定了基准元素以后,我们要做的就是把其他元素当中小于基准元素的都移动到基准元素一边,大于基准元素的都移动到基准元素另一边。

具体如何实现呢?有两种方法:

 

1.挖坑法

2.指针交换法

何谓挖坑法?我们来看一看详细过程。

给定原始数列如下,要求从小到大排序:

 

首先,我们选定基准元素Pivot,并记住这个位置index,这个位置相当于一个“坑”。并且设置两个指针left和right,指向数列的最左和最右两个元素:

 

接下来,从right指针开始,把指针所指向的元素和基准元素做比较。如果比pivot大,则right指针向左移动;如果比pivot小,则把right所指向的元素填入坑中。

在当前数列中,1<4,所以把1填入基准元素所在位置,也就是坑的位置。这时候,元素1本来所在的位置成为了新的坑。同时,left向右移动一位。

 

此时,left左边绿色的区域代表着小于基准元素的区域。

接下来,我们切换到left指针进行比较。如果left指向的元素小于pivot,则left指针向右移动;如果元素大于pivot,则把left指向的元素填入坑中。

在当前数列中,7>4,所以把7填入index的位置。这时候元素7本来的位置成为了新的坑。同时,right向左移动一位。

 

此时,right右边橙色的区域代表着大于基准元素的区域。

下面按照刚才的思路继续排序:

8>4,元素位置不变,right左移

 

2<4,用2来填坑,left右移,切换到left。

 

6>4,用6来填坑,right左移,切换到right。

 

3<4,用3来填坑,left右移,切换到left。

 

5>4,用5来填坑,right右移。这时候left和right重合在了同一位置。

这时候,把之前的pivot元素,也就是4放到index的位置。此时数列左边的元素都小于4,数列右边的元素都大于4,这一轮交换终告结束。

 

 

 
public static void quickSort(int[] arr, int startIndex, int endIndex) {// 递归结束条件:startIndex大等于endIndex的时候if (startIndex >= endIndex) {return;}// 得到基准元素位置int pivotIndex = partition(arr, startIndex, endIndex);// 用分治法递归数列的两部分quickSort(arr, startIndex, pivotIndex - 1);quickSort(arr, pivotIndex + 1, endIndex);
}private static int partition(int[] arr, int startIndex, int endIndex) {// 取第一个位置的元素作为基准元素int pivot = arr[startIndex];int left = startIndex;int right = endIndex;// 坑的位置,初始等于pivot的位置int index = startIndex;//大循环在左右指针重合或者交错时结束while ( right >= left  ){//right指针从右向左进行比较while ( right >= left ) {if (arr[right] < pivot) {arr[left] = arr[right];index = right;left++;break;}right--;}//left指针从左向右进行比较while ( right >= left ) {if (arr[left] > pivot) {arr[right] = arr[left];index = left;right--;break;}left++;}}arr[index] = pivot;return index;
}public static void main(String[] args) {int[] arr = new int[] {4,7,6,5,3,2,8,1};quickSort(arr, 0, arr.length-1);System.out.println(Arrays.toString(arr));
}
}

代码中,quickSort方法通过递归的方式,实现了分而治之的思想。

partition方法则实现元素的移动,让数列中的元素依据自身大小,分别移动到基准元素的左右两边。在这里,我们使用移动方式是挖坑法。

 

 

指针交换法

何谓指针交换法?我们来看一看详细过程。

给定原始数列如下,要求从小到大排序:

开局和挖坑法相似,我们首先选定基准元素Pivot,并且设置两个指针left和right,指向数列的最左和最右两个元素:

 

接下来是第一次循环,从right指针开始,把指针所指向的元素和基准元素做比较。如果大于等于pivot,则指针向移动;如果小于pivot,则right指针停止移动,切换到left指针。

在当前数列中,1<4,所以right直接停止移动,换到left指针,进行下一步行动。

轮到left指针行动,把指针所指向的元素和基准元素做比较。如果小于等于pivot,则指针向移动;如果大于pivot,则left指针停止移动。

由于left一开始指向的是基准元素,判断肯定相等,所以left右移一位。

由于7 > 4,left指针在元素7的位置停下。这时候,我们让left和right指向的元素进行交换

 

接下来,我们进入第二次循环,重新切换到right向左移动。right先移动到8,8>4,继续左移。由于2<4,停止在2的位置。

 

切换到left,6>4,停止在6的位置。

 

元素6和2交换。

进入第三次循环,right移动到元素3停止,left移动到元素5停止。

元素5和3交换。

进入第四次循环,right移动到元素3停止,这时候请注意,left和right指针已经重合在了一起。

当left和right指针重合之时,我们让pivot元素和left与right重合点的元素进行交换。此时数列左边的元素都小于4,数列右边的元素都大于4,这一轮交换终告结束。

 

 

 

public class QuickSort {
public static void quickSort(int[] arr, int startIndex, int endIndex) {// 递归结束条件:startIndex大等于endIndex的时候if (startIndex >= endIndex) {return;}// 得到基准元素位置int pivotIndex = partition(arr, startIndex, endIndex);// 根据基准元素,分成两部分递归排序quickSort(arr, startIndex, pivotIndex - 1);quickSort(arr, pivotIndex + 1, endIndex);
}private static int partition(int[] arr, int startIndex, int endIndex) {// 取第一个位置的元素作为基准元素int pivot = arr[startIndex];int left = startIndex;int right = endIndex;while( left != right) {//控制right指针比较并左移while(left<right && arr[right] > pivot){right--;}//控制right指针比较并右移while( left<right && arr[left] <= pivot) {left++;}//交换left和right指向的元素if(left<right) {int p = arr[left];arr[left] = arr[right];arr[right] = p;}}//pivot和指针重合点交换int p = arr[left];arr[left] = arr[startIndex];arr[startIndex] = p;return left;
}public static void main(String[] args) {int[] arr = new int[] {4,7,6,5,3,2,8,1};quickSort(arr, 0, arr.length-1);System.out.println(Arrays.toString(arr));
}
}

和挖坑法相比,指针交换法在partition方法中进行的元素交换次数更少。

非递归实现

 

 

为什么这样说呢?

因为我们代码中一层一层的方法调用,本身就是一个函数栈。每次进入一个新方法,就相当于入栈;每次有方法返回,就相当于出栈。

所以,我们可以把原本的递归实现转化成一个栈的实现,在栈当中存储每一次方法调用的参数:

下面我们来看一下代码:

public class QuickSortWithStack {
public static void quickSort(int[] arr, int startIndex, int endIndex) {// 用一个集合栈来代替递归的函数栈Stack<Map<String, Integer>> quickSortStack = new Stack<Map<String, Integer>>();// 整个数列的起止下标,以哈希的形式入栈Map rootParam = new HashMap();rootParam.put("startIndex", startIndex);rootParam.put("endIndex", endIndex);quickSortStack.push(rootParam);// 循环结束条件:栈为空时结束while (!quickSortStack.isEmpty()) {// 栈顶元素出栈,得到起止下标Map<String, Integer> param = quickSortStack.pop();// 得到基准元素位置int pivotIndex = partition(arr, param.get("startIndex"), param.get("endIndex"));// 根据基准元素分成两部分, 把每一部分的起止下标入栈if(param.get("startIndex") <  pivotIndex -1){Map<String, Integer> leftParam = new HashMap<String, Integer>();leftParam.put("startIndex",  param.get("startIndex"));leftParam.put("endIndex", pivotIndex -1);quickSortStack.push(leftParam);}if(pivotIndex + 1 < param.get("endIndex")){Map<String, Integer> rightParam = new HashMap<String, Integer>();rightParam.put("startIndex", pivotIndex + 1);rightParam.put("endIndex", param.get("endIndex"));quickSortStack.push(rightParam);}}
}private static int partition(int[] arr, int startIndex, int endIndex) {// 取第一个位置的元素作为基准元素int pivot = arr[startIndex];int left = startIndex;int right = endIndex;while( left != right) {//控制right指针比较并左移while(left<right && arr[right] > pivot){right--;}//控制right指针比较并右移while( left<right && arr[left] <= pivot) {left++;}//交换left和right指向的元素if(left<right) {int p = arr[left];arr[left] = arr[right];arr[right] = p;}}//pivot和指针重合点交换int p = arr[left];arr[left] = arr[startIndex];arr[startIndex] = p;return left;}public static void main(String[] args) {int[] arr = new int[] {4,7,6,5,3,2,8,1};quickSort(arr, 0, arr.length-1);System.out.println(Arrays.toString(arr));}}

和刚才的递归实现相比,代码的变动仅仅在quickSort方法当中。该方法中引入了一个存储Map类型元素的栈,用于存储每一次交换时的起始下标和结束下标。

每一次循环,都会让栈顶元素出栈,进行排序,并且按照基准元素的位置分成左右两部分,左右两部分再分别入栈。当栈为空时,说明排序已经完毕,退出循环。

 

 

 

 

 

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

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

相关文章

测试——《微服务设计》读书笔记

一.测试象限&#xff08;Brain Marick&#xff09; 二.测试金字塔&#xff08;Mike Cohn&#xff09; 1.单元测试 通常只测试一个函数或方法调用&#xff0c;通过TDD或者基于属性而写的测试就属于这一类&#xff0c;在UnitTest中&#xff0c;我们不会启动服务&#xff0c;对且对…

MyKtv点歌系统前台主要功能实现,内附数据库脚本,可以直接运行

C#开发工具&#xff1a;Visual Studio 2012 数据库&#xff1a;Sql Server Windows版本&#xff1a;Win10 分辨率&#xff1a;1366*768 文章的最后有KTV点歌系统的前后台源码下载链接。 在正式写代码之前先看一下运行效果图&#xff0c;如果觉得这个是你需要的&#xff0c;那么…

Java中“/”,“.”所代表的文件路径

转载自 Java中“/”&#xff0c;“.”所代表的文件路径 我们在开发的过程中&#xff0c;经常会去读、写文件。在读写文件的时候&#xff0c;就不得不写文件的路径&#xff0c;使用相对路径的方式有两种&#xff1a;”/”和 “.” 。在写文件的路径的时候&#xff0c;需要了解一…

Hibernate框架(1)

1.Hibernate框架简述 Hibernate的核心组件 在基于MVC设计模式的JAVA WEB应用中&#xff0c;Hibernate可以作为模型层/数据访问层。它通过配置文件(hibernate.properties或hibernate.cfg.xml)和映射文件(***.hbm.xml)把JAVA对象或PO(Persistent Object,持久化对象)映射到数据库中…

通过 Transifex 中文化开源软件

如果您对于汉化软件充满热情, 我软已经发布了以下的开源产品在 Transifex 平台&#xff0c;让社区的小伙伴们参与翻译以及审核: 如何参与? – Transifex 的新手 登录 Transifex 如果您第一次使用 Transifex, 您可以新建立一个账号或是通过您的 GitHub, Google 或 LinkedIn 账号…

第六期.Net开源社群联合分享--除了情结和价格,Azure最适合什么场景?等你来讲趟坑的实战经验!

嘿嘿&#xff0c;大家好啊&#xff01;好荣幸啊这一期&#xff0c;能够咱们.NET开源社区一块来做这次线上分享会。 我就是各位小伙伴可爱而且博学而且低调而且人见人爱花见花开而且谦虚但是经常口不择言的主持人老板娘Grace。 这次有新朋友&#xff0c;有老朋友&#xff0c;有…

支付系统的防重设计

转载自 支付系统的防重设计 导读 “目前在互联网应用的大部分支付场景中&#xff0c;对接支付宝、微信移动支付产品这样需要用户参与支付流程的支付方式已经变得非常普遍&#xff0c;类似的还有PC端银行网银支付&#xff1b;而通过绑定用户银行卡、对接银行卡快捷支付通道直接…

Windows Server Containers 支持 Windows 开发者使用 Docker

在过去几年里&#xff0c;Docker 和容器已成为全球开发界和企业最热门的话题之一。去年秋天发布的 Windows Server 2016 支持 Windows 开发者使用容器&#xff0c;使得这一热门话题再次升温。Windows 和 Docker 是如何走到一起的&#xff1f; 一切始于 2014 年隆重举办的普吉特…

漫画:什么是二叉堆?(修正版)

转载自 漫画&#xff1a;什么是二叉堆&#xff1f;&#xff08;修正版&#xff09; 什么是二叉堆&#xff1f; 二叉堆本质上是一种完全二叉树&#xff0c;它分为两个类型&#xff1a; 1.最大堆 2.最小堆 什么是最大堆呢&#xff1f;最大堆任何一个父节点的值&#xff0c;都…

漫画:什么是堆排序

转载自 漫画&#xff1a;什么是堆排序 在上一篇漫画中&#xff0c;小灰介绍了 二叉堆 这样一种强大的数据结构&#xff1a; 漫画&#xff1a;什么是二叉堆&#xff1f;&#xff08;修正版&#xff09; 那么&#xff0c;这个二叉堆怎样来使用呢&#xff1f;我们这一期将会详…

监控——《微服务设计》读书笔记

在单块应用的世界里&#xff0c;当我们遇到问题时&#xff0c;我们至少清楚从哪里开始调查。网站访问速度&#xff1f;网站访问异常&#xff1f;CPU占用过高&#xff1f;这些都是单块应用程序的问题&#xff0c;单一的故障点会极大地简化对问题的排查。 而现在我们面对了多个微…

什么是 TCC分布式事务

转载自 什么是 TCC分布式事务 近两年微服务变得越来越火热&#xff0c;各种框架与组件的出现&#xff0c;更是为微服务的开发提供了便利。 我们都知道&#xff0c;每个微服务都是一个对应的小服务&#xff0c;多个服务之间可以方便的进行功能的组合&#xff0c;来形成功能更…

.NET跨平台实践:再谈用C#开发Linux守护进程 — 完整篇

Linux守护进程是Linux的后台服务进程&#xff0c;相当于Windows服务&#xff0c;对于为Linux开发服务程序的朋友来说&#xff0c;Linux守护进程相关技术是必不可少的&#xff0c;因为这个技术不仅仅是为了开发守护进程&#xff0c;还可以拓展到多进程&#xff0c;父子进程文件描…

选择大公司还是小公司

转载自 选择大公司还是小公司 本文转载自公众号 stormzhang&#xff0c;文中的“我”是原文作者。 前一段时间&#xff0c;我一知识星球的球友问我这么一个问题&#xff0c;说他是某 985 应届生&#xff0c;拿到了 BAT 一家互联网大公司的 offer&#xff0c;但同时他还拿到了…

未来的C#之只读引用与结构体

C中提供了const特性&#xff0c;使用该特性定义的参数&#xff0c;其所引用的参数或对象将不会被调用函数修改&#xff08;当然const还提供了更多的特性&#xff0c;参见“Const正确性”&#xff09;。在新的建议中&#xff0c;C#也将提供类似的特性。 只读ref参数 在C#中&am…

漫画:什么是优先队列

转载自 漫画&#xff1a;什么是优先队列 在之前的漫画中&#xff0c;我们介绍了二叉堆和堆排序。没看过的小伙伴可以看一看前文&#xff1a; 漫画&#xff1a;什么是二叉堆&#xff1f;&#xff08;修正版&#xff09; 漫画&#xff1a;什么是堆排序&#xff1f; 这一次&a…

安全——《微服务设计》读书笔记

身份认证和授权 1.单点登录&#xff08;SSO&#xff09; 当主体试图访问一个资源&#xff0c;他会被定向到一个身份提供者那里进行身份验证&#xff0c;身份提供者验明正向后会发消息给服务提供者&#xff0c;让服务提供者来决定是否允许它访问资源。 SAML和OpenID Connect/OAu…