概述
快速排序,简称快排。是属于交换排序的一种。
所谓交换排序就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,通过不断地交换元素来实现排序的目的。
最经典的交换排序就是冒泡排序法。快排也是冒泡排序法的一种优化。
快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用。
快排许多大厂面试经常要求手撕的一道题。
经过查阅了许多快排的文章,觉得填坑法的思路实现快排是最容易理解的,因此写下此文章。
快排实现思路概述
首先明确的是:快排的最根本思路是给基准数据寻找其应在位置。
快排的实现方式就是将所有元素以某个元素为基准,通过与基准元素的大小对比划分为不同部分。
例如:
在这5个元素中,假如选定3为基准元素进行调整,调整后情况如下:
可以看出,此时以3为基准,将元素划分为左边部分(小于3)与右边部分(大于3)。
在一次划分过后,基准元素的位置就是其排序后的位置。
- 为什么呢?
以上面图中的元素为例,一次划分后比3小的所有元素都放在了3的左边,此时若是数组想要以升序排列,此时3的位置将会是排序完成后的位置,也就是基准元素一次划分后不再更改位置。
在一次划分结束后,第一个基准元素的位置已经确定。
接下的目标就是以分治的思想,分别对基准元素左侧与右侧的区域进行上述的操作,不断对左右区域进行划分,每次划分都能确定一个元素的位置,直到基准元素的左右区域不能再进行划分(也就是区域长度小于1)。
此时所有元素都找到其应在位置,排序完成。
因此,快速排序的实现步骤如下:
- 选择一个基准元素,将所有元素以基准元素为轴,将元素划分为大于基准元素部分与小于基准元素部分。
- 将基准元素的左部分与右部分不断进行第1步的操作,直到两个部分的长度都小于1,不可再进行划分。
- 排序完成。
所有排序算法的思想都是为了实现以上步骤。
填坑法实现快排:
所谓填坑法,就是把基准元素看成萝卜,每个萝卜都有它的坑位,为了把这个萝卜放回他的位置,先把该萝卜挖出来。
该萝卜挖出来之后,种萝卜的地方就出现了一个坑,为了填补这个坑,对于可能放在这个位置的其他萝卜,挖出来后填补在这个坑里。
直到找到基准萝卜应该在的坑。
这就是填坑法的基本思想,基准元素看成最先挖出来的萝卜,为了寻找它的位置,先将它挖出来。
此时形成了一个坑,然后再寻找一个应该放在此坑的位置的元素填补此坑。
直接举例说明:
此例中,我们把3这个元素看成是基准元素,把这个第一个基准挖出来,此时3这里就形成一个坑。(以橙色标出)
接下来我们就要找某个元素来填补这个坑。
如何找呢?
答案是从右边开始,也就是7元素所在位置开始,向前遍历,寻找到一个小于基准元素的值,找到该值后便填补在坑的位置:
此时从右边开始往左边遍历找到的第一个小于基准元素的数字是2,将其挖出并填第一个坑:
此时挖出2并且填到第一个坑后,第一个坑消失,此时原来2的位置出现了新的坑。
此时重新寻找一个数字填补此新坑。
这时候寻找的方法是什么呢?
答案是从左侧头部(2的位置)开始,寻找一个大于基准元素的值来填补此坑。
此时会发现遍历到了新坑的位置还是没有找到左侧部分大于基准元素的值。
此时说明新坑的位置就是基准元素的位置,将基准元素填下此坑。
此时,一次划分结束。一次填坑过程完美结束。此时3的位置就是其最终位置。
此后再根据快排的思想,将基准元素左部分与右部分分别挖坑填坑,直到完成排序:
左部分挖坑:
值得注意的是,左右部分划分后,再寻找元素填坑是以已经确定的基准元素为界线,也就是以3为数组界线。此时只能从1开始向左遍历找到比2小的元素。
完美符合,挖起1进行填坑:
此时想再以2为基准继续划分区域,发现2的两边区域都小于1,此时1,2,3元素都已经是该萝卜应该在的坑。
接下来处理右边区域:
挖坑:
找元素填坑:
在从右侧向左侧遍历后发现没有小于自己的元素,此时惊叹:要填坑的竟是我自己?!
此时自己默默填自己的坑:
接下来的元素都是这样子:
总的来说,填坑法有三步:
- 挖起来区域内第一个元素
- 从区域的右边边界开始遍历,寻找一个小于自己的元素,找到后挖起来填自己的坑。找不到就自己填自己的坑。
- 以第一个元素为基准,对该元素的左侧和右侧区域分别进行以上操作。
如何解释挖坑后以什么方式找填坑元素?
这个问题我想了半个小时,想出来以下的解释,如果说的不好请各位及时指正:
在选择某个基准元素挖起来后,假设该位置就是基准元素应该在的位置。
为了满足基准元素的划分原则,数组的左边应该全部小于基准元素,由于我们选择以第一个元素为基准元素,因此其没有左边区域,只能从右边区域寻找不符合情况的元素,也就是右边区域中小于基准元素的元素。
这也就是为什么第一次选择基准元素后从右边边界开始向前寻找元素。
找到后把该元素挖起来后,填补在基准元素的坑中。
此时再假设新坑为基准元素的位置,由于在第一次从右向左遍历过程中,已经确定现在右边区域的元素已经全部大于基准元素,因此寻找此时左边区域不符合情况的元素,也就是左边区域中大于基准元素的元素。
这也解释了从右向前寻找到一个元素位置后,下一次开始从左向右寻找元素。
基于上面的情况,再一次寻找过程中发现遍历到了坑的位置,依旧没有找到不符合情况的元素,说明此时左右边元素的情况都符合划分的情况,此时本次划分结束。
希望我的解释大家能够明白,如果有什么讲的不好或者有错误,请各位评论指正!
代码实现
基于以上思路,我们进行代码实现:
public class QuickSort {
public static void main(String[] args) {
int arr [] = {
9,8,75,4,3,2,1,90};
System.out.println("遍历前:" + Arrays.toString(arr));
quickSort(arr,0,arr.length-1);
System.out.println("遍历后:" + Arrays.toString(arr));
}
public static void quickSort(int arr[],int low,int high){
if (low == high){
//递归结束的标志,对应思路中的区域长度小于1,不再进行划分
return ;
}
int lowIndex = low; //记录下开始的下标
int highIndex = high; //记录下结束的下表
int temp = arr[low]; //建立基准值
boolean flag = false; //遍历结束标志位
while(low < high ){
//low >= high 说明此时遍历到了基准值所在位置,遍历结束
//第一次先从右边界开始向左遍历,如果发现了小于基准值的元素,退出遍历
while(arr[high] >= temp){
if (high - 1 > low){
//在左移是要先判断移动后是否已经到达基准值点
high --; //左移
}else {
//如果已经到达基准值点,将基准值点的坑用自己的值补上,并且将将遍历结束标志符设置为true
arr[low] = temp;
flag = true;
break;
}
}
if (!flag) //如果遍历结束不为true,说明是发现了在遍历过程中发现了小于基准值的元素,此时将该元素拿起来填坑
arr[low++] = arr[high]; //填坑
else break; //否则结束遍历
while(arr[low] <= temp){
//填完后由新基准值开始从左向右遍历,寻找一个大于基准值的点
if (low +1 < high){
//在右移是要先判断移动后是否已经到达基准值点
}else {
arr[high] =temp; //如果已经到达基准值点,将基准值点的坑用自己的值补上,并且将将遍历结束标志符设置为true
flag = true;
break;
}
}
if (!flag) //如果遍历结束不为true,说明是发现了在遍历过程中发现了小于基准值的元素,此时将该元素拿起来填坑
arr[high --] = arr[low]; //填坑
else break; //否则结束遍历
}
quickSort(arr,lowIndex,low ); //开始划分基准值左侧区域
quickSort(arr,high ,highIndex); //开始划分基准值右侧区域
}
}
运行结果: