当前位置: 首页 > news >正文

【LeetCode 热题 100】215. 数组中的第K个最大元素——(解法一)快速选择

Problem: 215. 数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:O(N) (平均情况), O(N^2) (最坏情况)
    • 空间复杂度:O(1)

整体思路

这段代码旨在解决一个经典的 Top-K 问题:数组中的第K个最大元素 (Kth Largest Element in an Array)。问题要求在一个未排序的数组中,找到第 k 大的元素。

该算法采用了一种非常高效的、基于 快速排序 (QuickSort) 思想的 快速选择 (Quickselect) 算法。与完整排序不同,快速选择算法只关心目标元素所在的分区,从而避免了对整个数组进行完全排序,大大降低了平均时间复杂度。

  1. 问题转化

    • 算法首先将“找到第 k 大的元素”这个问题,转化为“找到排序后索引为 n-k 的元素”。
    • 例如,在一个长度为 5 的数组中找第 2 大的元素,等价于找排序后索引为 5-2=3 的元素(假设索引从0开始)。这个 target = n-k 就是我们的目标索引。
  2. 核心:partition 分区函数

    • partition 函数是快速选择算法的心脏。它的作用是,在数组的 [left, right] 区间内,随机选择一个元素作为 枢轴 (pivot),然后将数组重新排列,使得:
      • 所有小于 pivot 的元素都在 pivot 的左边。
      • 所有大于 pivot 的元素都在 pivot 的右边。
    • partition 函数最终会返回 pivot 元素在分区后的最终索引 p
    • 随机化:代码通过 RANDOM.nextInt 随机选择一个 pivot,然后将其与区间的第一个元素交换。这是一个至关重要的优化,它可以极大地避免在遇到有序或接近有序的数组时,算法退化到最坏的 O(N^2) 情况。
  3. 迭代选择与缩小范围

    • findKthLargest 方法的主体是一个 while(true) 循环,它不断地调用 partition 函数并缩小搜索范围,直到找到目标。
    • 在每次分区后,我们得到枢轴的最终索引 p。然后比较 p 和我们的目标索引 target
      • 如果 p == target:这说明我们选中的 pivot 正好就是我们要找的第 k 大的元素。我们非常幸运,直接返回 nums[p] 即可。
      • 如果 p < target:这说明目标元素在 pivot右侧。我们就可以完全忽略左半部分,只需在 [p+1, right] 这个更小的区间内继续搜索。
      • 如果 p > target:这说明目标元素在 pivot左侧。我们就可以完全忽略右半部分,只需在 [left, p-1] 这个更小的区间内继续搜索。
    • 这个过程不断重复,每次都将搜索范围缩小,直到找到目标。

完整代码

class Solution {// 使用一个静态的 Random 实例,以当前时间为种子,用于随机化选择private final static Random RANDOM = new Random(System.currentTimeMillis());/*** 在未排序的数组中找到第 k 大的元素。* @param nums 整数数组* @param k 第 k 大* @return 第 k 大的元素值*/public int findKthLargest(int[] nums, int k) {int n = nums.length;int left = 0;int right = n - 1;  // 核心转化:第 k 大的元素,等价于排序后索引为 n-k 的元素。int target = n - k;// 使用循环不断缩小搜索范围,直到找到目标索引while (true) {// 对当前 [left, right] 区间进行分区,返回枢轴的最终位置 pint p = partition(nums, left, right);if (p == target) {// 如果枢轴的位置就是目标位置,则找到了答案return nums[p];} else if (p < target) {// 如果枢轴位置在目标左侧,说明目标在右半部分,更新左边界left = p + 1;} else { // p > target// 如果枢轴位置在目标右侧,说明目标在左半部分,更新右边界right = p - 1;}}}/*** 对 nums 数组的 [left, right] 区间进行分区操作(三路快排的变体)。* @param nums 数组* @param left 区间左边界* @param right 区间右边界* @return 枢轴元素分区后的最终索引*/private int partition(int[] nums, int left, int right) {// 随机化:随机选择一个索引作为枢轴,避免最坏情况int randomIndex = left + RANDOM.nextInt(right - left + 1);swap(nums, left, randomIndex);// 选择区间的第一个元素作为枢轴 pivotint pivot = nums[left];// le: 小于 pivot 的区域的右边界// ge: 大于 pivot 的区域的左边界int le = left + 1;int ge = right;while (true) {// 从左向右找到第一个 >= pivot 的元素while (le <= ge && nums[le] < pivot) {le++;}// 从右向左找到第一个 <= pivot 的元素while (le <= ge && nums[ge] > pivot) {ge--;}// 如果两个指针交错,分区完成if (le >= ge) {break;}// 交换找到的两个元素,使数组更有序swap(nums, le, ge);// 移动指针继续下一轮查找le++;ge--;}// 将枢轴元素放到其最终位置(ge 指向的位置)swap(nums, left, ge);return ge;}/*** 辅助函数:交换数组中两个索引的元素。*/private void swap(int[] nums, int i, int j) {int t = nums[i];nums[i] = nums[j];nums[j] = t;}
}

