(Java) 快速排序

最近在学习数据结构与算法,但是对于学习过的知识总是学了又忘,忘了又学,没有深入的去理解。现在我将我学过的在博客上总结分享出来,一方面加深自己的理解,一方面供大家参考交流学习。

排序算法之快速排序

基本思想
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
排序流程
a. 先从数组中取出一个基准数
b. 分区,将比基准数大的放在右边,小的放在左边
c. 在对左右部分进行重复第二步,直到只剩一个数
在这里插入图片描述
代码实现

public static void quickSort(int[] arr, int p, int r) {
        if (p < r) {
            //int q = partition1(arr, p, r);
            int q = partition2(arr, p, r);
            quickSort(arr, p, q - 1);
            quickSort(arr, q + 1, r);
        }
    }

    /**
     * 方法一:单向扫描
     * @param arr
     * @param p
     * @param r
     * @return
     */
    private static int partition1(int[] arr, int p, int r) {
        int pivot = arr[p];//以数组最左侧的值为目标值
        int sp = p + 1; //扫描指针
        int bigger = r; //右侧指针
        while (sp <= bigger) {
            if (arr[sp] <= pivot) { //扫描元素小于主元,左指针右移
                sp++;
            } else { //扫描元素大于主元,二指针元素交换,右指针左移
                swap(arr, sp, bigger);
                bigger--;
            }
        }
        swap(arr, p, bigger);
        return bigger;
    }

    /**
     * 方法二:双向扫描
     * @param arr
     * @param p
     * @param r
     * @return
     */
    public static int partition2(int[] arr,int p,int r){
        int pivot = arr[p];以数组最左侧的值为目标值
        int left = p + 1; //扫描指针
        int right = r; //右侧指针
        while (left <= right){
            //left不停的往右走,知道遇到大于主元的元素
            while (left <= right&& arr[left] <= pivot) left++;//循环退出时,left一定指向第一个大于主元的元素
            while (left <= right&& pivot < arr[right]) right--;//循环退出时,right一定指向第一个小于主元的元素
            if(left<=right){
                swap(arr,left,right);
            }
        }
        //while退出时,两者交错,right一定指向第一个小于主元的元素
        swap(arr, p, right);
        return right;
    }

优化快速排序,上述两种方法,每次只搞定一个数,而我们进行优化,优化后,可以将所有等于这个数的都搞定,时间复杂度虽然差不多,但是常数项肯定会变小。

	public static void quickSort(int[] arr,int p,int r){
        if(p < r){
            int[] q = partition3(arr,p,r);
            quickSort(arr,l,q[0] - 1);
            quickSort(arr,q[1] + 1,r);
        }
    }
	/**
     * 方法三:优化快速排序
     * 将所有等于目标值的独立出来,大于目标值的和小于目标值的继续递归
     * @param arr
     * @param p
     * @param r
     * @return
     */
    private static int[] partition3(int[] arr, int p, int r) {
       int pivot = arr[r]; //以数组最右侧的值为目标值
       int less = p - 1; //小于区域的指针
       int more = r; //大于区域的指针
       int cur = p; 
       while (cur < more){
           if(arr[cur] < pivot){ //当前值小于目标值
               less ++; //小于区域指针右移
               swap(arr,less,cur);
               cur ++; 
           }else if (arr[cur] > pivot){ //当前值大于目标值
               more --; //大于区域指针右移
               swap(arr,more,cur);
           }else {
               cur ++;
           }
       }
       swap(arr,more,r); //more此时为第一个大于目标值的坐标
       // 返回存储等于目标值区域的左边界和右边界的数组
       return new int[]{less + 1,more};
    } 	

性能分析:
快速排序的一次划分算法从两头交替搜索,直到low和high重合,因此其时间复杂度是O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为O(nlog2n)。
最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n^2)。

最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)


算法优化
1、随机快速排序
为了避免最坏情况的发生,我们可以设置目标值为数组中的随机元素。

public static void quickSort(int[] arr,int p,int r){
        if(p < r){
        	//设置目标值为数组中的随机元素,改善最坏情况下的时间性能
            swap(arr,l + (int)(Math.random() * (r - p + 1)),r); 
            int[] p = partition(arr,p,r);
            quickSort(arr,l,p[0] - 1);
            quickSort(arr,p[1] + 1,r);
        }
    }

2、三点中值法
将数组中最左边,中间,最右边三个值对比,中值作为目标值

	public static void quickSort(int[] arr, int p, int r) {
        if (p < r) {
            int q = partition4(arr, p, r);
            quickSort(arr, p, q - 1);
            quickSort(arr, q + 1, r);
        }
    }
	/**
     * 三点中值法
     * @param arr
     * @param p
     * @param r
     * @return
     */
    public static int partition4(int[] arr,int p,int r){
        //优化,在P,r,mid之间,选一个中间值作为主元
        int midIndex = p + ((r - p) >> 1); //中间下标
        int midValueIndex = -1 ; //中值下标
        if(arr[p] <= arr[midIndex]&&arr[p] >= arr[r]){
            midValueIndex = p;
        } else if(arr[r] <= arr[midIndex]&&arr[r] >= arr[p]){
            midValueIndex = r;
        }else {
            midValueIndex = midIndex;
        }
        swap(arr,p,midValueIndex);
        
        int pivot = arr[p];
        int left = p + 1; //扫描指针
        int right = r; //右侧指针
        while (left <= right){
            //left不停的往右走,知道遇到大于主元的元素
            while (left <= right&& arr[left] <= pivot) left++;//循环退出时,left一定指向第一个大于主元的元素
            while (left <= right&& pivot < arr[right]) right--;//循环退出时,right一定指向第一个小于主元的元素
            if(left<=right){
                swap(arr,left,right);
            }
        }
        //while退出时,两者交错,right一定指向第一个小于主元的元素
        swap(arr, p, right);
        return right;
    }

但是三点中值法还是不能够保证没有最坏情况的发生,如果要完全避免最坏情况,则需要使用绝对中值法。
3、绝对中值法
通过将待排序数组以5个元素分为一组,取中间值,取到整个数组的各组中间值,再将这些数排序,再取中间值作为主元。因为寻找绝对中值,也会花费时间,所以使用三点中值法居多。

	/**
   * 获取绝对的中值数,O(N)的样子
   */
  public static int getMedian(int[] arr, int p, int r) {
    if (arr.length == 1)
      return arr[p];
    int size = r - p + 1;// 数组长度
    //每五个元素一组
    int groupSize = (size % 5 == 0) ? (size / 5) : (size / 5 + 1);
    //存储各小组的中值
    int medians[] = new int[groupSize];
    int indexOfMedians = 0;
    //对每一组进行插入排序
    for (int j = 0; j < groupSize; j++) {
      //单独处理最后一组,因为最后一组可能不满5个元素
      if (j == groupSize - 1) {
        InsertionSort.sort(arr, p + j * 5, r); // 排序最后一组
        medians[indexOfMedians++] = arr[(p + j * 5 + r) / 2]; // 最后一组的中间那个
      } else {
        InsertionSort.sort(arr, p + j * 5, p + j * 5 + 4);  // 排序非最后一组的某个组
        medians[indexOfMedians++] = arr[p + j * 5 + 2];  // 当前组(排序后)的中间那个
      }
    }

    return getMedian(medians, 0, medians.length - 1);
  }

4、待排序列表较短时,用插入排序
当排序列表小于8个时,通过计算发现插入排序比快速排序的性能要好。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值