漫画:什么是快速排序?(完整版)

简介: 同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。不同的是,冒泡排序在每一轮只把一个元素冒泡到数列的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。

image.png

image.png




—————  第二天  —————


image.png

image.png


image.png


image.png

image.png

image.png

————————————

image.pngimage.png

image.pngimage.png

image.png


image.png



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


不同的是,冒泡排序在每一轮只把一个元素冒泡到数列的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。


image.png

这种思路就叫做分治法


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


假如给定8个元素的数列,一般情况下冒泡排序需要比较8轮,每轮把一个元素移动到数列一端,时间复杂度是O(n^2)。


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

image.png


如图所示,在分治法的思想下,原数列在每一轮被拆分成两部分,每一部分在下一轮又分别被拆分成两部分,直到不可再分为止。


这样一共需要多少轮呢?平均情况下需要logn轮,因此快速排序算法的平均时间复杂度是 O(nlogn)

image.png

image.png


基准元素的选择


基准元素,英文pivot,用于在分治过程中以此为中心,把其他元素移动到基准元素的左右两边。


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


最简单的方式是选择数列的第一个元素:


image.png


这种选择在绝大多数情况是没有问题的。但是,假如有一个原本逆序的数列,期望排序成顺序数列,那么会出现什么情况呢?

image.png

..........


image.png

image.png


image.png


我们该怎么避免这种情况发生呢?


其实很简单,我们可以不选择数列的第一个元素,而是随机选择一个元素作为基准元素

image.png



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


当然,即使是随机选择基准元素,每一次也有极小的几率选到数列的最大值或最小值,同样会影响到分治的效果。


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



元素的移动


选定了基准元素以后,我们要做的就是把其他元素当中小于基准元素的都移动到基准元素一边,大于基准元素的都移动到基准元素另一边。


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


1.挖坑法

2.指针交换法



何谓挖坑法?我们来看一看详细过程。


给定原始数列如下,要求从小到大排序:


image.png



首先,我们选定基准元素Pivot,并记住这个位置index,这个位置相当于一个“坑”。并且设置两个指针left和right,指向数列的最左和最右两个元素:

image.png



接下来,从right指针开始,把指针所指向的元素和基准元素做比较。如果比pivot大,则right指针向左移动;如果比pivot小,则把right所指向的元素填入坑中。


在当前数列中,1<4,所以把1填入基准元素所在位置,也就是坑的位置。这时候,元素1本来所在的位置成为了新的坑。同时,left向右移动一位。

image.png


此时,left左边绿色的区域代表着小于基准元素的区域。


接下来,我们切换到left指针进行比较。如果left指向的元素小于pivot,则left指针向右移动;如果元素大于pivot,则把left指向的元素填入坑中。


在当前数列中,7>4,所以把7填入index的位置。这时候元素7本来的位置成为了新的坑。同时,right向左移动一位。


image.png

此时,right右边橙色的区域代表着大于基准元素的区域。


下面按照刚才的思路继续排序:


8>4,元素位置不变,right左移

image.png


2<4,用2来填坑,left右移,切换到left。

image.png


6>4,用6来填坑,right左移,切换到right。

image.png



3<4,用3来填坑,left右移,切换到left。

image.png

5>4,用5来填坑,right右移。这时候left和right重合在了同一位置。

image.png



这时候,把之前的pivot元素,也就是4放到index的位置。此时数列左边的元素都小于4,数列右边的元素都大于4,这一轮交换终告结束。

image.png

image.png


image.png


public class QuickSort {

  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. private static int partition(int[] arr, int startIndex, int endIndex) {
  13.    // 取第一个位置的元素作为基准元素
  14.    int pivot = arr[startIndex];
  15.    int left = startIndex;
  16.    int right = endIndex;
  17.    // 坑的位置,初始等于pivot的位置
  18.    int index = startIndex;
  19.    //大循环在左右指针重合或者交错时结束
  20.    while ( right >= left  ){
  21.        //right指针从右向左进行比较
  22.        while ( right >= left ) {
  23.            if (arr[right] < pivot) {
  24.                arr[left] = arr[right];
  25.                index = right;
  26.                left++;
  27.                break;
  28.            }
  29.            right--;
  30.        }
  31.        //left指针从左向右进行比较
  32.        while ( right >= left ) {
  33.            if (arr[left] > pivot) {
  34.                arr[right] = arr[left];
  35.                index = left;
  36.                right--;
  37.                break;
  38.            }
  39.            left++;
  40.        }
  41.    }
  42.    arr[index] = pivot;
  43.    return index;
  44. }

  45. public static void main(String[] args) {
  46.    int[] arr = new int[] {4,7,6,5,3,2,8,1};
  47.    quickSort(arr, 0, arr.length-1);
  48.    System.out.println(Arrays.toString(arr));
  49. }

}


代码中,quickSort方法通过递归的方式,实现了分而治之的思想。


partition方法则实现元素的移动,让数列中的元素依据自身大小,分别移动到基准元素的左右两边。在这里,我们使用移动方式是挖坑法。


