如何优雅的实现快速排序

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

前言

这是"优雅系列"的第五篇,也是目前阶段的最后一篇。

1. 如何优雅的实现二分法查找

2. 何优雅的实现冒泡排序

3. 优雅的实现选择排序

4. 如何优雅的实现插入排序

今天我们一起看一看快速排序,我感觉这个是优雅系列中最难的一个了,大家准备好,马上发车!

算法描述

  1. 首先设定一个分界值,通过该分界值将数组分成左右两部分。
  2. 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
  3. 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
  4. 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

看了上面的描述不知道你们有什么想法,其实这体现的就是一种分而治之的思想,分区比较再分区再比较。这个分界值就是我们需要的一个元素,我们多次求出分界值让它无法再分的时候,就是得到一个有序数组的时候。

而快速排序的实现又有多种多样,我这里提供两种实现方式。

lomuto 分区方案

lomuto 分区也称为单边循环快排。具体实现如下:

  1. 选择最右元素作为基准点元素
  2. i 针维护小于基准点元素的边界,也是每次交换的目标索引
  3. j 负责找到比基准点小的元素,一旦找到则与 i 进行交换
  4. 最后基准点与 i 交换,i 即为分区位置

单边循环实现

private static void quick(int[] a, int low, int high){
    if(low >= high) {
        return;
    }
    int p = partition(a, low, high);
    quick(a, low, p - 1);
    quick(a, p + 1, high);
}

// 找到基准点的位置,并返回
private static int partition(int[] a, int low, int high) {
    int pv = a[high]; // 基准点,这里是 omuto 分区方案的设定
    int i = low;
    for(int j = low; j < high; j ++) {
        if(a[j] < pv) {
            swap(a, i, j);
            i ++;
        }
    }
    swap(a, i, high);
    return i;
}

private static void swap(int[] a, int i, int j) {
    int t = a[i];
    a[i] = a[j];
    a[j] = t;
}
复制代码

这里需要理解的是,partition 这个方法是为了得到具体基准点的位置,而在 quick 方法中,我们也是使用递归调用,对基准点的左右两边依次进行分区,知道 low >= high 的时候,说明所有的元素都已经遍历过了。

实现二:双边循环快排

  1. 择最左边作为基准点元素
  2. j 指针负责从右向左找比基准点小的元素,i 指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至 i j 相交
  3. 最后基准点与 i (此时 i 和 j 相等)交换,i 即为分区位置
private static void quick(int[] a, int low, int high){
    if(low >= high) {
        return;
    }
    int p = partition(a, low, high);
    quick(a, low, p - 1);
    quick(a, p + 1, high);
}

// 找到基准点的位置,并返回
private static int partition(int[] a, int low, int high) {
    int pv = a[low],  i = low, j = high;
    while(i < j){
        // j 从右向左查找
        while(i < j && a[j] > pv) {
            j --;
        }

        while(i < j && a[i] <= pv) {
            i ++;
        }

        // 大小各放一边
        swap(a, i, j);
    }

    // 基准点元素的置换
    swap(a, low, i);
    return i;
}

private static void swap(int[] a, int i, int j) {
    int t = a[i];
    a[i] = a[j];
    a[j] = t;
}
复制代码

这里可以看出来,明显的要比单边循环复杂一下,重写了 partition 这个方法,这里有个注意事项,准点在左边,并且要先 j 后 i 也就是先从右向左查找, while(i < j && a[i] <= pv) 这样写是因为基准点在左边,i 也是在左边,会重合。

快速排序的特点

是用来优化冒泡排序的,冒泡排序每次只能得到一个元素,而快速排序第一次可以冒出来一个元素(基准点)第二次就可以冒出来 2 个元素,第三次就可以冒出 4 个元素,所以在数组非常长的情况下,效率提升非常明显。还有一点,快速排序属于不稳定排序。

最后

这就是优雅系列的最后一篇文章了,希望大家多多点赞,评论,大家一起交流,得到更加优雅的解法。

猜你喜欢

转载自juejin.im/post/7016713502572150814