深入浅出排序算法之快速排序(重要)⭐⭐⭐⭐⭐

目录

1. 算法介绍⭐⭐⭐⭐⭐

1.1 图示解析

2. 执行流程和代码实现

2.1 挖坑法⭐⭐⭐⭐

2.2 Hoare法⭐⭐⭐⭐

2.3 前后指针法(了解即可)

2.4  非递归实现快速排序(了解即可)

4. 性能分析

5. 算法改进

5.1 三数选中法

5.2 最后几行数据采用直接插入排序


1. 算法介绍⭐⭐⭐⭐⭐

快速排序也是“交换”类的排序,运用了分治的思想,它通过多次划分操作实现排序。以升序为例,其执行流程可以概括为:每一趟选择当前所有子序列中的一个关键字(通常是第一个)作为基准值,将子序列中比基准值小的移动到基准值的前边,比基准值大的移到基准值后边;当本趟所有子序列都被基准值以上述规则划分完毕后,会得到新的一组更短的子序列,它们成为下一趟划分的初始序列集。

(1)从待排序区间选择一个数,作为基准值(mid);

(2)Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;

(3)采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间的长度 == 0,代表没有数据。

1.1 图示解析

2. 执行流程和代码实现

2.1 挖坑法⭐⭐⭐⭐

小伙伴们,自行放大图片看一下,很细滴!!!

    /*
    下面性能部分细讲
    时间复杂度:
        最好情况是:O(NlogN) 均匀分割待排序列,尽量满二叉树
        最坏情况是:O(N^2),有序或者逆序,单分支的树
    空间复杂度:
        最好情况是:O(logN) 树的高度,把持着mid变量
        最坏情况是:O(N),有序或者逆序,单分支的树
    稳定性:不稳定
     */
    //这里为了统一其他排序算法的接口,所以只传一个参数——数组
    public static void quickSort(int[] array) {

        quick(array, 0, array.length - 1);
    }

    //划分
    private static void quick(int[] array, int start, int end) {
        //这里为什么是>=,因为防止end到了start后面,导致越届
/*
        s e
        mid
    例如:1 2
        再递归进去,end == -1,start == 0,所以必须交 =
 */
        if (start >= end) {
            return;
        }

        int mid = partition1(array, start, end);
        //递归左边
        quick(array, start, mid - 1);
        //递归右边
        quick(array, mid + 1, end);
    }

    //第一种:快速排序,挖坑法,无优化版本
    //每一趟的操作,交换,填坑
    private static int partition1(int[] array, int left, int right) {
        int tmp = array[left];//保存最左位置的值
        //开始找坑
        //多次左右走
        while (left < right) {
            //先右往左走,找一个值比tmp小的,一次循环或者说一次向右走
            // 这里必须要是 >= ,不然当下标0和下标array.length的话,就会陷入死循环,这两个下标的值一直在交换
            //提问:为什么不需要再次++,去进入循环呢?
            while (left < right && array[right] >= tmp)
                right--;
            array[left] = array[right];
            //从左往右走,找一个值比tmp大的,一次循环或者说一次向左走
            while (left < right && array[left] <= tmp)
                left++;
            array[right] = array[left];
        }
        array[left] = tmp;
        return left;
    }

    public static void main(String[] args) {
        int[] a = {10,10,9,8,7,6,5,4,3,2,1};
        Sort.quickSort2(a);
        for (int x : a) {
            System.out.print(x + " ");
        }
    }

2.2 Hoare法⭐⭐⭐⭐

