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

题目

原题链接:https://leetcode-cn.com/problems/kth-largest-element-in-an-array/

在未排序的数组中找到第\(k\)个最大的元素。请注意,你需要找的是数组排序后的第\(k\)个最大的元素,而不是第\(k\)个不同的元素。
示例:

示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

本题希望返回数组排序之后倒数第\(k\)个位置

解题思路

基于快速排序的选择方法

快速排序是一个典型的分治算法。对数组\(a[l \cdots r]\)做快速排序的过程是:

  • 分解:将数组\(a[l \cdots r]\)「划分」成两个子数组\(a[l \cdots q - 1]\)\(a[q + 1 \cdots r]\),使得\(a[l \cdots q - 1]\)中的每个元素小于等于\(a[q]\),且\(a[q]\)小于等于\(a[q + 1 \cdots r]\)中的每个元素。其中,计算下标\(q\)也是「划分」过程的一部分。

  • 解决:通过递归调用快速排序,对子数组\(a[l \cdots q - 1]\)\(a[q + 1 \cdots r]\)进行排序。

  • 合并因为子数组都是原址排序的,所以不需要进行合并操作\(a[l \cdots r]\)已经有序。

  • 上文中提到的「划分」过程是:从子数组\(a[l \cdots r]\)中选择任意一个元素\(x\)作为主元,调整子数组的元素使得左边的元素都小于等于它,右边的元素都大于等于它,\(x\)的最终位置就是\(q\)

优化

  • 由于每次经过「划分」操作后,一定可以确定一个元素的最终位置,即\(x\)的最终位置为\(q\),并且保证\(a[l \cdots q - 1]\)中的每个元素小于等于\(a[q]\),且\(a[q]\)小于等于\(a[q + 1 \cdots r]\)中的每个元素。所以只要某次划分的\(q\)为倒数第\(k\)个下标的时候,就已经找到了答案。至于\(a[l \cdots q - 1]\)\(a[q+1 \cdots r]\)是否是有序的,并不关心

  • 因此可以改进快速排序算法来解决这个问题:在分解的过程当中,对子数组进行划分,如果划分得到的\(q\)正好就是需要的下标,就直接返回\(a[q]\);否则,如果\(q\)比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。这就是「快速选择」算法。

代码实现

实现思路:

  • 欲找到数组中的第K个最大元素,即返回数组排序之后的倒数第K个位置
    quickSelect(nums, 0, nums.length - 1, nums.length - k)
    
  • 随机找一个分隔值(主元),并以主元为界,对数组进行左右划分;
    int q = randomPartition(array, left, right);
    
    • 划分过程为:
      • 将找到的partition先与最后一位数交换;
        swap(array, i, right);
        
      • 然后开始遍历,并根据与partition的大小关系,调整位置;
        for (int j = left; j < right; j++) {
              if (array[j] <= x) {
                  // 因为i是从left - 1开始,所以需++i
                  swap(array, ++i, j);
              }
          }
        
    • 如果设定的分隔值q比目标值小,则目标值在主元右侧区间。
      return q < index ?
                      quickSelect(array, q + 1, right, index) : quickSelect(array, left, q - 1, index);
      

具体代码实现:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Random;

/**
 * 215. 数组中的第K个最大元素
 * @author chenzufeng
 */
public class No215_FindKthLargest {

    public static void main(String[] args) {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            String[] strings = reader.readLine().split(",");
            /// System.out.println(Arrays.toString(strings));
            int[] nums = new int[strings.length];
            for (int i = 0; i < strings.length; i++) {
                nums[i] = Integer.parseInt(strings[i]);
            }

            int k = Integer.parseInt(reader.readLine());

            System.out.println(findKthLargest(nums, k));

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static Random random = new Random();

    public static int findKthLargest(int[] nums, int k) {
        // 数组中的第K个最大元素,即返回数组排序之后的倒数第K个位置(nums.length - k)
        return quickSelect(nums, 0, nums.length - 1, nums.length - k);
    }

    public static int quickSelect(int[] array, int left, int right, int index) {
        // 随机找一个分隔值(主元),并以主元为界,对数组进行左右划分
        int q = randomPartition(array, left, right);
        if (q == index) {
            return array[q];
        } else {
            // 如果q比目标值小,则目标值在主元右侧区间
            return q < index ?
                    quickSelect(array, q + 1, right, index) : quickSelect(array, left, q - 1, index);
        }
    }

    public static int randomPartition(int[] array, int left, int right) {
        // 随机选择一个范围内的数
        int i = random.nextInt(right - left + 1) + left;
        // 将找到的partition先与最后一位数交换
        swap(array, i, right);
        // 将数组中的元素以主元为界,进行“左右”划分
        return partition(array, left, right);
    }

    public static int partition(int[] array, int left, int right) {
        // x为分隔值
        int x = array[right];
        // i对应的数值比x小,i+1对应的数值可能比x大
        int i = left - 1;
        // 注意这里的j的最大值
        for (int j = left; j < right; j++) {
            if (array[j] <= x) {
                // 因为i是从left - 1开始,所以需++i
                swap(array, ++i, j);
            }
        }

        // 数组中从left到i的数都是小于分隔值的
        swap(array, i + 1, right);
        return i + 1;
    }

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

复杂度分析

快速排序的性能和「划分」出的子数组的长度密切相关。如果每次规模为\(n\)的问题,都划分成\(1\)\(n - 1\),每次递归的时候又向\(n - 1\)的集合中递归,这种情况是最坏的,时间代价是\(O(n ^ 2)\)。可以引入随机化来加速这个过程,它的时间代价的期望是\(O(n)\)

  • 时间复杂度:\(O(n)\)
  • 空间复杂度:\(O(\log n)\),递归使用栈空间的空间代价的期望为\(O(\log n)\)
posted @ 2021-03-09 11:44  chenzufeng  阅读(130)  评论(0编辑  收藏  举报