图解快速排序算法

 

 

 

 

 

快速排序定义

 

一般来说,算法就像数学公式,前人经过不断优化和验证得到有规律性的公式留给后人使用,当然也会交给后人验证的思路。那么快速排序算法就是这样,它有基本固定的定义如下:

 

tip:如果一时不理解定义可以直接跳过,直接看图解然后再回来看定义,你就会豁然开朗。另外,文章描述时下标从1开始,实际代码数组大多都是从0开始,所以在代码中需要减去1来调整下标差异。

 

1、将待排序数组A[1...n]某个任意下标i元素值v作为主元素,简单做法可以直接拿最后元素。

 

2、基于主元素下标i值为v进行左右分区(实际上分区就是分解数组进行排序,类似归并排序的分治法),分区后正序(从小到大)需要满足原则如下:

左边分区值A[1...i - 1]  <=  主元素值A[i]右边分区值A[i+1...n]  >  主元素值A[i]

倒序(从大到小)则需要满足原则如下:

左边分区值A[1...i - 1]  >=  主元素值A[i]右边分区值A[i+1...n]  <  主元素值A[i]

 

下图是正序(从小到大)分区前后抽象图解,如果看完图解还是不理解就直接跳过继续往下看:

3、将分区后的左右分区(不包含主元素)分别作为子数组递归第1、2步骤,直到每个子数组小于或等于一个元素为止,递归则开始层层回升,排序完成

 

 

图解快速排序算法

 

 

定义主元素

根据定义我们知道第一步需要根据给定数组定义一个主元素,我们暂时用最简单方式,拿数组最后一个元素作为主元素(下标=6,值=3),图解如下:

 

分区

根据定义我们得到主元素后接下来进行分区,按分区原则实现图解如下:

 

1、获取主元素

2、定义和维护一个左分区下标计数器变量(leftPartitionIndex),遍历数组下标1-5,一旦发现其中某个元素小于等于主元素,则进行交换。图解如下:

完整排序和分区代码如下:

 /**
     * 快速排序 正序
     *
     * @param param      待排序数组或左右分区子数组参数
     * @param beginIndex 数组开始下标
     * @param endIndex   数组结束下标,一般是主元素(如果是直接拿最后元素作为主元素时)
     */
    private void quickSortAsc(Integer[] param, int beginIndex, int endIndex) {
        //当某个分区排序完成后,不再有右分区时,递归开始回升
        if (beginIndex < endIndex) {
            //分区并返回分区界限下标值,实际就是分区后主元素值交换后所在的下标
            int partitionIndex = this.partition(param, beginIndex, endIndex);
            //将左分区作为子数组进行递归快排
            this.quickSortAsc(param, beginIndex, partitionIndex - 1);
            //将右分区作为子数组进行递归快排
            this.quickSortAsc(param, partitionIndex + 1, endIndex);
        }

    }


    /**
     * 分区,注意,分区后数组元素下标对应的值会发生移动
     *
     * @param param      待排序数组参数
     * @param beginIndex 数组需要排序开始下标
     * @param endIndex   数组需要排序结束下标
     * @return 分区下标
     */
    private int partition(Integer[] param, int beginIndex, int endIndex) {

        //定义左分区下标变量,默认是开始下标-1,每次交换值时自增加1
        int leftPartitionIndex = beginIndex - 1;
        //主元素值,拿最后元素
        int mainEleVal = param[endIndex];
        //遍历(数组开始元素-最后元素-1)范围
        for (int currentIndex = beginIndex; currentIndex <= endIndex - 1; currentIndex++) {
            //如果其中某个元素小于等于主元素值
            if (param[currentIndex] <= mainEleVal) {
                //每次交换值时左分区下标变量自增加1
                leftPartitionIndex++;
                //将当前下标对应的值与自增后左分区下标变量对应值进行交换
                this.swapElement(param, currentIndex, leftPartitionIndex);
            }
        }
        //每次交换值时左分区下标变量自增加1
        leftPartitionIndex++;
        //将主元素下标对应的值与自增后左分区下标变量对应值进行交换
        this.swapElement(param, endIndex, leftPartitionIndex);
        //当前最后的左分区下标(主元素值所在的下标)作为分区下标
        int partitionIndex = leftPartitionIndex;
        return partitionIndex;
    }

    /**
     * 交换元素
     *
     * @param param      数组参数
     * @param leftIndex  左边索引
     * @param rightIndex 右边索引
     */
    private void swapElement(Integer[] param, int leftIndex, int rightIndex) {

        Integer oldMaxIndexVal = param[leftIndex];
        param[leftIndex] = param[rightIndex];
        param[rightIndex] = oldMaxIndexVal;
    }

}

3、第一轮执行完上面的分区代码后,就得到了我们上面算法定义时给出的图解如下:

 

4、继续将左分区进行分区图解如下。注意左分区子数组递归调用的代码在上面已经给出:

 

 

