数组中第K大元素

博客探讨了在面试中常见的数组中第K大元素问题,指出全排序不是最优解。文章介绍了两种常见思路:构建最大堆和快速排序。构建最大堆的时间复杂度为O(n+klgn),而快速排序在平均情况下时间复杂度为O(N),最坏情况为O(N^2)。此外,提到了《算法导论》中关于优化快速排序算法的方法,选择中位数作为pivot以达到线性时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数组中第K大元素

在面试的时候遇到过这个问题,后来在leetcode上也遇到了这个问题,于是记录下来以便以后快速回忆。

题目有多种思路,全排序是比较直观的想法,然而最低的时间复杂度为O(nlgn),并且不符合该题目的初衷。

题目更多想问的是如何在不进行全排序的条件下找到数组中第K大的元素,个人认为比较被面试官中意的解答有两个(理应也有其他的..),如下:

构建最大堆

简单分析时间复杂度: 构建堆的时间O(n),k次取最大元素要k(lgn),最终的时间复杂度为O(n+klgn).

显然是比全排序要好一些,但是当时面试的时候我回答这个思路,面试官认为并不是一个好的思路,因为面试官认为构建最大堆更适合题目:求数组中前K大的元素(维护一个大小为K的最大堆).
然而我还是认为这是一个可以的思路…..恩,代码还是贴一下,以后就省事了..

   int heap_size;
   int parent(int idx) {
        return (idx - 1) >> 1; //求父节点
    }
   int left(int idx) {
        return (idx << 1) + 1;//左子树
    }
   int right(int idx) {
        return (idx << 1) + 2;//右子树
    }

   void max_heapify(vector<int>& nums, int idx) {
        int largest = idx;
        int l = left(idx), r = right(idx);
        if (l < heap_size && nums[l] > nums[largest]) largest = l;
        if (r < heap_size && nums[r] > nums[largest]) largest = r;
        if (largest != idx) {
            swap(nums[idx], nums[largest]);
            max_heapify(nums, largest);
        }
     }
    //构建最大堆,从非叶子节点开始操作,可证明时间复杂度是O(n)
    void build_max_heap(vector<int>& nums) {
        heap_size = nums.size();
        for (int i = (heap_size >> 1) - 1; i >= 0; i--)
            max_heapify(nums, i);
    }

    int findKthLargest(vector<int>& nums, int k) {
        build_max_heap(nums);
        for (int i = 0; i < k; i++) {
            swap(nums[0], nums[heap_size - 1]);
            heap_size--;
            max_heapify(nums, 0);
        }//不断交换末尾元素和最大元素,之后重新max_heapify
        return nums[heap_size];
    }

快速排序思路

该思想类似快速排序:

  1. 以某一个元素为pivot元素,将元素分为两个集合,一个集合元素比pivot小,另一个比pivot大。

  2. 若比pivot大的元素数目正好为k-1,那么pivot就是我们要找到元素;若比pivot大的元素为m(小于k), 那么就在比pivot小的集合里面找第(k-m)大的元素; 若是比pivot大的元素为m(大于k),那就继续在该集合里面找第k大的元素。

  3. 重复上面步骤,直到找到第k大的元素

代码如下:

int partition(vector<int>& nums, int i, int j)
    {//类似快速排序的分组
        if (i == j) return i;

        int pivot = nums[i];
        std::swap(nums[i], nums[j]);

        int i0 = i;
        for(int k = i; k < j; k ++)
        {
            if(nums[k] <= pivot)
            {
                std::swap(nums[k], nums[i0 ++]);
            }
        }
        std::swap(nums[i0], nums[j]);
        return i0;
    }

    int findKthEle(vector<int>& nums, int i, int j,int k)
    {
        int index = partition(nums,i,j);
        int length = j-index+1;
        if(length == k)
            return nums[index];
        else if(length > k)
            return findKthEle(nums,index+1,j,k);
        else if(length <k)
            return findKthEle(nums,i,index-1,k-length);
    }

    int findKthLargest(vector<int>& nums, int k) 
    {
        size_t len = nums.size();

        return findKthEle(nums,0,len-1,k);
    }

时间复杂度:
该算法的平均时间复杂度为O(N)(详细的推导过程看算法导论9.2节),最坏情况为N^2,即每次划分把数组变为为(n-1) 和1的两断。

同时算法导论上有关于O(n)的算法(9.3节),其思路是选择pivot元素时是通过选择多组元素中的中位数,根据这个元素来划分,能够得到最坏情况为线性时间的选择算法(具体推导我还真是吃不消)。

