文章目录

  • 前言
  • 双边循环法
    • 思路梳理
    • 代码展示
    • 总结
  • 单边循环法
    • 思路梳理
    • 代码展示
  • 非递归的实现
    • 思路梳理
    • 代码展示
  • 总结

前言

上一篇文章讲解了冒泡排序的优化,现在来总结一下快速排序。快速排序作为经典的排序算法之一,其实也用到了冒泡排序的思想,冒泡排序是每一轮将最大(最小)的元素放到一端,而快速排序是每一轮找出比基准元素大、小的所有元素,然后放在基准元素的两边。
快速排序也有很多的实现方式,包括单边循环法、双边循环法,这两种都是通过递归来实现的,除此之外,还可以通过栈来实现。

双边循环法

思路梳理

先简单介绍一下双边循环法的思路。
给出如下的初始数组,现在需要进行从小到大的排序,首先找到一个基准元素(我们期望每一轮排好的结果是所有比基准元素小的数字在它的左边,所有比基准元素大的数字在它的右边),这里选择第一个元素作为基准元素,然后需要两个指针,一个左指针,负责从左向右移动,一个右指针,负责从右向左移动。
双边循环法的思路是:先从right指针开始(根据个人习惯,也可以从left开始),right指向的元素如果大于或等于基准元素,则该元素不动(right指针的作用是保证它指向的元素以及它右边的元素都是大于基准元素的),然后right指针向左移动一位(进行比较下一个);如果right指针指向的元素比基准元素小,则right指针停止移动(right指针一直指向这个元素,做一个标识,直到left指针移动到比基准元素大的数字位置,然后和right指针进行交换),然后切换到left指针,如果left指向的元素小于或等于基准元素,则该元素不动,left指针继续向右移动,寻找下一个元素,如果指向的元素比基准元素大,则和right指针指向的元素进行元素交换(实现比基准元素小的数字放在它的左边,大的数字放在它的右边),然后right指针向左移动一位(之所以left指针不向右移动,是因为如果left和right指针相邻,二者同时移动,会出现right指针在left指针左边的情况),之后再从right指针开始新的一轮比较。

现在进行第一轮比较:
首先right指针指向的是1,1比基准元素4小,所以元素1和right指针都不动,换到left指针,left指针指向4,小于等于基准元素(也就是它自己),然后left指针向右移动一位,之后的效果图如下:

这时left指针指向7,7大于基准元素4,所以7应该在它的右面,要和right指针指向的元素进行交换,交换之后,right指针向左移动一位,开始新的比较,效果图如下:

这时right指针指向元素8,8大于基准元素4,符合预期,所以继续向左移动,移动到元素2的位置,2小于基准元素4,不符合预期,所以right指针不动,换到left指针,left指针向右移动一位,移动到元素6,6大于基准元素4,不符合预期,所以和right指针进行元素交换,然后right指针向左移动一位。


这时right指针指向元素3,3小于基准元素4,不符合预期,所以不动,切换到left指针,left指向2,小于基准元素4,符合预期,继续移动,移动到元素5,5大于4,所以和right指针交换元素,right指针并向左移动一位。

最后,基准元素再和两个指针重合的那个元素进行交换,这样就实现了比基准元素小的在它的左边,比它大的在右边。这样,第一轮就比较完毕,然后按照同样的方式比较4左右两边的数组。

代码展示