分区后,此时,在当前子数组(上一次分区后的原左分区)基础上又产生了左分区子数组,但没有右分区子数组,我们发现此时的左右子数组元素小于等于一个元素,满足上面算法定义第3条,此时(上一次分区后的原左分区)递归开始层层回升,左子数组正序排序完毕。

 

5、继续将右分区进行分区图解如下:

分区后,此时,在当前子数组(上一次分区后的原右分区)基础上又产生了左右分区子数组,我们发现此时的左右子数组元素小于等于一个元素,满足上面算法定义第3条,此时(上一次分区后的原右分区)递归开始层层回升,右子数组正序排序完毕,原左右子数组正序排序完毕后,那么整个数组正序排序完毕。结果如下:

 

验证算法

最后我们来验证算法,代码如下:

/**

 * <p>
 * 快速排序算法
 * </p>
 *
 * @author laizhiyuan
 * @since 2019/9/21.
 */
public class QuickSortAlgorithm {


    public static void main(String[] args) {
        Integer[] param = new Integer[]{2, 6, 7, 1, 10, 3};

        QuickSortAlgorithm algorithm = new QuickSortAlgorithm();

        long t = System.currentTimeMillis();
        System.out.println("排序前:" + JSON.toJSONString(param));

        //实际代码数组下标从0开始,所以对应length(10)需要减去1
        algorithm.quickSortAsc(param, 0, param.length - 1);

        System.out.println("排序后:" + JSON.toJSONString(param));
        long t2 = System.currentTimeMillis();

        System.out.println("算法耗时:" + (t2 - t) + "ms");
    }

    /**
     * 快速排序 正序
     *
     * @param param      待排序数组或左右分区子数组参数
     * @param beginIndex 数组开始下标
     * @param endIndex   数组结束下标,一般是主元素(如果是直接拿最后元素作为主元素时)
     */
    private void quickSortAsc(Integer[] param, int beginIndex, int endIndex) {
        //当某个分区排序完成后,不再有右分区时,递归开始回升
        if (beginIndex < endIndex) {
            //分区并返回分区界限下标值,实际就是分区后主元素值交换后所在的下标
            int partitionIndex = this.partition(param, beginIndex, endIndex);
            //将左分区作为子数组进行递归快排
            this.quickSortAsc(param, beginIndex, partitionIndex - 1);
            //将右分区作为子数组进行递归快排
            this.quickSortAsc(param, partitionIndex + 1, endIndex);
        }

    }


    /**
     * 分区,注意,分区后数组元素下标对应的值会发生移动
     *
     * @param param      待排序数组参数
     * @param beginIndex 数组需要排序开始下标
     * @param endIndex   数组需要排序结束下标
     * @return 分区下标
     */
    private int partition(Integer[] param, int beginIndex, int endIndex) {

        //定义左分区下标变量,默认是开始下标-1,每次交换值时自增加1
        int leftPartitionIndex = beginIndex - 1;
        //主元素值,拿最后元素
        int mainEleVal = param[endIndex];
        //遍历(数组开始元素-最后元素-1)范围
        for (int currentIndex = beginIndex; currentIndex <= endIndex - 1; currentIndex++) {
            //如果其中某个元素小于等于主元素值
            if (param[currentIndex] <= mainEleVal) {
                //每次交换值时左分区下标变量自增加1
                leftPartitionIndex++;
                //将当前下标对应的值与自增后左分区下标变量对应值进行交换
                this.swapElement(param, currentIndex, leftPartitionIndex);
            }
        }
        //每次交换值时左分区下标变量自增加1
        leftPartitionIndex++;
        //将主元素下标对应的值与自增后左分区下标变量对应值进行交换
        this.swapElement(param, endIndex, leftPartitionIndex);
        //当前最后的左分区下标(主元素值所在的下标)作为分区下标
        int partitionIndex = leftPartitionIndex;
        return partitionIndex;
    }

    /**
     * 交换元素
     *
     * @param param      数组参数
     * @param leftIndex  左边索引
     * @param rightIndex 右边索引
     */
    private void swapElement(Integer[] param, int leftIndex, int rightIndex) {

        Integer oldMaxIndexVal = param[leftIndex];
        param[leftIndex] = param[rightIndex];
        param[rightIndex] = oldMaxIndexVal;
    }

}

执行结果如下:

排序前:[2,6,7,1,10,3]排序后:[1,2,3,6,7,10]算法耗时:117ms

 

算法时间复杂度

快速排序最坏情况时间复杂度是O(n²)

平均情况时间复杂度是O(nlgn)

 

算法适用场景

快速排序算法适合小规模(排序元素不多,一般不超过10000)排序需求,大规模排序应该考虑使用其它算法,例如归并排序、堆排序。

 

-END-

 

 

    长按扫码关注【Java软件编程之家】公众号,一起学习,一起成长!

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值