最小最大堆 数据结构说解

最小最大堆的定义

最小最大堆(min-max heap)是支持两种操作 DeleteMin 和 DeleteMax 的数据结构,每个操作用时 O(log N)。该结构与二叉堆相同,不过,其堆序性质为:对于在偶数深度上的任意节点 X,存储于 X 上的关键字小于它的父亲但是大于它的祖父(这是有意义的),对于奇数深度上的任意节点 X,存储在 X 上的关键字大于它的父亲但是小于它的祖父。

以上内容节选自 “Data Structures and Algorithm Analysis in C” 一书,其中文译名为《数据结构与算法分析——C语言描述》。在实际应用中,最小最大堆一般作为双端优先队列使用。注意,在定义中,根节点的深度为 0。

由于堆的性质,最小最大堆不会将相同权值的节点合并(一般情况下,合并相同权值的节点对堆而言是不可能的)。最小最大堆一般用隐式树实现即可。对于最小最大堆的定义代码如下:

int Heap[MAX_HEAP],Heap_Size=0;

最小最大堆的单元素深度查询

在最小最大堆中,它的性质决定了它与普通的二叉堆不同的结构。不难发现,如果任意选取一条从根节点到叶节点的路径,则该路径上的点必定满足该点权值同时大于相邻两点权值或同时小于相邻两点权值。

换言之,若该路径为 (a1, a2, a3, .. ,an),则当 n 为奇数时满足 a1 < a3 < a5 < .. < an < an-1 < .. < a4 < a2,当 n 为偶数时满足 a1 < a3 < a5 < .. < an-1 < an < .. < a4 < a2(此处略去了相等的情况)。于是,为了方便起见,我们将最小最大堆分为奇数层和偶数层,将两种层中的元素分开讨论。

在最小最大堆中,定义根节点位于偶数层上,与根节点直接相连的节点位于奇数层上,则我们可以发现任意节点的层都可以由其父节点的层推出。一种更简便的方法是,判断该节点权值与其父节点权值和祖父节点权值的相对大小来确定点层数的奇偶。不过这种方法有一个风险,若是发现权值相等则情况略显复杂。对于最小最大堆的单元素深度查询代码如下:

bool Heap_Depod(int np)//判断是否为奇数层
{
    if(np==1)
        return false;
    return 1^Heap_Depod(np>>1);
}

最小最大堆的单元素插入维护

最小最大堆的插入操作较普通的二叉堆而言大同小异,具体操作步骤为在堆尾添加待插入元素,与其父节点权值比较判断是否满足性质,而后只需一路与其祖父节点权值比较判断是否满足性质即可。我们可以用空位的方法优化。

以这个过程相当于在一条路径的末尾插入一个新的节点,从权值大小来看相当于在有序权值序列 a1 < a3 < a5 < .. < a4 < a2 的中间插入了一个新的权值,而与父节点权值比较的过程,相当于判断是应该在有序权值序列中向前交换元素以维护权值序列的有序,还是应该向后交换元素以维护其有序。这样,与其祖父节点权值比较判断的过程也就不难理解了。

在这里需要注意的一点是,每次插入我们只考虑一条路径的情况以满足性质,但事实上,每次插入后性质被破坏的路径只可能有一条而已。最小最大堆只维护每条由根节点到叶节点的路径满足性质,而不维护每层与每层之间满足性质。换言之,任何对最小最大堆进行层次遍历的行为都是毫无意义的。对于最小最大堆的单元素插入维护代码如下:

void Heap_FixUp(int np)
{
    int nq=np;
    Heap[0]=Heap[np];
    if(np>1&&!Heap_Depod(np)&&Heap[np>>1]>Heap[0])
        Heap[np]=Heap[nq=np>>1];
    else if(np>1&&Heap_Depod(np)&&Heap[np>>1]<Heap[0])
        Heap[np]=Heap[nq=np>>1];
    if(!Heap_Depod(nq))
        while(nq>3&&Heap[nq>>2]<Heap[0])
            Heap[nq]=Heap[nq>>2],nq>>=2;
    else
        while(nq>3&&Heap[nq>>2]>Heap[0])
            Heap[nq]=Heap[nq>>2],nq>>=2;
    Heap[nq]=Heap[0];
}
void Heap_Insert(int add)
{
    Heap[++Heap_Size]=add;
    Heap_FixUp(Heap_Size);
}

最小最大堆的最值查询删除维护

最大最小堆得名的原因在于它能同时以较优的时间复杂度查询最小值和最大值。对于一个一般的最小最大堆而言,其最小值即在根节点处,而其最大值在与根节点直接相连的两个节点处,这不难证明。但需注意,当最小最大堆中只有一个元素时,其最大值即在根节点处。

我们可以用与普通的二叉堆相类似的方法完成最小最大堆的最值删除维护。我们先将堆尾的元素移动到待删除元素的位置覆盖,再通过将与其同奇数层或同偶数层的节点通过上移的方法在不破坏性质的前提下将移动的元素下移至倒数第一层或倒数第二层,再相当于完成一次插入维护的上移即可。和插入一样,我们也可以用空位的方法优化。

