215. 数组中的第K个最大元素
1985. 找出数组中的第 K 大整数1738. 找出第 K 大的异或坐标值 给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
方法一:排序
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
# nums.sort(reverse = True)
# return nums[k-1]
# nums.sort()
# return nums[-k]
return sorted(nums)[-k]
方法二:
快速排序和堆排序的标准代码
方法一:基于快速排序的选择方法
先对原数组排序,再返回倒数第 k 个位置,这样平均时间复杂度是 ,但其实我们可以做的更快。
首先我们来回顾一下快速排序,这是一个典型的分治算法。我们对数组
分解: 将数组 「划分」成两个子数组 ,使得 中的每个元素小于等于 ,且 小于等于$ a[q + 1 \cdots r]$ 中的每个元素。其中,计算下标 q 也是「划分」过程的一部分。
解决: 通过递归调用快速排序,对子数组 和 进行排序。
合并: 因为子数组都是原址排序的,所以不需要进行合并操作, 已经有序。
上文中提到的 「划分」 过程是:从子数组 中选择任意一个元素 x 作为主元,调整子数组的元素使得左边的元素都小于等于它,右边的元素都大于等于它, x 的最终位置就是 q。
由此可以发现每次经过「划分」操作后,我们一定可以确定一个元素的最终位置,即 x 的最终位置为 q,并且保证 中的每个元素小于等于 ,且 小于等于 中的每个元素。所以只要某次划分的 q 为倒数第 k 个下标的时候,我们就已经找到了答案。 我们只关心这一点,至于 和
因此我们可以改进快速排序算法来解决这个问题:在分解的过程当中,我们会对子数组进行划分,如果划分得到的 q 正好就是我们需要的下标,就直接返回 ;否则,如果 q 比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。这就是「快速选择」算法。
我们知道快速排序的性能和「划分」出的子数组的长度密切相关。直观地理解如果每次规模为 n 的问题我们都划分成 1 和 n - 1,每次递归的时候又向 n - 1 的集合中递归,这种情况是最坏的,时间代价是 。我们可以引入随机化来加速这个过程,它的时间代价的期望是 O(n),证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」。
class Solution {
public:
int quickSelect(vector<int>& a, int l, int r, int index) {
int q = randomPartition(a, l, r);
if (q == index) {
return a[q];
} else {
return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
}
}
inline int randomPartition(vector<int>& a, int l, int r) {
int i = rand() % (r - l + 1) + l;
swap(a[i], a[r]);
return partition(a, l, r);
}
inline int partition(vector<int>& a, int l, int r) {
int x = a[r], i = l - 1;
for (int j = l; j < r; ++j) {
if (a[j] <= x) {
swap(a[++i], a[j]);
}
}
swap(a[i + 1], a[r]);
return i + 1;
}
int findKthLargest(vector<int>& nums, int k) {
srand(time(0));
return quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
}
};
我们也可以使用堆排序来解决这个问题——建立一个大根堆,做 k - 1k−1 次删除操作后堆顶元素就是我们要找的答案。在很多语言中,都有优先队列或者堆的的容器可以直接使用,但是在面试中,面试官更倾向于让更面试者自己实现一个堆。所以建议读者掌握这里大根堆的实现方法,在这道题中尤其要搞懂「建堆」、「调整」和「删除」的过程。
友情提醒:「堆排」在很多大公司的面试中都很常见,不了解的同学建议参考《算法导论》或者大家的数据结构教材,一定要学会这个知识点哦!_
class Solution {
public:
void maxHeapify(vector<int>& a, int i, int heapSize) {
int l = i * 2 + 1, r = i * 2 + 2, largest = i;
if (l < heapSize && a[l] > a[largest]) {
largest = l;
}
if (r < heapSize && a[r] > a[largest]) {
largest = r;
}
if (largest != i) {
swap(a[i], a[largest]);
maxHeapify(a, largest, heapSize);
}
}
void buildMaxHeap(vector<int>& a, int heapSize) {
for (int i = heapSize / 2; i >= 0; --i) {
maxHeapify(a, i, heapSize);
}
}
int findKthLargest(vector<int>& nums, int k) {
int heapSize = nums.size();
buildMaxHeap(nums, heapSize);
for (int i = nums.size() - 1; i >= nums.size() - k + 1; --i) {
swap(nums[0], nums[i]);
--heapSize;
maxHeapify(nums, 0, heapSize);
}
return nums[0];
}
};
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
def adju_max_heap(nums_list, in_node): # 从当前内部节点处修正大根堆
""""in_node是内部节点的索引"""
l, r, large_idx= 2*in_node+1, 2*in_node+2, in_node # 最大值的索引默认为该内部节点
if l < len(nums_list) and nums_list[large_idx] < nums[l]:
# 如果左孩子值大于该内部节点的值,则最大值索引指向左孩子
large_idx = l
if r < len(nums_list) and nums_list[large_idx] < nums[r]:
# 如果执行了上一个if语句,此时最大值索引指向左孩子,否则还是指向该内部节点
# 然后最大值索引指向的值和右孩子的值比较
large_idx = r
# 上述两个if就是得到(内部节点,左孩子,右孩子)中最大值的索引
if large_idx != in_node: # 如果最大值在左孩子和右孩子中,则和内部节点交换
nums_list[large_idx], nums_list[in_node] = nums_list[in_node], nums_list[large_idx]
# 如何内部节点是和左孩子交换,那就递归修正它的左子树,否则递归修正它的右子树
adju_max_heap(nums_list, large_idx)
def build_max_heap(nums_list): # 由列表建立大根堆
""""从后往前遍历所有内部节点,其中最后一个内部节点的公式为len(nums_list)//2 - 1"""
for in_node in range(len(nums_list)//2 - 1, -1, -1):
adju_max_heap(nums_list, in_node)
def find_kth_max(nums_list, k): # 从列表中找到第k个最大的
build_max_heap(nums_list) # 先建立大根堆
for _ in range(k-1):
nums_list[0], nums_list[-1] = nums_list[-1], nums_list[0] # 堆头和堆尾交换
nums_list.pop() # 删除堆尾
adju_max_heap(nums_list, 0) # 从堆头处开始修正大根堆
return nums_list[0]
return find_kth_max(nums, k)