image.png


image.png



指针交换法


何谓指针交换法?我们来看一看详细过程。


给定原始数列如下,要求从小到大排序:

image.png



开局和挖坑法相似,我们首先选定基准元素Pivot,并且设置两个指针left和right,指向数列的最左和最右两个元素:


image.png



接下来是第一次循环,从right指针开始,把指针所指向的元素和基准元素做比较。如果大于等于pivot,则指针向移动;如果小于pivot,则right指针停止移动,切换到left指针。


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


轮到left指针行动,把指针所指向的元素和基准元素做比较。如果小于等于pivot,则指针向移动;如果大于pivot,则left指针停止移动。


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

image.png




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

image.png



接下来,我们进入第二次循环,重新切换到right向左移动。right先移动到8,8>4,继续左移。由于2<4,停止在2的位置。


image.png


切换到left,6>4,停止在6的位置。


image.png


元素6和2交换。

image.png

进入第三次循环,right移动到元素3停止,left移动到元素5停止。

image.png


元素5和3交换。

image.png


进入第四次循环,right移动到元素3停止,这时候请注意,left和right指针已经重合在了一起。

image.png

当left和right指针重合之时,我们让pivot元素和left与right重合点的元素进行交换。此时数列左边的元素都小于4,数列右边的元素都大于4,这一轮交换终告结束。

image.png

image.png


image.png



public class QuickSort {

  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. private static int partition(int[] arr, int startIndex, int endIndex) {
  13.    // 取第一个位置的元素作为基准元素
  14.    int pivot = arr[startIndex];
  15.    int left = startIndex;
  16.    int right = endIndex;
  17.    while( left != right) {
  18.        //控制right指针比较并左移
  19.        while(left<right && arr[right] > pivot){
  20.            right--;
  21.        }
  22.        //控制right指针比较并右移
  23.        while( left<right && arr[left] <= pivot) {
  24.            left++;
  25.        }
  26.        //交换left和right指向的元素
  27.        if(left<right) {
  28.            int p = arr[left];
  29.            arr[left] = arr[right];
  30.            arr[right] = p;
  31.        }
  32.    }
  33.    //pivot和指针重合点交换
  34.    int p = arr[left];
  35.    arr[left] = arr[startIndex];
  36.    arr[startIndex] = p;
  37.    return left;
  38. }

  39. public static void main(String[] args) {
  40.    int[] arr = new int[] {4,7,6,5,3,2,8,1};
  41.    quickSort(arr, 0, arr.length-1);
  42.    System.out.println(Arrays.toString(arr));
  43. }

}


和挖坑法相比,指针交换法在partition方法中进行的元素交换次数更少。


非递归实现

image.png

image.png


为什么这样说呢?


因为我们代码中一层一层的方法调用,本身就是一个函数栈。每次进入一个新方法,就相当于入栈;每次有方法返回,就相当于出栈。


所以,我们可以把原本的递归实现转化成一个栈的实现,在栈当中存储每一次方法调用的参数:

image.png



下面我们来看一下代码:


public class QuickSortWithStack {

  1. public static void quickSort(int[] arr, int startIndex, int endIndex) {
  2.    // 用一个集合栈来代替递归的函数栈
  3.    Stack<Map<String, Integer>> quickSortStack = new Stack<Map<String, Integer>>();
  4.    // 整个数列的起止下标,以哈希的形式入栈
  5.    Map rootParam = new HashMap();
  6.    rootParam.put("startIndex", startIndex);
  7.    rootParam.put("endIndex", endIndex);
  8.    quickSortStack.push(rootParam);
  9.    // 循环结束条件:栈为空时结束
  10.    while (!quickSortStack.isEmpty()) {
  11.        // 栈顶元素出栈,得到起止下标
  12.        Map<String, Integer> param = quickSortStack.pop();
  13.        // 得到基准元素位置
  14.        int pivotIndex = partition(arr, param.get("startIndex"), param.get("endIndex"));
  15.        // 根据基准元素分成两部分, 把每一部分的起止下标入栈
  16.        if(param.get("startIndex") <  pivotIndex -1){
  17.            Map<String, Integer> leftParam = new HashMap<String, Integer>();
  18.            leftParam.put("startIndex",  param.get("startIndex"));
  19.            leftParam.put("endIndex", pivotIndex -1);
  20.            quickSortStack.push(leftParam);
  21.        }
  22.        if(pivotIndex + 1 < param.get("endIndex")){
  23.            Map<String, Integer> rightParam = new HashMap<String, Integer>();
  24.            rightParam.put("startIndex", pivotIndex + 1);
  25.            rightParam.put("endIndex", param.get("endIndex"));
  26.            quickSortStack.push(rightParam);
  27.        }
  28.    }
  29. }
  30. private static int partition(int[] arr, int startIndex, int endIndex) {
  31.    // 取第一个位置的元素作为基准元素
  32.    int pivot = arr[startIndex];
  33.    int left = startIndex;
  34.    int right = endIndex;
  35.    while( left != right) {
  36.        //控制right指针比较并左移
  37.        while(left<right && arr[right] > pivot){
  38.            right--;
  39.        }
  40.        //控制right指针比较并右移
  41.        while( left<right && arr[left] <= pivot) {
  42.            left++;
  43.        }
  44.        //交换left和right指向的元素
  45.        if(left<right) {
  46.            int p = arr[left];
  47.            arr[left] = arr[right];
  48.            arr[right] = p;
  49.        }
  50.    }
  51.    //pivot和指针重合点交换
  52.    int p = arr[left];
  53.    arr[left] = arr[startIndex];
  54.    arr[startIndex] = p;
  55.    return left;
  56. }
  57. public static void main(String[] args) {
  58.    int[] arr = new int[] {4,7,6,5,3,2,8,1};
  59.    quickSort(arr, 0, arr.length-1);
  60.    System.out.println(Arrays.toString(arr));
  61. }

}


