本文经授权转载自公众号程序员小灰(ID:chengxuyuanxiaohui)

—————  第二天  —————





————————————

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

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




这种思路就叫做分治法

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

假如给定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 class QuickSort {

public static void quickSort(int[] arr, int startIndex, int endIndex) {    // 递归结束条件:startIndex大等于endIndex的时候   if (startIndex &gt;= 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 &gt;= left  ){   //right指针从右向左进行比较   while ( right &gt;= left ) {   if (arr[right] &lt; pivot) {    arr[left] = arr[right];    index = right; left++;   break;  }   right--;    }   //left指针从左向右进行比较    while ( right &gt;= left ) {   if (arr[left] &gt; 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 &gt;= 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&lt;right &amp;&amp; arr[right] &gt; pivot){  right--;    }   //控制right指针比较并右移    while( left&lt;right &amp;&amp; arr[left] &lt;= pivot) {   left++;   }   //交换left和right指向的元素 if(left&lt;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&lt;Map&lt;String, Integer&gt;&gt; quickSortStack = new Stack&lt;Map&lt;String, Integer&gt;&gt;();    // 整个数列的起止下标,以哈希的形式入栈    Map rootParam = new HashMap(); rootParam.put("startIndex", startIndex);  rootParam.put("endIndex", endIndex);  quickSortStack.push(rootParam); // 循环结束条件:栈为空时结束 while (!quickSortStack.isEmpty()) { // 栈顶元素出栈,得到起止下标 Map&lt;String, Integer&gt; param = quickSortStack.pop();   // 得到基准元素位置 int pivotIndex = partition(arr, param.get("startIndex"), param.get("endIndex"));   // 根据基准元素分成两部分, 把每一部分的起止下标入栈    if(param.get("startIndex") &lt;  pivotIndex -1){  Map&lt;String, Integer&gt; leftParam = new HashMap&lt;String, Integer&gt;();   leftParam.put("startIndex",  param.get("startIndex"));  leftParam.put("endIndex", pivotIndex -1); quickSortStack.push(leftParam); }   if(pivotIndex + 1 &lt; param.get("endIndex")){   Map&lt;String, Integer&gt; rightParam = new HashMap&lt;String, Integer&gt;();  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&lt;right &amp;&amp; arr[right] &gt; pivot){  right--;    }   //控制right指针比较并右移    while( left&lt;right &amp;&amp; arr[left] &lt;= pivot) {   left++;   }   //交换left和right指向的元素 if(left&lt;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类型元素的栈,用于存储每一次交换时的起始下标和结束下标。

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

几点补充:

本漫画纯属娱乐,还请大家尽量珍惜当下的工作,切勿模仿小灰的行为哦。


—————END—————

推荐阅读

今日问题

描述一下什么是快速排序算法?

打卡格式:打卡第n天,答:xxx

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

  1. 完整版看漫画学Python全套资料,新手入门不容错过哦~

    你还在枯燥无味地学编程吗? 你还在闷头背诵那些根本没有理解的内容吗? 根本不用那么煎熬!想不想来体验一下翻着漫画就搞定Python的感觉?? <看漫画学Python:有趣.有料.好玩.好用(全彩 ...

  2. 高中计算机选修代码,高中信息技术新课标(完整版)

    <高中信息技术新课标(完整版)>由会员分享,可在线阅读,更多相关<高中信息技术新课标(完整版)(24页珍藏版)>请在人人文库网上搜索. 1.高中信息技术课程标准一.课程的基本理 ...

  3. java web学习项目20套源码完整版

    java web学习项目20套源码完整版 自己收集的各行各业的都有,这一套源码吃遍所有作业项目! 1.BBS论坛系统(jsp+sql) 2.ERP管理系统(jsp+servlet) 3.OA办公自动化 ...

  4. Anaconda中安装Orange3脚本-完整版

    2019独角兽企业重金招聘Python工程师标准>>> #Anaconda中安装Orange3脚本,完整版.包括插件的安装,在脚本中一次完成. sudo apt-get update ...

  5. 图解GPT-2(完整版)!

    ↑↑↑关注后"星标"Datawhale 每日干货 & 每月组队学习,不错过 Datawhale干货 译者:张贤, 哈尔滨工程大学,Datawhale原创作者 干货长文,建议 ...

  6. PyTorch核心贡献者开源书:《使用PyTorch进行深度学习》完整版现已发布!

    来源|新智元 [导读]<使用PyTorch进行深度学习>一书的完整版现已发布!教你如何使用PyTorch创建神经网络和深度学习系统,内含图解与代码,操作易上手. 由Luca Antiga. ...

  7. 甘肃省计算机二级考试题库,2011甘肃省计算机等级考试二级最新考试试题库(完整版)...

    2011甘肃省计算机等级考试二级最新考试试题库(完整版) 1.在软件生命周期中,能准确地确定软件系统必须做什么和必须具备哪些功能的阶段是(D) A. 概要设计 B. 详细设计 C. 可行性分析 D. ...

  8. php留言板源码免mysql_PHPMYSQL留言板源码(终极完整版).doc

    PHPMYSQL留言板源码(终极完整版).doc 数据库结构?:(库名:lyb) 表一: admin? 字段: id(int11?) name(varch?vr) passw?ord(varch?vr ...

  9. c语言s开头的函数以及作用,C语言函数大全-s开头-完整版.doc

    C语言函数大全-s开头-完整版 C语言函数大全(s开头) 函数名: sbrk 功能: 改变数据段空间位置 用法: char *sbrk(int incr); 程序例: #include#include ...

  10. 计算机图形学画圆vc代码,计算机图形学实验--完整版-带结果--vc++实现.doc

    计算机图形学实验--完整版-带结果--vc实现 计算机图形学实验报告 信息学院计算机专业 20081060183 周建明 综括: 利用计算机编程语言绘制图形,主要实现以下内容: (1).中点算法生成任 ...

最新文章

  1. .NET中获取电脑名、IP及用户名方法
  2. 【Java常识】6.0面向对象认知和Eclipse的实用使用、==号和equals方法的区别
  3. 算法战:需要人工智能生态系统来增强安全性
  4. 转帖:HttpStatusCode状态说明C#版
  5. 智能计米器jk76怎么安装_智能电视怎么安装软件?详细教程一学就会
  6. linux下,MySQL默认的数据文档存储目录为/var/lib/mysql。
  7. String与Date转换
  8. MySQL 高级 函数
  9. CCleaner 2.11.636 - 系统优化和隐私保护工具
  10. Java开发知识之Java的包装类
  11. java连接SqlServer2000类,比较完整,比较强大
  12. lol鲁大师帧数测试软件,lol配置检测
  13. 《JavaScript高级程序设计》- 第一章:介绍JavaScript
  14. 高程(DEM) ASCII数据获取
  15. codesys 简单案例_CoDeSys编程手册范本
  16. 机器学习中的GPU硬件设备及共享访问
  17. 2020强网杯Siri记录
  18. SQLite数据库中的.db-shm文件和.db-wal文件
  19. golang开发环境之Sublime Text 2+GoSublime+gocode+MarGo
  20. Java WebSocket 基础 建立端点

热门文章

  1. Python 实现机器学习前后端页面的交互
  2. 腾讯第100个开源项目:微信开源推理加速工具TurboTransformers
  3. 马云:腾讯不是阿里要打败的对手,是同为社会创造价值的伴侣
  4. 儿科医生的眼泪,全被数据看见了
  5. GitHub超全机器学习工程师成长路线图,开源两日收获3700+Star!
  6. 2017图灵奖出炉:Google母公司董事长和杰出科学家共获殊荣
  7. Tomaso Poggio:深度学习需要从炼金术走向化学
  8. 跨界 | 隐形AI与设计
  9. 我终于决定要放弃 okhttp、httpClient,选择了这个牛逼的神仙工具!贼爽!
  10. 十个最常用的JVM 配置参数