快速排序
举个例子:
对 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的数,然后交换它们。这里可以用两个变量 i 和 j,分别指向序列左边和右边。我们为这两个变量起个好听的名字 “ 哨兵 i ” 和 “ 哨兵 j ”。刚开始的时候让哨兵 i 指向序列的左边(即 i = 1),指向数字 6。让哨兵 j 指向序列的右边(即 j = 10),指向数字 8。例:
如下图所示:
- 首先哨兵 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 个数字按照从大到小的顺序进行输出
明确题目之后,我们该怎么做呢?
- 创建一个长度为 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){
......
}
函数的具体实现如下:
- 首先选定一端作为分割点,为基准数
// refer就是用来参考的基准数
int refer = a[begin];
// begin即左指针最初的位置,指向需要排序的数字的开端数字
int left = begin; // 左指针,即哨兵i
// begin即右指针最初的位置,指向需要排序的数字的末尾数字
int right = end; // 右指针,即哨兵j
- 然后使用这两枚指针,分别从左往右,从右往左进行查询
从左往右查找大于基准数的值,并记录下这个值的下标
从右往左查找小于基准数的值,并记录下这个值的下标
根据下标交换对应的两个值,然后继续寻找
// 循环进行的条件就是左指针的下标小于右指针的下标
// 如果不符合上面的条件就说明左指针和右指针相遇了,然后跳出循环
// 因此循环进行的条件也可以写成 left != right
while(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];
}
}
- 当两指针相遇时,记录相遇地点下标,交换相遇地点下标和基准数下标,
此时基准数左边的值均为小于基准数的值,基准数右边均为大于基准数的值
// 第一次基准数排序完毕以后,即左指针和右指针相遇,
// 然后基准数和数组最开始的数字对换
a[begin] = a[left];
// 且把基准数换成左、右指针相遇的数字,
a[left] = refer;
- 然后对两部分分别重复进行上述操作,直到排序完成
// 递归
// 第一次基准数左边的数字继续重复上面排序的步骤
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 != right
while(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 != right
while(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);
}
}
运行结果如图所示:
小总结:
快速排序就是一个优于冒泡排序的高级冒泡排序。