非稳定性排序——快速排序

稳定性排序概念

稳定性排序和非稳定性排序:通常把参加排序的项称为排序码或者排序项,排序序列可能含有多个相同的排序码,如果排序前后它们的相对位置保持不变化,则排序方法就是稳定的,否则就是非稳定的。

例如a和b相同,排序前a在b前面,排序后a依旧在b前面,这种排序就是稳定性排序。

快速排序的核心思想

在当前待排序的序列中,选择一个元素作为分界元素【pivot】,把小于等于该分界元素的所有元素都移动到分界元素的左边,把大于等于该分界元素的所有元素都移动到分界元素的右边,这样分界元素正好处于排序的最终位置上,并且得到了两个子序列,前一个子序列的元素都小于等于比分界元素,后一个子序列的元素都大于等于分界元素。然后分别对这两个子序列递归的进行上述过程,直到所有的元素都到达排序后它们应处的最终位置上。

基于该算法的核心思想可以看到,该排序是非稳定性排序

时间复杂度分析

最优情况:每一次分界元素最终正好定位于序列的中间,从而把当前待排序的序列分成了两个长度相等的子序列,则对长度为n的序列进行快速排序所需要的时间复杂度为O( n l o g 2 n n{log_2{n}} nlog2n)

T(n) = n + 2T(n/2) = 2n + 4T(n/4) = 4n + 8T(n/8) = … = ( l o g 2 n ) n ({log_2{n}})n (log2n)n + nT(1)

最坏情况:针对于有序数组,每一次分界元素都落在数组的一端,第一趟排序需要n-1次比较次数,第二趟排序需要n-2次比较次数;以此类推,总的比较次数为(n-1)+(n-2)+…+1=n(n-1)/2 = O( n 2 n^2 n2)

算法实现分析

重点是如何确定分解元素的最终位置,选择最左端元素为分界元素(也称为枢轴、支点或基准元素),定义两个变量i和j,j从右往左寻找小于分界元素的元素,i从左往右寻找比分界元素大的元素,找到后交换i与j对应元素的位置,先移动i再移动j,当i与j相遇时就是分界元素排序后的最终位置。

思考1:i和j谁先移动?会不会有区别?

待排序序列[3, 5, 3, 5, 1],枢纽元素选择分片最左端元素,对以下四种情况进行分析:

  1. i小于等于枢纽元素时前进,j大于等于枢纽元素时前进,先移动i,再移动j
  2. i小于等于枢纽元素时前进,j大于等于枢纽元素时前进,先移动j,再移动i
  3. i小于枢纽元素时前进,j大于枢纽元素时前进,先移动i,再移动j
  4. i小于枢纽元素时前进,j大于枢纽元素时前进,先移动j,再移动i
一:i小于等于枢纽元素时前进,j大于等于枢纽元素时前进,先移动i,再移动j
[3, 5, 3, 5, 1] 
第一次循环后 i=1, j=4; 交换后[3, 1, 3, 5, 5]
第二次循环后 i=3, j=3时退出循环, i和j都指向了5,
则分界元素最终位置在索引3处[5, 1, 3, 3, 5]
显然存在5比枢纽元素3大,逻辑不对
二:i小于等于枢纽元素时前进,j大于等于枢纽元素时前进,先移动j,再移动i
[3, 5, 3, 5, 1] 
第一次循环后 i=1, j=4; 交换后[3, 1, 3, 5, 5]
第二次循环后 i=1, j=1;退出循环, [1, 3, 3, 5, 5]逻辑正确
三:i小于枢纽元素时前进,j大于枢纽元素时前进,先移动i,再移动j
[3, 5, 3, 5, 1]
第一次循环后 i=1, j=4; 交换后[3, 1, 3, 5, 5]
第二次循环后 i=3, j=3时退出循环, i和j都指向了5,
则分界元素最终位置在索引3处[5, 1, 3, 3, 5]
同样逻辑不对
四:i小于枢纽元素时前进,j大于枢纽元素时前进,先移动j,再移动i
[3, 5, 3, 5, 1]
第一次循环后 i=1, j=4; 交换后[3, 1, 3, 5, 5]
第二次循环后 i=1, j=1;退出循环, [1, 3, 3, 5, 5]逻辑正确

结论:在分界元素为最左端元素时,先移动i后移动j,最终i与j指向的元素可能比分界元素大,所以需要先移动j后移动i。

思考2:i<=分界元素,j>=分界元素再前进时,快速排序是不是变成了非稳定排序?

不会

依旧选择最左端元素为分界元素,先移动j再移动i。
[3, 1, 3, 1, 5] 第一次循环,i=j=3,之后就退出循环,交换分界元素的位置发现,原先两个3的相对位置发生了改变

代码

模板代码

public class QuickSort {

   public static void quickSort(int[] a) {
      quickSort(a, 0, a.length - 1);
   }

   /**
    * 对数组i-j区间进行快排
    * @param a 数组
    * @param l 起始索引
    * @param h 结束索引
    */
   private static void quickSort(int[] a, int l, int h) {
      if (l >= h) {
         return ;
      }
      int p = partition(a, l, h);
      quickSort(a, l, p - 1);
      quickSort(a, p + 1, h);
   }
   
