vlambda博客
学习文章列表

【漫画算法】17初识快速排序

【漫画算法】17初识快速排序

【漫画算法】17初识快速排序

同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达

到排序的目的。

不同的是,冒泡排序在每一轮中只把1个元素冒泡到数列的一端,而快速排序则在每

一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到

数列的另一边,从而把数列拆解成两个部分。

【漫画算法】17初识快速排序

这种思路就叫作分治法。

每次把数列分成两部分,究竟有什么好处呢?

假如给出一个8个元素的数列,一般情况下,使用冒泡排序需要比较7轮,每一轮把1

个元素移动到数列的一端,时间复杂度是O(n 2 )。

而快速排序的流程是什么样子呢?

【漫画算法】17初识快速排序

如图所示,在分治法的思想下,原数列在每一轮都被拆分成两部分,每一部分在下一

轮又分别被拆分成两部分,直到不可再分为止。

每一轮的比较和交换,需要把数组全部元素都遍历一遍,时间复杂度是O(n)。这样的

遍历一共需要多少轮呢?假如元素个数是n,那么平均情况下需要logn轮,因此快速排序

算法总体的平均时间复杂度是O(nlogn)。

【漫画算法】17初识快速排序

【漫画算法】17初识快速排序

基准元素的选择

基准元素,英文是pivot,在分治过程中,以基准元素为中心,把其他元素移动到它

的左右两边。

那么如何选择基准元素呢?

最简单的方式是选择数列的第1个元素。

【漫画算法】17初识快速排序

这种选择在绝大多数情况下是没有问题的。但是,假如有一个原本逆序的数列,期望

排序成顺序数列,那么会出现什么情况呢?

【漫画算法】17初识快速排序

【漫画算法】17初识快速排序

【漫画算法】17初识快速排序

那么,该怎么避免这种情况发生呢?

其实很简单,我们可以随机选择一个元素作为基准元素,并且让基准元素和数列首

元素交换位置。

【漫画算法】17初识快速排序

这样一来,即使在数列完全逆序的情况下,也可以有效地将数列分成两部分。

当然,即使是随机选择基准元素,也会有极小的几率选到数列的最大值或最小值,同

样会影响分治的效果。

所以,虽然快速排序的平均时间复杂度是O(nlogn),但最坏情况下的时间复杂度

是O(n 2 )。

在后文中,为了简化步骤,省去了随机选择基准元素的过程,直接把首元素作为基准

元素。

元素的交换

选定了基准元素以后,我们要做的就是把其他元素中小于基准元素的都交换到基准元

素一边,大于基准元素的都交换到基准元素另一边。

具体如何实现呢?有两种方法。

1. 双边循环法。

2. 单边循环法。

何谓双边循环法?下面来看一看详细过程。

给出原始数列如下,要求对其从小到大进行排序。

【漫画算法】17初识快速排序

接下来进行第1次循环,从right指针开始,让指针所指向的元素和基准元素做比较。

如果大于或等于pivot,则指针向左移动;如果小于pivot,则right指针停止移动,切换

到left指针。

在当前数列中,1<4,所以right直接停止移动,换到left指针,进行下一步行动。

轮到left指针行动,让指针所指向的元素和基准元素做比较。如果小于或等于pivot,

则指针向右移动;如果大于pivot,则left指针停止移动。

由于left开始指向的是基准元素,判断肯定相等,所以left右移1位。

【漫画算法】17初识快速排序

由于7>4,left指针在元素7的位置停下。这时,让left和right指针所指向的元素进行交换。

【漫画算法】17初识快速排序

接下来,进入第2次循环,重新切换到right指针,向左移动。right指针先移动到8,

8>4,继续左移。由于2<4,停止在2的位置。

按照这个思路,后续步骤如图所示。

【漫画算法】17初识快速排序

【漫画算法】17初识快速排序

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方法则实现了元素的交换,让数列中的元素依据自身大小,分别交换到基准

元素的左右两边。在这里,我们使用的交换方式是双边循环法。