【学习笔记 5】堆

题外话

其实本身不打算写堆的,主要因为 priority_queue 太香了,但是又怕考场上有一些题目需要手打堆,还是复习一下

堆简介

堆是一种数据结构的总称,本质上是一颗完全二叉树。但它的特殊性质就是两个子节点一定不大于(大根堆)或不小于(小根堆)它们的父节点。这样一来,不难想出,堆顶的元素(根节点)一定是堆中最大(大根堆)或最小(小根堆)的元素。因此,堆常常用来维护序列的最大值或最小值,维护最大值的叫做大根堆,维护最小值的叫做小根堆。

朴素的算法求序列最值,在末尾插入和删除的时间复杂度都为 (Theta(1)),但是求最值的时间复杂度为 (Theta(n));而堆的插入删除(维护)的时间复杂度为 (Theta(log_2n)),求最值则为 (Theta(1))。所以,堆在维护最值的问题中有广泛运用。

接下来我以线性存储的小根堆为例,讲一下堆的基本操作(红点代表位置待定的点)。

手写堆

虽然 STL 好,但是为了提高对堆的理解,手打堆还是要练习的。

求最值

如图,这是一个堆。

当我们要输出堆的最小元素,就是输出堆顶元素,就是数组中的 (heap[1])

cout<<heap[1]<<endl;

插入

假如我们要将一个元素 0 插入这个堆里,首先将它插到堆底:

然后我们发现,这样不满足小根堆的性质,就将此元素和它的父节点交换:

交换完,我们发现,还是不能满足性质,就再和父节点交换:

这时,元素满足了性质,插入就完成,元素 0 的位置定下来。

代码实现(while 循环):

void push(int x)
{
	heap[++cnt]=x;  //插入堆底,cnt 为堆底指针(变量代替)。 
	int idx=cnt;    //idx 为当前位置(下标)。 
	while(idx>1)   //不为根节点时,判断是否符合性质。 
	{
		if(heap[idx]<heap[idx>>1])     //不符合,就交换。 
			swap(heap[idx],heap[idx>>1]),idx=idx>>1;
		else       //否则结束循环,定下位置。 
			break;
	}
}

删除

还以上图为例,加入我们要将 0 删除,首先将 0 与堆底元素交换,并删除交换后的 0

此时,要删除的元素没有了,但是 4 的位置不符合性质,就将 4值较小的子节点交换,这里 4 的子节点中值较小的节点是 1,将它们交换:

此时发现满足性质,4 的位置就可以定下来,交换完成:

代码:

void pop(int x)
{
	swap(heap[x],heap[cnt]);  //交换元素。 
	--cnt;   //删除。 
	int idx1=x,swa;   //idx1 表示当前节点位置,swa 表示要与其交换的节点的位置。 
	while((idx1<<1)<=cnt)
	{
		swa=idx1<<1;
		if(heap[swa]>heap[swa+1]&&swa+1<=cnt)   //根据左右子节点的大小确定 swa。 
			swa+=1;
		if(heap[swa]<heap[idx1])    //如果不符合性质,就交换。 
			swap(heap[idx1],heap[swa]),idx1=swa;
		else       //否则,结束。 
			break;
	}
}

完整操作代码(1 表示插入元素 b,2 表示输出最小元素,3 表示删除下标为 b 的节点):

#include<bits/stdc++.h>
using namespace std;
long long heap[10000005],cnt,n,a,b;
void push(int x)
{
	heap[++cnt]=x;  
	int idx=cnt;     
	while(idx>1)    
	{
		if(heap[idx]<heap[idx>>1])    
			swap(heap[idx],heap[idx>>1]),idx=idx>>1;
		else       
			break;
	}
}
void pop(int x)
{
	swap(heap[x],heap[cnt]);
	--cnt;
	int idx1=x,swa;
	while((idx1<<1)<=cnt)
	{
		swa=idx1<<1;
		if(heap[swa]>heap[swa+1]&&swa+1<=cnt)
			swa+=1;
		if(heap[swa]<heap[idx1])
			swap(heap[idx1],heap[swa]),idx1=swa;
		else 
			break;
	}
}
int main()
{
	cin>>n;
	for(register int i=0;i<n;++i)
	{
		cin>>a;
		if(a==1)
		{
			cin>>b;
			push(b);
		}
		if(a==2)
			cout<<heap[1]<<endl;
		if(a==3)
		{ 
			cin>>b; 
			pop(b);
		} 
	}	
}

例题:P3378 【模板】堆 如果想巩固的,可以试试自己打一下例题。

STL 的堆(priority_queue)

priority 是 queue 头文件下的一种容器,翻译为优先队列,可以方面地实现堆。

定义:

对于单个元素的比较,我们可以采取下面这种定义方式:

priority_queue<tepename,vector<tepename>,greater<tepename> > name;  //小根堆。
priority_queue<tepename,vector<tepename>,less<tepename> > name;  //大根堆。

其中 tepename 为数据类型或标准容器,name 是优先队列的名字。(当类型为字符或字符串,则按字典序比较大小)

但如果我们定义了一个结构体,想按照某种方法比较结构体中的元素来实现堆,那我们就要用一种神奇的操作:重载运算符

假如我们定义了一个结构体 (node)

struct node 
{
    int a,b;
};

如果我们想按照 (a) 由大到小,如果 (a) 相同,则按照 (b) 由大到小这种比较方式,那我们可以在结构体中这样重载运算符(其实和 sort 的 cmp 的定义很像):

struct node
{
    int a,b;
    bool operator <(const Node &c)const 
	 {
        if(a!=c.a) 
			return a>c.a;
        return b>c.b;
    }
};

再这样定义 priority_queue:

priority_queue<node> name;

就可以按要求比较了。

函数

  1. top() 取堆顶元素。复杂度为 (Theta(1))

  2. pop() 删除堆顶元素。复杂度为 (Theta(log_2n))

  3. push() 插入一个元素到优先队列中。复杂度为 (Theta(log_2n))

  4. size() 返回队列中的元素个数。复杂度为 (Theta(n))

  5. empty() 判断队列是否为空。复杂度为 (Theta(1))

例题代码:

那么,上面的例题用 STL 就可以这样:

#include<bits/stdc++.h>
using namespace std;
priority_queue<int,vector<int>,greater<int> > q;
int n,a,b;
int main()
{
	cin>>n;
	for(register int i=0;i<n;++i)
	{
		cin>>a;
		if(a==1)
		{
			cin>>b;
			q.push(b);
		}
		if(a==2)
			cout<<q.top()<<endl;
		if(a==3)
			q.pop();
	}
	return 0;	
}

总结比较

STL 的堆最大的缺点就是只能删除堆顶元素,并且一些操作会受到牵制。所有,考场上如果会手打堆就尽量不用 STL。手打堆的灵活性是 STL 永远比不上的。

原文地址:https://www.cnblogs.com/win10crz/p/12835802.html