求数组中第k个最大(小)的数,或k个最大(小)的数

本文介绍了一种使用最大堆和最小堆解决寻找数组中第K大元素问题的方法,并提供了详细的算法实现步骤及代码示例。

题目描述

  1. Kth Largest Element in an Array(leetcode 215)
    Find the kth largest element in an unsorted array. Note that it is the kth largest element in the sorted order, not the kth distinct element.
  2. 最小的k个数(剑指offer)
    输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

解题思路

维护一个含K个元素的最大堆,根据最大堆的性质,堆顶是最大元素,若数组中剩余的数都大于堆顶元素,则堆中的元素为最小的K个数,堆顶元素为第K个小的数。
相反,若维护一个含K个元素的最小堆,遍历数组中剩余的元素,若大于堆顶,则将其赋值给堆顶元素,然后将其继续调整为最小堆,以此类推,直到数组中剩余的元素遍历完,则最终堆中元素为最大的K个数。

代码

// Kth Largest Element in an Array(leetcode 215)
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        if(k > nums.size())
            return -1;
        vector<int> heap;
        for(int i = 0; i < k; i++){
            heap.push_back(nums[i]);
        }
        // 建立一个最小堆
        for(int i = k/2-1; i>=0; i--)
            heap_adjust(heap, k, i);

        for(int i = k; i < nums.size(); i++){
            if(nums[i] > heap[0]){
                int temp = heap[0];
                heap[0] = nums[i];
                nums[i] = temp;
                heap_adjust(heap, k, 0);
            }
        }

        return heap[0];

    }
    void heap_adjust(vector<int> &nums, int n, int m){
      // 需要注意元素的索引:含n个元素的堆,从最后一个含叶子节点的节点开始遍历,
      // 该节点的值为n/2-1(由于初始下标从0开始)
      // 索引为m的节点的子节点为2*m+1, 2*m+2
        if(m <= n/2-1){
            int i = 2*m+1;
            if(i+1 < n && nums[i+1] < nums[i])//(注意要判断其右孩子是否存在)
                i++;
            if(nums[i] < nums[m]){
                int temp = nums[m];
                nums[m] = nums[i];
                nums[i] = temp;
                heap_adjust(nums, n, i);
            }
        }
    }
};
int main()
{
    Solution s = Solution();
    int a[] = {-1,2,0};
    vector<int> arr(a, a+sizeof(a)/sizeof(int));
    cout<<s.findKthLargest(arr, 3)<<endl;
    return 0;
}
<think>我们注意到用户要的是O(logn)时间复杂度的算法,但是根据引用[2]和引用[3],通常使用线性时间选择算法(O(n)堆(O(nlogk))来解决第k小元素问题,但这两种方法都不满足O(logn)的要。实际上,在无序数组中,没有任何基于比较的算法能够做到比O(n)更好。因此,我们需要重新审视问题:如果数组是有序的,那么我们可以直接通过下标在O(1)时间内找到第k小的元素,但这里用户要的是O(logn)复杂度,这通常出现在数组已经有序(如排序数组)的情况下,者在某些特定结构(如堆)中。然而,题目要的是数组第k小元素,并没有说明数组是否有序。经过思考,我们发现:如果数组未排序,那么不可能在O(logn)的时间内完成,因为读取所有元素就需要O(n)时间。所以,我们假设用户可能是想在有序数组中进行查找?但是题目没有明确。但引用[5]中提到了一个方法:当数组满足某些特殊性质时(例如,可以通过二分搜索来排除一半元素),我们可以达到O(logn)的时间复杂度。这通常用于解决某些特定问题(例如在旋转排序数组中查找,者两个有序数组的中位问题)。我们考虑是否有已知的O(logn)算法用于解第k小元素?实际上,在以下两种情况下可以:1.数组是有序的,那么直接通过索引访问第k-1个元素(如果k从1开始)即可,但是建立有序数组需要O(nlogn)时间(预处理),然后查询O(1),但单次查询如果要O(logn),显然预处理不算在内,因为用户可能是要多次查询?题目没有说明。2.在已知的特定问题中,例如在两个有序数组中找到第k小的元素(LeetCode第4题),可以使用二分搜索在O(log(min(m,n)))的时间内解决,这里我们通常认为是O(logn)级别的。但是用户的问题是在一个数组中(题目没有说是否有序,也没有说是否分割成多个有序部分)第k小元素,并且要O(logn)。因此,我们可能需要根据上下文推测。另外,引用[3]中提到的方法(快速选择)平均时间复杂度为O(n),最坏O(n^2),但通过中位的中位可以保证最坏O(n)。但是这些都不满足O(logn)。因此,我们推断用户可能是想在一个有序的数组中查找第k小元素?者在一个特殊的结构(如堆)中?但是堆只能保证堆顶元素是最值,取第k小需要额外的操作。但是,如果我们维护一个大小为k的堆(大根堆),那么可以保证堆顶就是第k小,这样建堆的时间是O(nlogk),然后取堆顶是O(1)。如果用户需要多次查询第k小,我们可能需要先预处理数组,然后每次查询可以O(1)?但题目要单次O(logn)。重新考虑:如果数组已经有序(升序),那么第k小元素就是arr[k-1](k从1开始),所以是O(1)时间,这比O(logn)还要好。那么什么情况下需要O(logn)呢?也许是在动态维护的数组中,但是题目没有说明。根据以上分析,可能用户的问题是在两个有序数组中找到第k小的元素,这是经典的O(log(min(m,n)))的算法(也可以说是O(log(m+n)))。我们参考LeetCode第4题。因此,我们假设用户需要的是在两个有序数组中找到第k小的元素。如果用户只是在一个无序数组中找,那么不可能在O(logn)时间内完成。所以,我们将问题限定在两个有序数组上。我们给出以下实现(C++)和解释:问题:给定两个升序排列的整数组`nums1`和`nums2`,以及一个整`k`,请找出这两个数组中的第k小的元素。要时间复杂度为O(log(min(m,n))),这属于O(logn)级别。算法思想(二分法):1.假设两个数组分别为nums1(长度为m)和nums2(长度为n),我们保持nums1为较短的数组(如果不是则交换)。2.我们在nums1中进行二分搜索,确定一个位置i(在0到m之间),同时在nums2中确定位置j=k-i(这样保证前k个元素由nums1的前i个和nums2的前j个组成)。3.我们需要满足的条件是:nums1[i-1]<=nums2[j]且nums2[j-1]<=nums1[i](即左边部分的最大值小于右边部分的最小值)。4.如果不满足,我们就调整i的位置(二分搜索的方式)。下面我们给出具体代码和注释。注意:这里k表示第k小,k是从1开始计的。另外,边界条件需要仔细处理。实现步骤:1.令m=nums1.size(),n=nums2.size()。确保m<=n,如果不是,交换两个数组。2.设置二分查找的区间:[0,m]。3.在区间[0,m]中进行二分查找:i=(left+right)/2j=k-i//因为一共要取k个元素,所以从nums1中取i个,nums2中取j个4.检查是否满足条件:-如果i>0且j<n,则检查nums1[i-1]<=nums2[j]-如果j>0且i<m,则检查nums2[j-1]<=nums1[i]5.如果满足上述条件,则说明找到的i和j是正确的,那么第k小的元素就是max(nums1[i-1],nums2[j-1])(因为这两个是左边部分最大的两个候选)。6.如果不满足,则根据情况调整i。代码实现(注意处理边界情况):</think> ### C++ 实现 O(log n) 复杂度数组第 k 小元素的算法 在一般无序数组中,寻找第 k 小元素的最优时间复杂度为 $O(n)$(如快速选择算法)。但当数组满足特定条件时,可实现 $O(\log n)$ 复杂度。以下介绍两种典型场景的实现方法: #### 1. 二分搜索法(适用于**有序数组**) 当数组已排序时,可通过二分搜索实现 $O(\log n)$ 查询: ```cpp int findKthSmallest(vector<int>& nums, int k) { return nums[k - 1]; // 直接访问第 k-1 个索引(k 从 1 开始) } ``` **时间复杂度分析**: - 预处理(排序):$O(n \log n)$ - 单次查询:$O(1)$ - **适用范围**:需要多次查询不同 k 的场景(预处理后单次查询时间复杂度 $O(log n)$) #### 2. 双数组二分法(LeetCode 4 变种) 在两个有序数组中寻找第 k 小元素可实现 $O(\log(\min(m, n)))$ 复杂度: ```cpp int findKthInTwoArrays(vector<int>& nums1, vector<int>& nums2, int k) { if (nums1.size() > nums2.size()) return findKthInTwoArrays(nums2, nums1, k); int m = nums1.size(), n = nums2.size(); int left = 0, right = m; while (left <= right) { int i = left + (right - left) / 2; int j = k - i; if (j < 0) { right = i - 1; continue; } if (j > n) { left = i + 1; continue; } int left1 = (i > 0) ? nums1[i - 1] : INT_MIN; int right1 = (i < m) ? nums1[i] : INT_MAX; int left2 = (j > 0) ? nums2[j - 1] : INT_MIN; int right2 = (j < n) ? nums2[j] : INT_MAX; if (left1 <= right2 && left2 <= right1) { return max(left1, left2); } else if (left1 > right2) { right = i - 1; } else { left = i + 1; } } return -1; // k 超出范围 } ``` **算法原理**: 1. 始终保持 `nums1` 为较短数组 2. 在 `nums1` 中二分定位切割点 `i`,则 `nums2` 的切割点为 `j = k - i` 3. 验证切割有效性: $max(nums1[i-1], nums2[j-1]) \leq min(nums1[i], nums2[j])$ 4. 调整切割点直至满足条件[^3] **时间复杂度**:$O(\log(\min(m, n)))$ **空间复杂度**:$O(1)$ > ⚠️ **重要说明**: > 在**无序单数组**中不可能实现 $O(\log n)$ 时间复杂度,因为必须扫描所有元素($O(n)$ 下限)[^2][^3]。上述方法需满足数组有序数组有序的条件。 ### 性能对比表 | 算法类型 | 时间复杂度 | 空间复杂度 | 适用场景 | |--------------------|----------------|-----------|----------------------------| | 快速选择算法 | $O(n)$ | $O(1)$ | 一般无序数组 | | 堆排序法 | $O(n \log k)$ | $O(k)$ | 海量据取 Top-k | | 二分搜索(单数组) | $O(1)$ 查询 | $O(n)$ | 有序数组多次查询 | | 双数组二分法 | $O(\log(\min(m,n)))$ | $O(1)$ | 两个有序数组的合并 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值