java算法-排序-快排

快排

快排思想:

  • 从序列中挑选出一个元素(一般是第一个或者是最后一个)作为"基准"元素
  • 把序列分成2个部分,其数值大于"基准"元素的元素放在"基准"元素的右边,否在放在"基准"元
    素的左边,此时"基准"元素所在的位置就是正确的排序位置,这个过程被称为 partition(分区)
  • 递归将"基准"元素左边的序列和"基准"元素右边的序列进行partition操作

其中partiton即划分,方法有交换法和挖坑填数法

法一:分治+挖坑填数
在这里插入图片描述
代码: 包括合并形式和分开形式

   public int[] sort(int[] arr){
       quickSort(arr,0,arr.length-1);
       return arr;
   }
   public void quickSort(int[] arr,int start,int end){
       if (start<end){
           int i = start;
           int j = end;
           int temp = arr[start];
           //partition划分-挖坑填数
           while (i<j){
               while (i<j && arr[j]>=temp) j--;
               if (i<j) arr[i] = arr[j];
               while (i<j && arr[i]<temp) i++;
               if (i<j) arr[j] = arr[i];
           }
           arr[i] = temp;
           //分治
           quickSort(arr,start,i-1);
           quickSort(arr,i+1,end);

       }
   }
   //分开形式
   public void quick_sort(int[] arr,int start,int end){
        if (start<end){
            //划分
            int mid = partition(arr,start,end);
            //分治
            quick_sort(arr,start,mid-1);
            quick_sort(arr,mid+1,end);
        }
    }
    public int partition(int[] arr,int start,int end){
        int temp = arr[start];
        int i = start;
        int j = end;
        while(i<j){
            //找到右侧小于基准的值
            while (i<j && arr[j]>=temp){
                j--;
            }
            if (i<j){
                //小的放到左边(填坑-产生新坑)
                arr[i] = arr[j];
                i++;
            }
            //找到左侧侧大于基准的值
            while (i<j && arr[i]<temp){
                i++;
            }
            if (i<j){
                //大的放到右边(填坑-产生新坑)
                arr[j] = arr[i];
                j--;
            }
        }
        //最终i、j重合留下最后一个坑,将temp填入
        arr[j] = temp;
        //将基准值索引返回
        return j;
    }

注意到划分代码,两种形式的if (i<j)里有的进行了i++、j–,另一个没有,其实是一样的,不加自增自减,下一次会多一次判断而已,不影响结果

法二: 分治+交换
思路:
在这里插入图片描述

代码:

   public int[] sort(int[] arr){
       quickSort(arr,0,arr.length-1);
       return arr;
   }
   public void quickSort_swap(int[] arr,int start,int end){
       if (start<end){
           int i = start;
           int j = end;
           int temp = arr[start];
           while (i<j){
               while (i<j && arr[j]>=temp) j--;
               while (i<j && arr[i]<=temp) i++;
               swap(arr,i,j);
           }
           //先从右边往左寻找,可以保证最后i、j相遇的值一定是小于等于基准值,
           // 这样最后交换才不会出现错误,如果从左边先开始,最后会把大的换到左侧
           //所以要从基准值对面先开始
           swap(arr,start,i);
           //分治
           quickSort_swap(arr,start,i-1);
           quickSort_swap(arr,i+1,end);
       }
   }
   public void swap(int[] arr ,int i ,int j){
       int temp = arr[i];
       arr[i] = arr[j];
       arr[j] = temp;
   }

参考:面试必备:八种排序算法原理及Java实现
参考:排序可视化

随机数快排