public class QuickSort {//先确定入参:数组、起始的元素位置public static void quickSort(int[] arr, int startIndex, int endIndex) {//分割最小的数组为1个数字的时候就returnif (startIndex >= endIndex) {return;}//基准元素取第一个位置int pivotIndex = startIndex;int left = startIndex;int right = endIndex;while (left != right) {//从右指针开始,当右指针指向的元素大于基准元素,并且右指针指向元素的下标大于左指针while (arr[right] > arr[pivotIndex] && right > left) {//右指针向左移动一位right--;}while (arr[left] <= arr[pivotIndex] && left < right) {//左指针向右移动一位left++;}//进行元素的交换if (right > left) {int temp = 0;temp = arr[right];arr[right] = arr[left];arr[left] = temp;//交换元素之后,右指针向左移动一位,从新的元素开始比较right--;}//两个指针重合,就和基准元素进行交换位置if (right == left) {int temp = 0;temp = arr[left];arr[left] = arr[pivotIndex];arr[pivotIndex] = temp;}}pivotIndex = left;//基准元素左边的循环quickSort(arr, startIndex, pivotIndex - 1);//右边循环quickSort(arr, pivotIndex + 1, endIndex);}public static void main(String[] args) {int[] arr = new int[]{4, 4, 9, 10, 43, 6, 5, 3, 26, 8, 1};quickSort(arr, 0, arr.length - 1);System.out.println(Arrays.toString(arr));}
}

总结

right指针指向的元素以及它右边的元素都要大于等于基准元素,left指针指向的元素以及它左边的元素都要小于等于基准元素,两个指针不断的靠拢、不断的寻找不符合条件的元素,遇到不符合条件的元素就停下来,当两个指针都不符合预期时,就进行交换,就能达到符合条件的状态了。
双指针最终靠拢重合的位置也就是基准元素最后的位置,最后指针重合指向的元素在和基准元素进行交换,因此来实现比基准元素小的在它的左边,比它大的在它的右边。

单边循环法

双边循环法思路很清晰,但是代码比较复杂,单边循环法相对来说代码写起来要简单的多。

思路梳理

单边循环法只需要一个指针mark,该指针的作用是做一个分割点,保证该指针以及它左边的元素都要小于等于基准元素,以此来进行排序。元素不断的向右移动比较,目的是寻找小于基准元素的值,放到mark指针的左边,以此来实现分割。所以当遇到小于基准元素的值,就将mark指针向右移动一位,然后将该值和mark指针指向的元素进行交换。

现在进行第一轮排序,基准元素取第一个数字,然后向右遍历,遍历到7的时候,7大于基准元素4,不做任何处理,继续向右遍历,遍历到3的时候,3小于基准元素4,所以它应该在mark指针的左边,这时的操作是将mark指针向右移动一位(因为多了一个比基准元素4小的元素,mark指针的左边要放小于4的元素,所以向右移动一个位置),然后将mark指针指向的元素7和3进行位置交换

然后继续向右遍历,遍历到5、6的时候都是大于基准元素,不用管,当遍历到元素2的时候,2小于基准元素4,它应该在mark指针的左边,所以mark指针再留出一个位置,继续向右移动一位,移动到7的位置,然后和2进行元素交换。

继续向右遍历,遍历到8,不用管,遍历到1,1小于基准元素4,同理,mark指针向右移动一位,指向元素5,然后和1进行元素交换

最后,基准元素4和mark指针指向的位置进行元素交换,就完成了第一轮的排序。