和刚才的递归实现相比,代码的变动仅仅在quickSort方法当中。该方法中引入了一个存储Map类型元素的栈,用于存储每一次交换时的起始下标和结束下标。


每一次循环,都会让栈顶元素出栈,进行排序,并且按照基准元素的位置分成左右两部分,左右两部分再分别入栈。当栈为空时,说明排序已经完毕,退出循环。

image.png

image.png


image.png



几点补充:


本漫画纯属娱乐,还请大家尽量珍惜当下的工作,切勿模仿小灰的行为哦。


—————END—————

相关文章
|
算法 搜索推荐
冒泡排序通俗易懂 图文详细操作
冒泡的代码还是相当简单的,两层循环,外层冒泡轮数,里层依次比较,江湖中人人尽皆知。 我们看到嵌套循环,应该立马就可以得出这个算法的时间复杂度为O(n2)。
冒泡排序通俗易懂 图文详细操作
|
算法
【切图仔的算法修炼之旅】LeetCode206:反转链表
【切图仔的算法修炼之旅】LeetCode206:反转链表
60 0
【切图仔的算法修炼之旅】LeetCode206:反转链表
|
算法 搜索推荐
【算法实践】有始有终,雨露均沾--手把手带你手撸选择排序
选择排序是一个非常经典且简单直观的排序算法,无论什么数据进去都是 O(n^2) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。其排序时,元素交换次数最差的情况为n−1次。选择排序的原理是先固定每个元素的位置,在序列中找到最小的元素,将这个元素与第一个元素交换位置,其次是除去第一个元素,找到剩余序列中最小的元素,与第二个元素交换位置,以此类推,直到所有的元素排完,就能实现选择排序,所以说选择排序是有始有终,雨露均沾的一个算法,哈哈~ 选择排序的基本思想是:每一趟在n−i+1 ( i = 1 , 2 , . . . , n − 1 ) 个元素中选择最小的
93 0
|
搜索推荐 算法
齐姐漫画:排序算法(一)
借用《算法导论》里的例子,就是我们打牌的时候,每新拿一张牌都会把它按顺序插入,这,其实就是插入排序。
125 0
齐姐漫画:排序算法(一)
|
存储 缓存 搜索推荐
漫画:“排序算法” 大总结
冒泡排序: 漫画:什么是冒泡排序? 选择排序: 漫画:什么是选择排序? 插入排序: 漫画:什么是插入排序? 此外还有冒泡排序的变种,鸡尾酒排序: 漫画:什么是鸡尾酒排序?
138 0
漫画:“排序算法” 大总结
|
搜索推荐 算法
漫画:三种 “奇葩” 的排序算法
介绍三种“异想天开”的排序算法。
124 0
漫画:三种 “奇葩” 的排序算法
漫画:什么是选择排序?
我们假定要获得升序数列,冒泡排序的原理是什么呢? 顾名思义,就是把每一元素和下一个元素进行比较和交换,使得较大的元素像气泡一样向右侧移动:
漫画:什么是选择排序?
|
存储 算法
漫画:什么是归并排序?
举个例子,有A、B、C、D、E、F、G、H一共8个武术家参考参加比武大会。 第一轮,两两一组,有4名选手胜出(四分之一决赛) 第二轮,两两一组,有两名选手胜出(半决赛) 第三轮,仅剩的两人一组,冠军胜出(总决赛)
漫画:什么是归并排序?
|
搜索推荐
漫画:什么是冒泡排序?
什么是冒泡排序?冒泡排序的英文Bubble Sort,是一种最基础的交换排序。 大家一定都喝过汽水,汽水中常常有许多小小的气泡,哗啦哗啦飘到上面来。这是因为组成小气泡的二氧化碳比水要轻,所以小气泡可以一点一点向上浮动。
229 0
漫画:什么是冒泡排序?
|
算法
漫画:什么是二分查找?(修订版)
如果我们把场景转换成最初的面试问题:在包含1000个整型元素的有序数组中查找某个特定整数,又该如何去做呢?
104 0
漫画:什么是二分查找?(修订版)