题目

代码-sort
最开始最简单粗暴的解法,当然也是顺利通过了。
javascript
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function(nums, k) {
nums.sort((a, b) => b - a)
return nums[k - 1]
};
代码-快速选择
但是呢这题如果真就这样写的话那肯定不能是中等难度了,于是想到会不会是要自己手搓排序。而我想到的快排时间复杂度是O(nlogn),跟题目要求的O(n)比起来还是太高了,经过题解和d老师的指导,了解到可以对快速排序做一个优化,也就是------快速选择。
我们知道快速排序是选一个基准,然后小于基准的放左边,大于基准的放右边,最后再找到基准应该在的位置放进去。之后左右分别递归以上过程,递归的终止条件是需要排序的数组长度为1。而这题要找的是第k大的元素,所以只需要找这个元素在的那半边就行了。
那我们怎么知道这元素在哪半边呢?用下标判断。第k大的元素是排序好了之后的数组的numsnums.length - k,而我们选择的基准在放到它该在的位置的时候也能获取到下标,因此就可以去做判断:如果相等,就找到了;如果不相等,那就去处理目标元素在的那半边,另一边就不用再排序处理了。
javascript
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function(nums, k) {
const target = nums.length - k
const swap = (arr, i, j) => {
[arr[i], arr[j]] = [arr[j], arr[i]]
}
// 分区
const partition = (left, right) => {
// 随机选基准
const pivotId = Math.floor(Math.random() * (right - left + 1)) + left
const pivot = nums[pivotId]
// 交换基准与最右侧数的位置,便于比较
swap(nums, pivotId, right)
// 记录当前交换位置
let storeId = left
// 比较
for (let i = left; i < right; i++) {
if (nums[i] <= pivot) {
swap(nums, i, storeId)
storeId++
}
}
// 把基准放到正确的位置
swap(nums, storeId, right)
return storeId
}
let left = 0, right = nums.length - 1
while (left <= right) {
const pivotIndex = partition(left, right)
if (pivotIndex === target) {
return nums[pivotIndex]
} else if (pivotIndex < target) {
left = pivotIndex + 1
} else {
right = pivotIndex - 1
}
}
return -1
};
tip:这里随机选基准是为了避免最坏的情况,也就是完全降序的数组。
这个代码看起来万事大吉对吧!时间复杂度符合要求理论上也应该没问题。但是!遇到下面这样的样例就超时了:

也就是有很多很多重复元素的场景下,快速选择算法的时间复杂度会退化成O(n²),导致超时。d老师有话说:
为什么重复元素会导致 O(n²)?
因为
partition使用<= pivot,把所有等于 pivot 的元素都放到了左边 。当数组里绝大多数元素都等于 pivot(比如随机选到了
1),左边会包含几乎整个数组(因为1 <= 1成立),而右边只有很少的元素。这样每次递归只能排除掉一个或几个元素,总操作数变成n + (n-1) + (n-2) + ...,就是 O(n²)。虽然用了随机基准,但碰到这种大量重复的数字,随机选到
1的概率极高(因为 1 占了绝大部分),所以基本每次都会退化。
那这时候咋办呢?一看问题其实是出在重复的元素上,那能不能单独处理这些重复的元素呢?可以!三路划分(荷兰国旗)快速选择可以!
代码-三路划分
把数组分成三块:小于 pivot 、等于 pivot 、大于 pivot 。
这样当 pivot 是重复值时,所有等于 pivot 的元素一次性归位,不再参与后续递归,避免了重复元素的干扰。
javascript
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function(nums, k) {
const target = nums.length - k
const swap = (i, j) => {
[nums[i], nums[j]] = [nums[j], nums[i]]
}
// 三路划分,分为小于pivot、等于pivot、大于pivot的三个区间
const threePathPartition = (left, right) => {
const pivotId = Math.floor(Math.random() * (right - left + 1)) + left
const pivot = nums[pivotId]
let lt = left, gt = right
let i = left
while (i <= gt) {
if (nums[i] < pivot) {
swap(lt, i)
i++
lt++
} else if (nums[i] > pivot) {
swap(gt, i)
gt--
} else {
i++
}
}
return [lt, gt]
}
let left = 0, right = nums.length - 1
while (left <= right) {
const [lt, gt] = threePathPartition(left, right)
if (target < lt) {
// 目标在小于区间
right = lt - 1
} else if (target > gt) {
// 目标在小于区间
left = gt + 1
} else {
// 目标在等于区间
return nums[target]
}
}
return -1
};
过了!!!!
