0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

详解一道高频算法题:数组中的第 K 个最大元素

算法与数据结构 来源:五分钟学算法 2020-06-03 17:37 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

今天分享的题目来源于 LeetCode 第 215 号问题,是面试中的高频考题。

题目描述

在 未排序 的数组中找到第 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 总是有效的,且 1 ≤ k ≤ 数组的长度。

题目解析

方法一:返回升序排序以后索引为 len - k 的元素

题目已经告诉你了:

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

因此,升序排序以后,返回索引为 len - k 这个元素即可。

这是最简单的思路,如果只答这个方法,可能面试官并不会满意,但是在我们平时的开发工作中,还是不能忽视这种思路简单的方法,我认为理由如下:

1、最简单同时也一定是最容易编码的,编码成功的几率最高,可以用这个最简单思路编码的结果和其它思路编码的结果进行比对,验证高级算法的正确性;

2、在数据规模小、对时间复杂度、空间复杂度要求不高的时候,真没必要上 “高大上” 的算法;

3、思路简单的算法考虑清楚了,有些时候能为实现高级算法铺路。这道题正是如此,“数组排序后的第 k 个最大的元素” ,语义是从右边往左边数第 k 个元素(从 1 开始),那么从左向右数是第几个呢,我们列出几个找找规律就好了。

一共 6 个元素,找第 2 大,索引是 4;
一共 6 个元素,找第 4 大,索引是 2。

因此,目标元素的索引是 len - k,即找最终排定以后位于 len - k 的那个元素;

4、低级算法往往容错性最好,即在输入不满足题目条件的时候,往往还能得到正确的答案,而高级算法对输入数据的要求就非常苛刻。

参考代码

importjava.util.Arrays; publicclassSolution{ publicintfindKthLargest(int[]nums,intk){ intlen=nums.length; Arrays.sort(nums); returnnums[len-k]; } }

复杂度分析

时间复杂度:O(NlogN)。这里 N 是数组的长度,算法的性能消耗主要在排序,JDK 默认使用快速排序,因此时间复杂度为O(NlogN)。

空间复杂度:O(1)。这里是原地排序,没有借助额外的辅助空间。

到这里,我们已经分析出了:

1、我们应该返回最终排定以后位于 len - k 的那个元素;
2、性能消耗主要在排序,JDK 默认使用快速排序。

学习过 “快速排序” 的朋友,一定知道一个操作叫 partition,它是 “分而治之” 思想当中 “分” 的那一步。

经过 partition 操作以后,每一次都能排定一个元素,并且这个元素左边的数都不大于它,这个元素右边的数都不小于它,并且我们还能知道排定以后的元素的索引。

于是可以应用 “减而治之”(分治思想的特例)的思想,把问题规模转化到一个更小的范围里。

于是得到方法二。

方法二:借助 partition 操作定位

方法二则是借助 partition 操作定位到最终排定以后索引为 len - k 的那个元素。

以下的描述基于 “快速排序” 算法知识的学习,如果忘记的朋友们可以翻一翻自己的《数据结构与算法》教材,复习一下,partition 过程、分治思想和 “快速排序” 算法的优化。

【图解数据结构】 一组动画彻底理解快速排序

我们在学习 “快速排序” 的时候,接触的第 1 个操作就是 partition(切分),简单介绍如下:

partition(切分)操作,使得:

对于某个索引 j,nums[j] 已经排定,即 nums[j] 经过 partition(切分)操作以后会放置在它 “最终应该放置的地方”;

nums[left] 到 nums[j - 1] 中的所有元素都不大于 nums[j];

nums[j + 1] 到 nums[right] 中的所有元素都不小于 nums[j]。

partition(切分)操作总能排定一个元素,还能够知道这个元素它最终所在的位置,这样每经过一次 partition操作就能缩小搜索的范围,这样的额思想叫做 “减而治之”(是 “分而治之” 思想的特例)。

切分过程可以不借助额外的数组空间,仅通过交换数组元素实现。下面是参考代码:

参考代码

