一,思想描述:

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

给定原始数列如下,要求从小到大排序:
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

二,代码演示—以首位元素为基准值:

  1. public class KuaiPai13 {
  2. public static void main(String[] args) {
  3. int[] arr = new int[]{4, 7, 6, 5, 3, 2, 8, 1};
  4. quickSort(arr, 0, arr.length - 1);
  5. System.out.println(Arrays.toString(arr));
  6. }
  7. /**
  8. * 快速排序--挖坑法
  9. * @param arr 数组
  10. * @param startIndex 左边界索引
  11. * @param endIndex 右边界索引
  12. */
  13. public static void quickSort(int[] arr, int startIndex, int endIndex) {
  14. // 递归结束条件:startIndex大等于endIndex的时候
  15. if (startIndex >= endIndex) {
  16. return;
  17. }
  18. // 得到基准元素位置
  19. int pivotIndex = partition(arr, startIndex, endIndex);
  20. // 用分治法递归数列的两部分
  21. quickSort(arr, startIndex, pivotIndex - 1);
  22. quickSort(arr, pivotIndex + 1, endIndex);
  23. }
  24. /**
  25. * 具体每一轮的快速排序:
  26. * @param arr 数组
  27. * @param startIndex 左边界索引
  28. * @param endIndex 右边界索引
  29. * @return 返回基准值的位置,此时基准值左边的元素都小于基准值,基准值右边的元素都大于基准值
  30. */
  31. private static int partition(int[] arr, int startIndex, int endIndex) {
  32. // 取第一个位置的元素作为基准元素
  33. int pivot = arr[startIndex];
  34. // 初始化坑的位置,初始等于pivot基准值的位置
  35. int kengIndex = startIndex;
  36. //初始化左右游标/指针
  37. int leftYb = startIndex;
  38. int rightYb = endIndex;
  39. //大循环在左右指针重合时结束
  40. while ( leftYb<rightYb ) {
  41. //right指针从右向左进行比较
  42. // leftYb<rightYb ,左游标永远小于右游标,是遍历元素并发生元素变动的前提:
  43. while ( leftYb<rightYb) {
  44. // 先遍历右边,
  45. // 如果元素大于基准值--右游标左移:
  46. if (arr[rightYb] >= pivot) {
  47. rightYb--;
  48. }else{ //如果右边的当前元素小于基准值了,那么将该元素填入坑中,该元素本来的位置成为新的坑;
  49. arr[kengIndex] = arr[rightYb];
  50. kengIndex = rightYb;
  51. leftYb++;
  52. break;
  53. }
  54. }
  55. //再遍历左边,
  56. // leftYb<rightYb ,左游标永远小于右游标,是遍历元素并发生元素变动的前提:
  57. while (leftYb<rightYb) {
  58. //如果元素小于基准值--左游标右移,
  59. if (arr[leftYb] <= pivot) {
  60. leftYb++;
  61. }else{ //如果左边的当前元素大于基准值了,那么将该元素填入坑中,该元素本来的位置成为新的坑;
  62. arr[kengIndex] = arr[leftYb];
  63. kengIndex = leftYb;
  64. rightYb--;
  65. break;
  66. }
  67. }
  68. }
  69. //跳出了大循环,说明此时此刻左右游标是重合的,这时将基准值放在重合位置(最后一个坑),
  70. // 此时,基准值左边的元素都小于基准值,基准值右边的元素都大于基准值,这一轮交换宣告结束;
  71. arr[kengIndex] = pivot;
  72. return kengIndex;
  73. }
  74. }

三,随机选取基准值—代码演示:

对于快速排序来说,最简单的方式是选择数列的第一个元素,这种选择在绝大多数情况是没有问题的。
但是,假如有一个原本逆序的数列,期望排序成顺序数列,那么会出现什么情况呢?
当数据有序逆序时,以第一个关键字为基准分为两个子序列,前一个子序列为空,此时执行效率最差,如果每一轮快排都是如此,那么时间复杂度为n^2。
随机选择基准 可以降低逆序带来的O(n^2)复杂度的可能性。

基准选择的三种方式:

  1. 固定基准 (比如第一个),当原始序列或子序列逆序时,复杂度接近O(n^2),所以应该避免使用这种方式。
  2. 随机选择 ,降低了逆序复杂度变高的可能性,但仍然不容乐观。
  3. 三数取中,随机选择三个数,取中间大小的作为基准,当然,本身三个数都是不确定的,随机选择无意义,直接取原始数列的头、中、尾三个元素,得出中间大小作为基准即可。进一步降低了数列逆序的可能性。

