《啊哈!算法》第一章 - 第三节 - 快速排序(Java实现)

  • 快速排序
    • 升序排序
    • 降序排序

快速排序

举个例子:

6 1 2 7 9 3 4 5 10 8 这 10 个数进行排序

首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,这就是一个用来参照的数,待会儿你就知道它用来做啥了)。为了方便,就让第一个数 6 作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在 6 的右边,比基准数小的数放在 6 的左边,类似下面这种排列:

3 1 2 5 4 6 9 7 10 8

在初始状态下,数字 6 在序列的第 1位。我们的目标是将 6 挪到序列中间的某个位置, 假设这个位置是 k。现在就需要寻找这个 k,并且以第 k位为分界点,左边的数都小于等于 6, 右边的数都大于等于 6。 想一想,你有办法可以做到这点吗? 给你一个提示吧。请回忆一下冒泡排序是如何通过“交换”一步步让每个数归位的。此时你也可以通过 “ 交换 ” 的方法来达到目的。具体是如何一步步交换呢?怎样交换才既方便又节省时间呢?
方法其实很简单:

分别从初始序列 “ 6 1 2 7 9 3 4 5 10 8 ” 两端开始 “ 探测 ” 。先从右往左找一个小于6的数,再从左往右找一个大于 6的数,然后交换它们。这里可以用两个变量 ij,分别指向序列左边和右边。我们为这两个变量起个好听的名字 “ 哨兵 i ” 和 “ 哨兵 j ”。刚开始的时候让哨兵 i 指向序列的左边(即 i = 1),指向数字 6。让哨兵 j 指向序列的右边(即 j = 10),指向数字 8。例:

如下图所示:

  1. 首先哨兵 j 开始出动。因为此处设置的基准数是左边的数,所以需要让哨兵 j 先出动, 这一点非常重要(请自己想一想为什么)。哨兵 j 一步一步地向左挪动(即 j–),直到找到 一个小于 6 的数停下来。接下来哨兵 i 再一步一步向右挪动(即 i++),直到找到一个大于 6 的数停下来。最后哨兵 j 停在了数字 5面前,哨兵 i 停在了数字 7 面前。

如下图所示:
2. 现在交换哨兵 i 和哨兵 j 所指向的元素的值。交换之后的序列如下:
6 1 2 5 9 3 4 7 10 8

如下图所示:

