被排序算法吊打之—快速排序

在这里插入图片描述

1. 快速排序思想

快速排序(Quick_Sort)基于分治算法实现

它的基本思想是:

  •   选择一个基准数(就是以它作为参考数),通过一趟排序这个基准数将要排序的一组数分割成两个部分;基准数就像一个中间的标签,分成的两个子序列,一部分比它大,一部分比它小。
  •   通过分治的思想(化解为子问题,递归调用),对两个子序列再分别进行快速排序,而中间的标签位置固定好之后就不再发生改变。以此达到整个数据变成有序的序列。

快速排序流程:

在这里插入图片描述
【动图详解】

在这里插入图片描述

2. 快速详解

现在我们要对下面序列进行从左到右由小到大的排序

在这里插入图片描述
在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。想一想,你有办法可以做到这点吗?

我们可以联想到冒泡排序,依次和相邻的数进行比较,直到让6到达中间的分界点;但这样每次只能移动一个数,效率相对低下而复杂。怎样才能让6快速到达中间的位置呢?我们引入哨兵思想!

哨兵思想:

  • 引入哨兵 i , j,分别放在数列的队头和队尾;

  • 让哨兵 i 指向队头元素,让哨兵 j 指向队尾一个元素;

    在这里插入图片描述
    在这里插入图片描述

  • i ,j 哨兵分别从初始序列 “6 1 2 7 9 3 4 5 10 8” 两端开始 “探测”

  • j 先从右往左找一个小于6的数,i 再从左往右找一个大于6的数,然后交换他们。

  • 刚开始的时候让哨兵 i 指向序列的最左边(即 i=1),指向数字6。让哨兵j指向序列的最右边(即 j=10),指向数字8

