题目:给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
注意:请使用 自然语言 、伪代码等描述算法的过程(自然语言必须要有,伪代码可选)。最后分析算法的 时间复杂度和空间复杂度 。
题目要求找到数组中第k大的元素,并且要求时间复杂度为O(n),这暗示了我们需要采用一种在线处理或者近似线性时间复杂度的算法。直接排序的方案虽然可以解决问题,但其时间复杂度为O(n log n),不符合题目要求。因此,我们可以使用优先队列(也称为堆)来优化这个问题的解决方案。
解题思路
使用最小堆: 我们可以维护一个大小为k的最小堆。最小堆的特点是父节点的值总是小于或等于其子节点的值。对于本题来说,这意味着堆顶元素将是堆中最小的元素,即在已处理的元素中,堆顶元素将是第k大的元素。
遍历数组,动态调整堆: 遍历整个数组,对于每个元素执行以下操作:
如果堆的大小还没有达到k,直接将当前元素插入堆中。
如果堆的大小已经达到k,并且当前元素大于堆顶元素(即当前元素比堆中最小的元素还大),则将堆顶元素移除并将当前元素插入堆中。这样保证堆中始终是当前遇到的最大的k个元素,且堆顶元素是最小的那个,即第k大的元素。
返回结果: 当遍历完整个数组后,堆顶的元素就是数组中第k大的元素。
Java代码实现
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Scanner;
public class KthLargestElementInArray { public static void main(String[]args){ Scanner scanner = new Scanner(System.in);
System.out.println("请输入数组的元素,用空格分隔:");
String[] input = scanner.nextLine().split("");
int[] nums = new int[input.length];
for(int i = 0;i < input.length;i++){
nums[i] = Integer.parseInt(input[i]);
}
System.out.println("输入数组值:" + Arrays.toString(nums));
System.out.println("请输入k的值:");
int k = scanner.nextInt();
KthLargestElementInArray solution = new KthLargestElementInArray();
int ans = solution.findKthLargest(nums,k);
System.out.println("数组中第" + k + "大的元素是:" + ans);
scanner.close();
}
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int num : nums) {
if (minHeap.size() < k) {
minHeap.offer(num);
} else if (num > minHeap.peek()) {
minHeap.poll();
minHeap.offer(num);
}
}
return minHeap.peek();
}
详细版代码注释
//用户交互与输入处理
//1.导入所需库
import java.util.Arrays;
//Arrays用于数组操作
import java.util.PriorityQueue;
//PriorityQueue用于实现最小堆
import java.util.Scanner;
//Scanner用于接收用户输入
//类名
public class KthLargestElementInArray {
public static void main(String[]args){
Scanner scanner = new Scanner(System.in);
/*读取数组输入:使用Scanner类创建一个扫描器实例,
用于读取控制台输入。提示用户输入数组元素,要求各个元素
以空格分隔。通过scanner.nextLine()读取一行文本,
然后使用split("")按空格切割字符串,得到一个字符串数组input
*/
System.out.println("请输入数组的元素,用空格分隔:");
String[] input = scanner.nextLine().split(" ");
//String[] input是一个Java中的字符串数组声明,
//用来存储通过用户输入并按空格分割后的多个字符串
//1、scanner.nextLine()用于读取控制台输入的一行文本,
// 用户应该输入一系列整数,这些整数之间以空格分隔
//2、.split(" ")方法则是将读取到的这行文本按照空格进行切分
//返回一个字符串数组。每个数组元素对应用户输入的每个数字的字符串表示
int[] nums = new int[input.length];
for(int i = 0;i < input.length;i++){
nums[i] = Integer.parseInt(input[i]);
/*转化为整型数组:遍历字符串input,
使用Integer.parseInt()
方法将每个字符串元素转换为整数,存储到整型数组nums中
*/
}
System.out.println("输入数组值:" + Arrays.toString(nums));
/*显示数组:使用Arrays.toString(nums)将整型数组转换为
易于阅读的字符串形式,并打印出来
*/
System.out.println("请输入k的值:");
int k = scanner.nextInt();
/*读取k的值:提示用户输入整数k,
使用scanner.nextInt()读取并存储这个值*/
KthLargestElementInArray solution = new KthLargestElementInArray();
//创建一个名为solution的对象,该对象是KthLargestElementInArray类的实例
//1、KthLargestElementInArray是类名,表示我正在使用的类定义了一组特定的功能和属性,
//在这个例子中,这个类是用来找到数组中第k大的元素的解决方案
//2、solution是变量名,用来引用新创建的对象。
//3、new关键字:这是Java中的一个关键字,用来创建一个新的对象实例
//当我使用new时,Java会调用指定类的构造函数来初始化这个新对象
//4、KthLargestElementInArray()这部分调用了KthLargestElementInArray类
//的默认构造函数(没有参数的构造函数)。如果类中没有明确定义任何构造函数,
// Java会自动生成一个默认构造函数
//综上所述,这一行代码是实例化一个KthLargestElementInArray类的对象,
// 并将其引用赋值给名为solution的变量,
// 之后就可以通过solution来调用该类中定义的方法,
// 比如findKthLargest方法。
int ans = solution.findKthLargest(nums,k);
//定义查找方法:定义findKthLargest方法,
// 接受一个整型数组nums和一个整数k作为参数
System.out.println("数组中第" + k + "大的元素是:" + ans);
scanner.close();
}
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
//创建最小堆:这行代码是在Java中声明并初始化一个优先队列实例
//(priority)专门用于处理整数(Integer)的集合。
// 优先队列是一种特殊的队列,其特点是队列中的元素按照一定的顺序
// 进行排序,
//对于Java中的PriorityQueue,默认情况下它会创建一个最小堆
//即队列顶部的元素(或者说队列头)总是最小的元素。
//1、priorityQueue<Integer>:声明了一个类型参数为
// Integer的优先队列
//这意味着这个队列中只能存放Integer类型的对象,即整数。
//2、minHeap是变量名,用于引用新创建的优先队列对象
//3、=new PriorityQueue<>()这部分是实际创建优先队列对象的操作
//new关键字用于创建对象实例,而PriorityQueue<>()是无参构造函数,
//表示创建一个默认的优先队列。
// 这里表示创建的优先队列内部存储的元素类型推断自
// 左侧的类型参数,即Integer类型。
for (int num : nums) {
//for(int num : nums)是Java中的一种特殊循环语法,
// 称为"增强型for循环"或""for-each循环"。这种循环提供了一种
//简洁的方式来遍历数组或集合中的每个元素,
// 而不需要显式地管理索引或迭代器
//1、for表示一个循环结构
//2、(int num声明一个局部变量num,用于每次循环迭代中
//临时存储数组nums中的一个元素。这里num的数据类型为int整型
//3、: nums冒号后面跟着要遍历的集合或数组的名字,在这里是nums
//这表示循环会依次取出nums数组中的每一个元素,
// 并将它赋值给前面声明的变量num
if (minHeap.size() < k) {
minHeap.offer(num);
} else if (num > minHeap.peek()) {
minHeap.poll();
minHeap.offer(num);
}
}//遍历数组并维护堆:遍历数组nums中的每个元素,执行以下操作:
//(1)若堆的大小小于k,直接将当前元素添加到堆中。
//(2)否则,如果当前元素大于堆顶元素(即当前堆中最小的元素),
// 则移除堆顶元素,并将当前元素加入堆中。这样,堆中始终保持着
//数组中最大的k个元素,且堆顶元素为这k个元素中的最小值,即数组中
//的第k大元素。
return minHeap.peek();
//返回结果:遍历完成后,直接返回堆顶元素,即为所求的第k大的元素
}
}
时间复杂度和空间复杂度分析
时间复杂度
构建堆: 遍历数组,将前k个元素添加到最小堆中,这一过程的时间复杂度为O(k log k),因为每次插入操作可能需要对堆进行调整,调整的代价是O(log k)。
剩余元素处理: 对于剩下的n-k个元素,每个元素都需要与堆顶元素比较一次,如果当前元素大于堆顶元素,则进行堆的调整(删除堆顶并插入新元素),这一步的操作次数最多为n-k次。每次调整的时间复杂度为O(log k)。因此,这一步的总时间复杂度为O((n-k) log k)。
综合两步,整体的时间复杂度为O(k log k) + O((n-k) log k) = O(n log k)。这是因为当n远大于k时,后者是主要的贡献项。
空间复杂度
最小堆: 使用了大小为k的最小堆来存储可能成为第k大元素的候选值,因此空间复杂度为O(k)。
输入数组: 输入数组nums本身也占用空间,但因为它不是由函数内部创建的,所以在分析空间复杂度时不计入。主要考虑的是额外的空间,即最小堆。
综上所述,该算法的空间复杂度为O(k),时间复杂度为O(n log k)。