看了算法导论以及网上相关的博客,发现这是个经典的老题目了…然而我并没有研究的非常透彻…

<think>好的,用户想用C++实现快速选择算法数组中的第K元素。我需要先回忆一下快速选择的基本原理。快速选择是基于快速排序的,但不需要完全排序整个数组,而是通过分治的方法到第K元素。 首先,用户可能已经了解快速排序,所以应该先简要说明快速选择的不同之处。然后,需要给出示例代码。根据引用中的资料,比如引用[2]提到快速选择的时间复杂度平均O(n),最坏O(n²),同时是原地算法。因此,在代码中需要注意分区的实现,以及递归处理左或右部分,而不是两边。 接下来,用户可能需要知道如何将问题转化为第K元素。通常,第K元素在排序后的数组中是倒数第K个位置,即索引为n-K的位置。所以,在快速选择中,需要调整目标位置为n-K。 然后,代码的结构应该包括分区函数和递归选择部分。分区函数和快速排序类似,选择一个基准元素,将数组分成两部分。这里需要注意的是,不同的分区策略可能影响效率,比如随机选择基准可以减少最坏情况的发生概率。例如,引用[2]提到快速选择是基于快速排序的,所以分区是关键步骤。 此外,用户可能关心如何处理重复元素和边界情况,比如数组长度为1时直接返回。需要确保代码能够正确处理这些情况,避免死循环或者越界访问。 示例代码的结构致如下:一个主函数findKthLargest调用快速选择函数,快速选择函数递归地进行分区,并根据当前基准的位置与目标位置的关系决定处理左半部分还是右半部分。分区函数使用双指针法,将于基准的元素移到左边,小于的移到右边,这样基准的位置就是其正确的位置。 需要检查代码的正确性,比如在交换元素时的逻辑是否正确,基准的选择是否合理。可能还需要考虑随机化基准的选择以提高效率,但示例中可以简化,选择中间元素作为基准。 最后,测试代码的正确性,例如用示例数组验证是否能正确到第K元素。同时,需要提到时间复杂度,虽然平均是O(n),但最坏情况可能较差,可以建议使用随机化基准或三数取中法优化。 另外,用户可能还想知道其他方法,比如堆排序(如引用[4]中的最小堆方法),但当前问题聚焦于快速选择算法,所以示例代码应重点突出这一方法。同时,引用[1]中的直接排序方法虽然简单,但效率不如快速选择,所以需要强调快速选择的优势。 总结步骤:解释快速选择原理,给出代码结构,实现分区函数,处理递归选择,调整K的索引,确保边界条件正确,最后测试和优化建议。</think>以下是用C++实现的快速选择算法示例代码,用于高效查数组中第K元素。该算法基于快速排序的分区思想,平均时间复杂度为$O(n)$: ```cpp #include <vector> #include <algorithm> using namespace std; int partition(vector<int>& nums, int left, int right) { int pivot = nums[(left + right) / 2]; // 选择中间元素作为基准 swap(nums[(left + right) / 2], nums[right]); // 基准移到最右端 int i = left; for (int j = left; j < right; j++) { if (nums[j] >= pivot) { // 注意这里是>=,因为第K元素 swap(nums[i], nums[j]); i++; } } swap(nums[i], nums[right]); // 将基准放到正确位置 return i; } int quickSelect(vector<int>& nums, int left, int right, int k) { if (left == right) return nums[left]; int pivotIndex = partition(nums, left, right); if (k == pivotIndex) { return nums[k]; } else if (k < pivotIndex) { return quickSelect(nums, left, pivotIndex - 1, k); } else { return quickSelect(nums, pivotIndex + 1, right, k); } } int findKthLargest(vector<int>& nums, int k) { int n = nums.size(); return quickSelect(nums, 0, n - 1, n - k); // 转换为第n-k小元素 } ``` ### 算法说明 1. **分区函数**:将数组划分为于基准值和小于基准值的两部分,返回基准值的最终位置。这里选择中间元素作为基准并交换到最右端,避免极端情况下的性能问题[^2] 2. **快速选择**:根据基准位置与目标位置的关系,递归处理左/右子数组。当基准位置恰好等于目标位置时直接返回结果 3. **索引转换**:第K元素在排序数组中的位置是$n-k$,因此需要将目标位置转换为该索引值[^4] ### 性能优化建议 - 随机选择基准元素可避免最坏$O(n^2)$时间复杂度 - 当数组较小时切换为插入排序 - 使用三数取中法选择基准元素
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值