排序3_快排

快排(不稳定)

1.递归

原理:

  1. 从待排序区间选择一个数,作为基准值(pivot);
    (基准值在最左侧,先从右向左找;基准值在最右侧,先从左向右找)
  2. Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
  3. 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1, 代表已经有序,或者小区间的长度 == 0,代表没有数据。
    public static void quickSort(int[] array) { //当前形参不适合递归
        quickSortInter(array, 0, array.length - 1);
         //用quickSortInter实现递归 (数组,左下标,右下标)
    }
    
    // [left, right]
    private static void quickSortInter(int[] a, int left, int right) {
        if (left >= right) { //递归出口
            // 直到 长度 <= 1
            return;
        }
        // 1. 选择基准值 array[left]
        // 2. 做 partition
        int pivotIndex = partition(a, left, right);
        // 左边小区间 [left, pivotIndex - 1]
        // 右边小区间 [pivotIndex + 1, right]
        // 3. 分别对左右小区间按同样方式处理
        quickSortInter(a, left, pivotIndex - 1);
        quickSortInter(a, pivotIndex + 1, right);
    }

1.1 hoare法—partition(常用)

在这里插入图片描述

private static int partition1(int[] a, int left, int right) {
        int begin = left;
        int end = right;
        int pivot = a[left];
        // [left, begin)        <= pivot
        // (end, right]         >= pivot
        while (begin < end) {
            while (begin < end && a[end] >= pivot) {
                end--;
            }
            while (begin < end && a[begin] <= pivot) {
                begin++;
            }
            swap(a, begin, end);
        }
        swap(a, left, begin);
        return begin;
    }

1.2 挖坑法—partition

基本思路和Hoare 法一致,只是不再进行交换,而是进行赋值(填坑+挖坑)

private static int partition2(int[] a, int left, int right) {
        int begin = left;
        int end = right;
        int pivot = a[left];
        // [left, begin)        <= pivot
        // (end, right]         >= pivot
        while (begin < end) {
            while (begin < end && a[end] >= pivot) {
                end--;
            }
            a[begin] = a[end];

            while (begin < end && a[begin] <= pivot) {
                begin++;
            }
            a[end] = a[begin];
        }
        a[begin] = pivot;
        return begin;
    }

1.3 前后遍历法—partition

 private static int partition3(int[] a, int left, int right) {
        // pivot = array[left]
        // 比较区间 [left + 1, right]
        int pivot = a[left];
        int d = left + 1;
        //[left,d)  <=pivot
        //[d,i)     >=pivot
        for (int i = left + 1; i <= right; i++) {
            if (a[i] < pivot) {
                swap(a, i, d++);
            }
        }

        swap(a, d - 1, left);
        return d - 1;
    }

1.4 快排注意事项

1)partition不是快排,只是快排的一个步骤。
2)partition一定要所有的数据都和基准值作比较。
      eg:基准值在左,先走右边;基准值在右,先走左边
3)partition时间复杂度O(n),空间复杂度O(1)。
4)快排时间复杂度 (遍历*递归深度): O(n)*O(log(n))~O(n^2)
在这里插入图片描述

快排完整代码:递归版

选取最左侧元素为基准值(三种partion):

public class QuickSort {
    public static void quickSort(int[] array) { 
        // 当前形参不适合递归
        // 用quickSortInter实现递归 (数组,左下标,右下标)
        quickSortInter(array, 0, array.length - 1); 
    }

    // [left, right]
    private static void quickSortInter(int[] a, int left, int right) {
        if (left >= right) {
            // 直到 长度 <= 1
            return;
        }
        // 1. 选择基准值 array[left]
        // 2. 做 partition
        int pivotIndex = partition3(a, left, right);
        // 左边小区间 [left, pivotIndex - 1]
        // 右边小区间 [pivotIndex + 1, right]
        // 3. 分别对左右小区间按同样方式处理
        quickSortInter(a, left, pivotIndex - 1);
        quickSortInter(a, pivotIndex + 1, right);
    }

    private static int partition1(int[] a, int left, int right) {
        int begin = left;
        int end = right;
        int pivot = a[left];
        // [left, begin)        <= pivot
        // (end, right]         >= pivot
        while (begin < end) {
            while (begin < end && a[end] >= pivot) {
                end--;
            }
            while (begin < end && a[begin] <= pivot) {
                begin++;
            }

            swap(a, begin, end);
        }
        swap(a, left, begin);
        return begin;
    }

    private static int partition2(int[] a, int left, int right) {
        int begin = left;
        int end = right;
        int pivot = a[left];
        // [left, begin)        <= pivot
        // (end, right]         >= pivot
        while (begin < end) {
            while (begin < end && a[end] >= pivot) {
                end--;
            }
            a[begin] = a[end];

            while (begin < end && a[begin] <= pivot) {
                begin++;
            }
            a[end] = a[begin];
        }
        a[begin] = pivot;
        return begin;
    }