思路:设置两个下标指针i和j,j向左走,找到比基准值(pivot)小的值先停下来,不交换,i向右走,找到比基准值(pivot)大的值先停下来,然后交换array[i] 和 array[j],当i和j相遇后,交换array[i] 和 array[left]。

    /*
    下面性能部分细讲
    时间复杂度:
        最好情况是:O(NlogN) 均匀分割待排序列,尽量满二叉树
        最坏情况是:O(N^2),有序或者逆序,单分支的树
    空间复杂度:
        最好情况是:O(logN) 树的高度,把持着mid变量
        最坏情况是:O(N),有序或者逆序,单分支的树
    稳定性:不稳定
     */
    //这里为了统一其他排序算法的接口,所以只传一个参数——数组
    public static void quickSort(int[] array) {

        quick(array, 0, array.length - 1);
    }

    //划分
    private static void quick(int[] array, int start, int end) {
        //这里为什么是>=,因为防止end到了start后面,导致越届
/*
        s e
        mid
    例如:1 2
        再递归进去,end == -1,start == 0,所以必须交 =
 */
        if (start >= end) {
            return;
        }

        int mid = partition2(array, start, end);
        //递归左边
        quick(array, start, mid - 1);
        //递归右边
        quick(array, mid + 1, end);
    }

    //第二种快速排序,Hoare法,主要是交换的方式不同
    private static int partition2(int[] array, int left, int right) {
        int tmp = array[left];
        int i = left;//把left开始的下标保存起来先
        while (left < right) {
/*
为什么不能先走左呢?
答:因为先走左的话,最后right会停在比mid大的值的下标,left找到right停下来,交换i和left的值,导致不满足快排的结果
 */
            //这两个循环,永远是right追left
            while (left < right && array[right] >= tmp)
                right--;
            while (left < right && array[left] <= tmp)
                left++;
            swap(array, left, right);//自定义的交换方法
        }
        swap(array, i, left);
        return left;
    }

    public static void main(String[] args) {
        int[] a = {10,10,9,8,7,6,5,4,3,2,1};
        Sort.quickSort2(a);
        for (int x : a) {
            System.out.print(x + " ");
        }
    }


2.3 前后指针法(了解即可)

总体思路:prev一直往后走(prev++),当array[prev]小于array[left]的时候,cur++。当array[cur] != array[prev]时。

具体看如下的过程:

    //第三种排序算法:前后指针法
    //写法一
    private static int partition3(int[] array, int left, int right) {
        int cur = left;//prev记录的是最后一位比array[left]的下标
        int prev = left + 1;
        while (cur <= right) {
            if (array[prev] < array[left] && array[++cur] != array[prev]) {
                swap(array, prev, cur);
            }
            prev++;
        }
        swap(array, left, cur);
        return cur;
    }

第一步:先把第一个元素定为基准值。

cur 记录第一个元素6的下标,prev记录第二个元素的1的下标。

第二步:因为第一步的1 < 6,cur++且没发生交换,prev++(注意:prev无论如何都会一直往前走) 。

第三步:因为第二步的2 < 6,cur++且没发生交换,prev++(注意:prev无论如何都会一直往前走) 。

第四步:因为第三步的7 > 6,所以 cur不往前走,prev++,来到元素9的下标。

第五步:因为第四步的9 > 6,所以 cur不往前走,prev++,来到元素3的下标。

第六步:因为第五步的3 < 6,所以cur++来到元素7的下标,接着交换array[cur]和array[prev]的元素,后prev++(代码有体现)。

第七步:因为第五步的4 < 6,所以cur++来到元素的下标,接着交换array[cur]和array[prev]的元素,后prev++(代码有体现)。

第八步:因为第七步的5 < 6,所以cur++来到元素的下标,接着交换array[cur]和array[prev]的元素,后prev++(代码有体现)。

第九步:因为第八步的10 > 6,所以 cur不往前走,prev++,来到元素8的下标。

然后排序元素6左边子序列和右边子序列,一直重复,直至排序完成!