可以看到,即使是性能最差的 固定基准 ,也不一定是 选择第一个元素作为基准 ,所以,我们应该掌握的快排,是不以第一个元素为基准的快速排序。

既然,以 第一个元素为基准的快速排序 ,理解和代码实现都是最简单的,那么我们为什么不这样写呢?

无论你使用了哪种 基准选择 的方式,在 元素移动 之前,你都可以让你的 基准 先与 第一个元素 进行 交换,这样是不是就变成了你手到擒来的快速排序?

所以你需要记住快速排序的总过程是:

  • 选择基准
  • 与第一元素交换
  • 元素移动

通过代码分析,我们得出结论,当 随机选择基准 时,无论

  • 挖坑法,增加判断条件
  • 指针交换法,影响指针优先移动顺序

都会增加代码复杂度,甚至,循环判断降低了代码性能。不妨,将复杂问题简单化,统一快速排序的过程。

  • 选择基准
  • 与第一元素交换
  • 元素移动
  1. /**
  2. * 快速排序--挖坑法:
  3. * 1,任意选择一个数作为基准值,
  4. * 2,将基准值与首位元素进行交换,
  5. * 3,基准值所在的位置成为原始坑kengIndex;
  6. * 4,先遍历右边,如果元素大于基准值--右游标左移,如果元素小于基准值--将该元素填入坑中,该元素本来的位置成为新的坑;
  7. * 5,再遍历左边,如果元素小于基准值--左游标右移,如果元素大于基准值--将该元素填入坑中,该元素本来的位置成为新的坑;
  8. * 6,直到左右游标重合,这时将基准值放在重合位置(最后一个坑),此时,基准值左边的元素都小于基准值,基准值右边的元素都大于基准值,这一轮交换宣告结束;
  9. * 7,递归调用该方法;
  10. * @param arr
  11. * @param leftIndex
  12. * @param rightIndex
  13. */
  14. public static void quicksort2(int[] arr, int leftIndex, int rightIndex) {
  15. //初始化左右游标:
  16. int leftYb = leftIndex;
  17. int rightYb = rightIndex;
  18. //随机选择一个数作为基准值:
  19. //一般简单的来说都选择第一个数即可,但是存在逆序情况导致效率极低的风险;
  20. // 随机选择的元素的位置:
  21. int suijiIndex = (leftIndex + rightIndex) / 2; //基准值所在位置为原始坑位;
  22. //我们将基准值和首位元素交换位置:
  23. int temp = arr[suijiIndex];
  24. arr[suijiIndex] = arr[leftIndex];
  25. arr[leftIndex] = temp;
  26. //基准值和首位元素交换位置之后,坑位位置为首位,基准值在首位;
  27. int kengIndex = leftIndex;
  28. int pivotVal = arr[kengIndex]; //基准值
  29. //左游标永远小于右游标,是遍历元素并发生元素变动的前提:
  30. while (leftYb < rightYb) {
  31. //先遍历右边,
  32. //如果元素大于基准值--右游标左移:
  33. while (leftYb < rightYb && arr[rightYb] >= pivotVal) {
  34. rightYb--;
  35. }
  36. //跳出了循环,说明此时的右游标所在元素小于基准值,那么将该元素填入坑中,该元素本来的位置成为新的坑;
  37. arr[kengIndex] = arr[rightYb];
  38. kengIndex = rightYb;
  39. //再遍历左边,
  40. //如果元素小于基准值--左游标右移,
  41. while (leftYb < rightYb && arr[leftYb] <= pivotVal) {
  42. leftYb++;
  43. }
  44. //跳出了循环,说明此时的左游标所在元素大于基准值,那么将该元素填入坑中,该元素本来的位置成为新的坑;
  45. arr[kengIndex] = arr[leftYb];
  46. kengIndex = leftYb;
  47. }
  48. //跳出了大循环,说明此时此刻左右游标是重合的,这时将基准值放在重合位置(最后一个坑),此时,基准值左边的元素都小于基准值,基准值右边的元素都大于基准值,这一轮交换宣告结束;
  49. arr[kengIndex] = pivotVal;
  50. //递归调用:
  51. if (leftYb - 1 > leftIndex) {
  52. quicksort2(arr, leftIndex, kengIndex - 1);
  53. }
  54. if (rightIndex > rightYb + 1) {
  55. quicksort2(arr, kengIndex + 1, rightIndex);
  56. }
  57. }

image.png