from: http://blog.youkuaiyun.com/beenlast/article/details/6707192


最近学了下堆,堆就是一坨一坨的东西,最普通的堆有两种,大根堆(也叫最大堆)和小根堆(最小堆)。

堆是一棵树,普通的二叉堆是完全二叉树,每棵子树也是一个堆,树中结点个数就是堆的大小(size)。

对于最大堆,就是上面的一定比下面的大,但同一层之间互相的关系完全不清楚,因此最大的就是根节点,也就是堆顶。

因为堆也就这些信息,一般就只对堆顶进行操作。

一颗完全二叉树,如果自顶向下,自左向右对节点编号的话,设根节点编号为1,那么对于每个节点u其左儿子是2u,右儿子是2u+1,其父亲就是u/2取整。

有这个性质,二叉堆就非常容易写了,用数组来存堆即可。



最大堆的几个主要函数:

最主要的是保持堆性质的heapify函数:

[cpp] view plain
copy

void heapify(int u){
int max,l,r;
max=u;
l=u<<1;r=l|1;
if(l<=siz&&node[max]<node[l])
max=l;
if(r<=siz&&node[max]<node[r])
max=r;
if(max!=u){
swap(node[u],node[max]);
heapify(max);
}
}
我们假设u这个节点的左子树和右子树已经是个好的堆,那么我们只要调整u节点数值的位置就能使以u这个节点为根节点的堆成为一个好的堆。


那么我们找出u,u的左儿子与u的右儿子中哪个最大,如果u最大那么已经是一个好的堆了。否则,就将u和最大的那个节点交换,u的值就是正确了。

但是这样就改变了被交换的那个节点值,可能破坏了那个子堆的性质,于是就对该子节点继续heapify,直到整个堆正确。

由于这个函数是以dfs实现的,复杂度和dfs树的层数有关,于是这个函数最坏的复杂度为O(log(n))。





有了这个函数,几乎我们就已经掌握了整个堆怎么写了,下面给出建堆函数:

[cpp] view plain
copy

void build(int &n){
siz=n;
for(int i=siz>>1;i>=1;i--)
heapify(i);
}

因为对于任意一棵完全二叉树,其叶子节点一定是满足堆的性质的 (因为叶子节点没有儿子,自己就是自己这个堆的堆顶)。


那么我们从最后一个叶子节点的父亲开始从后往前从下往上运行heapify函数就能建好整个堆。

这个过程的复杂度我不知道怎么算,但书上给出是O(n)的。



那么我们取出堆顶的函数也很简单了:

[cpp] view plain
copy

int top(){
return node[1];
}


复杂度O(1)。



怎么删除堆顶元素呢?

其实我们只要把堆顶元素和堆的最后一个元素交换,然后减小size再对堆顶运行一次heapify即可:

[cpp] view plain
copy

void pop(){
swap(node[1],node[siz]);
siz--;
heapify(1);
}

复杂度显然就是O(log(n))了。

另外堆排序的话,用这个pop函数你就直接建个最大堆然后不停pop直到siz等于1,整个数组从1开始就是升序排列的了,复杂度O(nlog(n)),是不稳定排序(因为堆除了保持顶部数值最大外其他信息一概不清晰,于是对于其他键值不能保持稳定)。



其实优先队列也就是堆,STL的优先队列我现在所用到的功能(虽然我用的功能很少。。)全都可以由最大堆实现。





上面没有给出怎么给堆添加元素(push),其实很简单,你自己稍稍动动脑子就能想到了。











下面给出我自己写的最大堆(部分功能优先队列):

[cpp] view plain
copy

struct __heap{
#define CONT int //改变CONT可以改变堆元素类型。
#define VV 2000001 //VV是堆的最大大小。
private:
CONT node[VV];
int siz;
public:
void heapify(int u){
int max,l,r;
max=u;
l=u<<1;r=l|1;
if(l<=siz&&node[max]<node[l])
max=l;
if(r<=siz&&node[max]<node[r])
max=r;
if(max!=u){
swap(node[u],node[max]);
heapify(max);
}
}
int size(){
return siz;
}
void clr(){
siz=0;
}
bool empty(){
return !siz;
}
void push(CONT &val){ //加点函数(简单吧~)
node[++siz]=val;
int now=siz,parent=siz>>1;
while(parent>0&&node[parent]<node[now]){
swap(node[now],node[parent]);
parent=(now=parent)>>1;
}
}
CONT top(){
return node[1];
}
void pop(){
swap(node[1],node[siz]);
siz--;
heapify(1);
}
void in(CONT &val){
node[++siz]=val;
}
void cpy(CONT *a,int l,int r){
siz=0;
for(int i=l;i<=r;i++)
node[++siz]=a[i];
}
void build(int &n){
siz=n;
for(int i=siz>>1;i>=1;i--)
heapify(i);
}
};



这只是最普通的二叉堆,只有取出顶部和删除顶部的功能,对于堆的合并要O(n)的时间(把两个表示堆的数组并起来build下),效率不佳。