publicclassSolution{ publicintfindKthLargest(int[]nums,intk){ intlen=nums.length; intleft=0; intright=len-1; //转换一下,第k大元素的索引是len-k inttarget=len-k; while(true){ intindex=partition(nums,left,right); if(index==target){ returnnums[index]; }elseif(index< target) {                 left = index + 1;             } else {                 assert index >target; right=index-1; } } } /** *在nums数组的[left,right]部分执行partition操作,返回nums[i]排序以后应该在的位置 *在遍历过程中保持循环不变量的语义 *1、(left,k]< nums[left]      * 2、(k, i] >=nums[left] * *@paramnums *@paramleft *@paramright *@return */ publicintpartition(int[]nums,intleft,intright){ intpivot=nums[left]; intj=left; for(inti=left+1;i<= right; i++) {             if (nums[i] < pivot) {                 // 小于 pivot 的元素都被交换到前面                 j++;                 swap(nums, j, i);             }         }         // 最后这一步不要忘记了         swap(nums, j, left);         return j;     }     private void swap(int[] nums, int index1, int index2) {         if (index1 == index2) {             return;         }         int temp = nums[index1];         nums[index1] = nums[index2];         nums[index2] = temp;     } }

复杂度分析

时间复杂度:O(N)。这里 N 是数组的长度。

空间复杂度:O(1)。这里是原地排序,没有借助额外的辅助空间。

方法三:优先队列

优先队列的写法就很多了,这里例举一下我能想到的。

假设数组有 len 个元素。

思路 1 :把 len 个元素都放入一个最小堆中,然后再 pop() 出 len - k 个元素,此时最小堆只剩下 k 个元素,堆顶元素就是数组中的第 k 个最大元素。

思路 2 :把 len 个元素都放入一个最大堆中,然后再 pop() 出 k - 1 个元素,因为前 k - 1 大的元素都被弹出了,此时最大堆的堆顶元素就是数组中的第 k 个最大元素。

思路 3 :只用 k 个容量的优先队列,而不用全部 len 个容量。

思路 4:用 k + 1 个容量的优先队列,使得上面的过程更“连贯”一些,到了 k 个以后的元素,就进来一个,出去一个,让优先队列自己去维护大小关系。

思路 5:综合考虑以上两种情况,总之都是为了节约空间复杂度。即 k 较小的时候使用最小堆,k 较大的时候使用最大堆。

根据以上思路,分别写出下面的代码:

思路 1 参考代码

//思路 1 :把`len`个元素都放入一个最小堆中,然后再 pop()出 len - k 个元素,此时最小堆只剩下`k`个元素,堆顶元素就是数组中的第`k`个最大元素。 importjava.util.PriorityQueue; publicclassSolution{ publicintfindKthLargest(int[]nums,intk){ intlen=nums.length; //使用一个含有 len 个元素的最小堆,默认是最小堆,可以不写 lambda 表达式:(a, b)-> a - b PriorityQueueminHeap=newPriorityQueue<>(len,(a,b)->a-b); for(inti=0;i< len; i++) {             minHeap.add(nums[i]);         }         for (int i = 0; i < len - k; i++) {             minHeap.poll();         }         return minHeap.peek();     } }

思路 2 参考代码

//思路 2 :把`len`个元素都放入一个最大堆中,然后再 pop()出 k - 1 个元素,因为前 k - 1 大的元素都被弹出了,此时最大堆的堆顶元素就是数组中的第`k`个最大元素。 importjava.util.PriorityQueue; publicclassSolution{ publicintfindKthLargest(int[]nums,intk){ intlen=nums.length; //使用一个含有 len 个元素的最大堆,lambda 表达式应写成:(a, b)-> b - a PriorityQueuemaxHeap=newPriorityQueue<>(len,(a,b)->b-a); for(inti=0;i< len; i++) {             maxHeap.add(nums[i]);         }         for (int i = 0; i < k - 1; i++) {             maxHeap.poll();         }         return maxHeap.peek();     } }

思路 3 参考代码

//思路 3 :只用`k`个容量的优先队列,而不用全部`len`个容量。 importjava.util.PriorityQueue; publicclassSolution{ publicintfindKthLargest(int[]nums,intk){ intlen=nums.length; //使用一个含有k个元素的最小堆 PriorityQueueminHeap=newPriorityQueue<>(k,(a,b)->a-b); for(inti=0;i< k; i++) {             minHeap.add(nums[i]);         }         for (int i = k; i < len; i++) {             // 看一眼,不拿出,因为有可能没有必要替换             Integer topEle = minHeap.peek();             // 只要当前遍历的元素比堆顶元素大,堆顶弹出,遍历的元素进去             if (nums[i] >topEle){ minHeap.poll(); minHeap.add(nums[i]); } } returnminHeap.peek(); } }

思路 4 参考代码

//思路 4:用`k + 1`个容量的优先队列,使得上面的过程更“连贯”一些,到了`k`个以后的元素,就进来一个,出去一个,让优先队列自己去维护大小关系。 importjava.util.PriorityQueue; publicclassSolution{ publicintfindKthLargest(int[]nums,intk){ intlen=nums.length; //最小堆 PriorityQueuepriorityQueue=newPriorityQueue<>(k+1,(a,b)->(a-b)); for(inti=0;i< k; i++) {             priorityQueue.add(nums[i]);         }         for (int i = k; i < len; i++) {             priorityQueue.add(nums[i]);             priorityQueue.poll();         }         return priorityQueue.peek();     } }

思路 5 参考代码

//思路 5:综合考虑以上两种情况,总之都是为了节约空间复杂度。即`k`较小的时候使用最小堆,`k`较大的时候使用最大堆。 importjava.util.PriorityQueue; publicclassSolution{ //根据k的不同,选最大堆和最小堆,目的是让堆中的元素更小 //思路 1:k 要是更靠近0的话,此时 k 是一个较大的数,用最大堆 //例如在一个有6个元素的数组里找第5大的元素 //思路 2:k 要是更靠近 len 的话,用最小堆 //所以分界点就是k=len-k publicintfindKthLargest(int[]nums,intk){ intlen=nums.length; if(k<= len - k) {             // System.out.println("使用最小堆");             // 特例:k = 1,用容量为 k 的最小堆             // 使用一个含有 k 个元素的最小堆             PriorityQueueminHeap=newPriorityQueue<>(k,(a,b)->a-b); for(inti=0;i< k; i++) {                 minHeap.add(nums[i]);             }             for (int i = k; i < len; i++) {                 // 看一眼,不拿出,因为有可能没有必要替换                 Integer topEle = minHeap.peek();                 // 只要当前遍历的元素比堆顶元素大,堆顶弹出,遍历的元素进去                 if (nums[i] >topEle){ minHeap.poll(); minHeap.add(nums[i]); } } returnminHeap.peek(); }else{ //System.out.println("使用最大堆"); assertk>len-k; //特例:k = 100,用容量为 len - k + 1 的最大堆 intcapacity=len-k+1; PriorityQueuemaxHeap=newPriorityQueue<>(capacity,(a,b)->b-a); for(inti=0;i< capacity; i++) {                 maxHeap.add(nums[i]);             }             for (int i = capacity; i < len; i++) {                 // 看一眼,不拿出,因为有可能没有必要替换                 Integer topEle = maxHeap.peek();                 // 只要当前遍历的元素比堆顶元素大,堆顶弹出,遍历的元素进去                 if (nums[i] < topEle) {                     maxHeap.poll();                     maxHeap.add(nums[i]);                 }             }             return maxHeap.peek();         }     } }

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 算法
    +关注

    关注

    23

    文章

    4800

    浏览量

    98515
  • 数组
    +关注

    关注

    1

    文章

    420

    浏览量

    27458

原文标题:超详细!详解一道高频算法题:数组中的第 K 个最大元素

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    芯片的“第一道体检”:文读懂CP测试,半导体人必看!

    在芯片从晶圆到成品的漫长旅程里,有一道看不见却至关重要的关卡——CP测试。它被称为芯片良率的“守门员”、封装成本的“节流阀”,更是半导体产业链里前端制造与后端封测之间的关键枢纽。今天这篇,用通俗
    的头像 发表于 04-17 10:03 308次阅读
    芯片的“第<b class='flag-5'>一道</b>体检”:<b class='flag-5'>一</b>文读懂CP测试,半导体人必看!

    EOL测试系统——电池包下线前的最后一道品质防线

    在储能电池包的生产线上,EOL测试系统 是产品交付前的最后一道关口,也是最重要的品质防线。EOL测试的全面性与可靠性,直接决定了流入市场的储能产品是否存在早期失效风险。 完整的电池包EOL测试
    的头像 发表于 03-23 16:40 351次阅读
    EOL测试系统——电池包下线前的最后<b class='flag-5'>一道</b>品质防线

    嵌入式春招笔试高频算法(附解题思路)

    实现”的逻辑,帮你吃透解题技巧,举反三。 高频算法1:数组排序(冒泡排序,必考) 嵌入式
    发表于 03-18 10:08

    单片机常用的14C语言算法分享

    ) 基本思想:(将相邻两个数比较,小的调到前头) 1)有n个数(存放在数组a(n)),第趟将每相邻两个数比较,小的调到前头,经n-1次两两相邻比较后,最大的数已“沉底”,放在最后
    发表于 01-29 06:59

    FFT算法原理详解

    ]={1,1,1,1,1,1,1,1}; struct complex1{ //定义复数结构体 double real; //实部 double image; //虚部 }; //将input的实数结果
    发表于 01-22 06:36

    C语言插入排序算法和代码

    和待插入的元素。第轮时,将第一个元素作为排序好的子数组,插入第二
    发表于 01-15 06:44

    3秒响应、实时告警!智能井盖如何成为城市安全的“第一道防线”?

    IP68防护、-40℃~80℃宽温运行及10年超长续航,支持自定义报警阈值与多级告警机制,大幅降低误报率。作为城市物联网感知层的关键节点,智能井盖已融入智慧城管与应急管理体系,成为守护市民脚下安全的“第一道防线”。
    的头像 发表于 12-09 11:57 457次阅读
    3秒响应、实时告警!智能井盖如何成为城市安全的“第<b class='flag-5'>一道</b>防线”?

    线性搜索与二分搜索介绍

    线性搜索(Linear Search):从数组的第一个元素开始,依次将当前元素与目标值进行比较,直到找到目标值或搜索完整个数组。 二分搜索(
    发表于 12-01 07:36

    数组的初体验

    名称[1] = 元素2; 数组名称[n-1] = 元素n; 我们将数据放到数组之后又如何获取数组
    发表于 11-25 08:06

    二维数组介绍

    定义是这样的: int array[n][m] 访问: array[a] 那么被访问元素地址的计算方式就是: array + (m * a + b) 这个就是二维数组在内存的本质,其实和
    发表于 11-25 07:42

    不间断电源(UPS):电力保障的“最后一道防线”

    (UninterruptiblePowerSupply,简称UPS)作为电力保障的“最后一道防线”,通过储能装置与智能转换技术,在市电中断时实现零切换时间供电,成为现代社会的“电力守护者”。、UP
    的头像 发表于 10-29 09:02 1930次阅读
    不间断电源(UPS):电力保障的“最后<b class='flag-5'>一道</b>防线”

    RISCV-K指令集扩展分享

    的计算。 在Decode模块中用于对K类型指令进行解码的关键代码: K扩展的针对的AES加密算法主要由以下四步骤组成:AddRoundKey:矩阵
    发表于 10-23 06:12

    电能质量在线监测装置的高频噪声滤波功能有哪些参数可以配置?

    )动态调整,以实现 “精准滤除噪声、完整保留有用信号” 的目标。以下是可配置的核心参数及其工程意义: 、硬件滤波参数(信号采集前端) 硬件滤波是高频噪声抑制的 “第一道防线”,其参数配置直接影响噪声衰减能力与有用信号完整性。
    的头像 发表于 10-15 16:43 735次阅读

    顶坚国产防爆手持终端如何成为石化企业安全生产的第一道防线

    顶坚国产防爆手持终端之所以能成为石化企业安全生产的第一道防线,源于其通过防爆设计、功能集成、实时交互与系统协同,从物理安全、功能安全、管理安全、应急安全等维度,覆盖了安全生产的全流程(预防、监测
    的头像 发表于 08-26 10:31 976次阅读
    顶坚国产防爆手持终端如何成为石化企业安全生产的第<b class='flag-5'>一道</b>防线

    【嘉楠堪智K230开发板试用体验】高校竞赛-2025电赛-E

    2025年全国大学生电子设计大赛本科组有三题目涉及视觉,K230的选用率也很高,所以我的视角简单分析K230在本次电赛的优势,以及01studio厂商
    发表于 08-21 15:32