快速排序-QuickSort

一.基本思想

1.在快速排序中,采取的主要的"拆"的思想
2.每一次将数组拆为两个部分,令中间元素为pivot,pivot左边的所有元素都会小于pivot,而pivot的右边的所有元素会大
于等于pivot的值
3.再拆出来的部分,分别对其左右两部分通过递归的方式,继续对它进行拆分
4.直到最左侧的索low<high条件不成立,此时的数组有序

二.Eg:假如数组arr={15,18,13},low=0,high=arr.length-1=2,利用拆的思想,将它拆为两部分

图解上述例子

1. 先确定pivot的值=arr[low],并且需要明确我们的low和high的指针不仅仅是充当界限,并且low的索引也是比pivot元素要小的元素要插入的位置,high则是比pivot大于等于的元素要插入的位置
在这里插入图片描述
2. 将右侧指针high指向的比pivot要小的元素,换到左侧位置中
在这里插入图片描述
3. 找到左侧比pivot元素要大或等于pivot元素的位置
在这里插入图片描述
4. 将左侧大于等于pivot的元素换到右侧位置处
在这里插入图片描述
5. 继续上述步骤,从右侧找到一个比pivot要小的元素
在这里插入图片描述
6. 将右侧指针所指的比pivot要小的元素换到左侧中,即arr[low]=arr[high],但是由于此时的low=high,所以这一步并不会实际改变什么
7. 从左侧查找一个大于等于pivot的元素,并将其换到右侧去
在这里插入图片描述
8. 此时low<high的条件已经不满足了,所以我们退出整个步骤,并且在这个时候,我们可以发现,low所指的位置,其实就是我们pivot要实际存储的位置,所以在结束整个循环后,我们令arr[low]=pivot,此时的数组就满足pivot左侧的元素都小于pivot,pivot右侧的元素都大于或等于pivot
在这里插入图片描述
9. 由于在该例中只有3个元素,在进行一次拆分后,数组就已经有序了,但是在实际运用中,可能有多个元素,所以在我们每次拆分完成之后,还需要对pivot左右两侧再次进行拆分操作,所以我们在每次拆分完成后,需要返回 pivot的索引位置,即return low

三.代码

package com.cdc.algorithm.sort;

import java.util.Arrays;

/**
 * @author cdc
 * @email c925638766@163.com
 * @date 2022/6/1 21:13
 */
public class QuickSortDemo {
    public static void main(String[] args) {
        int[] arr = {49, 38, 65, 97, 76, 13, 27, 49};
        quickSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

    /**
     * 快速排序主要采取的方式是将数组中的元素,不断的分为两个部分,并且以一个元素pivot为边界
     * pivot左边的元素都比pivot要小
     * pivot右边的元素都比pivot要大
     * 由于要将其划分为左右两个部分,所以左侧索引left和右侧索引right是必不可少的
     * 当left<right满足时,需要不断的将已经分好的两部分再次进行上述操作
     *
     * @param arr  数组
     * @param low  数组的左侧索引部分
     * @param high 数组的右侧索引部分
     */
    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            //将完整的数组拆分为以pivot元素为边界的两部分,并对pivot左右两部分的元素继续递归进行拆分操作
            int pivotPos = partition(arr, low, high);
            //对pivot左侧元素进行递归拆分
            quickSort(arr, low, pivotPos - 1);
            //对pivot右侧元素进行递归拆分
            quickSort(arr, pivotPos + 1, high);
        }
    }

    /**
     * 将给定的数组拆分为两个部分,以pivot为界,pivot左侧都比pivot小,pivot右侧都比pivot大于或等于
     *
     * @param arr  需要进行拆分的数组
     * @param low  数组的左侧索引
     * @param high 数组的右侧索引,high可取
     * @return pivot的下标
     */
    public static int partition(int[] arr, int low, int high) {
        //以数组的最左侧元素为边界,将数组拆分为两部分
        int pivot = arr[low];
        while (low < high) {
            //先从右侧开始,确保右侧的元素都比pivot大于或等于,直到找到一个比pivot元素要小的元素
            while (low < high && arr[high] >= pivot) {
                high--;
            }
            //将右侧中比pivot元素要小的元素,放到pivot的左侧
            arr[low] = arr[high];

            //从左侧开始,要确保pivot左侧的元素都是小于它的,所以需要找到一个大于等于pivot的元素,然后将它进行交换位置
            while (low < high && arr[low] < pivot) {
                low++;
            }
            //将左侧大于等于pivot的元素,放到pivot的右侧
            arr[high] = arr[low];
        }
        /*
        将pivot元素放到它应该放到的位置,以49,27,65为例,
        在上面的循环中
            第一步从右侧开始查找的操作中low=0,high=2,pivot=49
            先从右边查找比pivot小的元素也就是27,此时的high=1,
            arr[low]=arr[high],即数组变为27,27,65

            第二步从左侧开始查找的操作中,low=0,high=1,pivot=49
            找到左侧大于或等于pivot的元素,49>27,low++=1
            low=high,所以退出循环,由于low=high,所以下面的赋值操作可以忽略不计

            low=high,退出整个训话,并且我们可以发现,pivot它应该插入的元素是与low所指的位置是一致的
            所以我们令arr[low]=pivot,数组变为:27,49,65
         */
        arr[low] = pivot;

        //返回pivot元素的位置
        return low;
    }
}