3. 到此,第一次交换结束。接下来哨兵 j 继续向左挪动(再次友情提醒,每次必须是哨兵 j 先出发)。它发现了 4(比基准数 6 要小,满足要求)之后停了下来。哨兵 i 也继续向右挪动,他发现了 9(比基准数 6 要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:
6 1 2 5 4 3 9 7 10 8

如下图所示:

4. 第二次交换结束,“ 探测 ” 继续。哨兵 j 继续向左挪动,他发现了 3(比基准数 6 要小, 满足要求)之后又停了下来。哨兵 i 继续向右移动,糟啦!此时哨兵 i 和哨兵 j 相遇了,哨兵 i 和哨兵 j 都走到 3 面前。说明此时以 6 为基准数的 “ 探测 ” 结束。我们将基准数 6 和 3进行交换。交换之后的序列如下:
3 1 2 5 4 6 9 7 10 8


到此第一轮 “ 探测 ” 真正结束。此时以基准数 6 为分界点,6 左边的数都小于等于 6,6 右边的数都大于等于 6。
回顾一下刚才的过程,其实哨兵 j 的使命就是要找小于基准数的数, 而哨兵 i 的使命就是要找大于基准数的数,直到 i 和 j碰头为止。 OK,解释完毕。现在基准数 6 已经归位,它正好处在序列的第 6 位。此时我们已经将原来的序列,以 6 为分界点拆分成了两个序列,左边的序列是 “ 3 1 2 5 4 ”,右边的序列是 “ 9 7 10 8 ”。

可以看到 6 左边和右边的序列目前都还是很混乱的,我们可以按照上面的方法继续分别处理这两个序列,现在先来处理 6左边的序列吧。 左边的序列是 “ 3 1 2 5 4 ”,将这个序列按照上述方法以 3 为基准数进行调整,使得 3 左边的数都小于等于 3,3 右边的数都大于等于 3。
哨兵 j (指向 4) 先走,遇到 2 (比基准数 3 要小, 满足要求)之后停了下来,哨兵 i (指向 3)此时等于基准数 3, 满足要求,此时进行交换,调整完毕之后的序列的顺序如下:

2 1 3 5 4

OK,现在 3 已经归位。接下来需要处理 3 左边的序列 “ 2 1 ” 和右边的序列 “ 5 4 ”。对 序列 “ 2 1 ” 以 2 为基准数进行调整,处理完毕之后的序列为 “ 1 2 ”,到此 2 已经归位。序列 “1”只有一个数,也不需要进行任何处理。至此我们对序列 “ 2 1 ” 已全部处理完毕,得到的序列是“1 2”。序列 “ 5 4 ” 的处理也仿照此方法,最后得到的序列如下:

1 2 3 4 5 6 9 7 10 8

对于序列 “ 9 7 10 8 ” 也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列:

1 2 3 4 5 6 7 8 9 10

到此,排序完全结束。可以发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。整个算法的处理过程如下图所示:
小总结
快速排序是改进的冒泡排序,快速排序相比冒泡排序,每次交换是跳跃式的。
快速排序每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样只能在相邻的数之间进行交换,交换的距离就大得多了。因此总的比较和交换次数就少了,速度就提高了。
当然在坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的,都是 O(N2),它的平均时间复杂度为 O (NlogN)。
快速排序是基于一 种叫做 “ 二分 ” 的思想。 之后再聊。

了解完冒泡排序是什么,我们就开始实操吧!

升序排序

上题:

期末考试完了老师要将同学们的分数按照从高到低排序。小哼的班上只有 5 个同学,这 5 个同学分别考了 5分、3分、 5分、2分和8分,考得真是惨不忍睹(满分是 10 分)。接下来将分数进行从大到小排序, 排序后是 8 5 5 3 2。你有没有什么好方法编写一段程序,让计算机随机读入 5 个数然后将这 5个数从大到小输出?请先想一想,至少想 5 分钟再往下看吧(__) 。

思路解析:
题目可以用下面的一句话进行概括:

给你 5 个 0~10 的乱序数字,将这 5 个数字按照从大到小的顺序进行输出

明确题目之后,我们该怎么做呢?

  1. 创建一个长度为 5 的数组,并将需要排序的数字循环读入数组,然后调用快速排序的函数(自己写的)进行排序,最后输出结果
//       输入需要排序的数字个数System.out.println("请输入需要排序的数字个数:");Scanner s1 = new Scanner(System.in);int n = s1.nextInt();int a[] = new int[n];
//      输入需要排序的数字System.out.println("请输入需要排序的数字:");Scanner s2 = new Scanner(System.in);for(int i = 0; i < a.length; i++) {//          循环把输入的数读到数组中a[i] = s2.nextInt();}quicksort(a,0,a.length-1); // 调用快速排序的函数(自己写的)进行排序System.out.println(Arrays.toString(a)); // 输出结果

因为需要不断的循环并利用上次操作完的数据操作数字,所以我们创建一个函数名为 quicksort 进行递归,根据快速排序的原理讲述可知,我们需要向函数传入的参数有:数组 a[ ] 、数组的开端和末尾,如下:

 public static void quicksort(int a[],int begin,int end){......}

函数的具体实现如下:

  1. 首先选定一端作为分割点,为基准数
//       refer就是用来参考的基准数int refer = a[begin];// begin即左指针最初的位置,指向需要排序的数字的开端数字int left = begin; // 左指针,即哨兵i// begin即右指针最初的位置,指向需要排序的数字的末尾数字int right = end; // 右指针,即哨兵j
  1. 然后使用这两枚指针,分别从左往右,从右往左进行查询
    从左往右查找大于基准数的值,并记录下这个值的下标
    从右往左查找小于基准数的值,并记录下这个值的下标
    根据下标交换对应的两个值,然后继续寻找
//       循环进行的条件就是左指针的下标小于右指针的下标
//      如果不符合上面的条件就说明左指针和右指针相遇了,然后跳出循环
//      因此循环进行的条件也可以写成  left != rightwhile(left < right) { // 因为不清楚需要循环多少次,所以使用 while 循环
//          快速排序(降序):左指针找比基准数小的数字,右指针找比基准数大的数字,
//          两个指针都找到符合要求的数字以后,左、右指针指向的数字对调,
//          这样就可以大数在前,小数在后,矫正一次顺序了//          一定要先让右指针走
//          如果右指针指向的数字大于基准数,并且左指针也没有和右指针相遇,右指针就继续向左走while(a[right] <= refer  && left < right) {right--;}
//          如果左指针指向的数字小于基准数,并且左指针也没有和右指针相遇,左指针就继续向右走while(a[left] >= refer && left < right) {left++;}
//          如果右指针指向的数字大于基准数,左指针指向的数字小于基准数,
//          并且左指针也没有和右指针相遇,则左、右指针指向的数字对调,if(left<right) {//              交换值的方式一:中间变量,双斜线int t = a[left];a[left] = a[right];a[right] = t;//             交换值的方式二:异或运算交换两个数的值
//              a[left] = a[left]^a[right];
//              a[right] = a[right]^a[left];
//              a[left] = a[left]^a[right];
//
//              交换值的方式三:两值之和
//              int t = a[left] + a[right];
//              a[left] = t - a[left];
//              a[right] = t - a[right];}}
  1. 当两指针相遇时,记录相遇地点下标,交换相遇地点下标和基准数下标,
    此时基准数左边的值均为小于基准数的值,基准数右边均为大于基准数的值
//       第一次基准数排序完毕以后,即左指针和右指针相遇,
//      然后基准数和数组最开始的数字对换a[begin] = a[left];
//      且把基准数换成左、右指针相遇的数字,a[left] = refer;
  1. 然后对两部分分别重复进行上述操作,直到排序完成
//       递归
//      第一次基准数左边的数字继续重复上面排序的步骤quicksort(a,begin,left-1);
//      第一次基准数右边的数字继续重复上面排序的步骤quicksort(a,left+1,end);

完整代码如下:

import java.util.Arrays;
import java.util.Scanner;
public class T3 {public static void main(String[] args) {// 快速排序(升序排序)
//      输入需要排序的数字个数System.out.println("请输入需要排序的数字个数:");Scanner s1 = new Scanner(System.in);int n = s1.nextInt();int a[] = new int[n];
//      输入需要排序的数字System.out.println("请输入需要排序的数字:");Scanner s2 = new Scanner(System.in);for(int i = 0; i < a.length; i++) {//          循环把输入的数读到数组中a[i] = s2.nextInt();}quicksort(a,0,a.length-1);System.out.println(Arrays.toString(a));}public static void quicksort(int[] a,int begin,int end) {//      防止输入异常报错,且如果数组只有一个数就直接返回无需再进行排序if(begin >= end) {return;}
//      refer就是用来参考的基准数int refer = a[begin];int left = begin;int right = end;
//      循环进行的条件就是左指针的下标小于右指针的下标
//      如果不符合上面的条件就说明左指针和右指针相遇了,然后跳出循环
//      因此循环进行的条件也可以写成  left != rightwhile(left < right) {
//          快速排序(升序排序):左指针找比基准数大的数字,右指针找比基准数小的数字,
//          两个指针都找到符合要求的数字以后,左、右指针指向的数字对调,
//          这样就可以大数在后,小数在前,矫正一次顺序了//          一定要先让右指针走
//          如果右指针指向的数字大于基准数,并且左指针也没有和右指针相遇,右指针就继续向左走while(a[right] >= refer  && left < right) {right--;}
//          如果左指针指向的数字小于基准数,并且左指针也没有和右指针相遇,左指针就继续向右走while(a[left] <= refer && left < right) {left++;}
//          如果右指针指向的数字小于基准数且左指针指向的数字大于基准数,
//          并且左指针也没有和右指针相遇,则左、右指针指向的数字对调,if(left<right) {//              交换值的方式一:中间变量,双斜线int t = a[left];a[left] = a[right];a[right] = t;//             交换值的方式二:异或运算交换两个数的值
//              a[left] = a[left]^a[right];
//              a[right] = a[right]^a[left];
//              a[left] = a[left]^a[right];
//
//              交换值的方式三:两值之和
//              int t = a[left] + a[right];
//              a[left] = t - a[left];
//              a[right] = t - a[right];}}//       第一次基准数排序完毕以后,即左指针和右指针相遇,
//      然后基准数和数组最开始的数字对换a[begin] = a[left];
//      且把基准数换成左、右指针相遇的数字,a[left] = refer;
//      递归
//      第一次基准数左边的数字继续重复上面排序的步骤quicksort(a,begin,left-1);
//      第一次基准数右边的数字继续重复上面排序的步骤quicksort(a,left+1,end);}
}

运行结果如下图所示:

降序排序

那么利用快速排序进行升序排序写完了,但是题目要求的是将 5 个数字按照从大到小的顺序输出,这该怎么办呢?别急,我们只需要改动一个条件即可。
如果你仔细观察了可以发现,快速排序中决定输出顺序的关键就在于左、右指针继续前行的while循环里的条件,升序排序里,左指针(哨兵 i )负责找到比基准数大的数字,右指针(哨兵 j )负责找到比基准数小的数字,左、右找到符合要求的数字之后将数字进行对调,这样基准数的左边都是比基准数小的数字,基准数的右边都是比基准数大的数字,重复操作以后,数字就是升序排序。
但是我们需要的是降序排序,因此只需要改动一下左、右指针继续前行的while循环里的条件即可,让左指针(哨兵 i )负责找到比基准数小的数字,右指针(哨兵 j )负责找到比基准数大的数字,左、右找到符合要求的数字之后将数字进行对调,这样基准数的左边都是比基准数大的数字,基准数的右边都是比基准数小的数字,重复操作以后,数字就是降序排序啦!

改动代码如下:

//           一定要先让右指针走
//          如果右指针指向的数字小于基准数,并且左指针也没有和右指针相遇,右指针就继续向左走while(a[right] <= refer  && left < right) {right--;}
//          如果左指针指向的数字大于基准数,并且左指针也没有和右指针相遇,左指针就继续向右走while(a[left] >= refer && left < right) {left++;}

一定要注意:

这两个 while 循环负责的是左、右指针可以继续前行而不是停下来交换值!!!

完整代码如下:

import java.util.Arrays;
import java.util.Scanner;
public class T3 {public static void main(String[] args) {// 快速排序
//      输入需要排序的数字个数System.out.println("请输入需要排序的数字个数:");Scanner s1 = new Scanner(System.in);int n = s1.nextInt();int a[] = new int[n];
//      输入需要排序的数字System.out.println("请输入需要排序的数字:");Scanner s2 = new Scanner(System.in);for(int i = 0; i < a.length; i++) {//          循环把输入的数读到数组中a[i] = s2.nextInt();}quicksort(a,0,a.length-1);System.out.println(Arrays.toString(a));}public static void quicksort(int[] a,int begin,int end) {//      防止输入异常报错,且如果数组只有一个数就直接返回无需再进行排序if(begin >= end) {return;}
//      refer就是用来参考的基准数int refer = a[begin];int left = begin;int right = end;
//      循环进行的条件就是左指针的下标小于右指针的下标
//      如果不符合上面的条件就说明左指针和右指针相遇了,然后跳出循环
//      因此循环进行的条件也可以写成  left != rightwhile(left < right) {
//          快速排序(降序):左指针找比基准数小的数字,右指针找比基准数大的数字,
//          两个指针都找到符合要求的数字以后,左、右指针指向的数字对调,
//          这样就可以大数在前,小数在后,矫正一次顺序了//          一定要先让右指针走
//          如果右指针指向的数字小于基准数,并且左指针也没有和右指针相遇,右指针就继续向左走while(a[right] <= refer  && left < right) {right--;}
//          如果左指针指向的数字大于基准数,并且左指针也没有和右指针相遇,左指针就继续向右走while(a[left] >= refer && left < right) {left++;}
//          如果右指针指向的数字大于基准数,左指针指向的数字小于基准数,
//          并且左指针也没有和右指针相遇,则左、右指针指向的数字对调,if(left<right) {//              交换值的方式一:中间变量,双斜线int t = a[left];a[left] = a[right];a[right] = t;//             交换值的方式二:异或运算交换两个数的值
//              a[left] = a[left]^a[right];
//              a[right] = a[right]^a[left];
//              a[left] = a[left]^a[right];
//
//              交换值的方式三:两值之和
//              int t = a[left] + a[right];
//              a[left] = t - a[left];
//              a[right] = t - a[right];}}//       第一次基准数排序完毕以后,即左指针和右指针相遇,
//      然后基准数和数组最开始的数字对换a[begin] = a[left];
//      且把基准数换成左、右指针相遇的数字,a[left] = refer;
//      递归
//      第一次基准数左边的数字继续重复上面排序的步骤quicksort(a,begin,left-1);
//      第一次基准数右边的数字继续重复上面排序的步骤quicksort(a,left+1,end);}
}

运行结果如图所示:

小总结:

快速排序就是一个优于冒泡排序的高级冒泡排序。

《啊哈!算法》第一章 - 第三节 - 快速排序(Java实现)相关推荐

  1. 高一信息技术课件python编程_教科版高中信息技术选修第一章第三节Python入门--奇妙曲线的绘制 课件(22张ppt) 教案 (2份打包)...

    ID:10986924 资源大小:11671KB 资料简介: 教学设计 [课程标准要求] 体验算法思想,了解算法和程序设计在解决问题过程中的地位及作用:能从简单的问题出发,设计解决问题的算法,并能初步 ...

  2. 数据结构与算法 --- 第一章 绪论

    数据结构与算法 第一章 绪论 1. 作者的话 2. 为什么要学习数据结构与算法 3. 数据结构与算法的作用 4. 数据结构的概念 4.1 名词解读 4.2 什么是数据 4.3 数据结构 4.4 逻辑结 ...

  3. 用Groovy思考 第一章 用Groovy简化Java代码

    用Groovy思考  第一章 用Groovy简化Java代码 作者:chszs,转载需注明.博客主页:http://blog.csdn.net/chszs 1. Groovy的安装 目前Groovy的 ...

  4. 斗地主AI算法——第一章の业务逻辑

    转眼间快到了五月,帝都的天气也变的非常梦幻. 时而酷暑炎热,时而狂风席卷. 而不管外面如何,我们也只能在办公室里茕茕无依的撸着代码,无可奈何的负着韶华. 世界是寂寞的,寂寞到不只是寂寞,而是死一般的寂 ...

  5. 自动驾驶决策规划算法第一章笔记 忠厚老实的老王

    第一章 自动驾驶决策规划算法数学基础 第一节:决策规划算法的地位和作用 该笔记来自b站up主(偶像):憨厚老实的老王视频链接主页 第二节:为什么规划中经常见到五次多项式

  6. PTA数据结构与算法-第一章——褚论

    文章目录 第一章--褚论 第二章--线性表 第三章--栈与队列 第四章--字符串 第五章--树与二叉树 第六章--图 第七章--排序 第八章--检索 判断题 单选题 程序填空题 第一章--褚论 第二章 ...

  7. 啊哈算法——第一章:排序

    第一章:排序 桶排序:O(N+M) 桶排序的思想应该是一个非常好理解的思想,假设对一批正整数数进行排序,这些数中最大的数为N,则创建一个[0,N]的数组A.接着输入,输入ai,则A[ai] ++.假如 ...

  8. 算法第一章作业(c++代码规范+数学之美读后感+规划)

    c++代码规范: 一.文件结构 每个 C++/C 程序通常分为两个文件.一个文件用于保存程序的声明(declaration),称为头文件.另一个文件用于保存程序的实现,称为定义(definition) ...

  9. 清华大学-邓俊辉MOOC数据结构与算法-第一章

    第一节 1.计算 对象:规律.技巧 目标:高效.低耗 例子 绳索计算机及其算法 尺规计算机及其算法 总结 计算 = 信息处理 计算模型 = 计算机 = 信息处理工具 算法,即在特定计算模型下,旨在解决 ...

最新文章

  1. 【整理】ABAP 7.40新特性介绍(下)
  2. 使用ONVIF协议控制海康威视球机
  3. 小汤学编程之JavaScript学习day03——对象、Array数组、String字符、Date日期、JSON
  4. java编程executor框架_Java并发编程 - Executor框架(一)Executor,
  5. 《位置大数据隐私管理》—— 1.5 典型的位置隐私保护技术
  6. ADB工具华为鸿蒙,adb工具包华为版
  7. 如何合并apk和odex文件
  8. 【下载一】NI 系列软件卸载工具
  9. FIT2CLOUD入选2018 Gartner Cool Vendor
  10. php 查找同义词,php – 同义词查找器算法
  11. Ubuntu系统下python编程入门
  12. mysql中位数函数_如何使用简单的SQL查询在MySQL中计算中位数
  13. MOOCad Visual Analysis of Anomalous Learing Activities in Massive Open Online Courses
  14. 智能合约节省GAS的小技巧:避免使用>=和<=
  15. 微信公众号文章存在敏感词被屏蔽
  16. CSS - 使表格td中的文字垂直居中
  17. 在windows系统中安装Sulley
  18. 出版一本书可以赚多少钱_出版商精选:2015年29本书
  19. 求解二阶偏微分方程c语言,科学网—求解偏微分方程开源有限元软件deal.II学习--Step 3 - 亓欣波的博文...
  20. css3动画+svg实现水球进度条

热门文章

  1. UEFI是什么意思?UEFI和BIOS的区别是什么?
  2. hive2.3.2+mysql5.7.21驱动包_2018-08-30期 Hive外部元数据库配置
  3. 【爬虫】利用Python爬虫爬取小麦苗itpub博客的所有文章的连接地址并写入Excel中(2)...
  4. 魅族魅蓝新品15日发布 售价或超过千元?
  5. 为什么误差采取平方和形式
  6. 如何解决个人信息泄露问题
  7. 《你充满电了吗?》读后感
  8. 程序员怎样才能实现财富自由
  9. 输入框限制输入表情的方法
  10. layer.open打不开弹窗的问题