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 个位置,这样平均时间复杂度是 215. 数组中的第K个最大元素_算法,但其实我们可以做的更快。

首先我们来回顾一下快速排序,这是一个典型的分治算法。我们对数组 215. 数组中的第K个最大元素_递归_02

分解: 将数组 215. 数组中的第K个最大元素_递归_02 「划分」成两个子数组 215. 数组中的第K个最大元素_算法_04,使得 215. 数组中的第K个最大元素_python_05 中的每个元素小于等于 215. 数组中的第K个最大元素_算法_06,且 215. 数组中的第K个最大元素_算法_06 小于等于$ a[q + 1 \cdots r]$ 中的每个元素。其中,计算下标 q 也是「划分」过程的一部分。
解决: 通过递归调用快速排序,对子数组 215. 数组中的第K个最大元素_python_05215. 数组中的第K个最大元素_算法_09 进行排序。
合并: 因为子数组都是原址排序的,所以不需要进行合并操作,215. 数组中的第K个最大元素_递归_02 已经有序。
上文中提到的 「划分」 过程是:从子数组 215. 数组中的第K个最大元素_递归_02 中选择任意一个元素 x 作为主元,调整子数组的元素使得左边的元素都小于等于它,右边的元素都大于等于它, x 的最终位置就是 q。
由此可以发现每次经过「划分」操作后,我们一定可以确定一个元素的最终位置,即 x 的最终位置为 q,并且保证 215. 数组中的第K个最大元素_python_05 中的每个元素小于等于 215. 数组中的第K个最大元素_算法_06,且 215. 数组中的第K个最大元素_算法_06 小于等于 215. 数组中的第K个最大元素_算法_09 中的每个元素。所以只要某次划分的 q 为倒数第 k 个下标的时候,我们就已经找到了答案。 我们只关心这一点,至于 215. 数组中的第K个最大元素_python_05215. 数组中的第K个最大元素_子数组_17

因此我们可以改进快速排序算法来解决这个问题:在分解的过程当中,我们会对子数组进行划分,如果划分得到的 q 正好就是我们需要的下标,就直接返回 215. 数组中的第K个最大元素_算法_06;否则,如果 q 比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。这就是「快速选择」算法。

我们知道快速排序的性能和「划分」出的子数组的长度密切相关。直观地理解如果每次规模为 n 的问题我们都划分成 1 和 n - 1,每次递归的时候又向 n - 1 的集合中递归,这种情况是最坏的,时间代价是 215. 数组中的第K个最大元素_python_19。我们可以引入随机化来加速这个过程,它的时间代价的期望是 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)