如果堆可以合并,那么所有操作都统一了,加点就是把单节点的堆并入原来的堆,删除就是将堆顶的左右儿子合并起来。

为了方便合并,就不用完全二叉树了,因为接下来的结构有可能根本就不是完全二叉树了,不是完全二叉树了就不好用数组写了,那就用原始的指针开始写就好了。



我们知道如果一个过程是递归的那么代码也会很好写,那我们就让合并也成为一个递归的过程。

假设我们要合并A,B两个最大堆,如果A顶的值小于B顶的值就交换A和B(保持堆的性质)。

如果A是空堆就返回B,如果B是空堆就返回A。

否则我们就把A的右儿子(不一定非得是右,但整个递归过程必须是统一方向的儿子,例如全部都是右)和B合并,然后让A的右儿子变成A的右儿子和B合并起来的堆。

大概这样:



[cpp] view plain
copy

heap* merge(heap *a,heap *b)
{
if(a==NULL) return b;
if(b==NULL) return a;
if(a->key < b->key) swap(a,b);
a->r = merge(a->r,b);
return a;
}

这样虽然很简洁,但是合并着合并着右边就会越来越多,右边越多合并速度就会越慢,这样下去合并的速度就会退化。


那该怎么办?

于是我们想办法让堆的左边尽量多然后合并后如果右边多于左边就把右边的甩到左边去。



定义一个节点如果儿子不全(没有左儿子或右儿子,或者全没有)就称其为外部节点,每个节点往下走到其最近的外部节点所走的边数叫做该节点的距离,外部节点的距离为0。

然后我们维护一棵树,使其每个节点左儿子的距离不小于其右儿子的距离,这样的树就是左偏树。

左偏树又称为左高树、左式堆……。

左偏树有个很方便的性质:每个节点的距离等于其右儿子距离+1(证明网上搜一下就有)。

另外空节点的距离为-1。



那么合并函数就变成了这样:

[cpp] view plain
copy

_node* merge(_node *a,_node *b)
{
if(a == NULL) return b;
if(b == NULL) return a;
if(a->key < b->key) swap(a,b);
a->r = merge(a->r,b);
if(dis(a->l) < dis(a->r)) swap(a->l,a->r);
a->dis = dis(a->r) + 1;
return a;
}

其最坏复杂度为O(logn1+logn2),n1是a的节点数,n2是b的节点数。




加点就成了这样:

[cpp] view plain
copy

void push(CONT val)
{
siz++;
_node *p = newnode();
p->l = p->r = NULL;
p->key = val;
head = merge(head,p);
}



删除顶部元素就是这样:

[cpp] view plain
copy

void pop()
{
if(siz == 0) return ;
siz--;
_node *p = head;
head = merge(head->l,head->r);
delnode(p);
}

虽然写起来比二叉堆麻烦了点,空间占用也比二叉堆多,但是其合并速度远远超过二叉堆。


总体来说左偏树是一个中庸的数据结构,其合并速度慢于二项堆和fibonacci堆,但是变成复杂度远小于后两者。

也可以说左偏树是兼顾了合并速度和编程复杂度低的两个优点。



其实左偏树还有个变种,叫做斜堆,斜堆没有了距离这个概念,它在合并的时候不管左儿子多还是右儿子多总是交换一次:

[cpp] view plain
copy

_node* merge(_node *a,_node *b)
{
if(a == NULL) return b;
if(b == NULL) return a;
if(a->key < b->key) swap(a,b);
a->r = merge(a->r,b);
swap(a->l,a->r);
return a;
}
虽然复杂度有可能会变成O(n),但总体上和左偏树是相差无几的。





附上我自己写的左偏树代码:

[cpp] view plain
copy

struct leftist_tree{
#define CONT int
private:
struct _node{
CONT key;
int dis;
_node *l,*r;
};
_node *head;
_node* newnode()
{
_node *p = (_node*)malloc(sizeof(_node));
return p;
}
void delnode(_node *p)
{
free(p);
}
public:
int siz;
leftist_tree(){siz=0;head=NULL;}
void clr()
{
head=NULL;
siz=0;
}
int dis(_node *p)
{
if(p==NULL) return -1;
return p->dis;
}
_node* merge(_node *a,_node *b)
{
if(a == NULL) return b;
if(b == NULL) return a;
if(a->key < b->key) swap(a,b);
a->r = merge(a->r,b);
if(dis(a->l) < dis(a->r)) swap(a->l,a->r);
a->dis = dis(a->r) + 1;
return a;
}
void merge(leftist_tree &b)
{
head = merge(head,b.head);
siz += b.siz;
}
CONT top()
{
if(head == NULL) return NULL;
return head->key;
}
void push(CONT val)
{
siz++;
_node *p = newnode();
p->l = p->r = NULL;
p->key = val;
head = merge(head,p);
}
void pop()
{
if(siz == 0) return ;
siz--;
_node *p = head;
head = merge(head->l,head->r);
delnode(p);
}
};






据说二项堆、fibonacci堆编程复杂度高得那啥,过一阵子学吧。。。。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值