215. 数组中的第K个最大元素
给定整数数组
nums
和整数k
,请返回数组中第k
个最大的元素。请注意,你需要找的是数组排序后的第
k
个最大的元素,而不是第k
个不同的元素。你必须设计并实现时间复杂度为
O(n)
的算法解决此问题。示例 1:
输入: [3,2,1,5,6,4], k = 2 输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4 输出: 4
提示:
1 <= k <= nums.length <= 10(5)
-10(4) <= nums[i] <= 10(4)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int> pq(nums.begin(), nums.end());
k = k - 1;
while (k--) {
pq.pop();
}
return pq.top();
}
};
优先队列的简单实现
#include <iostream>
using namespace std;
#include <vector>
// priority_queue--->堆
namespace Mypriority_queue {
template<class T>
struct less {
bool operator()(const T& left, const T& right) {
return left < right;
}
};
template<class T>
struct greater {
bool operator()(const T& left, const T& right) {
return left > right;
}
};
template<class T, class Container = std::vector<T>, class Compare = less<T>>
class priority_queue {
public:
// 创造空的优先级队列
priority_queue() : c() {}
template<class Iterator>
priority_queue(Iterator first, Iterator last)
: c(first, last) {
// 将c中的元素调整成堆的结构
int count = c.size();
int root = ((count - 2) >> 1);
for (; root >= 0; root--)
AdjustDown(root);
}
void push(const T& data) {
c.push_back(data);
AdjustUP(c.size() - 1);
}
void pop() {
if (empty())
return;
swap(c.front(), c.back());
c.pop_back();
AdjustDown(0);
}
size_t size()const {
return c.size();
}
bool empty()const {
return c.empty();
}
// 堆顶元素不允许修改,因为:堆顶元素修改可以会破坏堆的特性
const T& top()const {
return c.front();
}
private:
// 向上调整
void AdjustUP(int child) {
int parent = ((child - 1) >> 1);
while (child) {
if (Compare()(c[parent], c[child])) {
swap(c[child], c[parent]);
child = parent;
parent = ((child - 1) >> 1);
} else {
return;
}
}
}
// 向下调整
void AdjustDown(int parent) {
size_t child = parent * 2 + 1;
while (child < c.size()) {
// 找以parent为根的较大的孩子
if (child + 1 < c.size() && Compare()(c[child], c[child + 1]))
child += 1;
// 检测双亲是否满足情况
if (Compare()(c[parent], c[child])) {
swap(c[child], c[parent]);
parent = child;
child = parent * 2 + 1;
} else
return;
}
}
private:
Container c;
};
}
void TestQueuePriority() {
Mypriority_queue::priority_queue<int> q1;
q1.push(5);
q1.push(1);
q1.push(4);
q1.push(2);
q1.push(3);
q1.push(6);
cout << q1.top() << endl;
q1.pop();
q1.pop();
cout << q1.top() << endl;
vector<int> v{ 5, 1, 4, 2, 3, 6 };
Mypriority_queue::priority_queue<int, vector<int>, Mypriority_queue::greater<int>> q2(v.begin(), v.end());
cout << q2.top() << endl;
q2.pop();
q2.pop();
cout << q2.top() << endl;
}
int main() {
TestQueuePriority();
}
less
和 greater
的仿函数
template<class T>
struct less {
bool operator()(const T& left, const T& right) {
return left < right;
}
};
template<class T>
struct greater {
bool operator()(const T& left, const T& right) {
return left > right;
}
};
这两个模板结构体 less
和 greater
是比较函数对象(或称为仿函数),它们分别实现了小于和大于的比较逻辑。在C++中,这种模式允许你将比较行为封装在一个对象中,这样的对象可以在需要进行比较操作的地方使用,比如排序算法、容器(如 std::set
或 std::priority_queue
)等。
less
结构体
less
结构体模板接受一个类型参数 T
,并重载了 operator()
,使得该结构体的实例可以像函数一样被调用。
当你创建一个 less
实例并调用它时,它会比较两个类型为 T
的参数,并返回一个 bool
值,指示第一个参数是否小于第二个参数。
这在默认情况下用于构建最大堆或进行升序排序。
greater
结构体
greater
结构体模板同样接受一个类型参数 T
,并重载了 operator()
,允许它比较两个类型为 T
的参数。
它返回一个 bool
值,指示第一个参数是否大于第二个参数。
这通常用于构建最小堆或进行降序排序。
priority_queue 的模版参数
template<class T, class Container = std::vector<T>, class Compare = less<T>>
class priority_queue {
模板参数
T
:队列中元素的类型。
Container
:用于底层存储的容器类型,默认为 std::vector<T>
。这个容器需要支持随机访问迭代器、back()
、push_back()
和 pop_back()
操作。
Compare
:比较函数对象的类型,默认为 less<T>
,这意味着默认情况下队列是一个最大堆。你可以通过提供不同的比较函数对象来改变队列的行为,例如使用 greater<T>
来创建一个最小堆。
成员变量
Container c;
创建底层存储的容器类型的对象c。
构造函数
// 创造空的优先级队列
priority_queue() : c() {}
template<class Iterator>
priority_queue(Iterator first, Iterator last)
: c(first, last) {
// 将c中的元素调整成堆的结构
int count = c.size();
int root = ((count - 2) >> 1);
for (; root >= 0; root--)
AdjustDown(root);
}
默认构造函数
它使用成员初始化列表来初始化底层容器 c
。这里,c()
调用了容器的默认构造函数,创建一个空的容器。如果 Container
是 std::vector<T>
,这相当于创建了一个空的 std::vector
。
范围构造函数
这个构造函数允许从一个现有的元素范围创建优先队列,其中 first
和 last
是迭代器,分别指向范围的开始和结束。它通过成员初始化列表初始化底层容器 c
,使得 c
包含了给定范围内的所有元素。
接着,构造函数通过一系列向下调整操作,将这些元素调整成堆的结构。这是通过从最后一个非叶子节点开始,向根节点逐个进行向下调整操作来完成的。这里使用的计算方法 ((count - 2) >> 1)
是找到最后一个元素的父节点的索引,这个父节点也是最后一个需要进行向下调整的节点。向下调整确保每个节点都遵循堆的性质:对于最大堆,任何给定节点的值都大于其子节点的值;对于最小堆,则相反。
在一个堆中,父节点和子节点之间的下标关系可以通过简单的数学计算得到。假设数组表示堆,下标从0开始:
-
对于任意节点 i 的父节点,其下标为 (i-1)/2。如果 i 是根节点(即 i = 0),则其父节点也是它自己。
-
对于任意节点 i 的左子节点,其下标为 2i + 1。
-
对于任意节点 i 的右子节点,其下标为 2i + 2。
push、pop、size、empty、top函数
void push(const T& data) {
c.push_back(data);
AdjustUP(c.size() - 1);
}
void pop() {
if (empty())
return;
swap(c.front(), c.back());
c.pop_back();
AdjustDown(0);
}
size_t size()const {
return c.size();
}
bool empty()const {
return c.empty();
}
// 堆顶元素不允许修改,因为:堆顶元素修改可以会破坏堆的特性
const T& top()const {
return c.front();
}
push(const T& data)
将新元素添加到优先队列的底层容器末尾。
调用 AdjustUP
方法,从添加元素的位置开始向上调整堆,以保持堆的特性。这确保了所有父节点都会大于(或小于,取决于比较准则)它们的子节点。
pop()
如果队列为空,则直接返回,不执行任何操作。
交换底层容器中的第一个元素(堆顶元素)和最后一个元素,然后移除最后一个元素(原堆顶元素)。
调用 AdjustDown
方法从根节点开始向下调整堆,以保持堆的特性。
size() const
返回优先队列中元素的数量,即底层容器的大小。
empty() const
检查优先队列是否为空,即底层容器是否不包含任何元素。
top() const
返回对优先队列顶部元素的引用,即底层容器的第一个元素。
该方法标记为 const
,说明调用它不会修改优先队列的状态。
注意,堆顶元素不允许通过 top
方法直接修改,因为任何修改都可能破坏堆的结构特性。如果需要修改堆顶元素,应先将其 pop
出队列,做出修改后再通过 push
方法重新添加到队列中,以确保堆的整体特性得以保持。
AdjustUP 向上调整算法
// 向上调整
void AdjustUP(int child) {
int parent = ((child - 1) >> 1);
while (child) {
if (Compare()(c[parent], c[child])) {
swap(c[child], c[parent]);
child = parent;
parent = ((child - 1) >> 1);
} else {
return;
}
}
}
child
是新加入堆的元素的索引,或者是需要向上调整的堆元素的索引。
计算父节点索引:通过表达式 ((child - 1) >> 1)
计算出当前节点的父节点索引。这里使用的是位运算符 >>
,实际上是对 (child - 1)
进行除以 2 的操作,符合堆结构的父子关系定位方法。
循环向上调整:当 child
不是根节点(即 child
的索引不为 0)时,循环开始。比较当前节点(child
)与其父节点(parent
)的值:
如果当前节点应该在父节点之前(对于最大堆,意味着 child
节点的值大于 parent
节点的值;对于最小堆,则相反),则交换它们的值,并更新 child
和 parent
的索引,继续循环。
如果不需要交换(即已满足堆的条件),则结束循环。
Compare()
函数对象:Compare
是在类模板参数中指定的比较函数对象类型,用于决定元素之间的排序准则。通过调用 Compare()(c[parent], c[child])
,判断是否需要交换父子节点来保持堆性质。这种方式允许灵活定义堆的性质,比如构建最大堆或最小堆。
AdjustDown 向下调整算法
// 向下调整
void AdjustDown(int parent) {
size_t child = parent * 2 + 1;
while (child < c.size()) {
// 找以parent为根的较大的孩子
if (child + 1 < c.size() && Compare()(c[child], c[child + 1]))
child += 1;
// 检测双亲是否满足情况
if (Compare()(c[parent], c[child])) {
swap(c[child], c[parent]);
parent = child;
child = parent * 2 + 1;
} else
return;
}
}
parent
是需要向下调整的节点的索引。
初始化子节点索引:计算出 parent
节点的左子节点索引,即 parent * 2 + 1
。这是因为在堆的数组表示中,给定父节点索引为 i
,其左子节点索引为 2*i + 1
。
循环向下调整:当 child
索引在容器 c
的范围内时,循环开始。这个循环确保如果还有子节点,就继续检查和调整。
选择较大(或较小)的子节点:如果 child
不是最后一个节点,并且右子节点(child + 1
)的值大于左子节点的值(对于最大堆),则 child
索引加一,指向较大的子节点。对于最小堆,比较逻辑会相反,选择较小的子节点。
交换父子节点:如果父节点的值小于所选子节点的值(对于最大堆),则交换父子节点的值,并更新 parent
和 child
的索引,以继续向下调整。对于最小堆,如果父节点的值大于所选子节点的值,则进行交换。
终止条件:如果父节点的值不需要与子节点交换(即已满足堆的条件),则结束循环。
Compare()
函数对象:这里使用了 Compare
函数对象来决定元素之间的比较准则,允许这个方法灵活地应用于构建最大堆或最小堆。通过 Compare()(c[child], c[child + 1])
判断是否需要选择右子节点,以及通过 Compare()(c[parent], c[child])
判断是否需要进行父子节点的交换。
对向上调整算法和向下调整算法的探究
结尾
最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。
同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。
谢谢您的支持,期待与您在下一篇文章中再次相遇!