215. 数组中的第 K 个最大元素
215. 数组中的第K个最大元素 - 力扣(LeetCode)
class Solution {
template<typename Compare = std::less<>>
void PercolateDown(vector<int> &V, int N, Compare C = {}) {
int Size = V.size();
// XXX size() returns size_t type, which is an UNSIGNED value!!!
// while (N <= V.size() / 2 - 1) {
while (N <= Size / 2 - 1) {
int L = N * 2 + 1, R = N * 2 + 2;
int Chosen = N;
if (C(V[Chosen], V[L]))
Chosen = L;
// Mind that right child possibly doesn't exist. Left always exist though.
// Also, we must compare with LargestIdx instead of N here, since we
// want the max of THREE. The comparison pattern is NL -> LR.
if (R < Size && C(V[Chosen], V[R]))
Chosen = R;
if (Chosen == N)
return;
std::swap(V[N], V[Chosen]);
N = Chosen;
}
}
void BuildHeap(vector<int> &V) {
for (int i = V.size() / 2 - 1; i >= 0; --i)
PercolateDown(V, i);
}
void Pop(vector<int> &V) {
std::swap(V.front(), V.back());
V.resize(V.size() - 1);
PercolateDown(V, 0);
}
public:
int findKthLargest(vector<int>& nums, int k) {
BuildHeap(nums);
for (int i = 1; i < k; ++i)
Pop(nums);
return nums.front();
}
};
我服了啊,我真的受不了了,size() 返回的是 size_t,如果你不强制转换的话在前面把它跟 int 比较的时候 int 会被自动提升到 size_t——参见 Implicit conversions - cppreference.com——然后整个式子就变成无符号数比较了。我服了啊,我操。
话说回来,关于堆的几个操作要搞搞清楚。从 Size / 2 - 1 下标开始构建堆的原因是,这个下标是「倒数第一个非叶子节点」;PercolateDown() 在选择最大(最小)下标的时候是三路比较(LN 和 NR),所以要设一个 Chosen 作为中间变量;Pop() 的时候要把最后一个元素交换到堆顶,然后向下冒泡。
此外你可以发现,这里的解法是原地建堆的。我一开始其实也写了一个 class Heap 但后来发现没这个必要,还徒增空间复杂度。
但是除了官解给的大根堆的写法,事实上还有一种小根堆的写法,也比较直观:先原地建立一个尺寸为 \(k\) 的小根堆,然后一边读取数组的后续内容,一边调整堆,完成后处在堆顶的即为第 \(k\) 大的元素。
class Solution {
template<typename Compare = std::less<>>
void PercolateDown(vector<int> &V, int N, int Size, Compare C = {}) {
while (N <= Size / 2 - 1) {
int L = N * 2 + 1, R = N * 2 + 2;
int Chosen = N;
if (C(V[Chosen], V[L]))
Chosen = L;
if (R < Size && C(V[Chosen], V[R]))
Chosen = R;
if (Chosen == N)
return;
std::swap(V[N], V[Chosen]);
N = Chosen;
}
}
template<typename Compare = std::less<>>
void BuildHeap(vector<int> &V, int Size, Compare C = {}) {
for (int i = Size / 2 - 1; i >= 0; --i)
PercolateDown(V, i, Size, C);
}
template<typename Compare = std::less<>>
void Pop(vector<int> &V, int Size, Compare C = {}) {
std::swap(V.front(), V.back());
PercolateDown(V, 0, Size - 1, C);
}
public:
int findKthLargest(vector<int>& nums, int k) {
// Firstly, make a heap of k elements in-place.
BuildHeap(nums, k, std::greater<int>());
for (int i = k; i < nums.size(); ++i) {
// Then, read the remaining elements and check if it's needed to adjust
// the heap. For this problem, we choose elements larger than top.
if (nums[i] > nums.front()) {
std::swap(nums.front(), nums[i]);
PercolateDown(nums, 0, k, std::greater<int>());
}
}
return nums.front();
}
};
其实这个就叫 Top-K,我是真没想到这个名字竟然是这个意思。不过这相比原地建堆的操作,时间复杂度 \(\mathrm O(n\log k)\) 没有特别多的提升,且本题不要求实现 online 算法,所以官解似乎也没有继续考虑。
至于快排,采用 y 总的模板可以这么写,但是有两件事情要注意一下。
class Solution {
// For quicksort related algorithms, we'll use inclusive range. You can see
// that both musl libc and glibc use inclusive ranges.
int QuickFind(vector<int> &A, int L, int R, int K) {
if (L == R)
return A[K];
int Pivot = A[L], i = L - 1, j = R + 1;
while (i < j) {
while (A[++i] < Pivot);
while (A[--j] > Pivot);
if (i < j)
std::swap(A[i], A[j]);
}
// If you use this kind of quicksort algorithm, you'll find that [L, j] are
// all LESS THAN (<) Pivot, and [j + 1, R] are NOT LESS THAN (>=) Pivot.
// However, where the pivot lays can't be determined. This is really
// interesting. Keep that in mind and just use it for now.
if (j < K)
return QuickFind(A, j + 1, R, K);
else
return QuickFind(A, L, j, K);
}
public:
int findKthLargest(vector<int>& nums, int k) {
int N = nums.size();
// Remember that we need to find the kth LARGEST number, not smallest; so
// we need to pass N - k here instead of k.
return QuickFind(nums, 0, N - 1, N - k);
}
};
第一件事情是,这种模板在每次划分之后并不保证 pivot 的位置,而只是以下标 \(j\) 为界,把数组分成了 \([L, j]<\text{Pivot}\) 和 \((j, R]>=\text{Pivot}\) 两个部分;其次,要注意这样排序之后数组是正序的,所以我们应该传入的参数应该是 \(N-k\) 而不是 \(k\)。
关于 pivot 位置啊、分区啊之类的事情就之后再研究吧,现在还是多接触题型要紧。
二刷
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
int K = nums.size() - k;
int l = 0, r = nums.size() - 1;
// Keep in mind that every time the loop completed, the original nums[] was
// modified. So we'll EVENTUALLY move Kth largest number to our l pointer.
// The swap() below indicates this clearly.
while (l != r) {
int Pivot = nums[l];
int i = l - 1, j = r + 1;
while (i < j) {
// You muse remember those operator directions!
while (nums[++i] < Pivot) {}
while (nums[--j] > Pivot) {}
if (i < j)
swap(nums[i], nums[j]);
}
if (j < K)
l = j + 1;
else
r = j;
}
// Returning nums[l] is basically the same as returning nums[K]. That's
// because if we went here, we already have l == r == K as stated above.
return nums[l];
}
};
千万要注意,快速选择算法本质上就是快排。因为我们是通过交换的手段,修改原数组,从而达到逐步将待查元素放到 \(K\) 的位置上的。理解了这一点之后,就可以理解为什么官解的不动点写的是 if (l == r) return nums[k]; 了。
还有就是他妈的把大于小于号搞搞清楚。一开始写反了我是真绷不住了。
class Solution {
static void PercolateDown(vector<int> &V, int i, int Size) {
// The minus one here is to convert 1-indexed Size to 0-indexed index.
while (i <= Size / 2 - 1) {
// We use 0-indexed index here.
int Left = i * 2 + 1, Right = i * 2 + 2;
int Smallest = i;
if (V[Left] < V[Smallest])
Smallest = Left;
if (Right < Size && V[Right] < V[Smallest])
Smallest = Right;
swap(V[i], V[Smallest]);
// Notice that we don't have to recurse. Just use the while loop is fine.
if (Smallest == i)
return;
if (Smallest == Left)
i = Left;
else
i = Right;
}
}
static void BuildHeap(vector<int> &V, int Size) {
// Still, mind the minus one here.
for (int i = Size / 2 - 1; i >= 0; --i)
PercolateDown(V, i, Size);
}
public:
int findKthLargest(vector<int>& nums, int k) {
BuildHeap(nums, k);
for (int i = k; i < nums.size(); ++i) {
if (nums[i] > nums.front()) {
swap(nums[i], nums.front());
PercolateDown(nums, 0, k);
}
}
return nums.front();
}
};
Top-K 的堆还是得多写写,主要是注意 Size 是一个 1-indexed 的东西,所以要作一个 \(-1\) 变换,然后就是 PercolateDown() 里头其实用不着写递归,直接一个 while 就解决了,毕竟就算要写也是尾递归,跟上面那个迭代版快选算法一样的。
三刷
class Solution {
void MakeHeap(vector<int> &H) {
for (int i = H.size() / 2 - 1; i >= 0; --i)
PercolateDown(H, i);
}
void PercolateDown(vector<int> &H, int i) {
// This will ensure i is non-leaf.
while (i <= H.size() / 2 - 1) {
int SmallestIdx = i;
// This won't work?? Why??
if (H[SmallestIdx] > H[i * 2 + 1])
SmallestIdx = i * 2 + 1;
if (i * 2 + 2 < H.size() && H[SmallestIdx] > H[i * 2 + 2])
SmallestIdx = i * 2 + 2;
if (i == SmallestIdx)
break;
swap(H[i], H[SmallestIdx]);
i = SmallestIdx;
}
}
public:
int findKthLargest(vector<int>& nums, int k) {
vector<int> H(nums.begin(), nums.begin() + k);
MakeHeap(H);
// bro wtf
// for (int i = k; i < nums.size(); ++k) {
for (int i = k; i < nums.size(); ++i) {
if (nums[i] > H.front()) {
swap(nums[i], H.front());
PercolateDown(H, 0);
}
}
return H.front();
}
};
一种非原地的写法,虽然还是推荐原地建堆,但这个写法大体上也是正确的——然而在\(k=1\)时竟然会出现越界问题?我操,为什么啊?按理说它完全进不去循环体啊?
不管了,以后还是用原地解法吧,原地解法真没什么问题。此外就是注意比较的时候要用逐步比较的思想,也就是三个里面找最小永远是跟SmallestIdx比,而不是用某些固定的index去比。