    private static int partition3(int[] a, int left, int right) {
        // array[left]
        // [left + 1, right]
        int pivot = a[left];
        int d = left + 1;
        for (int i = left + 1; i <= right; i++) {
            if (a[i] < pivot) {
                swap(a, i, d++);
            }
        }

        swap(a, d - 1, left);
        return d - 1;
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

选取最右侧元素为基准值,采取hoare法做partion示例:

public class TestSort {
    public static void quickSort(int[] array){
        // []
        quickSortHelp(array,0,array.length-1);
    }

    private static void quickSortHelp(int[] array, int left, int right) {
        if(left >= right){
            // 区间中有 0 个元素或者 1 个元素. 此时不需要排序
            return;
        }
        
        // 区间中有 0 个元素或者 1 个元素. 此时不需要排序
        int pivotIndex = partition(array,left,right);
        quickSortHelp(array,left,pivotIndex-1);
        quickSortHelp(array,pivotIndex+1,right);
    }

    private static int partition(int[] array, int left, int right) {
        int baseIndex = right;
        int base = array[right]; //最右侧为基准值
        while (left<right){
            // 从左向右找比基准值大的元素
            while (left<right && array[left] <= base){
                left++;
            }// 当上面循环结束时,
            // left 要么和right重合,要么left指向一个大于base的值

            // 从左向右找比基准值小的元素
            while (left<right && array[right] >= base){ // 此处等号不能省略 不然基准值可能被交换到其他位置
                right--;
            }// 当上面循环结束时,
            // right 要么和left重合,要么right指向一个小于base的值

            swap(array,left,right);
        }

        // 此时 left = right 相遇了,将相遇位置元素和基准值进行交换
        swap(array,left,baseIndex); 
        // 返回此时基准值的下标,即重合位置下标
        return left;
    }

    private static void swap(int[] array, int left, int right) {
        int temp = array[left];
        array[left] = array[right];
        array[right] = temp;
    }

    public static void main(String[] args) {
        int[] array = {6,1,2,7,9,3,4,5,10,8};
        quickSort(array);
        System.out.println(Arrays.toString(array));
    }
}

为什么最后一步将相遇位置元素和基准值进行交换swap(array,left,baseIndex);,仍然能满足快排的要求呢?
答:baseIndex 是这个序列中最后一个元素的下标,要求 left 和 right 重合位置元素必需是 >= 基准值的元素才可以交换。
left++; 导致和 right 重合。此时最终的值取决于上次循环中 right 指向的值。上次循环中,right应该是找到了一个小于基准值的值,然后和一个大于基准值的值交换了,此处最终的 g 一定是大于基准值的。
right--;导致和 left 重合。此时最终的值取决于上次循环中 left 指向的值。上面的 left++ 退出一定是 left 找到了比基准值大的元素,此时 left 和 right 重合元素则一定也是大于基准值的。

测试结果:
在这里插入图片描述
性能分析:
在这里插入图片描述
当数组逆序时,取第一个或最后一个元素为基准值,时间复杂度O(n2),空间复杂度O(n)。
快排的空间复杂度,主要与递归深度有关。

2.非递归

原理:利用栈实现区间的管理。

   import java.util.*;
   
   // 升序
   public static void quickSort(int[] array) {
        // 借助栈, 模拟实现递归的过程
        // stack 用来存放数组下标. 通过下标来表示接下来要处理的区间是什么
        Stack<Integer> stack = new Stack<>();
        // 初始情况下, 先把右侧边界下标入栈, 再把左侧边界下标入栈, 左右边界仍然构成前闭后闭区间
        stack.push(array.length - 1);
        stack.push(0);

        while (!stack.isEmpty()) {
            // 这个取元素的顺序要和push的顺序正好相反
            int left = stack.pop();
            int right = stack.pop();
            if (left >= right) {
                // 区间中只有 1 个或 0 个元素, 不需要整理
                continue;
            }
            // 通过 partition 把区间整理成以基准值为中心, 左侧小于等于基准值, 右侧大于等于基准值的形式
            int pivotIndex = partition(array, left, right);

            // 准备处理下个区间.
            // [index + 1, right] 基准值右侧区间
            stack.push(right);
            stack.push(pivotIndex + 1);

            // [left, index - 1] 基准值左侧区间
            stack.push(pivotIndex - 1);
            stack.push(left);
        }
    }

3.快排优化

3.1优化基准值

快速排序的效率和基准值先取的好坏密切相关。如果基准值是一个接近数组中位数的元素,比较平衡;如果基准值刚好取到最大值(最小值),比较差。

1)选择第一个数作为pivot——只有顺序/逆序才是单支树。
2)随机选择任意数作为pivot——不能消除单支树,但减少了退化成单支树的概率。
3)几数取中(一般三数取中)——消除单支树。

3.2小区间使用插排

当区间比较小时,再去递归效率已经不高了。此时不进行递归,直接进行插入排序。

3.3大区间使用堆排

如果区间比较大时,递归深度也会非常深。当递归深度达到一定程度时,把当前区间的排序使用堆排序进行优化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值