快速排序及快速选择算法详解

  1. 快速排序

    1. 快速排序

      1. 快速排序就是一个二叉树的前序遍历

      2. 快速排序是先将一个元素(p)排好序,然后再将剩下的元素排好序

      3. 排序完成其实形成的 是一棵二叉搜索树,如此可以理解为,快速排序的过程是一个构造 二叉搜索树的过程

        image-20220606145533873

        即如下二叉搜索树

      4. 二叉搜索树的构造时,二叉搜索树不平衡的极端情况下二叉搜索树会退化成一个链表,导致操作效率大幅降低,快速排序中也有类似的情况,如图所示

        image-20220606144909822

        因此要引入随机性:可采用进行排序前对整个数组执行 洗牌算法 进行打乱,或者在 partition函数中随机选择数组元素作为切分点,我倾向于使用洗牌算法

    2. 快速排序的代码框架

      void sort(int[] nums, int lo, int hi) {
          if (lo >= hi) {
              return;
          }
          // 对 nums[lo..hi] 进行切分
          // 使得 nums[lo..p-1] <= nums[p] < nums[p+1..hi]
          int p = partition(nums, lo, hi);
          // 去左右子数组进行切分
          sort(nums, lo, p - 1);
          sort(nums, p + 1, hi);
      }
      
    3. 快速排序完整代码

      public static void quickSort(int[] nums) {
          shuffle(nums);
          quickSort(nums, 0, nums.length - 1);
      }
      
      private static void quickSort(int[] nums, int low, int high) {
      		//一个元素或者没有元素不再需要排序
          if (high <= low) return;
          int j = partition(nums, low, high);
          quickSort(nums, low,j - 1);
          quickSort(nums,j + 1,high);
      }
      
      private static int partition(int[] nums, int low, int high) {
          int i = low, j = high + 1;
          int value = nums[low];
      
          while (true) {
              //此while结束时恰好nums[i] >= value,即找到一个元素大于value
              while (nums[++i] - value < 0) {
                  if (i == high) {
                      break;
                  }
              }
              //此while结束时恰好nums[j] <= value,即找到一个元素小于value
              while (nums[--j] - value > 0) {
                  if (j == low) {
                      break;
                  }
              }
              if (i >= j) {
                  break;
              }
              //将大于value和小于value的两个元素原地交换
              swap(nums, i, j);
          }
          //j指针指向的就是切分点
          swap(nums, low, j);
          return j;
      }
      //洗牌算法,将输入的数组随机打乱
      private static void shuffle(int[] nums) {
          Random random = new Random();
          int n = nums.length;
          for (int i = 0; i < n; i++) {
              //生成[i, n - 1]的随机数, 即索引
              int r = i + random.nextInt(n - i);
              swap(nums, i, r);
          }
      }
      //原地交换数组中的两个元素
      private static void swap(int[] nums, int i, int j) {
          int temp = nums[i];
          nums[i] = nums[j];
          nums[j] = temp;
      }
      
    4. 快速排序的时间复杂度

      假设数组元素个数为 N,那么二叉树每一层的元素个数之和就是 O(N);分界点分布均匀的理想情况下,树的层数为 O(logN),所以理想的总时间复杂度为 O(NlogN)

    5. 与归并排序的比较

      快速排序是 不稳定排序,归并排序是 稳定排序

      在实际工程中我们经常会将一个复杂对象的某一个字段作为排序的 key,所以应该关注编程语言提供的 API 底层使用的到底是什么排序算法,是稳定的还是不稳定的,这很可能影响到代码执行的效率甚至正确

    6. 快速选择算法(Quick Select)

      是快速排序的变体,效率更高O(N),看LeetCode215

  2. LeetCode215 数组中的第K个最大元素

    1. 思路分析

      1. 可以使用优先级队列:Java的 PriorityQueue 默认实现是小顶堆,将pq看成一个筛子,较大的元素会沉淀下去,较小的元素会浮上来;当堆大小超过k个元素时,我们就删掉堆顶元素,因为这个元素小;当nums中的所有元素都过了一遍之后,堆中留下了最大的k个元素,堆顶元素就是第k大的元素

        二叉堆插入和删除的时间复杂度和堆中的元素个数有关,在这里我们堆的大小不会超过 k,所以插入和删除元素的复杂度是 O(logk),再套一层 for 循环,假设数组元素总数为 N,总的时间复杂度就是 O(Nlogk)

      2. 可以使用快速选择算法:非常像二分搜索框架

        1. 快速选择算法是快速排序的变体,效率更高

        2. 题目问第k大的元素,相当于数组升序排序后找索引为n - k的元素,即k = n - k

        3. 在partition函数中会将nums[p]放到正确的位置,使得nums[lo…p-1] < nums[p],nums[p+1…hi] > nums[p], 这个时候虽然没有把整个数组排好序,但是nums[p]左边的元素都比nums[p]小了,因此我们知道了nums[p]的排名,因此我们找索引为k的元素,等价于比价k和p的大小

          1. 如果 k == p,恭喜你,找到了
          2. 如果 k < p,则表示k在nums[lo…p-1]中,则使 hi = p - 1
          3. 如果 k > p,则表示k在nums[p+1…hi]中,则使lo = p + 1
        4. 为什么循环条件时 lo <= hi, 即lo > hi时退出,而不再是快速排序中的lo >= hi

          在快速排序中,lo == hi, 我们默认只有一个元素的时候他是有序的,不需要再调用partition

          而在快速选择算法中,我们需要知道lo == hi是不是所要找的k

        5. 注意partition中与快速排序不同点

          因为快速选择算法 结束循环条件是 lo > hi,lo == hi时循环继续
          为了后面nums[i++]不越界,且一个元素也不需要partition,所以要单独判断lo是否等于hi
          if (lo == hi) {
          return lo;
          }

        6. 时间复杂度为O(N)

    2. 代码实现

      1. 优先级队列

      2. 快速选择算法

        class Solution {
            public int findKthLargest(int[] nums, int k) {
                shuffle(nums);
                int lo = 0;
                int hi = nums.length - 1;
                //第k个最大元素,即升序找索引为n - k
                int reverseK = nums.length - k;
                //条件为lo <= hi的原因,lo == hi时,区间有一个元素,需要判断该元素即该切分点与reverseK的关系,否则就漏了
                while (lo <= hi) {
                    int p = partition(nums, lo, hi);
                    if (p < reverseK) {
                        //即reverseK在切分点右侧
                        lo = p + 1;
                    } else if (p > reverseK) {
                        //即reverseK在切分点左侧
                        hi = p - 1;
                    } else {
                        //找到
                        return nums[p];
                    }
                }
                return -1;
            }
        
            int partition(int[] nums, int lo, int hi) {
                int i = lo, j = hi + 1;
                int value = nums[lo];
        
                while (true) {
                    while (nums[++i] - value < 0) {
                        if (i == hi) {
                            break;
                        }
                    }
                    while (nums[--j] - value > 0) {
                        if (j == lo) {
                            break;
                        }
                    }
                    if (i >= j) {
                        break;
                    }
                    swap(nums, i, j);
                }
                swap(nums, lo, j);
                return j;
            }
            void shuffle(int[] nums) {
                Random random = new Random();
                int n = nums.length;
                for (int i = 0; i < n; i++) {
                    //生成[0, n-1]
                    int r = i + random.nextInt(n - i);
                    //交换nums[i]和nums[r]
                    swap(nums, i, r);
                }
            }
        
            void swap(int[] nums, int i, int j) {
                int temp = nums[i];
                nums[i] = nums[j];
                nums[j] = temp;
            }
        }
        
        class Solution {
            public int findKthLargest(int[] nums, int k) {
                shuffle(nums);
                int lo = 0;
                int hi = nums.length - 1;
                //第k个最大元素,即升序找索引为n - k
                int reverseK = nums.length - k;
                //条件为lo <= hi的原因,lo == hi时,区间有一个元素,需要判断该元素即该切分点与reverseK的关系,否则就漏了
                while (lo <= hi) {
        
                    int p = partition(nums, lo, hi);
                    if (p < reverseK) {
                        //即reverseK在切分点右侧
                        lo = p + 1;
                    } else if (p > reverseK) {
                        //即reverseK在切分点左侧
                        hi = p - 1;
                    } else {
                        //找到
                        return nums[p];
                    }
                }
                return -1;
            }
        
            int partition(int[] nums, int lo, int hi) {
           			//因为快速选择算法 结束循环条件是 lo > hi,lo == hi时循环继续
           			//为了后面nums[i++]不越界,要单独判断lo是否等于hi
           			//一个元素也不需要partition
                if (lo == hi) {
                    return lo;
                }
                int i = lo, j = hi + 1;
                int value = nums[lo];
        
                while (true) {
                    while (nums[++i] - value < 0) {
                        if (i == hi) {
                            break;
                        }
                    }
                    while (nums[--j] - value > 0) {
                        if (j == lo) {
                            break;
                        }
                    }
                    if (i >= j) {
                        break;
                    }
                    swap(nums, i, j);
                }
                swap(nums, lo, j);
                return j;
            }
            void shuffle(int[] nums) {
                Random random = new Random();
                int n = nums.length;
                for (int i = 0; i < n; i++) {
                    //生成[0, n-1]
                    int r = i + random.nextInt(n - i);
                    //交换nums[i]和nums[r]
                    swap(nums, i, r);
                }
            }
        
            void swap(int[] nums, int i, int j) {
                int temp = nums[i];
                nums[i] = nums[j];
                nums[j] = temp;
            }
        }
        
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值