对于该过程的分析与插入相似。首先当我们删除一个元素产生空位时,我们将其四个孙节点中最优的节点(可能是权值最大的节点,也可能是权值最小的节点,要视该节点深度的奇偶而定)移到该节点处以维护堆的性质。从权值大小来看相当于在有序权值序列 a1 < a3 < a5 < .. < am 中删除了一个元素 ak 而将 ak+2 到 am 元素向前平移的过程(若 k 为偶数,则相当于在 am < .. < a4 < a2 中删除 ak 而将 am 到 ak+2 向后平移)。在这之后的分析就和插入是类似的了。对于最小最大堆的最值查询删除维护代码如下:

void Heap_FixDown(int np)
{
    int nq=np;
    Heap[0]=Heap[np];
    if(!Heap_Depod(nq))
        while(nq*4<=Heap_Size)
            if(nq*4+3<=Heap_Size
                &&Heap[nq*4+3]>=Heap[nq*4+2]
                &&Heap[nq*4+3]>=Heap[nq*4+1]
                &&Heap[nq*4+3]>=Heap[nq*4])
                if(Heap[nq*4+3]>Heap[0])
                    Heap[nq]=Heap[nq*4+3],nq=nq*4+3;
                else
                    break;
            else if(nq*4+2<=Heap_Size
                &&Heap[nq*4+2]>=Heap[nq*4+1]
                &&Heap[nq*4+2]>=Heap[nq*4])
                if(Heap[nq*4+2]>Heap[0])
                    Heap[nq]=Heap[nq*4+2],nq=nq*4+2;
                else
                    break;
            else if(nq*4+1<=Heap_Size
                &&Heap[nq*4+1]>=Heap[nq*4])
                if(Heap[nq*4+1]>Heap[0])
                    Heap[nq]=Heap[nq*4+1],nq=nq*4+1;
                else
                    break;
            else
                if(Heap[nq*4]>Heap[0])
                    Heap[nq]=Heap[nq*4],nq=nq*4;
                else
                    break;
    else
        while(nq*4<=Heap_Size)
            if(nq*4+3<=Heap_Size
                &&Heap[nq*4+3]<=Heap[nq*4+2]
                &&Heap[nq*4+3]<=Heap[nq*4+1]
                &&Heap[nq*4+3]<=Heap[nq*4])
                if(Heap[nq*4+3]<Heap[0])
                    Heap[nq]=Heap[nq*4+3],nq=nq*4+3;
                else
                    break;
            else if(nq*4+2<=Heap_Size
                &&Heap[nq*4+2]<=Heap[nq*4+1]
                &&Heap[nq*4+2]<=Heap[nq*4])
                if(Heap[nq*4+2]<Heap[0])
                    Heap[nq]=Heap[nq*4+2],nq=nq*4+2;
                else
                    break;
            else if(nq*4+1<=Heap_Size
                &&Heap[nq*4+1]<=Heap[nq*4])
                if(Heap[nq*4+1]<Heap[0])
                    Heap[nq]=Heap[nq*4+1],nq=nq*4+1;
                else
                    break;
            else
                if(Heap[nq*4]<Heap[0])
                    Heap[nq]=Heap[nq*4],nq=nq*4;
                else
                    break;
    if(!Heap_Depod(nq))
    {
        if(nq*2+1<=Heap_Size
            &&Heap[nq*2+1]>=Heap[nq*2]
            &&Heap[nq*2+1]>Heap[0])
            Heap[nq]=Heap[nq*2+1],nq=nq*2+1;
        else if(nq*2<=Heap_Size
            &&Heap[nq*2]>Heap[0])
            Heap[nq]=Heap[nq*2],nq=nq*2;
    }
    else
        if(nq*2+1<=Heap_Size
            &&Heap[nq*2+1]<=Heap[nq*2]
            &&Heap[nq*2+1]<Heap[0])
            Heap[nq]=Heap[nq*2+1],nq=nq*2+1;
        else if(nq*2<=Heap_Size
            &&Heap[nq*2]<Heap[0])
            Heap[nq]=Heap[nq*2],nq=nq*2;
    Heap[nq]=Heap[0];
    Heap_FixUp(nq);
}
int Heap_DeleteMin()
{
    Heap[0]=Heap[1];
    Heap[1]=Heap[Heap_Size--];
    Heap_FixDown(1);
    return Heap[0];
}
int Heap_DeleteMax()
{
    if(Heap_Size==1)
        Heap[0]=Heap[Heap_Size--];
    else if(Heap_Size==2||Heap[2]>Heap[3])
        Heap[0]=Heap[2],
        Heap[2]=Heap[Heap_Size--],
        Heap_FixDown(2);
    else
        Heap[0]=Heap[3],
        Heap[3]=Heap[Heap_Size--],
        Heap_FixDown(3);
    return Heap[0];
}

最小最大堆的总结

尽管在 STL 里给出了双端优先队列的封装,但是由于其消耗时间之长使得其在算法竞赛中一直鲜有知之者。诚然,双端优先队列不止只有最小最大堆这一种实现方法,但本文在这里也不再做过多的探究,对此感兴趣的读者自行探究即可。最小最大堆作为一种基础数据结构,其思想与 k-d 树也是不无相通之处。若加以深究,笔者认为最小最大堆还是有许多可以论述延伸之处的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值