堆Heap和优先队列(Priority Queue)学习小结

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为元素比较方式。

如果不写后两个参数,那么容器默认用的是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 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是最小堆

也可以用以下办法定义最小堆:

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 pq;

注意:我们什么时候需要定义operator<和operator>呢?这里要注意最小堆需要跟top比,比top大的才能进堆,所以需要定义operator>,同理,最大堆需要定义operator<。但如果这里我们不用自定义类型(比如Node)的话,我们就不需要定义operator<和operator>了,因为C++可以直接对内部保留类型(如int, char)来比大小。

一个经典的用最大堆和最小堆的例子是求n个无序数中的第K大的数。该题用最大堆和最小堆都可以做 (重要!)
用最大堆做的代码如下。复杂度O(nlogn)

    // max-heap
    int findKthLargest(vector &nums, int k)
    {
        priority_queue 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 &nums, int k)
    {
        priority_queue, greater > 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,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, cmp> q;

实例代码如下 (from https://blog.csdn.net/txl199106/article/details/44588487):

#include 
#include 
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,cmp>q;
    for(int i=0;i<10;i++){
    	node.x=i;
    	node.y=10-i/2;
		q.push(node);
    }
    while(!q.empty()){
        cout<

该代码输出如下:
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)。

你可能感兴趣的:(堆Heap和优先队列(Priority Queue)学习小结)