堆(优先队列)

本文介绍了二叉堆的基础概念与实现方法,并详细探讨了左式堆、斜堆和二项队列等高级优先队列数据结构。这些数据结构在支持高效插入和删除操作的同时,还提供了如合并等额外功能。
堆是一种至少允许下面两种操作的数据结构
  • Insert(插入)
  • DeleteMin/DeleteMax(删除最小/大者),它的工作是找出、返回和删除优先队列中的最小/大元素。

实现堆的几种方式:

  • 第一种:定义一个单链表在表头执行插入操作,删除最小值时遍历该表找出最小的元素并删除。(插入省时间,删除费时间)
  • 第二种:始终让表保持排序状态。(插入费时间,删除省时间)
  • 第三种:二叉堆。插入使用二叉查找最小值,插入和删除最小值的时间差不多。
二叉堆(binary heap)

二叉堆是一个特殊的二叉树,需要满足以下结构性和堆序性。

  • 结构性:一棵需要被完全填满的二叉树,如果底层不填满时,需要从左到右填入。这样的二叉树被称为完全二叉树

这里写图片描述

一项很重要的发现:因为完全二叉树很有规律,所以它可以用一个数组表示而不需要指针。例如上图中的堆对应的数组如下图所示,对于数组中任一位置i上的元素,其左儿子在2i的位置,有儿子在左儿子后的单元(2i+1)中,他的父亲则在(i/2)上。因此。不仅指针在这里不需要,而且遍历该树所需要的操作也极简单,在大部分计算机上运行的也非常快。
这里写图片描述

  • 堆序性:保证每一个父节点的值都小于任意一个子节点。为了使操作被快速执行,最小(最大)元素应该在根上。为了保证任意的子树也应该是一个堆,那么任意节点就应该小于它的所有后裔。由此得出堆序性质:在一个堆中,对于每一个节点X,X的父亲中的关键字小于等于(或大于等于)X中的关键字,根节点除外。

通过二叉堆的结构性和堆序性可以看出下图左是一个堆,右图只是一棵树。
这里写图片描述

堆的基本操作:insert(插入)和deleteMin(删除最小)。

  • Insert(插入):如下图,为了插入14,我们在堆的下一个可用位置建立一个空穴,由于将14插入空穴破坏了堆序性质,因此将31移入该空穴。这样的策略被称为上滤
    这里写图片描述

由于堆的可被数组代替的规律,所以实现起来也比较容易。

#include <stdio.h>
#include <malloc.h>

typedef struct HeapStruct *PriorityQueue;
typedef struct ElementType *ElementType;

struct ElementType{};
int MinPQSize;

//声明堆
struct HeapStruct{
    int Capacity;
    int Size;
    ElementType *Elements;
};

//检查堆是否已经被装满了。
int IsFull(PriorityQueue H);

//判断堆是不是满的。
int IsEmpty(PriorityQueue H);

//初始化一个堆。
PriorityQueue Initialize(int MaxElement){
    PriorityQueue H;

    if(MaxElement < MinPQSize){
        printf("队列的宽度太小了。");
    }

    H = malloc(sizeof(struct HeapStruct));

    if(H == NULL){
        printf("Out of space.");
    }

    H->Elements = malloc((MaxElement + 1) * sizeof(ElementType));

    if(H->Elements == NULL){
        printf("Out of space.");
    }

    H->Capacity = MaxElement;
    H->Size = 0;
    H->Elements[0] = MinData;

    return H;
}

//向堆中添加元素。
void Insert(ElementType X,PriorityQueue H){
    int i;

    if(IsFull(H)){
        printf("Priority queue is full.");
    }

    for(i = ++H->; H->Elements[i/2] > X; i /=2){
        H->Elements[i] = H->Elements[i/2];
    }
    H->Elements[i] = X;
}
  • DeleteMin(删除最小元素):DeleteMin与Insert相似。例如下图删除堆中的元素13,此时需要正确的将31放到堆中。31不能放在空穴中,会破坏堆序性质。于是,将较小的儿子14放置空穴,同时空穴下滑一层。重复此过程,把19放入空穴,在更上一层建立一个新的空穴……。这种策略被叫做下滤

这里写图片描述

现实中经常发生的错误是当堆中存在偶数个元素时。此时将会有一个节点只有一个儿子。因此在DeleteMin的代码实现中,加了一步附加测试(代码注释中有提到),原理就是始终保持你的算法把每一个节点都看成有两个儿子。