代码展示

 public static void singleIndexQuickSort(int[] arr, int startIndex, int endIndex) {//分割最小的数组为1个数字的时候就returnif (startIndex >= endIndex) return;//指针默认取第一个int markIndex = startIndex;int pivotIndex = startIndex;for (int i = startIndex; i < endIndex + 1; i++) {//如果遍历到的值小于基准元素if (arr[i] < arr[pivotIndex]) {markIndex++;int temp = arr[i];arr[i] = arr[markIndex];arr[markIndex] = temp;}}//交换基准元素和mark指针指向的元素int temp = arr[markIndex];arr[markIndex] = arr[pivotIndex];arr[pivotIndex] = temp;//基准元素的索引下标进行更新pivotIndex = markIndex;//上一轮基准元素的左边singleIndexQuickSort(arr, startIndex, pivotIndex - 1);//上一轮基准元素的左边singleIndexQuickSort(arr, pivotIndex + 1, endIndex);}public static void main(String[] args) {int[] arr = new int[]{200, 40, 50, 4, 10, 5, 4, 7, 3, 5, 10, 9, 20, 6, 2, 8, 1};//quickSort(arr, 0, arr.length - 1);singleIndexQuickSort(arr, 0, arr.length - 1);System.out.println(Arrays.toString(arr));}

非递归的实现

思路梳理

递归就是自己不断调用自己的方法,直到有了突破口,才停止递归。进入一个方法就等于入栈,方法执行完毕就等于出栈,栈中没有了元素,也就停止了运行,所以根据这一点,是可以通过栈来实现快速排序的。

代码展示

public static void stackQuickSort(int[] arr, int startIndex, int endIndex) {//声明一个栈,用来存放每一个数组的起始值和结尾值。Stack<Map<String, Integer>> mapStack = new Stack<>();//第一轮数组的起、止下标,以hash的形式入栈Map<String, Integer> map = new HashMap<>();map.put("startIndex", startIndex);map.put("endIndex", endIndex);mapStack.push(map);System.out.println(mapStack);//当栈中有元素的时候while (!mapStack.isEmpty()) {//每一次都取最后一次入栈的元素,是基准元素的右半部分的数组Map<String, Integer> popParam = mapStack.pop();//获取每一轮排好序之后基准元素的位置int pivotIndex = partition(arr, popParam.get("startIndex"), popParam.get("endIndex"));//当基准元素左边有元素的时候就放到栈里面if (popParam.get("startIndex") < pivotIndex) {Map<String, Integer> leftParamMap = new HashMap<>();leftParamMap.put("startIndex", popParam.get("startIndex"));leftParamMap.put("endIndex", pivotIndex - 1);//把基准元素左半部分的起止下标放到栈中mapStack.push(leftParamMap);}//当基准元素右边有元素的时候就放到栈里面if (popParam.get("endIndex") > pivotIndex) {Map<String, Integer> rightParamMap = new HashMap<>();rightParamMap.put("startIndex", pivotIndex + 1);rightParamMap.put("endIndex", popParam.get("endIndex"));//把基准元素右半部分的起止下标放到栈中mapStack.push(rightParamMap);}System.out.println(mapStack);}}

总结

单边循环和双边循环法用到了递归。递归的思想是要想解决A问题,就先要解决B问题,但是要想解决B问题,就先要解决C问题,这些解决问题的方法都是一致的,所以当C问题解决了,B问题、A问题也就解决了。这里是要想给一个无序的大数组排好序,先给基准元素左边的小数组排好序,要想给左边的小数组排好序,就要给这个小数组中基准元素左边的小小数组排好序,直到最小的数组,也就是这个数组只有一个数字的时候,才完成递,然后再原路返回完成归(右边同理),这样就保证每一个数字的左边都小于它,右边都大于它。所以快排的灵魂不仅仅在于排序,更多的在于递归~
递归的方法是找到每一个被分割的小数组,都从这个数组中找到基准元素,保证基准元素左右各为小、大值,直到找到最后这个小数组只有1个数;换成栈就是找到每次最后一次放入栈中的小数组,对其进行基准元素选择和排序,如果这个小数组大于1个数,则继续放到栈中。
哪里有不对的地方,希望大佬多多帮忙,感谢您的指点,共同进步~

快速排序的实现(单边循环、双边循环、非递归的实现)相关推荐

  1. 单边、双边循环快速排序

    单边循环快排(lomuto 洛穆托分区方案 ) 选择最右元素作为基准点元素 j 指针负责找到比基准点小的元素,一旦找到则与 i 进行交换 i 指针维护小于基准点元素的边界,也是每次交换的目标索引 最后 ...

  2. 二叉树的各种操作(递归和非递归遍历,树深度,结点个数等等)

    目录 建立二叉树 递归前序和非递归前序 递归中序和非递归中序 递归后续和非递归后续(包括双栈法和设置pre结点) 层次遍历 寻找树中有没有值为x的结点 统计树中结点的个数 计算树的高度 判断两颗树是不 ...

  3. 数据结构——二叉树的递归遍历算法与非递归遍历算法+层次遍历算法

    (文章篇幅有点长,二叉树的递归遍历算法不作详细分析,但是二叉树的非递归遍历算法和层次遍历算法都有非常详细的分析过程,记得往下翻哦!) 二叉树的递归遍历算法实现 我们首先用递归的方法先序遍历创建这样一棵 ...

  4. 如何用堆栈和循环结构代替递归调用--递归转换为非递归的10条军规

    10 Rules (steps) for replacing the recursive function with stack and while-loop 转自http://www.codepro ...

  5. java非递归方式实现快速排序

    Java非递归方式实现快速排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ...

  6. C#实现(递归和非递归)快速排序和简单排序

    C#实现(递归和非递归)快速排序和简单排序 本人因为最近工作用到了一些排序算法,就把几个简单的排序算法,想冒泡排序,选择排序,插入排序,奇偶排序和快速排序等整理了出来,代码用C#代码实现,并且通过了测 ...

  7. Python实现快速排序(非递归实现)

    快速排序同样也是分治的思想,核心依然是分而治之,各个击破. 快速排序的思想是:找到一个数字x,对数组nums进行排序,使x左侧的数字都小于x,右侧的数字都大于x,然后对左侧和右侧重复同样的操作,直到所 ...

  8. 数据结构与算法 | 快速排序:Hoare法, 挖坑法,双指针法,非递归, 优化

    前两章讲解了排序中的选择排序和插入排序,这次就来讲一讲交换排序中的快速排序. 快速排序时间复杂度:平均 O(nlogn) 最坏 O(n2) 快速排序,顾名思义,它的排序的效率是非常高的,在数据十分杂乱 ...

  9. 算法之快速排序(递归和非递归)

    快速排序的两种实现方式.递归和非递归 1 package com.ebiz.sort; 2 3 import java.text.SimpleDateFormat; 4 import java.uti ...

最新文章

  1. 什么业务场景适合使用Redis?
  2. 教你几个写出原创文章的小方法
  3. C# readonly const
  4. DataSet与Xml之间的转换
  5. CentOS6.7上使用FPM打包制作自己的rpm包
  6. SCCM2012系列之四,SCCM2012部署前的SQL Server准备
  7. 蓝桥杯 ADV-228 算法提高 11-2删除重复元素
  8. DOM获取元素、事件基础、操作元素、节点操作
  9. Bootstrap_导航
  10. 【教程】适用于AIDE 2.1.5版的API文档设置
  11. 深入浅出谈LPWAN物联网通信技术
  12. 单片机通信接口:UART、I2C、SPI、TTL、RS232、RS422、RS485、CAN、USB
  13. 计算机网络(2.10)物理层- 宽带接入技术-ADSL 技术
  14. SQL注入 基础概述及相关知识
  15. 购买云服务器和搭建PHP环境,运行PHP个人网站
  16. 使用Origin根据数据画二维图(单x,双y)
  17. 对uni-app开发的某app安全分析
  18. 第八届蓝桥杯JavaB组省赛真题
  19. linux交换分区的命令,LInux下的交换分区以及相关查看命令
  20. 百练4124:海贼王之伟大航路(状压DP)

热门文章

  1. 考拉解析网站iPhone借助Documents保存视频
  2. 云图雅集—优美的诗词
  3. 读书笔记之一动气,你就输了一半
  4. Android高德地图配置及实现定位,目的地路线规划的路线绘制
  5. opengl学习2 绘制三角形和矩形
  6. gitlab 批量添加用户并去掉邮箱验证(亲测有效)
  7. static_cast和dynamic_cast
  8. 企业网站安全维护方案
  9. Http请求被拒绝,响应返回 The requested URL was rejected. Please consult with your administrator.
  10. 检测鼠标按钮(左右键)是否交换