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

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

描述

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例

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

题解

1. 堆排序

通过数组下标到堆结构的映射,构建大根堆,经过 k - 1 次交换调整后就能确定第 k 个最大值。

堆排序见:C++ 数据结构与算法 (十一)(排序算法)

时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),建堆的时间代价是 O ( n log ⁡ n ) O(n \log n) O(nlogn),迭代交换的总代价是 O ( k log ⁡ n ) O(k \log n) O(klogn) k < n k<n k<n
空间复杂度: O ( 1 ) O(1) O(1)

在这里插入图片描述

class Solution {
public:
    void heapAdjust(vector<int>& nums, int root, int n){    // 向下调整为大根堆
        int child = 2 * root + 1;
        while(child < n){
            if(child + 1 < n && nums[child] < nums[child + 1]) ++child;
            if(nums[root] >= nums[child]) break;
            swap(nums[root], nums[child]);
            root = child;
            child = 2 * root + 1;
        }
    }
   
    int findKthLargest(vector<int>& nums, int k) {
        int n = nums.size();
        for(int i = n / 2 - 1; i >= 0; --i){
            heapAdjust(nums, i, n);
        }
        // 迭代 k 次,返回nums[n-k]
        for(int i = 0; i < k; ++i){
            swap(nums[0], nums[n-1-i]);
            heapAdjust(nums, 0, n-1-i);
        }
        return nums[n-k];	// 或者 迭代 k-1 次,返回堆顶nums[0]
    }
};
2. 维护大小为 k 的小顶堆

上述堆排序过程中需要所有元素都加载到内存中进行计算,因此当数据量大的时候会超出内存范围,

为此,我们可以在内存中创建并构造大小为 k 的小顶堆,然后从外存中逐个遍历、加载数组元素,去维护小顶堆。

时间复杂度: O ( N log ⁡ K ) O(N \log K) O(NlogK),遍历数据 O ( N ) O(N) O(N),堆内元素调整 O ( log ⁡ K ) O(\log K) O(logK)
空间复杂度: O ( K ) O(K) O(K)

class Solution {
public:
    void heapAdjust(vector<int>& nums, int root, int n){    // 调整为小顶堆 
        int child = 2 * root + 1;
        if(child >= n) return;  // 叶子节点,没有左孩子
        while(child < n){       
            if(child + 1 < n && nums[child] > nums[child+1]){
                ++child;        // 存在右孩子且 右<左时,则与右比较
            }
            if(nums[root] > nums[child]){
                swap(nums[root], nums[child]);  // 交换
                root = child;           
                child = 2 * root + 1;           // 内层向下调整
            }else{
                break;          // 父节点最小,不需要调整了
            }
        }
    }

    int findKthLargest(vector<int>& nums, int k) {
        int n = nums.size();   
        vector<int> ans(nums.begin(), nums.begin() + k); 
        for(int i = k / 2 - 1; i >= 0; --i){    // 创建,并维护大小为k的小顶堆
            heapAdjust(ans, i, k);
        }
        for(int i = k; i < n; ++i){             // 遍历所有元素,并维护小顶堆
            if(nums[i] > ans[0]){
                ans[0] = nums[i];
                heapAdjust(ans, 0, k);          // 从根节点开始调整
            }
        }
        return ans[0];
    }

    
};
3. 基于快速排序的选择方法

借鉴快排的思路,不断随机选择基准元素,看进行交换分界(partition)之后,该元素是不是在 n - k 的位置;否则递归其左边或右边查找指定元素(根据当前交换索引与 n - k 的大小来快速选择下一个查找区间)。

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

随机初始化 pivot 元素,可以在循环一开始的时候,随机交换第 1 个元素与它后面的任意 1 个元素的位置。

  • 时间复杂度: O ( N ) O(N) O(N),这里 N 是数组的长度,证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」。
    O(以n为首项,1/2为等比数列q的等比数列的和) = O(n)
  • 空间复杂度: O ( l o g N ) O(logN) O(logN),递归调用栈

在这里插入图片描述

class Solution {
public:
    void quickFind(vector<int> & nums, int left, int right, int k){
        if(left >= right) return;
        int randIndex = rand() % (right - left + 1) + left; // 随机选取一个索引,与left进行替换,作为划分基准
        swap(nums[randIndex], nums[left]);
        int pivot = left;           // 划分基准
        int changePos = pivot;      // 交换索引
        for(int i = pivot + 1; i <= right; ++i){    // 比nums[pivot]小的全部移到紧贴 pivot
            if(nums[i] < nums[pivot]){
                ++changePos;
                swap(nums[i], nums[changePos]);    
            }
        }
        swap(nums[pivot], nums[changePos]);         // 移动nums[pivot],形成左右分界
        if(changePos == nums.size() - k) return;    // 找到第k大的元素,返回
        if(changePos < nums.size() - k){            // 第k大的元素在右边
            quickFind(nums, changePos+1, right, k);
        }else{                                      // 第k大的元素在左边
            quickFind(nums, left, changePos-1, k);  
        }    
    }

    int findKthLargest(vector<int>& nums, int k) {
        quickFind(nums, 0, nums.size()-1, k);
        return nums[nums.size()-k];
    }
};
  • 使用非递归的方法,循环中不断更新区间边界值,优化空间复杂度 O ( 1 ) O(1) O(1)
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        int left = 0;
        int right = nums.size() - 1;
        while(1){
            int randIndex = rand() % (right - left + 1) + left; // 随机选取一个索引,与left进行替换,作为划分基准
            swap(nums[randIndex], nums[left]);
            int pivot = left;           // 划分基准
            int changePos = pivot;      // 交换索引
            for(int i = pivot + 1; i <= right; ++i){    // 比nums[pivot]小的全部移到紧贴 pivot
                if(nums[i] < nums[pivot]){
                    ++changePos;
                    swap(nums[i], nums[changePos]);    
                }
            }
            swap(nums[pivot], nums[changePos]);         // 移动nums[pivot],形成左右分界
            if(changePos == nums.size() - k) return nums[nums.size()-k];    // 找到第k大的元素,返回
            if(changePos < nums.size() - k){            // 第k大的元素在右边
                left = changePos + 1; 
            }else{                                      // 第k大的元素在左边
                right = changePos - 1;  
            }    
        }
        return nums[0];
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值