【算法】排序01——从分治算法的角度理解快速排序(含代码实现)
快速排序的时间复杂度为 O[nlog (n)],空间复杂度是 O[log (n)] ,这种算法应用比较广泛(面试也爱考),非常适合在数据量较多且关键字随机分布的情况下使用。
记得博主第一次学习冒泡排序时觉得,这个冒泡排序很easy啊,然后就去看冒泡排序改进版的代码(也就是快速排序啦),然后。。。。
啊这 。。。
相信应该也有一些小伙伴和我过类似的经历吧。即使能读懂代码,却有难以理解其流程,面试手撕时也难以快速应对。
不过后来,当我接触到分治算法后,再回过头来这个快速排序的方法。。。。
就这?以后快排我不用提前准备,随时手撕!
好啦,废话不多说了,进入正题。
先说一说什么是分治算法:
就如字面意思,我们需要把一个大的问题分解成多个形式相同的小问题解决,并且这些小问题的解的合并就是大问题的解。
这可能理解起来还是抽象的,我们就以排序举例子:
假设我们的数组有这样九个数字: [ 5 8 9 6 7 4 1 2 3 ]
现在我们从数组中随便取一个数字(就去数组中的第一个数字吧),以它为阈值,所有小于他数的放其左边,并在逻辑上视为一个新的数组(就叫小”数组“吧)。所有大于他的数放其右边,同样在逻辑上视为一个新的数组(就叫大”数组“吧),然后我们再对新出现的大数组和小数组进行同样的操作直到新数组的大小小于2时停止:
以第一个数字5为阈值进行拆分操作,分成 [ 小数组 ] 阈值 [ 大数组 ] ,如下:
[ 4 1 2 3 ] 5 [ 8 9 6 7 ] (在快速排序中小”数组“其实会是[ 3 2 1 4 ],因为快排会从原数组首尾的两个指针向中间交替前进分大小,但这对我们这个例子并无影响)
[ 1 2 3 ] 4 [ ] 5 [ 6 7 ] 8 [ 9 ] 黄色部分是对[4 1 2 3]的拆分操作,绿色部分是对 [8 9 6 7]的拆分操作,红色部分要么是当前操作的阈值要么是长度不足2的数组,将不会参与后面的拆分操作
[ ] 1 [ 2 3 ] 4 [ ] 5 6 [ 7 ] 8 [ 9 ] 粉色部分是对[ 1 2 3 ]的拆分操作 ,淡蓝色部分是对[ 6 7 ] 的拆分操作, 红色部分要么是当前操作的阈值要么是长度不足2的数组,将不会参与后面的拆分操作
[ ] 1 2 [ 3 ] 4 [ ] 5 6 [ 7 ] 8 [ 9 ] 深蓝色部分是对‘[ 2 3 ]的拆分操作,红色部分要么是当前拆分操作的阈值要么是长度不足2的数组,将不会参与后面的拆分操作
现在你看,这个数组是不是有序了。
这就有点像建立一个二叉搜索树一样,把一个数组分成一个阀值(父节点)、一个小数组(左子树集合)和一个大数组(右子树集合),然后对两个新的数组(左、右子树)递归该操作。
当然,快排和这个流程还是有一点小区别的,当我们把一个“数组”拆分成两个新“数组”时,上面的演示流程为了方便大家理解,不凭空增加复杂性,采用的是从左向右遍历原数组的每个元素。
如 [ 5 8 9 6 7 4 1 2 3 ] 操作后的小数组顺序是 [ 4 1 2 3 ]。但在快排中,对原数组的遍历不是这样的,它是使用两个指针从原数组首尾开始向中间前进进行遍历的(小伙伴可以在下面的代码里体会),所以我才在上面的小括号里说快排中的小数组顺序是[ 3 2 1 4 ]。(即尾指针向中间前进,遇到小于阈值5的数字的顺序 3 2 1 4 )。
最后,大家从分治的思想来看看下面的快排代码,是不是就不觉得更好理解了呢?
1 import java.util.Arrays;
2
3 public class QuickSort {
4 public static void sort(int[] array,int begin_edge,int end_edge){
5 //左右边界下标重合时,就表示逻辑上的新数组大小不足2了,
6 // 故不再需要二分成两个新“数组”了
7 if(begin_edge>=end_edge){return;}
8 //获取新“数组”的首尾指针
9 int l_pointer = begin_edge;
10 int r_pointer = end_edge;
11 int threshold = array[l_pointer];
12 //循环结束时,所有l_pointer左边的元素会小于threshold,右边的会大于threshold(有点二叉搜索树的意思2333)
13 while (l_pointer<r_pointer){//跳出循环时完成对当前“数组”的二分
14 //尾指针向前遍历,遇到小于阈值的数就跳出循环,把它放进小“数组”
15 while (l_pointer<r_pointer && array[r_pointer]>=threshold){
16 r_pointer--;
17 }
18 array[l_pointer] = array[r_pointer];
19 //首指针向后遍历,遇到大于阈值的数就跳出循环,把它放进大“数组”
20 while (l_pointer<r_pointer && array[l_pointer]<=threshold) {
21 l_pointer++;
22 }
23 array[r_pointer] = array[l_pointer];
24 }
25 //把阈值插到两个“数组”的中间以区分两个“数组”的边界(此时l_pointer等于r_pointer)
26 array[l_pointer] = threshold;
27 //递归调用。分治思想,化大问题为形式相同的小问题
28 sort(array,begin_edge,l_pointer-1);
29 sort(array,l_pointer+1,end_edge);
30 }
31
32 public static void main(String[] args) {
33 int array[] = { 4, 2, 5, 1, 7, 3, 5, 9, 8,1 };
34 sort(array,0,array.length-1);
35 System.out.println(Arrays.toString(array));
36 }
37 }
测试结果:
最后的最后,如果小伙伴觉得这篇博文对你有帮助的话,就长按👍。。。。啊,呸,就点个推荐吧