四.性能分析

(一)时间复杂度(以代码中的数组{49, 38, 65, 97, 76, 13, 27, 49}为例)

  1. 在第一次QuickSort的排序后,将数组分为了两个部分,以第一个元素49为轴,分为左右两个部分。此时待排序的数组元素为:0~2与4~7。对这两个部分分别进行拆分即调用Partition()函数,这两个区间内,待排序的数组长度小于n,所以对这两个区间调用Partition()函数所使用的时间也肯定不会超过O(n)这个时间
    在这里插入图片描述
  2. 在第二次QuickSort排序后,又将49的左侧分为了以27为轴的左右两部分;右侧以76为轴的左右两部分。此时待排序的数组元素为:0~0、2~2、4~5、7~7。在这个待排序的元素中,它的长度也不会超过n,所以对它们进行Partition()的调用所使用的时间也肯定不会超过O(n)
    在这里插入图片描述
  3. 在第三次QuickSort中,对27的左侧即13与右侧即38进行了拆分
    对76的左侧,由于只有两个元素,并且轴是第一个元素,也就是49,所以49会被排序,但是65并不会被排序,所以65会被单独分为一个待排序的位置;
    对76的右侧,只有一个97,所以会对97进行排序。综上,在第三次QuickSort结束后,剩余的待排序只有5~5。与上面所得到的结论一样,在这个区间内调用Partiton()所使用的时间肯定不会超过O(n)
    在这里插入图片描述
  4. 第四次QuickSort中,对65进行排序,此时数组全部变为有序状态。
  5. 在每一次的QuickSort过程中,它的时间都不会超过O(n),所以在k次QuickSort即递归调用的次数中,它的时间不会超过O(n*k)

(二) 空间复杂度

  • 由于在每一次的递归调用过程中,都需要创建一个单独的栈用于保存变量信息,并且在每一次创建过程中,都只需要创建low、pivot、high以及对arr的引用,所以每次调用过程中,所花费的空间复杂度为O(1)
  • 那么k次的递归调用,就会花费O(K*1)=O(k)的空间
  • 所以不管是时间复杂度,还是空间复杂度,都与它的递归调用次数息息相关

(三)递归次数

  1. 在第一次QuickSort后,我们将原数组分为了这样的两个部分,我们如果将它们表示为二叉树的形式,就是这样的一个表示形式在这里插入图片描述
  2. 在第二次QuickSort后,我们又将49的左右两份又拆分开,并将它们表示为二叉树的形式,就形成了下面的表示形式
    在这里插入图片描述
  3. 在第三次的QuickSort后,我们又将27的左右两边以及76的左右两边拆开,并将它们用二叉树的形式表示,就得到了下面的表示形式
    在这里插入图片描述
  4. 在第四次QuickSort后,只需要将剩下的65进行拆分,并将它以二叉树的形式表示,就得到了下面的表示形式
    在这里插入图片描述
  5. 在将n个元素经历了k次的递归调用后,得到了上图的二叉树形式,树的高度实际上就是递归调用的次数。而对于n个结点能得到的最小高度为:(log2n)+1;最大高度为:n
  6. 因此对于快速排序算法而言,它的最好时间复杂度为:O(n*log2n);最坏时间复杂度为:O(n*n)=O(n2),它的平均复杂度为:O(n*log2n)
  7. 对于它的空间复杂度而言,最好空间复杂度为:O(log2n);最坏时间复杂度为:O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值