首先哨兵 j 开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要。哨兵j一步一步地向左挪动(即 j- -),直到找到一个小于6的数停下来。
在这里插入图片描述
接下来哨兵i再一步一步向右挪动(即 i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前
在这里插入图片描述
哨兵 i 从左向右找大于基准数的数,哨兵 j 从右向左找小于基准数的数

双方哨兵找到了目标,然后交换目标

在这里插入图片描述
接下来开始哨兵 j 继续向左挪动(每次必须是哨兵 j 先出发),哨兵 j 赶往下一个位置,指向4,再次发现目标;通知 i 哨兵行动
在这里插入图片描述
哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来,继续交换

在这里插入图片描述
哨兵 j 继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。

哨兵 i 继续向右移动,糟啦!此时哨兵i和哨兵 j 相遇了(确认过眼神,好鸡冻),哨兵 i 和哨兵 j 都走到3面前。说明此时“探测”结束,每次俩人相遇时,探测结束。
在这里插入图片描述
此时,哨兵 i 和 j 所指向的位置,就是我们要找的分界点 k
在这里插入图片描述
现在基准数6已经归位,它正好处在序列的第6位。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。

回顾一下刚才的过程,其实哨兵 j 的使命就是要找小于基准数的数,而哨兵 i 的使命就是要找大于基准数的数,直到 i 和 j 碰头为止。

到此,第一次交换结束。可以大体上将数分为大于基准数和小于基准数两组。

接下来还需要分别处理这两个子序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。

处理6左边的序列,6的位置就固定不动了
在这里插入图片描述

j 哨兵向左寻找比基准数3小的数,它发现了2;

i 哨兵开始出发。但是,跨过了山和大海,穿过了人山人海,走过了3,路过了1,任然没有发现比 3 大的数,只能空手和 j 哨兵哥哥相遇了
在这里插入图片描述
在这里插入图片描述
此时,3作为基准数,再次将数列分成2部分;然后又是两个子序列,分而治之


然后,哨兵 i, j对2 15 4再进行排序,3的位置固定不动;

对序列 2 1 以2为基准数进行调整,序列1只有一个数,直接将21交换位置即可。处理完毕之后的序列为1 2,到此2已经归位。

序列5 4的处理也仿照此方法,最后得到的序列如下:

在这里插入图片描述
以6为基准数的序列左面排序完成在这里插入图片描述
对于序列9 7 10 8也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列:
在这里插入图片描述


3. 代码实现

版本一

public void quickSort(int[] arr, int low, int high) {
    if (low < high) {
        // 左边界基准数,由于是递归调用,此处为arr[low],不能是arr[0]
        int key = arr[low];
        int i = low;
        int j = high;
        while (i < j) {
            // j从右向左移动寻找,临界条件为 j == left,已经扫描到最左边了,无需继续扫描
            while (i < j && arr[j] > key) {
                // 先从右向左找第一个小于key的数
                j--;
            }
            // 将左右找到的元素进行交换
            if (i < j)  arr[i++] = arr[j];

            // i从左向右移动寻找,临界条件为 i == right,已经扫描到了最右边了,无需继续扫描
            while (i < j && arr[i] < key) {
                // 再从左向右找第一个大于key的数
                i++;
            }
            if (i < j)  arr[j--] = arr[i];
        }
        // 基准数归位
        arr[i] = key;
        //让左子组有序(退出循环的临界条件为 i == j)
        quickSort(arr, low, i - 1);
        //让右子组有序
        quickSort(arr, i + 1, high);
    }

运行结果

在这里插入图片描述

QuickSort 排序结果为:[1, 2, 3, 4, 5, 5, 6, 7, 9, 10]


版本二 基准数方法独立

public class QuickSort {
    public static void quick(int[] arr, int low, int high) {
        if (low >= high) {
            return;
        }
        //需要对数组中low索引到high索引处的元素进行分组(左子组和右子组)
        //返回的是分组的分界值所在的索引,分界值位置变换后的索引
        int pivot = partition(arr, low, high);
        //递归调用让左子组有序
        quick(arr, low,pivot - 1);
        //让右子组有序
        quick(arr, pivot + 1, high);
    }

    /**
     * 对数组a中,从索引low到索引high之间的元素进行分组,并返回分组界限对应的索引
     */
    public static int partition(int[] arr, int low, int high) {
        int key = arr[low];
        int i = low;
        int j = high;
        while (i < j) {
        	// 先从右向左找第一个小于key的数
            while (i < j && arr[j] > key) {
                j--;
            }
            
            // 再从左向右找第一个大于等于key的数(切记加上=)
            while (i < j && arr[i] <= key) {
                i++;
            }
            //交换 i和 j指针所指向的元素
            if (i < j) {
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
        //key和指针重合点交换
        arr[low] = arr[i];
        arr[i] = key;
        // i == j
        return i; 
    }
}

运行结果

在这里插入图片描述

QuickSortMethod 排序结果为:[1, 2, 3, 4, 5, 5, 6, 7, 9, 10]


4. 复杂度分析

【时间复杂度分析】

在这里插入图片描述 最优情况:每一次切分选择的基准数字刚好将当前序列等分

快速排序的一次切分从两头开始交替搜索,直到 i 和 j 重合,因此,一次切分算法的时间复杂度为O(n),但整个快速排序的时间复杂度和切分的次数相关。把数组的切分看做是一个树,那么上图就是它的最优情况,共切分了logn次,所以,最优情况下快速排序的时间复杂度为O(nlogn)

最坏情况:每一次切分选择的基准数字是当前序列中最大数或者最小数

如由小到大排列9,8,7,6,5,4,这使得每次切分都会有一个子组,那么总共就得切分n次

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

【空间复杂度分析】

空间复杂度就是在交换元素时那个临时变量所占的内存空间;

  • 最优的空间复杂度:开始元素顺序已经排好了,空间复杂度为:0;
  • 最差的空间复杂度:开始元素逆序,则空间复杂度为:O(n);

  • 平均时间复杂度:O(nlogn)

  • 平均的空间复杂度为:O(1)


《被排序算法吊打系列汇总》

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值