LeetCode 记录-215. 数组中的第K个最大元素数
我的解法
思路
我的想法就是降序排序后取出k位置的值。但这显然不能满足的要求,因为排序的时间复杂度是。
官方解法 1: 基于快速排序的选择方法
思路
我们知道,快速排序是一个典型的分治算法。大致过程是:随机选择数组中的一个数,将大于、小于这个数的数各自划分在两边,然后对这两个划分继续上述过程,最后合并起来,就可以在的平均时间复杂度下排序好数组。
然而这并不满足的时间复杂度。但我们可以在快速排序的基础上进行优化,它就是「快速选择」算法:在快速排序算法中,会随机选择一个数(假设下标为idx),我们将比这个数大的划分在左侧,小的划分在右侧,那么这个数就是该数组中第idx+1个最大的数。然后我们可以通过判断idx+1和k的大小,继续选择在左分区还是右分区中继续「快速选择」算法,直到找到第k个最大的数。
代码
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function (nums, k) {
return quickSort(nums, k, 0, nums.length - 1);
};
const quickSort = (nums, k, left, right) => {
const index = Math.floor(Math.random() * (right - left + 1)) + left;
const flag = nums[index];
nums[index] = nums[left];
let i = left, j = right;
while (i < j) {
while (i < j && nums[j] <= flag) j--;
nums[i] = nums[j];
while (i < j && nums[i] >= flag) i++;
nums[j] = nums[i];
}
nums[i] = flag;
if (i === k - 1) {
return flag;
} else if (i < k - 1) {
return quickSort(nums, k, i + 1, right);
} else {
return quickSort(nums, k, left, i - 1)
}
}
复杂度分析
时间复杂度
,引入随机后,「快速选择」算法的时间复杂度为,证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」。
空间复杂度
,递归使用栈空间的空间代价的期望为。
官方解法 2: 基于堆排序的选择方法
思路
我们可以用堆排序来解决这个问题:首先建立一个大顶堆,做k-1次删除操作后,在堆顶的元素就是我们要的答案。
代码
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function (nums, k) {
let heapSize = nums.length;
buildMaxHeap(nums, heapSize);
for (let i = nums.length - 1; i >= nums.length - k + 1; --i) {
swap(nums, 0, i);
--heapSize;
maxHeapify(nums, 0, heapSize);
}
console.log(nums[0]);
return nums[0];
};
var buildMaxHeap = (nums, heapSize) => {
for (let i = Math.floor(heapSize / 2); i >= 0; --i) {
maxHeapify(nums, i, heapSize);
}
}
var maxHeapify = (nums, i, heapSize) => {
let l = i * 2 + 1, r = i * 2 + 2, largest = i;
if (l < heapSize && nums[l] > nums[largest]) {
largest = l;
}
if (r < heapSize && nums[r] > nums[largest]) {
largest = r;
}
if (largest !== i) {
swap(nums, i, largest);
maxHeapify(nums, largest, heapSize);
}
}
var swap = (nums, i, j) => {
const temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
复杂度分析
时间复杂度
,建堆的时间代价是,删除的总代价为,因为, 故渐进时间复杂度为。
空间复杂度
,递归使用栈空间的空间代价的期望为。
衍生知识点-堆
定义
堆是一种特殊的树,只要满足下面两个条件,它就是一个堆:
- 堆是一颗完全二叉树
- 堆中的某个节点总是不大于(或不小于)其父节点的值
其中,我们把根节点最大的堆叫大顶堆,根节点最小的堆叫小顶堆。
如何通过堆来排序?
给定一个待排序的数列,进行如下操作:
- 建堆:将数列建成一个堆
- 输出:删除根节点,将其输出,然后将最后一个叶子节点移动到根节点
- 调整:移动之后的树可能不满足堆的性质,此时需要进行一轮调整,使其成为一个新堆
- 不断进行步骤2,3就可以得到排好的数列
建堆:从上往下建堆vs.从下往上建堆
这里只写结论,具体逻辑和证明过程请看:zhuanlan.zhihu.com/p/341249538
- 从上往下建堆的时间复杂度为
- 从下往上建堆的时间复杂度为
输出和调整
一次调整的时间复杂度为:,最坏情况下,删除根节点后,堆高为,将最后一个节点置到根节点后,最多会下降次。