//DeleteMin(删除最小元素)
ElementType DeleteMin(PriorityQueue H){
    int i,Child;

    ElementType MinElement,LastElement;

    if(IsEmpty(H)){
        printf("queue is empty");
        return H->Elements[0];
    }

    MinElement = H->Elements[1];
    LastElement = H->Elements[H->Size--];

    for(i = 1; i*2 <= H->Size; i = Child){
        Child = i*2;
        //这一步是上面文字中提到的附加测试。
        if(Child != H->Size && H->Elements[Child + 1] < H->Elements[Child]){
            Child ++;
        }

        if(LastElement > H->Elements[Child]){
            H->Elements[i] = H->Elements[Child];
        } else{
            break;
        }

        H->Elements[i] = LastElement;

        return MinElement;
    }
}
d-堆

d-堆是所有节点都有d个儿子的堆,二叉堆又可被叫做2-堆。

左式堆
  • 左式堆是一种支持合并操作的二叉堆。所以左式堆不能用数组去实现了,因为把一个数组拷贝到另一个数组非常浪费时间。也正是因为如此,所有支持高效合并的高级数据结构都需要使用指针。
  • 左式堆也具有结构性和堆序性,左式堆与二叉堆唯一的区别是:左式堆不是理想平衡的,二十趋于非常不平衡。
  • 左式堆的核心约束条件就是:任意结点的左孩子的Npl大于等于右孩子的Npl。

左式堆的性质:任意一节点X的零路径长(null path length,Npl)Npl(X)定义为从X到一个没有两个儿子的节点的最短路径长。所以左式堆的性质是:对于堆中的每一个节点X,左儿子的零路径长大于等于右儿子的零路径长。

这里写图片描述

左式堆的合并操作:

以下面两个左式堆H1和H2的合并的过程为例。

这里写图片描述

首先将具有大的根值的堆与具有小的根值的堆的右子树合并。本例子中是递归的将H2与H1中根在8处的右子树合并(6-22)。现在让这个新的堆称为H1的根的右儿子(6-23)。最终的结果虽然是一个满足堆序性质的堆,但它不是左式堆,因为左子树的零路径长为1而右子树的零路径长为2,因此左式的性质在根处被破坏。不过可以看到根的右子树是左式的。这样一来只要交换根的左儿子和右儿子并更新零路径长就完成了合并(Merge)。

这里写图片描述

#include <stdio.h>
#include <malloc.h>

typedef struct TreeNode *PriorityQueue;
typedef struct ElementType *ElementType;

struct ElementType{};

//左式堆类型声明
struct TreeNode{
    ElementType Element;
    PriorityQueue Left;
    PriorityQueue Right;
    int Npl;//当前节点的零路径长。
};


//左式堆合并操作
PriorityQueue Merge(PriorityQueue H1,PriorityQueue H2){
    if(H1 == NULL){
        return H2;
    }

    if(H2 == NULL){
        return H1;
    }

    if(H1->Element < H2->Element){
        return Merge1(H1,H2);
    } else{
        return Merge1(H2,H1);
    }

}

//真正实现合并操作的部分
static PriorityQueue Merge1(PriorityQueue H1,PriorityQueue H2){
    if(H1->Left == NULL){
        H1->Left = H2;
    } else{
        H1->Right =Merge(H1->Right,H2);
        if(H1->Left->Npl <H1->Right->Npl){
            SwapChildren(H1);
            H1->Npl = H1->Right->Npl + 1;
        }
    }
    return H1;
}

//左式堆的插入
PriorityQueue Insert(ElementType X,PriorityQueue H){
    PriorityQueue SingleNode;

    SingleNode = malloc(sizeof(struct TreeNode));
    if(SingleNode == NULL){
        printf("out of space");
    }else{
        SingleNode->Element = X;
        SingleNode->Npl = 0;
        SingleNode->Left = SingleNode->Right = NULL;
        H = Merge(SingleNode,H);
    }

    return H;
};

//左式堆的删除最小元素。
PriorityQueue DeleteMin(PriorityQueue H){
    PriorityQueue LeftHeap,RightHeap;

    if(IsEmpty(H)){
        printf("Priority queue is empty.");
        return H;
    }

    LeftHeap = H->Left;
    RightHeap = H->Right;
    free(H);
    return Merge(LeftHeap,RightHeap);
}
斜堆
  • 斜堆(skew heap)是左式堆的自调节形式,实现起来极其简单。斜堆和左式堆的关系类似于伸展树和AVL树间的关系。
  • 斜堆(Skew Heap)与左式堆非常相似,唯一的不同是,其节点上并没有NPL属性。左式堆的merge操作总是在右子树上进行,当某节点出现左子树NPL小于右子树时,则交换左右子树。而斜堆不判断NPL属性,每一次合并都会无条件交换左右子树,通过这种方式来使其摊还时间复杂度达到O(logN)。
二项队列

二项队列(binomial queue)不同于优先队列(堆)在于,一个二项队列不是一棵堆序的树,而是堆序树的集合,称为森林(forest)。堆序树中的每一棵树都是具有约束的形式,叫做二项树。

这里写图片描述

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值