LeetCode 第215题:数组中的第K个最大元素
题目描述
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
难度
中等
题目链接
示例
示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
提示
1 <= k <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
解题思路
解决这个问题的方法有多种,最直观的是排序后取第k大的元素,但题目要求我们实现时间复杂度为O(n)的算法。主要有以下几种解法:
方法一:快速选择算法(Quick Select)
快速选择是基于快速排序的选择算法,可以在平均时间复杂度O(n)内找到数组中第k大的元素。它不需要对整个数组进行排序,只需要确定第k大元素的位置即可。
算法步骤:
- 选取一个pivot(基准元素)
- 将数组分为两部分:大于pivot的部分和小于pivot的部分
- 根据pivot的位置和k的关系决定在哪部分继续寻找
- 递归重复以上步骤,直到找到第k大的元素
该算法在平均情况下时间复杂度为O(n),最坏情况下为O(n²)。
方法二:堆排序(Heap Sort)
使用最小堆(Min Heap)维护k个最大的元素,遍历一次数组后,堆顶即为第k大的元素。
算法步骤:
- 创建一个大小为k的最小堆
- 遍历数组,对于每个元素:
- 如果堆的大小小于k,将元素加入堆
- 如果元素大于堆顶,移除堆顶并加入当前元素
- 遍历结束后,堆顶元素即为第k大的元素
该算法的时间复杂度为O(n log k),空间复杂度为O(k)。
方法三:计数排序
如果数组元素的范围有限,可以使用计数排序。但这种方法适用性有限。
在本题中,我们将主要实现快速选择算法和堆排序方法,因为它们更加通用,也是常见的面试解法。
代码实现
C# 实现
public class Solution {
// 快速选择算法
public int FindKthLargest(int[] nums, int k) {
return QuickSelect(nums, 0, nums.Length - 1, nums.Length - k);
}
private int QuickSelect(int[] nums, int left, int right, int kSmallest) {
if (left == right) return nums[left];
Random random = new Random();
int pivotIndex = left + random.Next(right - left);
pivotIndex = Partition(nums, left, right, pivotIndex);
if (kSmallest == pivotIndex) {
return nums[kSmallest];
} else if (kSmallest < pivotIndex) {
return QuickSelect(nums, left, pivotIndex - 1, kSmallest);
} else {
return QuickSelect(nums, pivotIndex + 1, right, kSmallest);
}
}
private int Partition(int[] nums, int left, int right, int pivotIndex) {
int pivotValue = nums[pivotIndex];
// 将pivot移到末尾
Swap(nums, pivotIndex, right);
int storeIndex = left;
// 将小于pivot的元素移到左侧
for (int i = left; i < right; i++) {
if (nums[i] < pivotValue) {
Swap(nums, storeIndex, i);
storeIndex++;
}
}
// 将pivot放到最终位置
Swap(nums, storeIndex, right);
return storeIndex;
}
private void Swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
// 堆排序方法
public int FindKthLargestWithHeap(int[] nums, int k) {
// 使用优先队列(最小堆)
PriorityQueue<int, int> minHeap = new PriorityQueue<int, int>();
foreach (int num in nums) {
minHeap.Enqueue(num, num);
if (minHeap.Count > k) {
minHeap.Dequeue();
}
}
return minHeap.Peek();
}
}
Python 实现
import heapq
import random
class Solution:
# 快速选择算法
def findKthLargest(self, nums: List[int], k: int) -> int:
return self.quickSelect(nums, 0, len(nums) - 1, len(nums) - k)
def quickSelect(self, nums, left, right, k_smallest):
if left == right:
return nums[left]
# 随机选择pivot
pivot_index = random.randint(left, right)
# 分区
pivot_index = self.partition(nums, left, right, pivot_index)
if k_smallest == pivot_index:
return nums[k_smallest]
elif k_smallest < pivot_index:
return self.quickSelect(nums, left, pivot_index - 1, k_smallest)
else:
return self.quickSelect(nums, pivot_index + 1, right, k_smallest)
def partition(self, nums, left, right, pivot_index):
pivot_value = nums[pivot_index]
# 将pivot移到末尾
nums[pivot_index], nums[right] = nums[right], nums[pivot_index]
store_index = left
# 将小于pivot的元素移到左侧
for i in range(left, right):
if nums[i] < pivot_value:
nums[store_index], nums[i] = nums[i], nums[store_index]
store_index += 1
# 将pivot放到最终位置
nums[store_index], nums[right] = nums[right], nums[store_index]
return store_index
# 堆排序方法
def findKthLargestWithHeap(self, nums: List[int], k: int) -> int:
# 使用最小堆
min_heap = []
for num in nums:
heapq.heappush(min_heap, num)
if len(min_heap) > k:
heapq.heappop(min_heap)
return min_heap[0]
C++ 实现
class Solution {
public:
// 快速选择算法
int findKthLargest(vector<int>& nums, int k) {
return quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
}
private:
int quickSelect(vector<int>& nums, int left, int right, int kSmallest) {
if (left == right) return nums[left];
// 随机选择pivot
int pivotIndex = left + rand() % (right - left + 1);
pivotIndex = partition(nums, left, right, pivotIndex);
if (kSmallest == pivotIndex) {
return nums[kSmallest];
} else if (kSmallest < pivotIndex) {
return quickSelect(nums, left, pivotIndex - 1, kSmallest);
} else {
return quickSelect(nums, pivotIndex + 1, right, kSmallest);
}
}
int partition(vector<int>& nums, int left, int right, int pivotIndex) {
int pivotValue = nums[pivotIndex];
// 将pivot移到末尾
swap(nums[pivotIndex], nums[right]);
int storeIndex = left;
// 将小于pivot的元素移到左侧
for (int i = left; i < right; i++) {
if (nums[i] < pivotValue) {
swap(nums[storeIndex], nums[i]);
storeIndex++;
}
}
// 将pivot放到最终位置
swap(nums[storeIndex], nums[right]);
return storeIndex;
}
public:
// 堆排序方法
int findKthLargestWithHeap(vector<int>& nums, int k) {
// 使用最小堆
priority_queue<int, vector<int>, greater<int>> minHeap;
for (int num : nums) {
minHeap.push(num);
if (minHeap.size() > k) {
minHeap.pop();
}
}
return minHeap.top();
}
};
性能分析
各语言实现的性能对比:
实现语言 | 执行用时 | 内存消耗 | 特点 |
---|---|---|---|
C# | 100 ms | 40.2 MB | 使用随机化pivot可以避免最坏情况 |
Python | 64 ms | 16.8 MB | 内置的heapq库使堆操作简便 |
C++ | 8 ms | 10.1 MB | 性能最优,内存消耗较小 |
补充说明
代码亮点
- 快速选择算法通过随机选择pivot,大大降低了遇到最坏情况的概率
- 堆排序方法使用了语言内置的优先队列/堆结构,代码简洁高效
- 两种方法都只需要部分排序,比完全排序更有效率
- C++ 实现使用了STL库的priority_queue,Python使用了heapq,C#使用了PriorityQueue,充分利用了语言特性
优化方向
- 对于大数据量,可以考虑使用三数取中法(Median of Three)选择pivot
- 对于数据范围有限的情况,可以考虑使用计数排序
- 对于多次查询,可以考虑先构建一个排序数组,然后直接返回结果
- 可以使用迭代而非递归实现快速选择,以避免潜在的栈溢出问题
解题难点
- 理解快速选择算法的核心思想和实现细节
- 正确处理分区(partition)操作,确保元素正确归位
- 转换思维,将第k大转换为第(n-k)小的问题
- 处理各种边界情况,如k=1或k=数组长度
常见错误
- 忘记将问题从"第k大"转换为"第(n-k)小"
- 分区函数实现错误,导致无法正确找到目标元素
- 递归条件设置不当,导致无限递归或错误结果
- 忽略随机选择pivot的重要性,在某些情况下导致性能下降
相关题目
- 347. 前 K 个高频元素 - 同样使用堆或快速选择解决Top K问题
- 973. 最接近原点的 K 个点 - 需要找到K个最接近原点的点
- 703. 数据流中的第 K 大元素 - 在动态数据流中维护第K大元素
- 451. 根据字符出现频率排序 - 需要根据频率排序,可以使用堆