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); } }
- 将找到的partition先与最后一位数交换;
- 如果设定的分隔值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)\)。