3-5 快速排序法

学习目标

深刻理解 quick sort 的 partition 的过程才有可能做出很好的实现。

快速排序在历史上的地位

Quick Sort,一段对于快速排序的描述:可能是二十世纪最伟大的排序算法之一。

我们首先先写一个基本的快速排序算法,后面我们再对这个基本的快速排序算法进行优化。

快速排序第 1 版的思路

从标定点后面一个一个地比较到底,(1)如果遇到比标定元素大的,就放过;(2)如果遇到比标定元素小的,就依次放在标定元素的后面(这句话说得有点含糊其辞,应该结合代码实现去理解这句话)。

理解快速排序的 partition

我们首先先回顾一下归并排序:归并排序不管数组的内容是什么,归并排序总是一分为二地去做排序运算,然后再归并起来。而快速排序的递归过程是这样的:

例如:待排序数组是 {4,6,2,3,1,5,7,8}

我们首先选择数组的第 1 个元素 4 作为基准。第 1 轮排好序以后,我们要达到的是这样一个效果:

  1. 第 1 个元素 4 的前面的所有的数都比 4 小,后面所有的数都比 4 大,也即是说:运算之前的第 1 个元素 4 它放在了它应该在的地方(最终排好序以后,4 就是在这个位置,我们没有理由再去无谓地挪动 4 的位置);
  2. 4 前面的元素 2,3,1 的相对位置是固定的,不保持它们原来数组的位置,所以不是原地排序;
  3. 4 后面的元素 6,5,7,8 的相对位置是固定的,不保持它们在原来数组中的位置,所以不是原地排序。

我们把上面这一轮的步骤称之为 Partition,Partition是快速排序算法的核心,正确地写出 partition 函数是实现快速排序的关键

先写出快速排序算法的框架:

public class QuickSortTest {

    // Partition
    @Test
    public void test01() {
        int[] arr = {9, 8, 7, 6, 5, 4, 3, 2, 1};
        int len = arr.length;
        quickSort(arr, 0, len - 1);
    }

    /**
     * @param arr
     * @param left 左边界,可以取到
     * @param right 右边界,可以取到
     */
    private void quickSort(int[] arr, int left, int right) {
        // 递归到底的条件是:当区间退化成一个元素的时候,就没有必要 partition 了
        if (left >= right) {
            return;
        }
        int p = partition(arr, left, right);
        quickSort(arr, left, p - 1);
        quickSort(arr, p + 1, right);
    }

    // 返回排好序的时候原来数组的首个元素最终应该放置的位置
    private int partition(int[] arr, int left, int right) {
    }
}

接下来,我们就要来实现 partition 了,每一次 partition 都将后面的元素进行整理,整理以后小于 4 的元素在数组的前半部分,大于 4 的元素在数组的后半部分。

下面我们具体讲解一下快速排序的实现。

l:通常我们选取左边界

j:是分界点,也就是例子中 4 这个元素的位置

我们逐渐遍历去“比较”,当前被访问的元素是 i。

我们首先写出来的这一版,经过随机生成数组元素的比较,已经比“归并排序”效率要高了。

代码实现:

/**
 * @param arr
 * @param left
 * @param right
 * @return
 */
private int partition(int[] arr, int left, int right) {
    int v = arr[left];// 基准值
    int j = left; // 这是要返回的那个元素的索引
    for (int i = left + 1; i <= right; i++) { // i 是循环变量,每一个元素都要和基准元素比较
        if (arr[i] < v) {
            swap(arr, j + 1, i);
            j++;
        }
    }
    swap(arr, left, j);
    return j;
}

private static void swap(int[] arr, int index1, int index2) {
    int temp = arr[index1];
    arr[index1] = arr[index2];
    arr[index2] = temp;
}

下面的图帮助我们理解 partition 的过程和边界值的选取。

理解递归思想的应用

快速排序的实现也利用了“递归”的思想。快速排序也是另一个O(nlogn) 级别的算法。

测试

对于近乎有序的数组而言,我们这一版的快速排序在 100 万这个级别的时候栈溢出了。

测试用例:1000000(100万),用于比较的排序方法:归并排序、快速排序

思考

接下来我们来谈一下,关于快速排序的优化措施。

1、底层使用插入排序,同样,我们使用 16 作为临界值

2、我们在脑子里想象一些比较极端的情况,也就是每一次作为基准的值迭代以后不落在中间那个地方。

快速排序最差的情况就是在数组近乎有序的时候,深度不是固定,最差的时候会退化成 O(n^2)。

思考一下快速排序的时间复杂度 O(nlogn) 是如何计算出来的,快速排序对于最糟糕的情况(逆序数组的排序)是可以达到 O(n^2) 这个级别的复杂度的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值