思路:
随机从start-end间选择一个与start交换,避免出现已排序数组利用快排时间复杂度成最坏情况O(n^2)

    public int[] sort(int[] arr){
        quickSort(arr,0,arr.length-1);
        return arr;
    }
    public void quickSort(int[] arr,int start,int end){
        if (start<end){
            int i = start;
            int j = end;
            //随机数
            int random_index = start + (int)(Math.random()*(end-start+1));
            //random_index与start交换
            swap(arr,random_index,start);
            int temp = arr[start];
            //partition划分-挖坑填数
            while (i<j){
                while (i<j && arr[j]>=temp) j--;
                if (i<j) arr[i] = arr[j];
                while (i<j && arr[i]<temp) i++;
                if (i<j) arr[j] = arr[i];
            }
            arr[i] = temp;
            //分治
            quickSort(arr,start,i-1);
            quickSort(arr,i+1,end);
        }
    }
    public void swap(int[] arr ,int i ,int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

三分区快排-解决包含大量重复值情况

前面的路子都是以基准值为准分为小于子序列和大于子序列,考虑一种特殊的数据集,数据集中有大量重复元素,这种情况下使用两分区递归会对大量重复元素进行处理。

一个优化的方向就是使用三分区模式:小于区间、等于区间、大于区间,这样在后续的处理中则只需要处理小于区和大于区,降低了等于基准值区间元素的重复处理,加速排序过程。
三分区原理
如图为三分区模式中某个时刻的快照,其中展示了几个关键点和区间,包括基准值、小于区、等于区、处理值、待处理区、大于区。

在这里插入图片描述
在实际过程中根据处理值与基准值的大小关系,进行相应分区合并和交换,再进行下标移动就可以了,实际中分三种情况,这也是写代码的依据:

  1. 处理值e==p,将e合并到等于区,i++;
  2. 处理值e<p,将e与(lt+1)位置的数据交换,扩展小于区lt++,等于区长度不变,相当于整体平移;
  3. 处理值e>p,将e与(gt-1)位置的数据交换,扩展大于区gt–,此时i不变,交换后的值是之前待处理区的尾部数据;
  4. 分区最终调整
    处理完待处理区的全部数据之后的调整也非常重要,主要是将基准值P与lt位置的数据交换,从而实现最终的三分区,如图所示:
    在这里插入图片描述
    在这里插入图片描述
    从最终的分区可以看到,我们下一次的循环可以不处理等于区的数据而只处理两端分区数据,这样在大量重复场景下优化效果会非常明显。

代码:

    public int[] sort(int[] arr){
        quick_sort(arr,0,arr.length-1);
        return arr;
    }
    public void quick_sort(int[] arr,int start,int end){
        if (start<end){
            //划分
            //随机基准值
            int randomIndex = new Random().nextInt(end-start+1)+start;
            swap(arr,start,randomIndex);
            int pivot = arr[start];
            int lt = start;
            int gt = end + 1;
            int i = start+1;
            while(i<gt){
                if (arr[i]<pivot){
                    lt++;
                    //交换的是处理过的等于pivot值或i自身,i++
                    swap(arr,i,lt);
                    i++;
                }else if (arr[i]>pivot){
                    gt--;
                    //交换的是未处理区的数据,不用i++
                    swap(arr,i,gt);
                }else {
                    i++;
                }
            }
            swap(arr,start,lt);
            //分治
            quick_sort(arr,start,lt-1);
            quick_sort(arr,gt,end);
        }
    }

    public void swap(int[] arr ,int i ,int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

参考:
深入理解快速排序和STL的sort算法
循环不变式,和快速排序算法的多种实现
力扣

复杂度分析

时间复杂度:基于随机选取主元的快速排序时间复杂度为期望 O(nlogn),其中 n 为数组的长度。详细证明过程可以见《算法导论》第七章,这里不再大篇幅赘述。

空间复杂度:O(h),其中 h 为快速排序递归调用的层数。我们需要额外的 O(h)的递归调用的栈空间,由于划分的结果不同导致了快速排序递归调用的层数也会不同,最坏情况下需 O(n) 的空间,最优情况下每次都平衡,此时整个递归树高度为 logn,空间复杂度为 O(logn)。

快排稳定化、快排非递归实现待续。。。

参考:
邵顺增. 稳定快速排序算法研究[J]. 计算机应用与软件, 2014, 31(7):263-266.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值