时空复杂度

时间复杂度:O(N) (平均情况), O(N^2) (最坏情况)

  1. 平均情况
    • 由于我们每次都随机选择 pivot,可以期望 partition 操作每次都能将搜索区间大约减半。
    • 第一次分区,处理 N 个元素。第二次,处理大约 N/2 个元素。第三次,处理大约 N/4 个元素,以此类推。
    • 总的操作次数是一个等比数列求和:N + N/2 + N/4 + ... + 1。这个级数的和收敛于 2N
    • 因此,平均情况下的时间复杂度为 O(N)
  2. 最坏情况
    • 尽管随机化大大降低了最坏情况的概率,但理论上仍然存在。
    • 最坏情况发生在我们每次选择的 pivot 都是当前区间的最大或最小值。这会导致分区后,搜索范围只减少了 1。
    • 此时,总操作次数为 N + (N-1) + (N-2) + ... + 1,这是一个 O(N^2) 的过程。
    • 然而,在实际应用中,由于随机化的存在,这种最坏情况几乎不可能发生。

空间复杂度:O(1)

  1. 主要存储开销:该算法是原地的,它直接在输入的 nums 数组上进行修改,没有使用任何与输入规模 N 成比例的额外数据结构。
  2. 辅助变量:算法只使用了 n, left, right, target, p, le, ge, pivot 等固定数量的变量。
  3. 递归深度:代码使用的是迭代(while 循环)而非递归,因此没有因递归调用而产生的栈空间开销。

综合分析
算法所需的额外辅助空间是常数级别的。因此,其空间复杂度为 O(1)

参考:liweiwei1419

http://www.lryc.cn/news/609955.html

相关文章:

  • 安卓逆向(基础①-Google Pixel-Root)
  • Visual Studio 2022安装与快捷键全攻略
  • 模型蒸馏(Distillation):原理、算法、应用
  • 【达梦MPP(带主备)集群搭建】
  • Selenium教程(Python 网页自动化测试脚本)
  • 华为2288H V5服务器闪红灯 无法开机案例
  • C++八股文——设计模式
  • JSON Schema
  • mybatis-plus报错Caused by: java.sql.SQLException: 无效的列类型: 1111
  • 使用 Aspose.OCR 将图像文本转换为可编辑文本
  • 微软WSUS替代方案
  • Druid手写核心实现案例 实现一个简单Select 解析,包含Lexer、Parser、AstNode
  • AJAX表单验证项目实战:实时用户名检查
  • curl发送文件bodyParser无法获取请求体的问题分析
  • Stanford CS336 assignment1 | Byte-Pair Encoding (BPE) Tokenizer
  • NeoBase:一款开源、基于AI的数据库管理助手
  • 《Python 实用项目与工具制作指南》· 2.2 变量
  • Java中给List<T> 对象集合去重
  • golang的数组
  • SpringMVC 6+源码分析(三)DispatcherServlet实例化流程 2--(url 与contrller类如何进行映射)
  • 【Spring AI快速上手 (一)】ChatModel与ChatCilent构建对话
  • 小鹏汽车前端面经
  • Python+QT开发环境搭建
  • 数据从mysql迁移到postgresql
  • 纯前端导出Excel
  • MCP安全机制深度剖析:权限控制与数据保护最佳实践
  • 体验Java接入langchain4j运用大模型OpenAi
  • 学习游戏制作记录(角色属性和状态脚本)8.4
  • 多源异构信号同步采集与赛道数据融合技术解析
  • 迅为RK3568开发板OpeHarmony学习开发手册-修改调试串口波特率