二叉堆这种数据结构对于优先队列的实现是如此的普遍。
二叉堆有两个性质:结构性和堆序性。
二叉堆的定义:
二叉堆是一棵完全二叉树。底层上的元素看做从左到右填入。二叉堆简称堆。
因为完全二叉树很有规律,所以它可以用一个数组来表示而不需要指针。对于数组上任一位置i的元素,其左儿子位置为2i,右儿子位置(2i+1)。则它的父亲在位置[ i / 2]上。
所以堆这种数据结构的声明,由一个数组(关键字不管什么类型),一个代表最大值的整数以及当前堆的大小组成。
struct Heap
{
int capacity;
int size;
int emt[maxn]; //数组
};
在一个堆中,对于每一个节点X,X的父亲中的关键字小于(或等于)X中的关键字,根节点除外(它没有父亲)。[也可以大于(或等于)看你怎么建堆了]
按照此性质,所以一个堆的任意子树也应该是一个堆,任意节点小于(或等于)它的所以后裔。
最小元总可以在根节点找到。
堆的初始化:
void init(Heap *H)
{
H->capacity = maxn; //maxn是数组的边界
H->size = 0;
H->emt[0] = MIN; //标记 MIN = -INF
}
堆的基本操作:
Insert()插入
为将一个元素X插入到堆中,我们在下一个空闲位置创建一个空穴(否则该堆就不是完全树了)。如果X放在该空穴中,而不破坏堆序性,那么插入完成。否则,我们把空穴的父亲节点元素移入到该空穴,这样,空穴就朝根的方向上行了一步。
继续改过程直至X能放入到空穴为止。
这种策略叫做 上滤。
insert()插入的时间复杂度是O(log N)(最坏情况)
void insert(int x, Heap *H) //插入
{
int i;
if (isFull(H))
{
cout<<"Qriority queue is full."<<endl;
return ;
}
H->size++;
for (i = H->size; H->emt[i/2] > x; i/= 2)
{
H->emt[i] = H->emt[i/2]; //空穴上滤
}
H->emt[i] = x;
}
MIN标记:
如果要插入的元素是新的最小值,那么它将一直被推向顶端。这一时刻,i 等于1,我们就需要令程序跳出循环。我们当然用可以明确的条件判断来做到。但是,我们采用的是把一个很小的值放在emt[0] 的位置以使循环终止,这个值必须保证小于堆中的任何值。这个值称之为 标记(sentinel)。这样避免了每个循环都要执行的条件判断,从而节省了时间。
DeteleMin()删除最小元
找到最小元是容易的,困难的是删除它。
当删除一个最小元时,在根节点处产生了一个空穴。由于现在堆少了一个元素,因此堆中最后一个元素X必须移动到该堆的某个地方。我们讲空穴的两个儿子中的较小者移入空穴,这样,就把空穴向下推了一层。重复该步骤,直到X可以被放入到空穴中。
因此,我们的做法是把X置入沿着从根开始包含最小儿子的一条路径上的一个正确的位置。
这种策略叫做 下滤。
最坏情况,根处的元素下滤到堆的底层。时间复杂度为O(log N).
void deleteMin(Heap *H) //删除最小元素
{
if (isEmpty(H))
{
cout<<"Qriority queue is empty."<<endl;
return ;
}
int lastemt, child, minemt, i;
minemt = H->emt[1];
lastemt = H->emt[H->size--];
for (i = 1; i*2 <= H->size; i=child)
{
child = i * 2;
if (H->emt[child+1] < H->emt[child])//选儿子结点中较小者
{
child++;
}
if (H->emt[child] < lastemt)
{
H->emt[i] = H->emt[child]; //空穴下滤
}
else break; //这句话不能省(当lastemt < H->emt[child]的时候,马上break;紧接着lastemt移到上一次child的位置)
}
H->emt[i] = lastemt;
cout<<"Current min key "<<minemt<<" has been deleted successful."<<endl;
return ;
}
buildHeap()构建堆
建堆最简单的方法,就是把N个关键字作为输入并把他们插入到空堆中。
使用N个相继的insert()插入操作来完成。
由于每个insert()将花费O(1)平均时间以及O(log N)的最坏情形时间,因此算法的总的运行时间是O(N)平均时间。
void buildHeap(Heap *H, int key[])//建堆
{
for (int i = 0; i < N; i++)
{
insert(key[i],H);
}
}
其他堆操作:
虽然求最小值操作可以在常数时间完成,但是,按照最小元设计的堆(也称作最小值堆)在求最大元时却无任何帮助。
我们只知道该最大元素在树叶上,但是半数的元素都位于树叶上。因此该信息是没什么用的。
方法:(下述三种操作对数最坏情形时间运行)
DecreaseKey(降低关键字的值)
通过降低位置P关键字的值,由于这可能破坏堆序性,因此会通过上滤对堆进行调整。
该操作对于系统管理员是有用的,可以使他们的程序以最高的优先级来运行。
IncreaseKey(增加关键字的值)
通过增加位置P关键字的值,下滤来完成。
许多调度程序自动降低正过多消耗CPU时间的进程的优先级。
Delete(删除)
此操作删除堆中位置P的节点
通过先执行DecreaseKey操作,然后再执行DeleteMin操作来完成。