优先队列:特殊的队列,取出元素的顺序是按照元素的优先权,而不是进入的先后。
堆是利用完全二叉树进行存储,分为最大堆和最小堆。最大堆的每一个节点都大于其子树的任何一个节点,最小堆的每一个节点都小于其子树的任何一个节点。
堆的操作集
堆的基本操作
MaxHeap Creat(int Maxsize); 创建一个空的最大堆
Boolean IsFull(MaxHeap H); 判断堆是否满
Inseret(MaxHeap H, ElemType e); 将元素插入最大堆
Boolean IsEmpty( MaxHeap H); 判断堆是否为空
Elemtype DeleteMax(MaxHeap H); 返回H中的最大值(取出并删除)
实现方法
结构
typedef struct HeapStruct *MaxHeap;
struct HeapStruct{
ElemType *Elements; //数组基地址
int Size; //当前的最后一个元素
int Capacity; //最大值
}
创建
MaxHeap Create( int Maxsize){
MaxHeap H=malloc((Maxsize+1)*sizeof(sturct HeapStruct));
H.Elements=(ElemType)malloc((MaxSize+1)*sizeof(ElemType));
H.Size=0;
H.Size=0;
H.Elements[0]=MaxData;
return H;
}
插入
以顺序方式存储,考虑将元素插入到数组的最后一个元素之后,此时根据完全二叉树的父节点是其节点编号除以二取整的特性,可以求得父节点,将两个节点值进行比较,若不符合最大堆的性质,则交换。
void insert(MaxHeap H, ElemType e){
int i;
if(IsFull(H)) return;
i=++H->Size;
for(;H->Elements[i]<e;i/=2){
H->Elements[i]=H->Elements[i/2]; //将父节点下调
}
H->Elements[i]=e;
}
这里我们就能发现哨兵的作用,可以防止到了数组下标为0的位置之后继续比较。
删除
为了保证最大堆完全二叉树的特性,首先想到用最后一个节点去替换根节点。替换过后,最大堆的有序性被改变,于是将根节点下调。
ElemType Delete(MaxHeap H){
int Parent,Child;
ElemType MaxItem,temp;
if(IsEmpty(H) return;
MaxItem=H->Elements[1];
temp=H->Elements[H->Size];
for(Parent=1;Parents*2<=H->Size;Parent=Child){
Child=Parent*2;
if(Child!=H->Size&&(H->Elements[Child]<H->Elements[Child+1])) Child++;
if(temp>=H->Elements[Child]) break;
else{
H->Elements[Parent]=H->Elements[Child];
}
H->Elements[Parent]=temp;
return MaxItem;
}
分析删除函数,通过将保存的原最大堆的最后一个值与子节点的最大值相比较,如果该子节点大于temp,说明temp的位置仍然在下一层,将该子节点上调一层,temp继续向下搜索。如果该子节点小于temp,说明temp的位置在上一层,则退出循环,然后将temp的值赋给Parent所指向的位置。通过上一步,我们知道Parent的值已经赋给其父节点,因此不会被破坏。同样,因为Parent这个位置之前有值存在,所以将temp放到这里时,二叉树仍然保持完全二叉树的形状。
有两个条件需要注意,首先第一个,循环进行的条件Parents*2<=H->Size
当这个条件不符合时,说明Parent
所指向的节点没有儿子,所以Parent
就是插入temp
的位置。但是,该条件判断时只关注了没有儿子的情况,如果Parent
只有左儿子,temp
显然应该插入到Parent
节点的右儿子处。因此,在没有右儿子或者右儿子大于左儿子时,都要将Child
的值加一,保证插入/下一次比较的节点是右儿子或者其子树。这也就是Child!=H->Size
的作用。
最大堆的建立
显然,最大堆的建立过程可以仿照二叉搜索树,先插入一个元素,后来插入的元素顺次调整。由完全二叉树的特性,每个元素插入时调整的时间复杂度为O(log2n),共需要插入n个元素,则时间复杂度为O(nlog2n).
而如果在数组中顺序存入n个元素,再将其调整为堆,则时间复杂度为O(n)。考虑删除算法,其本质是在两个堆中插入一个元素,将这两个堆合并成一个堆。因此,从第一个有子节点的节点开始考虑,由于它最多只有左右子节点,因此,这两个节点必然构成两个堆。将父节点按照删除算法的方式向下调整,则得到一个新的堆,新堆比原先的两个堆高度高一。接着按照数组中下标顺序向前调整,直到根节点。
void PercDown( MaxHeap H, int p )
{ /* 下滤:将H中以H->Data[p]为根的子堆调整为最大堆 */
int Parent, Child;
ElementType X;
X = H->Data[p]; /* 取出根结点存放的值 */
for( Parent=p; Parent*2<=H->Size; Parent=Child ) {
Child = Parent * 2;
if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
Child++; /* Child指向左右子结点的较大者 */
if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
else /* 下滤X */
H->Data[Parent] = H->Data[Child];
}
H->Data[Parent] = X;
}
void BuildHeap( MaxHeap H )
{ /* 调整H->Data[]中的元素,使满足最大堆的有序性 */
/* 这里假设所有H->Size个元素已经存在H->Data[]中 */
int i;
/* 从最后一个结点的父节点开始,到根结点1 */
for( i = H->Size/2; i>0; i-- )
PercDown( H, i );
}