题目描述
给定整数数组 nums 和整数 k,请返回数组中第 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
各种解法
快排,快速选择
class Solution {
Random random = new Random();
public int findKthLargest(int[] nums, int k){
return quickSelect(nums, 0, nums.length-1, nums.length-k); // 范围,想找的索引位置
}
public int quickSelect(int[] a, int l, int r, int index){
int q = randomPartition(a, l, r);
if(q == index){
return a[q];
}else{
return q < index?quickSelect(a,q+1, r, index) : quickSelect(a, l, q-1, index);
}
}
public int randomPartition(int[] a, int l, int r){
// 从l,r之间随机选择一个,换到最后的r位置,作为分界
int i = random.nextInt(r-l+1)+l;
swap(a, i, r);
return partition(a,l,r);
}
public int partition(int[] a, int l, int r){
// 以最右边的元素作为分界线,获取他应该在的位置
int x = a[r], i = l-1;
// 从左到右遍历元素,如果小于分界元素,让他与左边的元素交换
for(int j=l; j<r; ++j){
if(a[j] <= x){
swap(a, ++i, j);
}
}
// 到i位置,前面的都是比分界小的,确定分界的位置就是i+1
swap(a, i+1, r);
return i+1;
}
public void swap(int[] a, int i, int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
解题思路
- 快排的核心思想,就是要找到作为分界的q,在整个数组中的正确位置。把比他小的数,都交换到左边,最后的空出来的位置就是分界值q应该放置的位置。
- 因为要找到第k大的最大元素,所以应该找的是索引在nums.length-K的数。
- 如果当前得到的分界q的正确索引小于nums.length-K,那么只需要继续对分界右边的内容找另一个分界继续向下做;大于的,对分界左边的内容继续进行。
- 直到分界q的索引正好是nums.length-K,输出该值,就是第K大元素
- 理想情况下,每次划分都是绝对均匀划分,那么每次递归后,数组规模减小为一半,总共递归lgN次,每次对 n / 2 k n/2^k n/2k个元素比较,总的时间复杂度为 O ( ∑ k = 0 l g N N / 2 k ) = O ( N ) O(\sum_{k=0}^{lgN}N/2^k) = O(N) O(∑k=0lgNN/2k)=O(N)
- 除此之外,为了保证快排算法的稳定性,每次的分界值的选取采用随机的方式,近似于均匀,时间复杂度O(N)。如果从小到大排,防止每次取到的数刚好是最大的,这样就要每次都执行很多交换,退化为O(N^2)。也就是最坏情况下每次规模降低1。
堆排序
自己实现堆
class Solution {
public int findKthLargest(int[] nums, int k){
int heapSize = nums.length;
buildMaxHeap(nums, heapSize);
// 开始删除根
for(int i = nums.length-1; i>=nums.length-k+1; --i){
swap(nums,0,i);
--heapSize;
maxHeapify(nums,0,heapSize);
}
return nums[0];
}
public void buildMaxHeap(int[] a, int heapSize){
for(int i=heapSize/2; i>=0; --i){ // 考察父节点是否都比子节点大
maxHeapify(a, i, heapSize);
}
}
// 带着heap大小调整
public void maxHeapify(int[] a, int i, int heapSize){
int l = i*2+1, r = i*2+2, largest = i;
if(l<heapSize && a[l]>a[largest]){
largest = l;
}
if(r < heapSize && a[r]>a[largest]){
largest = r;
}
if(largest != i){
swap(a, i, largest);
maxHeapify(a, largest, heapSize);// 当前节点是否需要继续往下调整
}
}
public void swap(int[] a, int i, int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
解题思路
- 自己实现一个大根堆,使用数组实现。
- 首先需要基于当前的数组来构建一个原始的大根堆,对数组中的元素进行调整。因为是树结构,所以组织形式是 索引 i 为父节点,那么 索引 2*i+1为左子节点,索引 2*i+2 为右子节点。需要保证父节点的值大于两个子节点的值。
- maxHeapify函数:每个父节点和相应左右子节点的调整方式是,找到父节点和他的两个左右子节点的值进行比较,找到最大的,换到父节点的位置,父节点交换到该子节点的位置。然后作为父节点继续向下和下面的左右子节点比较。只要是父节点需要交换而且还在堆的大小范围内,都要一直向下调整下去。
- 构建原始堆时,需要对其中的一半的节点执行maxHeapify即可,因为只要父节点调整完毕,就可以了。而且是从后往前的,索引0位置的是最大的根,最后调整。
- 现在要拿到第k大的元素,需要每次把根换到最后,然后调整的范围-1,执行k-1次,把第1到第k-1都调到后面,最后剩下的nums[0]就是第k大的元素
- 每次把跟换到最后,把最后的换到当前位置,所以需要对换上来的元素的大小进行向下调整。
- 时间复杂度 O(N+klogN)
用优先队列PriorityQueue实现
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> pq = new PriorityQueue<Integer>(k);
for(int i=0; i<nums.length; i++){
if(pq.size()<k){
pq.add(nums[i]);
}else{
if(nums[i]>=pq.peek()){
pq.poll();
pq.add(nums[i]);
}
}
}
return pq.peek();
}
}
解题思路
- PriorityQueue默认是使用小根堆,把他的大小设置为k
- 因为要求的是第k大元素,所以保证最后优先队列中存的是最大的k个值,因为小根堆,所以堆顶就是第k大元素
- 首先按顺序把k个值存入。如果后面的数比堆顶大,那么就把堆顶删除,改为存入该元素,处理完所有数,堆顶就是所求的结果。
- 时间复杂度为 O(K+(N-K)logK)
BFPRT算法
- BFPRT取自5个作者的姓名首字母。
- 上面的快速选择算法,只能在理想情况下才能达到O(N),堆排序,只有当K远小于N时,才能近似于O(N)。
- 因为快速选择算法中,如果每次刚好拿到的分界值就是中位数是最理想的情况。BFPRT就是额外加了一步计算近似中位数的步骤。计算的是近似的,不是准确的,准确的时间复杂度太高。
- BFPRT做了两步:
- 数组划分,划分为N/5个子数组,每个数组中5个元素,找到每一组的中位数,O(N),递归找到中位数的中位数
- 按照上面找到的中位数的中位数,近似中位数的范围在30%-70%,按照递归函数的时间复杂度推导出为O(N)。具体推导参考 具体推导
- 快速选择的分界值,到底使用 随机选择 还是 BFPRT方案中用中位数的中位数。BFPRT中可以保证时间复杂度为O(N),而随机只能做到期望时间复杂度为O(N)
变形题
1. 无序数组找中位数
其实就是topK变成了top(N/2)了。