总结:我们可以发现cur永远都是记录着小于基准值的最后一个元素的下标,不断地把比基准值大的元素往右推! 

        /*
    下面性能部分细讲
    时间复杂度:
        最好情况是:O(NlogN) 均匀分割待排序列,尽量满二叉树
        最坏情况是:O(N^2),有序或者逆序,单分支的树
    空间复杂度:
        最好情况是:O(logN) 树的高度,把持着mid变量
        最坏情况是:O(N),有序或者逆序,单分支的树
    稳定性:不稳定
     */
    //这里为了统一其他排序算法的接口,所以只传一个参数——数组
    public static void quickSort(int[] array) {

        quick(array, 0, array.length - 1);
    }

    //划分
    private static void quick(int[] array, int start, int end) {
        //这里为什么是>=,因为防止end到了start后面,导致越届
/*
        s e
        mid
    例如:1 2
        再递归进去,end == -1,start == 0,所以必须交 =
 */
        if (start >= end) {
            return;
        }

        int mid = partition3(array, start, end);
        //递归左边
        quick(array, start, mid - 1);
        //递归右边
        quick(array, mid + 1, end);
    }

    //第三种排序算法:前后指针法
    //写法一
    private static int partition3(int[] array, int left, int right) {
        int cur = left;//prev记录的是最后一位比array[left]的下标
        int prev = left + 1;
        while (cur <= right) {
            if (array[prev] < array[left] && array[++cur] != array[prev]) {
                swap(array, prev, cur);
            }
            prev++;
        }
        swap(array, left, cur);
        return cur;
    }

    public static void main(String[] args) {
        int[] a = {6,1,2,7,9,3,4,5,10,8};
        Sort.quickSort2(a);
        for (int x : a) {
            System.out.print(x + " ");
        }
    }

2.4  非递归实现快速排序(了解即可)

使用栈完成效果

(1)先准备一个栈,先调用一次partition1()方法,把6放到最终位置。

 (2)将左边子序列的start和end下标进栈,右子序列的start和end下标进栈。

(3)然后出栈,先赋值个right,再给left,循环进行,就能完成排序。

注意:会将右子序列排序完成,再去排序左子序列,小伙伴们可以根据我们上面的部分流程,把整个算法流程画出来!

  
    //第一种:快速排序,挖坑法,无优化版本
    //每一趟的操作,交换,填坑
    private static int partition1(int[] array, int left, int right) {
        int tmp = array[left];//保存最左位置的值
        //开始找坑
        //多次左右走
        while (left < right) {
            //先右往左走,找一个值比tmp小的,一次循环或者说一次向右走
            // 这里必须要是 >= ,不然当下标0和下标array.length的话,就会陷入死循环,这两个下标的值一直在交换
            //提问:为什么不需要再次++,去进入循环呢?
            while (left < right && array[right] >= tmp)
                right--;
            array[left] = array[right];
            //从左往右走,找一个值比tmp大的,一次循环或者说一次向左走
            while (left < right && array[left] <= tmp)
                left++;
            array[right] = array[left];
        }
        array[left] = tmp;
        return left;
    }

    //非递归实现快速排序
    //用栈实现
    public static void quickSort2(int[] array){
        Deque<Integer> stack = new LinkedList<>();
        int left = 0;
        int right = array.length - 1;
        int mid = partition1(array,left,right);
        if(mid > left + 1){
            stack.push(left);
            stack.push(mid - 1);
        }
        if(mid < right - 1){
            stack.push(mid + 1);
            stack.push(right);
        }
        while(!stack.isEmpty()){
            right = stack.pop();
            left = stack.pop();
            mid = partition1(array,left,right);
            if(mid > left + 1){
                stack.push(left);
                stack.push(mid - 1);
            }
            if(mid < right - 1) {
                stack.push(mid + 1);
                stack.push(right);
            }
        }
    }

4. 性能分析

(1)最好的情况:快速排序最理想的情况就是满二叉树, 此时时间复杂度是:O(nlogn);空间复杂度是:O(logn)。

(2)最坏的情况:原数据有序或者逆序,这样快速排序就成了单分支树了,此时时间复杂度是:O(n^2),空间复杂度是:O(n)。

5. 算法改进

5.1 三数选中法

