【漫画算法】17初识快速排序
同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达
到排序的目的。
不同的是,冒泡排序在每一轮中只把1个元素冒泡到数列的一端,而快速排序则在每
一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到
数列的另一边,从而把数列拆解成两个部分。
这种思路就叫作分治法。
每次把数列分成两部分,究竟有什么好处呢?
假如给出一个8个元素的数列,一般情况下,使用冒泡排序需要比较7轮,每一轮把1
个元素移动到数列的一端,时间复杂度是O(n 2 )。
而快速排序的流程是什么样子呢?
如图所示,在分治法的思想下,原数列在每一轮都被拆分成两部分,每一部分在下一
轮又分别被拆分成两部分,直到不可再分为止。
每一轮的比较和交换,需要把数组全部元素都遍历一遍,时间复杂度是O(n)。这样的
遍历一共需要多少轮呢?假如元素个数是n,那么平均情况下需要logn轮,因此快速排序
算法总体的平均时间复杂度是O(nlogn)。
基准元素的选择
基准元素,英文是pivot,在分治过程中,以基准元素为中心,把其他元素移动到它
的左右两边。
那么如何选择基准元素呢?
最简单的方式是选择数列的第1个元素。
这种选择在绝大多数情况下是没有问题的。但是,假如有一个原本逆序的数列,期望
排序成顺序数列,那么会出现什么情况呢?
那么,该怎么避免这种情况发生呢?
其实很简单,我们可以随机选择一个元素作为基准元素,并且让基准元素和数列首
元素交换位置。
这样一来,即使在数列完全逆序的情况下,也可以有效地将数列分成两部分。
当然,即使是随机选择基准元素,也会有极小的几率选到数列的最大值或最小值,同
样会影响分治的效果。
所以,虽然快速排序的平均时间复杂度是O(nlogn),但最坏情况下的时间复杂度
是O(n 2 )。
在后文中,为了简化步骤,省去了随机选择基准元素的过程,直接把首元素作为基准
元素。
元素的交换
选定了基准元素以后,我们要做的就是把其他元素中小于基准元素的都交换到基准元
素一边,大于基准元素的都交换到基准元素另一边。
具体如何实现呢?有两种方法。
1. 双边循环法。
2. 单边循环法。
何谓双边循环法?下面来看一看详细过程。
给出原始数列如下,要求对其从小到大进行排序。
接下来进行第1次循环,从right指针开始,让指针所指向的元素和基准元素做比较。
如果大于或等于pivot,则指针向左移动;如果小于pivot,则right指针停止移动,切换
到left指针。
在当前数列中,1<4,所以right直接停止移动,换到left指针,进行下一步行动。
轮到left指针行动,让指针所指向的元素和基准元素做比较。如果小于或等于pivot,
则指针向右移动;如果大于pivot,则left指针停止移动。
由于left开始指向的是基准元素,判断肯定相等,所以left右移1位。
由于7>4,left指针在元素7的位置停下。这时,让left和right指针所指向的元素进行交换。
接下来,进入第2次循环,重新切换到right指针,向左移动。right指针先移动到8,
8>4,继续左移。由于2<4,停止在2的位置。
按照这个思路,后续步骤如图所示。
1. public static void quickSort(int[] arr, int startIndex,
int endIndex) {
2. // 递归结束条件:startIndex大于或等于endIndex时
3. if (startIndex >= endIndex) {
4. return;
5. }
6. // 得到基准元素位置
7. int pivotIndex = partition(arr, startIndex, endIndex);
8. // 根据基准元素,分成两部分进行递归排序
9. quickSort(arr, startIndex, pivotIndex - 1);
10. quickSort(arr, pivotIndex + 1, endIndex);
11. }
12.
13. /**
14. * 分治(双边循环法)
15. * @param arr 待交换的数组
16. * @param startIndex 起始下标
17. * @param endIndex 结束下标
18. */
19. private static int partition(int[] arr, int startIndex,
int endIndex) {
20. // 取第1个位置(也可以选择随机位置)的元素作为基准元素
21. int pivot = arr[startIndex];
22. int left = startIndex;
23. int right = endIndex;
24.
25. while( left != right) {
26. //控制right 指针比较并左移
27. while(left<right && arr[right] > pivot){
28. right--;
29. }
30. //控制left指针比较并右移
31. while( left<right && arr[left] <= pivot) {
32. left++;
33. }
34. //交换left和right 指针所指向的元素
35. if(left<right) {
36. int p = arr[left];
37. arr[left] = arr[right];
38. arr[right] = p;
39. }
40. }
41.
42. //pivot 和指针重合点交换
43. arr[startIndex] = arr[left];
44. arr[left] = pivot;
45.
46. return left;
47. }
48.
49. public static void main(String[] args) {
50. int[] arr = new int[] {4,4,6,5,3,2,8,1};
51. quickSort(arr, 0, arr.length-1);
52. System.out.println(Arrays.toString(arr));
53. }
在上述代码中,quickSort方法通过递归的方式,实现了分而治之的思想。
partition方法则实现了元素的交换,让数列中的元素依据自身大小,分别交换到基准
元素的左右两边。在这里,我们使用的交换方式是双边循环法。