   private static void swap(int[] data, int i, int j) {
      int temp = data[i];
      data[i] = data[j];
      data[j] = temp;
   }
}

测试代码

import java.util.Arrays;
import java.util.Random;

public class SortApp {
    public static void main(String[] args) {
        int[] a = buildArray();
        System.out.println(Arrays.toString(a));
        QuickSort.quickSort(a);
        System.out.println(Arrays.toString(a));
        isSorted(a);
    }

    public static int[] buildArray(){
        int num = 10000;
        int[] a = new int[num];
        Random random = new Random();
        for (int i = 0; i < num; i++) {
            a[i] = random.nextInt(10000);
        }
        
        return a;
    }

    /**
     * 利用冒泡排序的思想 验证数组是否有序
     * @param data
     */
    public static void isSorted(int[] data) {
        // 两两比较如果没有发生交换那说明有序了
        for (int i = 0; i < data.length - 1; i++) {
            if (data[i] > data[i + 1]) {
                System.out.printf("无序, 索引{%s}, 值{%s}%n", i, data[i]);
                return;
            }
        }
        System.out.println("有序");
    }
}

partition版本V1

private static int partition(int[] a, int l, int h) {
   // 基准点为最左端元素pv
   int pivot = a[l];
   // 这里的i不能等于l+1, 只有序列只有两个元素的时候不会进入循环
   // 直接swap(a, l, i), 导致两个元素的序列一定会交换位置, 显然逻辑不对
   // int i = l + 1;
   int i = l;
   int j = h;

   while (i < j) {
      
      while (i < j && a[j] > pivot){
         j--;
      }

      // 这里要 <= 否则 < 的时候 一开始索引i对应的值就是pivot, 直接与j交换了位置, 这样逻辑就错了.
      while (i < j && a[i] <= pivot){
         i++;
      }

      // 退出上面的两个循环: ①i==j; ②i>j, 代表j找到比pivot小的值, i找到比pivot大的值
      if (i < j) {
         swap(a, i, j);
         // System.out.println("l:"+l +" h:"+h+" pivotIndex:" + l + ", pivot:" + pivot + ", i:" + i + ", j:"+j + "\t" + Arrays.toString(a));
      }
   }
   swap(a, l, j);
   // System.out.println("l:"+l +" h:"+h+" pivotIndex:" + l + ", pivot:" + pivot + ", i:" + i + ", j:"+j + "\t" + Arrays.toString(a));
   return j;
}

注意:

①如果i与j对应的元素与分界元素相同时不移动数据,那么当待排序序列元素相同时会产生死循环,因为i和j的值都不会再变化。

②如果i定义为l+1, 当待排序序列只有两个元素时一定会交换位置

partition版本V2

使用do…while,先执行j–和i++操作, 这样可以实现元素大于分界元素再移动j,元素小于分界元素时再移动i,不会出现死循环

private static int partition(int[] a, int l, int h) {
   // 基准点为最左端元素pv
   int pivot = a[l];
   // 这里的i不能等于l+1, 只有序列只有两个元素的时候不会进入循环
   // 直接swap(a, l, i), 导致两个元素的序列一定会交换位置, 显然逻辑不对
   // int i = l + 1;
   int i = l;
   int j = h + 1;

   while (i < j) {
      do{
         j--;
      } while (i < j && a[j] > pivot);

      // 先i++, 因为第一个就是基准元素, 不需要和自己比较
      do{
         i++;
      }while (i < j && a[i] < pivot);

      // 退出上面的两个循环: ①i==j; ②j+1=i, 代表j找到比pivot小的值, i找到比pivot大的值
      if (i < j) {
         swap(a, i, j);
         // System.out.println("l:"+l +" h:"+h+" pivotIndex:" + l + ", pivot:" + pivot + ", i:" + i + ", j:"+j + "\t" + Arrays.toString(a));
      }
   }
   // 这里必须与j交换, i最终指向的是一个比分界元素大的数, j正好指向一个比分界元素小的数,
   swap(a, l, j);
   // System.out.println("l:"+l +" h:"+h+" pivotIndex:" + l + ", pivot:" + pivot + ", i:" + i + ", j:"+j + "\t" + Arrays.toString(a));
   return j;
}

partition版本V3【另一种思路】

以最右端元素为分界元素,记录数组区间内比分界元素小的元素个数counter,最终counter+数组起始索引就是该分界元素排序后的最终位置

private static int partition(int[] a, int l, int h) {
   // 选择最右端元素为分界元素
   int pivot = a[h];
   // 记录l-h区间内比分界元素小的元素个数, 最终+l就是分界元素排序后最终的位置
   int counter = l;

   for (int i = l; i < h; i++) {
      if (a[i] < pivot) {
         // 把比分区元素小的元素移动到前面
         swap(a, i, counter);
         counter++;
      }
   }

   // 将分区元素放置排序后的最终位置
   swap(a, h, counter);
   return counter;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值