取待排序数组中 头、中、尾三个位置的元素、取中间值作为基准元素。(此种方式最好,递归调用栈的深度最低)1.取待排序数组中 头、中、尾三个位置的元素、取中间值作为基准元素。(此种方式最好,递归调用栈的深度最低)。

    //优化的方案,均匀的分割!三数取中法
    private static int midThree(int[] array, int left, int right) {
        int mid = (left + right) / 2;
        if(array[left] > array[right]){
            if(array[mid] < array[right]){
                return right;
            }else if(array[mid] > array[left]){
                return left;
            }else {
                return mid;
            }
        }else{
            //array[right] > array[left]
            if(array[mid] < array[left]){
                return left;
            }else if(array[mid] > array[right]){
                return right;
            }else{
                return mid;
            }
        }
    }

改进后的代码是:

    /*
    时间复杂度:
        最好情况是:O(NlogN) 均匀分割待排序列,尽量满二叉树
        最坏情况是:O(N^2),有序或者逆序,单分支的树
    空间复杂度:
        最好情况是:O(logN) 树的高度,把持着mid变量
        最坏情况是:O(N),有序或者逆序,单分支的树
    稳定性:不稳定
     */
    //这里为了统一其他排序算法的接口,所以只传一个参数——数组
    public static void quickSort(int[] array) {

        quick(array, 0, array.length - 1);
    }

    //划分
    private static void quick(int[] array, int start, int end) {
        //这里为什么是>=,因为防止end到了start后面,导致越届
/*
        s e
        mid
    例如:1 2
        再递归进去,end == -1,start == 0,所以必须交 =
 */
        if (start >= end) {
            return;
        }

        //优化一:
        //排序,返回到最终位置的元素下标
        int index = midThree(array,start,end);
        swap(array,start,index);
        int mid = partition3(array, start, end);
        //递归左边
        quick(array, start, mid - 1);
        //递归右边
        quick(array, mid + 1, end);
    }

    //第一种:快速排序,挖坑法,无优化版本
    //每一趟的操作,交换,填坑
    private static int partition1(int[] array, int left, int right) {
        int tmp = array[left];//保存最左位置的值
        //开始找坑
        //多次左右走
        while (left < right) {
            //先右往左走,找一个值比tmp小的,一次循环或者说一次向右走
            // 这里必须要是 >= ,不然当下标0和下标array.length的话,就会陷入死循环,这两个下标的值一直在交换
            //提问:为什么不需要再次++,去进入循环呢?
            while (left < right && array[right] >= tmp)
                right--;
            array[left] = array[right];
            //从左往右走,找一个值比tmp大的,一次循环或者说一次向左走
            while (left < right && array[left] <= tmp)
                left++;
            array[right] = array[left];
        }
        array[left] = tmp;
        return left;
    }

    //优化的方案,均匀的分割!三数取中法
    private static int midThree(int[] array, int left, int right) {
        int mid = (left + right) / 2;
        if(array[left] > array[right]){
            if(array[mid] < array[right]){
                return right;
            }else if(array[mid] > array[left]){
                return left;
            }else {
                return mid;
            }
        }else{
            //array[right] > array[left]
            if(array[mid] < array[left]){
                return left;
            }else if(array[mid] > array[right]){
                return right;
            }else{
                return mid;
            }
        }
    }

5.2 最后几行数据采用直接插入排序

