你一定要看的快速排序 两路快排and三路快排

快速排序

  1. 时间复杂度O(nlogn) 对于基本有序的数据是不友好的

  2. 空间复杂度依照具体算法而定

  3. 不稳定

  4. 思路

    • 还是用到了递归的思路,在一趟中选择第一个(其实可以随机选择一个)作为基准pivot,然后对数据进行切分将通过切分,我们要达到这样一个效果:把「切分元素」放在排序以后最终应该呆的位置。【小于等于基准值的元素】基准值【大于等于基准值的元素】,然后对左边和右边再进行一次快速排序,递归地进行下去

    • 看到这里你或许还不明白,玛徳这有什么用,接下来我来给大家好好说道说道,好好理解理解快速排序

    • 首先你会发现快速排序和归并排序他们的思想都是分而治之(divide and conquer),好,那么什么是分而治之呢?

      基本思路是将一个大问题拆解成某几个小问题,如果可以直接解决那么就解决,如果不能则继续拆解,直到可以直接解决; 举一个栗子:斐波那契数列

       private static int fib(int n){
              if(n<=1){
                  return 1;
              }
              return fib(n-1)+fib(n-2);
          }
      

      要你直接计算第n项你不好计算,那么退而求其次,计算第n-1项和第n-2项递归地进行下去,直到n<=1,那么这样我们可以直接给出答案.

    • 那么请你思考,归并排序和快速排序是什么样的思路呢,他们其实很相近都是划分大区间到小区间,直至区间的长度小于某个值,那么直接进行插入排序就好(因为插入排序在小区间上是性能优异的).

    • 那么不同点是什么呢,归并排序无脑地将区间进行一划为二,但是因为这样不断划分,在最终完成插入排序后,不能保证元素在最终的位置上,所以需要再进行合并这一操作来保证元素呆在最终的位置上(再最坏的情况下,可能最后一次合并,才能保证元素呆在最终排好的位置上); 但是在快速排序的情况下结果就不是这样了,快速排序每次划分时都在区间内随机找一个数,然后对区间进行调整:【小于等于基准值的元素】基准值【大于等于基准值的元素】,这样在不断划分区间,直到区间的长度小于某个值进行插入排序,在这次的插入排序后就可以保证,这个区间元素的位置就在最终排好序的位置上,所以无需再进行合并这一操作.

       private static final int INSERTION_SORT_THRESHOLD = 2;
          private static final Random RANDOM = new Random();
      
          private static void quickSort(int[] nums, int left, int right) {
              if (right - left <= INSERTION_SORT_THRESHOLD) {
                  insertSort(nums, left, right);
                  return;
              }
              int pivotIndex = partition(nums, left, right);
              quickSort(nums, left, pivotIndex - 1);
              quickSort(nums, pivotIndex + 1, right);
          }
      
          private static int partition(int[] nums, int left, int right) {
              //随机摇出一个索引
              int randomIndex = RANDOM.nextInt(right - left + 1) + left;
              //和第一个索引交换
              swap(nums, left, randomIndex);
              int j = left;
              for (int i = left; i <= right; i++) {
                  //把小于pivot换到前面去,大于的换到后面去
                  if (nums[i] < nums[left]) {
                      j++;
                      swap(nums, j, i);
                  }
              }
              //j的位置即为pivot的最终位置,交换nums[j]和nums[left],这样我们随机选择的元素就被放到了它应该在的位置上,对其
              //左右的区间再进行快速排序
              swap(nums, left, j);
              return j;
          }
      
          private static void swap(int[] nums, int index1, int index2) {
              int temp = nums[index1];
              nums[index1] = nums[index2];
              nums[index2] = temp;
      
          }
      
          private static void insertSort(int[] nums, int left, int right) {
              for (int i = left; i <= right; i++) {
                  int temp = nums[i];
                  int j = i;
                  while (j > 0 && nums[j - 1] > temp) {
                      nums[j] = nums[j - 1];
                      j--;
                  }
                  nums[j] = temp;
      
              }
          }
      
      

双路/三路快排

  1. 双路快排为什么出现

    • 这其实是对快速排序基础版的优化,在上面的快速排序中存在一个问题,就是当数组的重复元素非常多,刚巧这个元素被随机选中会造成左右及其不平衡,算法的性能下降

    • 举个栗子 {6,1,8,3,9,4,5,6,6,6,6,6,6,6,7} 如果选择6来作一次划分结果是这样的

      [5, 1, 3, 4,] 6,[8, 9, 6, 6, 6, 6, 6, 6, 6, 7],会发现前面的部分过于短,而后面的部分过于长,造成递归树不平衡

  2. 三路快排为什么出现

    • 其实这又是一层优化,请你思考一个问题如果划分成这样[5, 1, 3, 4,],[6, 6, 6, 6, 6, 6, 6,6],[8,9,7]

      那么6这么多个元素是不是已经排定了不需要进行排序了

  3. 解决方法

    很简单修改partition的逻辑即可

    双路快排partition

     private static int partition(int[] nums, int left, int right) {
            int index = RANDOM.nextInt(right - left + 1) + left;
            swap(nums,left,index);
    
            int pivot = nums[left];
            //小区间指针从left+1开始
            int lt=left+1;
            //大区间指针
            int gt=right;
            //注意这里操作的区间是[left+1,right]
            while (true) {
                //这里已经进行了一次先加操作,指针会指向小区间第一个>=pivot的元素
                while (lt <= right && nums[lt] < pivot) {
                    lt++;
                }
                //指针会指向大区间的第一个<=pivot的元素
                while (gt > left && nums[gt] > pivot) {
                    gt--;
                }	
    
                if (lt >= gt) {
                    break;
                }
                swap(nums, lt, gt);
                lt++;
                gt--;
    
            }
            swap(nums, left, gt);
            return gt;
    
        }
    

    三路快排

    思路通过交换将小于pivot的放在左边,大于pivot的放在右边,等于pivot的放在中间,最终lt指向最大的小于pivot的元素,gt指向最小的大于pivot的元素

     int index = RANDOM.nextInt(right - left + 1) + left;
            swap(nums, index, left);
            int pivot = nums[left];
    
            int lt = left;
            int gt = right + 1;
            //注意细节,此时i在left前一位,gt同理
            int i = left + 1;
            while (i < gt) {
                if (nums[i] < pivot) {
                    //照应前文,先相加在交换
                    lt++;
                    swap(nums, lt, i);
                    i++;
                } else if (nums[i] == pivot) {
                    i++;
                } else {
                    gt--;
                    swap(nums, gt, i);
                }
            }
            swap(nums, left, lt);
            //去掉中间重复的元素,大大减少了排序的区间
            //由于最后把left与lt进行了交换所以lt此时指的是pivot,我们只要小于pivot的部分,所以减一
            quickSort(nums, left, lt - 1);
            quickSort(nums, gt, right);
    
  4. 最后来个总结

    • 归并排序和快速排序都是递归的排序算法,因为归并排序每次只是简单地一份为二,最后进行插入排序的时候,不能保证将元素放到最后拍好的位置,所以需要合并这一操作(可能最后一次合并后元素才会被排定),但是快速排序由于partition的关系,使得插入排序之后,元素就已经排定了,不需要合并的操作.

      两路快排是因为在某次刚好选择了一个大量在数组中重复的元素,会使得右边的区间太长,递归树太高,性能下降,所以将重复的元素分到左右两边来平衡.

      三路快排是因为与pivot相等的元素,其实位置已经可以确定了,不需要参与到下一次中,把两端摘出来就行

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值