Heap是一种数据结构,能保证取max/min是O(1)时间。通常如果查最小值/最大值,我们可以用Heap。如果是查是不是存在(Contain()),就用HashMap。如果又要查最小/大值,又要查是不是存在,就用Heap+HashMap.
Max-heap/Min-heap保证父节点都比子节点大/小,但兄弟节点之间没有大小关系。
Heap虽然是一个(满)二叉树,但通常用数组实现(因为容易实现)。一个节点i的左子节点编号是2*i,右子节点是2*i+1。Heap通常是完全二叉树(因为效率高),但没有要求一定是完全二叉树。
Heap Sort就是把所有元素放到Max Heap或Min Heap里面,再一个一个pop出来。时间复杂度最坏/最好/平均 都是O(nlogn),空间复杂度为O(1)。注意Heap Sort是不稳定排序,因为它不能保证重复元素排序后还保留前后一致的顺序。注意Heap Sort实际应用不多,为什么呢?因为Heap的维护很麻烦。实际场景中的数据是频繁发生变动的,而对于待排序序列的每次更新(增,删,改),我们都要重新做一遍堆的维护,以保证其特性(父节点大/小于其子节点),这在大多数情况下都是没有必要的。
另一个原因是Heap的cache locality不好,即如果在较短时间内访问多个元素,这多个元素在Heap中的位置可能较远,所以不能用cache一次读出来。
当数据更新不很频繁的时候,堆排序可能会比较实用。
Priority Queue也是一种数据结构。其中每个元素都有一个关键字key,元素之间的比较都是通过key来比较的。优先队列包括最大优先队列和最小优先队列,优先队列的应用比较广泛,比如作业系统中的调度程序,当一个作业完成后,需要在所有等待调度的作业中选择一个优先级最高的作业来执行,并且也可以添加一个新的作业到作业的优先队列中。一个典型的例子是实时输出最近的一个小时内访问频率最高的10个IP 。注意这里是实时输出,也就是说Priority Queue/Heap可以用于在线算法,不需要读完全部的数据,可以real time输出到当前为止的结果。
Priority Queue可以用Heap实现,也可以用其他方式,比如说直接对数组插入实现,但是效率不高。注意Priority Queue不是Queue,那为什么它的名字里面有queue呢?可能是因为它能提供queue的接口,比如remove(),pop(), insert(), top()等等。
Priority Queue和Heap有什么区别吗?如果是JAVA自带的Priority Queue的话,答案是有的(C++的Priority Queue可能也类似,具体要确认一下)。
具体的区别是在remove操作中,Priority Queue的时间复杂度是O(n),而Heap是O(logn)。因为Priority Queue需要找到这个数据,需要O(n)的时间,而Heap借助了HashMap,所以只需要O(1)的时间就可以找到。那为什么Priority Queue不用HashMap呢?这个就是JAVA提供的函数里面没有加上这一块而已,如果自己编程就也可以是O(logn)。
C++的priority_queue的定义是:
priority_queue<Type, Container, Functional>
Type为数据类型, Container为保存数据的容器,Functional为元素比较方式。
如果不写后两个参数,那么容器默认用的是vector,比较方式默认用operator<,也就是优先队列是大顶堆,队头元素最大。
下面的内容参考了链接
https://blog.csdn.net/nisxiya/article/details/45725857
另外,需要注意的是,C++ STL默认的priority_queue是将优先级最大的放在队列最前面,也即是最大堆。
假设有如下一个struct:
struct Node {
int value;
int idx;
Node (int v, int i): value(v), idx(i) {}
friend bool operator < (const struct Node &n1, const struct Node &n2) ;
};
inline bool operator < (const struct Node &n1, const struct Node &n2) {
return n1.value < n2.value;
}
priority_queue<Node> pq; // 此时pq为最大堆
如果需要最小堆,则需要如下实现:
struct Node {
int value;
int idx;
Node (int v, int i): value(v), idx(i) {}
// friend bool operator < (const struct Node &n1, const struct Node &n2) ;
friend bool operator > (const struct Node &n1, const struct Node &n2) ;
};
inline bool operator > (const struct Node &n1, const struct Node &n2) {
return n1.value > n2.value;
}
priority_queue< Node, vector< Node >, greater< Node > > pq; // 此时greater会调用 > 方法来确认Node的顺序,此时pq是最小堆
注意为什么greater是最小堆呢,而less是最大堆呢?因为如果是greater的时候,说明大的会沉下去,所以是最小堆。如果是less
priority_queue<int, vector<int>, greater<int>> minHeap;
priority_queue<int, vector<int>, less<int>> maxHeap;
也可以用以下办法定义最小堆:
class Node {
public:
Node(int r, int c, int v) : row(r), col(c), val(v) {}
bool operator < (const Node & obj) const {
return val > obj.val;
}
int row, col, val;
};
priority_queue<Node> pq;
注意:我们什么时候需要定义operator<和operator>呢?这里要注意最小堆需要跟top比,比top大的才能进堆,所以需要定义operator>,同理,最大堆需要定义operator<。但如果这里我们不用自定义类型(比如Node)的话,我们就不需要定义operator<和operator>了,因为C++可以直接对内部保留类型(如int, char)来比大小。
也可以用decltype来定义最大堆和最小堆。以最小堆为例:
map<int, int> freqs; //<num, freq>
int n = nums.size();
for (int i = 0; i < n; i++) {
freqs[nums[i]]++;
}
auto comp = [&](int a, int b){return freqs[a] > freqs[b];};
priority_queue<int, vector<int>, decltype(comp)> minHeap(comp);
记忆捷径:把 return a > b 翻译成“排序”如果你觉得底层的堆推导很绕,可以用 std::sort 的逆向思维来记忆:在 std::sort 中,如果你传入 a > b,代码会按从大到小降序排列(大元素在前面)。std::priority_queue 的堆顶,对应的是底层数组的“末尾”(因为为了效率,pop 操作是从数组末尾移除元素的)。如果数组是从大到小排列(大…中…小),那么末尾就是最小的元素。既然末尾是最小的元素,而堆顶对应的就是末尾,那么堆顶自然就是最小的元素(小顶堆)。
末尾是最小的元素,而堆顶对应的就是末尾。为什么? 这是一个非常硬核且底层的 C++ 标准库(STL)实现细节。简单直接的答案是:为了实现 (O(1)) 时间复杂度的删除(Pop)操作。如果堆顶对应数组的开头(索引 0),删除堆顶时就需要移动数组中的所有其他元素,这会导致性能暴跌。为了避免这个性能问题,C++ STL 的 std::priority_queue 巧妙地利用了 std::vector 的末尾。:
注意:sort()和priority_queue<> 的第3个参数不一样!sort不用加decltyp,priority_queue要加。
它们两个的第三个参数本质完全不同:std::sort 是一个函数(Function),它的第三个参数需要传入一个对象 / 实例(可调用对象)。std::priority_queue 是一个类模板(Class Template),它的第三个参数需要传入一个类型(Type)。这就是为什么一个不需要 decltype,而另一个必须要加(或者传入结构体)的原因。
另外,一个诀窍关于记住第3个参数到底是类型type还是实例。
- container(e.g., map, set, queue, priority_queue, vector, …) 的第3个参数是type; function(e.g., sort)的第3个参数是function。
注意:仿函数是一个类,仿函数的实例相当于一个function。Lambada函数也相当于一个function。
//这是一个仿函数,因为重载了operator()
//注意对unordered_map和unordered_set,这里opeator()要返回size_t。对map,set,priority_queue,sort,这里operator()要返回bool
struct Compare {
bool operator()(const Node& a, const Node& b) const {
return a.dist < b.dist;
}
};
//这是仿函数的实例, 另外Compare()或者Compare{}也是仿函数的实例
Compare compare;
sort(nodes.begin(), nodes.end(), compare); //注意,如果是priority_queue,第3个参数就用Compare就行了.
临时仿函数实例
sort(nodes.begin(), nodes.end(), Compare{});
Lambda 实例
sort(nodes.begin(), nodes.end(),
[](const Node& a, const Node& b) {
return a.dist < b.dist;
});
Lambda 表达式产生的也是一个匿名类的对象,该类重载了 operator()。
- 看符号:
尖括号 < >(如 map<…>, unordered_map<…>, priority_queue<…>),里面一律填 类型(Type)。
圆括号 ( )(如 sort(…), find_if(…)),里面一律填 对象/值(Object/Value)。
如果硬要用 Lambda:
传给尖括号 < >:必须写 decltype(your_lambda)(C++20 之前)。
传给圆括号 ( ):直接写 your_lambda 名字或者把 { … } 丢进去即可。
/////////////////////
- std::priority_queue(类模板:传类型)当你声明一个优先队列时,你是在定义一个变量(创建对象)。在 C++ 中,定义变量时尖括号 < > 里面填写的必须是编译期就能确定的“类型”。
正确写法:第三个参数是仿函数的“类型”
priority_queue<int, vector<int>, greater<int>> pq;
priority_queue 内部会根据你传入的类型 greater,在自己类里面实例化出一个该类型的成员变量。在后续调用 push 等操作时,它会在底层自己调用这个成员变量。
如果你想用 Lambda 函数,也必须像 unordered_map 一样使用 decltype 来获取其类型,并且还要把 Lambda 对象传给构造函数:
auto cmp = [](int a, int b) { return a > b; };
// 尖括号内填类型,小括号内填对象
priority_queue<int, vector, decltype(cmp)> pq(cmp); //cmp是Lambda函数,类型是函数。这里第3个参数要是类型,所以要加decltype.
- std::sort(函数模板:传对象)当你使用 std::sort 时,你是在执行一个具体的操作(调用函数)。函数的圆括号 ( ) 里面填写的必须是运行期的“对象、变量或临时值”。
vector<int> nums = {3, 1, 4};
// 正确写法 1:传入一个临时的仿函数“对象”(注意后面的小括号 {} 或 ())
sort(nums.begin(), nums.end(), greater<int>{});
// 正确写法 2:直接传入一个 Lambda 函数“对象”
sort(nums.begin(), nums.end(), [](int a, int b) { return a > b; });
std::sort 的第三个参数是一个可调用对象。编译器会通过自动类型推导(Template Argument Deduction),自己去识别你传进来的对象的类型,不需要你在尖括号里手动指定。
//////////////////////////////////////////////////
一个经典的用最大堆和最小堆的例子是求n个无序数中的第K大的数。该题用最大堆和最小堆都可以做 (重要!)。
用最大堆做的代码如下。复杂度O(nlogn)
// max-heap
int findKthLargest(vector<int> &nums, int k)
{
priority_queue<int> pq(nums.begin(), nums.end());
for (int i = 0; i < k - 1; i++)
{
pq.pop();
}
return pq.top();
}
用最小堆做代码如下。复杂度O(nlogk)
// min-heap
int findKthLargest(vector<int> &nums, int k)
{
priority_queue<int, vector<int>, greater<int> > pq;
for (int i = 0; i < nums.size(); i++)
{
if (pq.size() == k)
{
int x = pq.top();
if (nums[i] > x)
{
pq.pop();
pq.push(nums[i]);
}
}
else
{
pq.push(nums[i]);
}
}
return pq.top();
}
};
当然也可以用将乘以负数的办法来构造最小堆。比如要构造一个int类型的最小堆:
priority_queue pq; //
pq.push( -1 * v1) ; //
pq.push( -1 * v2) ; //
pq.push( -1 * v3) ; // 分别是插入v1, v2, v3变量的相反数,那么pq其实也就变相成为了最小堆
下面这个链接很不错:
https://blog.csdn.net/txl199106/article/details/44588487
另外,也可以通过定义struct cmp来实现最小堆,cmp里面只需给出operator()函数。给出operator()后则不需再定义operator < 或operator >。用这种方法必须通过以下方式定义priority_queue。
priority_queue<Node,vector<Node>,cmp>q;
注意:priority_queue第3个参数cmp必须是class/struct,而sort()的第3个参数可以是function或class/struct的实例(注意是实例,不是class/struct的定义)。
举例如下(定义最小堆)。如果要生成最大堆,只需将operator()里面的>改成<即可。
struct node{
int idx;
int key;
node(int a=0, int b=0):idx(a), key(b){}
};
struct cmp{
bool operator()(node a, node b){
return a.key > b.key;
}
};
priority_queue<node, vector<node>, cmp> q;
实例代码如下 (from https://blog.csdn.net/txl199106/article/details/44588487):
#include <iostream>
#include <queue>
using namespace std;
struct Node{
int x, y;
}node;
struct cmp{
bool operator()(Node a,Node b){
if(a.x==b.x) return a.y>b.y;
return a.x>b.x;}
};
int main(){
priority_queue<Node,vector<Node>,cmp>q;
for(int i=0;i<10;i++){
node.x=i;
node.y=10-i/2;
q.push(node);
}
while(!q.empty()){
cout<<q.top().x<<' '<<q.top().y<<endl;
q.pop();
}
return 0;
}
该代码输出如下:
0 10
1 10
2 9
3 9
4 8
5 8
6 7
7 7
8 6
9 6
如果将operator()里面的>改为<,则为最大堆。输出结果为:
9 6
8 6
7 7
6 7
5 8
4 8
3 9
2 9
1 10
0 10
另外,C++里面也可以用multiset实现最大堆和最小堆。代码如下:
multiset<int, greater<int>> greaterSet;
multiset<int, less<int>> lessSet;
multiset<int> defaultSet; //默认为lessSet
将元素插入堆后,greaterSet.begin()所指向的值就是最大值。lessSet.begin()所指向的值就是最小值。
一个小总结 (不一定对)
求第K大用最小堆:
求第K小用最大堆
这个好像不管是对动态数据和静态数据都实用,时间复杂度O(nlogk)。但静态数据求第k大/小用quickselect更好,时间复杂度O(n)。
注意:堆是完全二叉树,但不是平衡二叉树。为什么不是平衡二叉树呢?堆的左右两个子树的高度差的绝对值不超过1啊?因为平衡二叉树必须首先是binary search tree,堆不满足这个条件。
另外,下面这个链接解释了为什么sort和priority_queue的第三个参数不一样,sort是传入一个Compare类的实例,而priority_queue是传入一个类。这是因为本质上:
sort是一个函数,函数参数列表()中需要传入实例化后的具体对象;
priority_queue是一个模板类,模板的类型参数表中<>需要传入具体的类,而不是对象。
https://www.codenong.com/cs109745348/
本文详细介绍了堆数据结构的特点及应用,包括最大堆与最小堆的区别,如何使用堆进行排序,以及堆与优先队列的关系。同时探讨了在不同编程语言中实现堆的具体方法,并分析了它们的时间与空间复杂度。

3164

被折叠的 条评论
为什么被折叠?



