(快速排序)快排以及快排算法改进版本(面试必问)

荷兰国旗问题

问题一

给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度0(1),时间复杂度0(N)

问题二(荷兰国旗问题,涉及到快排的改进版本的应用)

给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度0(1),时间复杂度0(N)

解决方案:可以参考图中的区域,实际的代码在下面的quick_sort2方法中有体现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vOBIIKvX-1645540448273)(https://i.bmp.ovh/imgs/2022/01/574a3738bda82379.png)]

问题三(快排思想的应用)

1 快排

1.1 快排思想

1、快速排序的基本思想:

  • 快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。

  • 之后分别对这两部分记录递归地进行第一步中的步骤,以达到整个序列有序的目的。

2、快速排序的三个步骤:

(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot)

(2)分割操作(两种方法):

  • 以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大(以下代码中的partition方法实现了这一思想);
  • 此外还有一种提升效率的分割操作:以该基准在序列中的实际位置,把序列分成三个子序列。此时,在基准左边的元素都比该基准小,等于基准的元素都放在中间,在基准右边的元素都比基准大(以下代码中的partition2方法实现了这一思想)

(3)递归地对两个序列进行步骤(1)和(2),直到序列为空或者只有一个元素。

3、选择基准的方式

对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。

最理想的方法是,每一次选择的基准恰好能把待排序序列分成两个等长的子序列

1.2 代码


public class QuickSort {


    public static void main(String []args) {
        int []arr = {50, 11, 20, 20, 15, 17, 23, 20, 30, 40, 10, 50, 20};

        //quick_sort(arr,0, arr.length - 1);
        quick_sort2(arr,0, arr.length - 1);
		//partition(arr, 0, arr.length - 1);
        for (int i = 0; i < arr.length; i++)
            System.out.print(arr[i] + ", ");
        //quickSort(arr,0, arr.length - 1);

    }

    public static void quickSort(int[] num,int i,int j){
        if(i >= j){//只剩一个元素不用处理直接结束。
            return;
        }
        //选取基准数
        int val =num[i];
        int l = i;
        int r = j;
        while(l < r){//当l == r时,就是调整完成时
            //从后往前找第一个小于val的数字
            while (l < r && num[r] > val){
                r --;
            }
            if(l < r){//找到了数字
                num[l++] = num[r];
            }
            //从前往后找第一个大于val的数字
            while (l < r && num[l] < val){
                l ++;
            }
            if(l < r){//找到了数字
                num[r--] = num[l];
            }
        }
        //l==r,基准数放进去
        num[l] = val;
        quickSort(num,i,l-1);
        quickSort(num,l+1,j);
    }

    public static void quick_sort(int arr[], int low, int high)
    {
        if (arr.length < 2) return;
        // 这个必须要有,否则要
        if(low >= high){//只剩一个元素不用处理直接结束。
            return;
        }

        int l = partition(arr, low, high);
        System.out.println(l);
        quick_sort(arr, low, l - 1);
        quick_sort(arr, l + 1, high);

    }

    public static int partition(int arr[], int low, int high)
    {
        int l = low, r = high;
        int temp = arr[low];
        while(l < r)
        {
            while(l < r && arr[r] >= temp) --r;
            if (l < r)  arr[l++] = arr[r];

            while(l < r && arr[l] <= temp) ++l;
            if (l < r) arr[r--] = arr[l];

            //while(l < r && arr[l])
        }
        arr[l] = temp;
        return l;
    }

    // 快排的升级版本,解决荷兰国旗问题第二个问题
    public static void quick_sort2(int arr[], int low, int high)
    {
        if (arr.length < 2) return;
        // 这个必须要有,否则要
        if(low >= high){//只剩一个元素不用处理直接结束。
            return;
        }

        // 在快排开始前,先随机选中一个元素与最后一个元素交换
        swap(arr, low + (int)(Math.random() * (high - low + 1)), high);

        int p[] = partition2(arr, low, high, arr[high]); // partition函数的返回值是等于区域的左边界和右边界
        //System.out.println(l);
        quick_sort(arr, low, p[0] - 1);  // 小于区域里面
        quick_sort(arr, p[1] + 1, high); // 大于区

    }
     // 该方法的返回值返回的是等于区间的左右边界下标
    public static int[] partition2(int arr[], int low, int high, int target_value)
    {
        // low表示的是当前数的位置, 只使用当前数
        int less = low - 1; // 小于区域的右边界
        int more = high + 1; // 大于区域的左边界

        while(low < more)
        {
            // 如果数组的当前值小于等于目标值,则将该值与小于区域的右边界的下一个数做交换,小于区域右扩,即++less, 当前数跳下一个,即low++
            if (arr[low] < target_value)
                swap(arr, ++less, low++);
                // 如果数组的当前值大于目标值,则将该值与大于区域的左边界的前一个数做交换,大于区域左扩,即more--, 同时当前数的index不变
            else if (arr[low] > target_value)
                swap(arr, low, --more);
                // 如果数组的当前值等于目标值,则当前数跳到下一步,其他什么也不做
            else low++;
        }
        // swap(arr, more, high);
        return new int[]{less + 1, more};
    }
    public static void swap(int arr[], int i, int j)
    {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}
	// 一种建议版本的划分区间的方式
    void quickSort3(int[]arr,int i, int j) {
        if(i>=j)return;

        int pivot=partition(arr,i,j);
        quickSort3(arr,i,pivot-1);
        quickSort3(arr,pivot+1,j);
    }

    int partition(int[]arr,int l, int r){
        Random rand=new Random();
        int pivot = rand.nextInt(r-l+1)+l;
        swap(arr,pivot,r);
        
        int i=l-1;
        for(int j=l;j<r;j++){
            if(arr[j]<=arr[r]){
                swap(arr,++i,j);
            }
        }
        swap(arr,r,++i);
        return i;
    }

1.3 算法性能

  1. 时间复杂度

    • 最坏的情况时间复杂度是O(n^2), 此时序列是已经全部有序(只有在此时,每一次的划分,都只产生一个长度不为0的子序列),此时会进行n次划分;
    • 最好的情况是数组中的元素无序,且此时每一次的找出的基准,都能放到数列中的位置,此情况下的时间复杂度是O(n * log(2, n))。
    • 因为基准选择的好坏与否,是一个概率问题,
      经由概率公式的计算,平均的时间复杂度为O(n * log(2, n))
  2. 空间复杂度,

主要是递归造成的栈空间的使用,

- 最好情况,递归树的深度为 logn,其空间复杂度也就为 O(logn),

- 最坏情况,需要进行n‐1递归调用,其空间复杂度为O(n),

- 平均情况,空间复杂度也为O(logn)。
  1. 稳定性

    同简单选择排序:由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。

  2. 还有其他的n*log(2,n)的排序算法,为什么只有快排快,是因为相比的其他类型的nlogn的算法,快排的代码十分简洁,所以最快

  3. 快排的改进版本和原版相比:

    • 改进版本比原版算法的速度稍快,但是这只在序列中有多个相等元素的时候才成立,如果序列中所有的元素都不同,则他们两个的执行一样

1.4 一些问题分析

1.4.1

2 快排划分区间算法的应用

2 题目一:215. 数组中的第K个最大元素

在这里插入图片描述

2.1 答案

做这道题之前,建议先做一下快排,非常有帮助,912. 排序数组

2.1.1 方法一:每一轮区间划分都采用三个list分别存储小于、等于和大于目标值的三个元素集

    public int findKthLargest(int[] nums, int k) {
        int n=nums.length;

        List<Integer>list=new ArrayList<>();
        for(int i=0;i<n;i++){
            list.add(nums[i]);
        }
        int ans=dfs(list,k);
        return ans;
    }

    int dfs(List<Integer>list,int p){

        Random rand=new Random();
        
        int base=list.get(rand.nextInt(list.size()));

        List<Integer>s=new ArrayList<>();
        List<Integer>m=new ArrayList<>();
        List<Integer>l=new ArrayList<>();

        for(int k=0;k<list.size();k++){
            if(list.get(k)<base){
                s.add(list.get(k));
            }else if(list.get(k)==base){
                m.add(list.get(k));
            }else{
                l.add(list.get(k));
            }
        }
        // 在数组中的第p大等于在数组中的第list.size()-p+1大
        if(s.size()>=list.size()-p+1){
            // 第二个参数决定在small数组中,这个数是第几大的数
            return dfs(s,s.size()-(list.size()-p+1)+1);
        }else if(s.size()+m.size()>=list.size()-p+1){
            return m.get(0);
        }else{
            return dfs(l,p);
        }
    }

2.1.2 方法二(推荐):改进版的划分区间方式,与方法一相比,不需要计算复杂的k值变化,统一使用索引进行比较,方便理解; 与方法三相比,不需要考虑复杂的函数出口条件

  public int findKthLargest(int[] nums, int k) {
        int n=nums.length;
        int ans=dfs2(nums,0, n-1, n-k);

        return ans;
    }
	// 递归版
    int dfs2(int[]nums,int l, int r, int k){
        int pivot=nums[l];
        int p=l,q=r;
        for(int i=l+1;i<=q;){
            if(nums[i]>pivot){
                swap(nums,i,q--);
            }else if(nums[i]<pivot){
                swap(nums,i++,p++);
            }else{
                i++;
            }
        }
        if(p-1>=k){
            return dfs2(nums,l,p-1,k);
        }else if(q>=k){
            return nums[q];
        }else{
            return dfs2(nums,q+1,r,k);
        }
    }

    void swap(int[]nums, int i, int j){
        int t=nums[i];
        nums[i]=nums[j];
        nums[j]=t;
    }
    // 迭代版本
    int getK(int[]nums,int k){
        int p=0,q=nums.length-1;
        while(true){
            int l=p,r=q;
            int base=nums[l];
            for(int i=l+1;i<=r;){
                if(nums[i]<base){
                    swap(nums,i++,l++);
                }else if(nums[i]>base){
                    swap(nums,i,r--);
                }else{
                    i++;
                }
            }
            if(l-1>=k){
                q=l-1;
            }else if(r>=k){
                q=r;
                break; 
            } else{
                p=r+1;
            }
        }
        return nums[q];
    }

2.1.3 方法三:

	public int findKthLargest(int[] nums, int k) {
        int n=nums.length;
		// n-k表示(第k大或者n-k+1小)的所在元素的下标
        int ans=dfs3(nums,0, n-1, n-k);

        return ans;
    }

	int dfs3(int[]nums,int l, int r, int k){
        if(l>=r)return nums[k];
        Random rand=new Random();
        int pivot=rand.nextInt(r-l+1)+l;
        swap(nums,pivot,r);

        int i=partition(nums,l,r);
        if(k<=i)return dfs3(nums,l,i-1,k);
        return dfs3(nums,i,r,k);
    }
    //只划分小于等于和大于目标元素的两个区间
    public int partition(int[] nums, int l, int r) {
        int pivot = nums[r];
        int i = l - 1;
        for (int j = l; j <= r - 1; ++j) {
            if (nums[j] <= pivot) {
                i = i + 1;
                swap(nums, i, j);
            }
        }
        swap(nums, i + 1, r);
        return i + 1;
    }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值