直接插入排序:数据越有序,效率越高!

    /*
    时间复杂度:
        最好情况是:O(NlogN) 均匀分割待排序列,尽量满二叉树
        最坏情况是:O(N^2),有序或者逆序,单分支的树
    空间复杂度:
        最好情况是:O(logN) 树的高度,把持着mid变量
        最坏情况是:O(N),有序或者逆序,单分支的树
    稳定性:不稳定
     */
    //这里为了统一其他排序算法的接口,所以只传一个参数——数组
    public static void quickSort(int[] array) {

        quick(array, 0, array.length - 1);
    }

    //划分
    private static void quick(int[] array, int start, int end) {
        //这里为什么是>=,因为防止end到了start后面,导致越届
/*
        s e
        mid
    例如:1 2
        再递归进去,end == -1,start == 0,所以必须交 =
 */
        if (start >= end) {
            return;
        }

        //优化二:
        if(end - start + 1 <= 14){
            insertSort2(array,start,end);
        }
        //优化一:
        //排序,返回到最终位置的元素下标
        int index = midThree(array,start,end);
        swap(array,start,index);
        int mid = partition3(array, start, end);
        //递归左边
        quick(array, start, mid - 1);
        //递归右边
        quick(array, mid + 1, end);
    }

    //第一种:快速排序,挖坑法,无优化版本
    //每一趟的操作,交换,填坑
    private static int partition1(int[] array, int left, int right) {
        int tmp = array[left];//保存最左位置的值
        //开始找坑
        //多次左右走
        while (left < right) {
            //先右往左走,找一个值比tmp小的,一次循环或者说一次向右走
            // 这里必须要是 >= ,不然当下标0和下标array.length的话,就会陷入死循环,这两个下标的值一直在交换
            //提问:为什么不需要再次++,去进入循环呢?
            while (left < right && array[right] >= tmp)
                right--;
            array[left] = array[right];
            //从左往右走,找一个值比tmp大的,一次循环或者说一次向左走
            while (left < right && array[left] <= tmp)
                left++;
            array[right] = array[left];
        }
        array[left] = tmp;
        return left;
    }

    //直接插入排序
    public static void insertSort2(int[] arr,int left,int right) {
        //代码可以从i = 1开始算起,但是做题画图时,一定要从i = 0开始算起
        for (int i = left + 1; i < arr.length; i++) {
            int j = i - 1;
            int tmp = arr[i];
            for (; j >= left; j--) {
                //如果arr[j] > tmp变成arr[j] >= tmp就变成不稳定了
                if (arr[j] > tmp) {
                    arr[j + 1] = arr[j];
                } else {
                    break;
                }
            }
            arr[j + 1] = tmp;
        }
    }

  • 9
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
下面是一个用C++实现的快速排序算法的示例: ```cpp #include <iostream> using namespace std; // 交换两个元素的值 void swap(int* a, int* b) { int t = *a; *a = *b; *b = t; } // 以pivot为基准将数组划分为两部分,并返回pivot的索引 int partition(int arr[], int low, int high) { int pivot = arr[high]; // 选择最后一个元素作为pivot int i = (low - 1); for (int j = low; j <= high - 1; j++) { if (arr[j] < pivot) { i++; swap(&arr[i], &arr[j]); } } swap(&arr[i + 1], &arr[high]); return (i + 1); } // 快速排序算法 void quickSort(int arr[], int low, int high) { if (low < high) { int pi = partition(arr, low, high); // 将数组划分为两部分 quickSort(arr, low, pi - 1); // 对划分后的左半部分进行递归排序 quickSort(arr, pi + 1, high); // 对划分后的右半部分进行递归排序 } } // 打印数组元素 void printArray(int arr[], int size) { for (int i = 0; i < size; i++) { cout << arr[i] << " "; } cout << endl; } int main() { int arr[] = {64, 25, 12, 22, 11}; int n = sizeof(arr) / sizeof(arr[0]); cout << "原始数组:"; printArray(arr, n); quickSort(arr, 0, n - 1); cout << "排序后的数组:"; printArray(arr, n); return 0; } ``` 这个示例中,`swap`函数用于交换两个元素的值,`partition`函数用于以pivot为基准将数组划分为两部分,并返回pivot的索引。`quickSort`函数是快速排序算法的主要实现部分。在`main`函数中,我们定义了一个整数数组,然后调用`quickSort`函数对数组进行排序,并打印排序后的结果。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木子斤欠木同

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值