优先队列(堆)

本文深入探讨了优先队列的概念、实现原理及其在不同场景的应用。重点介绍了使用堆(二叉堆)来构建优先队列的方法,包括插入、删除最小元素、增加/减小元素优先度等基本操作。此外,还提供了优先队列的声明、初始化、基本操作的代码实现,以及注意事项和优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

优先队列简介


队列是计算机科学中的一类抽象数据类型。我们都知道普通队列结构具有数据元素“先进先出”的结构特点,也就是先输入的数据元素的优先权大于后输入元素的优先权,最先输入的元素具有最高优先权,此时仅需O(1)的时间复杂度就可以对它进行访问。

但是现实生活中数据元素的优先级往往是多变的,随着应用环境的改变元素之间的优先级关系往往也会随之改变。比如,在多用户环境中,操作系统调度程序必须根据程序运行的优先级在若干程序进程中选取将要下一个将要执行的程序(并不仅仅考虑程序的运行的先后性)。因此我们定义优先队列这一数据结构类型来表达数据元素之间的优先级关系,并根据数据元素的优先级进行优先队列的构建、插入、删除等操作。

优先队列基本模型

                                   图1.优先队列基本模型

建立优先队列


我们使用堆(二叉堆)来实现优先队列,它是优先队列的经典实现方式。同二叉查找树一样堆也具有两个性质,结构性以及堆序性。

堆的结构性是指堆可被看作是一颗完全二叉树。而由完全二叉树的定义我们可以知道一颗高为h的完全二叉树有(2的h次幂)到(2的(h+1)次幂-1)个元素。通过观察我们可以发现,完全二叉树具有规律性,结点同双亲结点以及孩子结点的关系不需要单独声明指针来表示,可以仅仅使用一个数组就表示这些信息。对于数组中任意位置i上的元素,其左右儿子分别在位置2i以及位置2i+1处(注意完全二叉树的性质),它的双亲结点则位于它位置的[ i / 2]处。由此,一个堆结构可由一个数组、一个代表最大值的整数以及当前堆的大小构成。完全二叉树的简单数组实现如下所示。

 -  A   B   C   D   E   F   G   H       
0   1   2   3   4   5   6   7   8   9   10

完全二叉树的数组实现

堆的堆序性质定义:在一个堆中,根节点除外,对于每一个结点X,X的双亲结点的优先度都不大于X的优先度。==根据堆序性,对于一个给定的堆,我们总可以在根节点处找到最小元,也即是说我们能以常数时间访问堆的最小元。

/*优先队列(堆)的声明*/
typedef int T;      // 不一定是int,可以定义为任意可用类型
const int Mindata = 0;

struct Heap
{
    int Capacity;
    int size;
    T * Elements;
};


typedef Heap* PriorityQueue;

PriorityQueue Initialize(int MaxElements)
{
    PriorityQueue H;

    H->Elements = (T*)malloc((MaxElements + 1) * sizeof(T));
    H = (PriorityQueue)malloc(sizeof(struct Heap));

    H->Capacity = MaxElements;
    H->size = 0;
    H->Elements[0] = Mindata;   // Mindata是一个哨兵,我们将它的值设置得很小,保证它小于等于堆中的最小元素,用于使上滤操作停止。
    return H;
}

优先队列的基本操作


一、插入操作

为将一个元素X插入到堆中我们需要进行如下操作。首先在下一个空闲位置(即二叉树下一个插入位置)创造一个空穴;若将X放入空穴位置不会破坏堆的堆序性,则插入完成;否则将空穴与该空穴双亲结点“交换位置“,继续上一步的判断直至X能被放入空穴为止。这种操作又称为上滤操作。

void Insert(T X, PriorityQueue H)   // T为数据元素的类型(eg.int)
{
    int i;
    if (isFull(H))
    {
        cout << "Heap is full!";
            return;
    }
    for (i = ++H->size; H->Elements[i / 2] > X; i /= 2) // 由堆的结构性可知,结点i处的元素其双亲结点位于i/2处  
        H->Elements[i] = H->Elements[i / 2];        // 双亲结点大于X,则将其移入空穴,并将空穴上滤。
    H->Elements[i] = X;
}

二、删除最小元操作

我们可以以常数时间访问最小元,难点在于删除它后堆的结构性以及堆序性被破坏了,需要重建这种秩序。当删除一个最小元时,在根节点处产生一个空穴(或称之为标记),由于堆中少了一个元素,堆的最后一个元素X必须移动到一个合适位置(注意思考这里),不断调整空穴位置直至最后一个元素X可以被放置到空穴中。调整空穴的具体过程为,首先将根节点的两个孩子结点中较小的元素放入根节点,将该孩子结点设置为空穴;然后判断X是否可以被放置在空穴中,若可以,则置入X结束;否则重复上一步操作将空穴下移至合适位置。这种操作又称为下滤操作。

T Front(PriorityQueue H)
{
    if (isEmpty(H))
    {
        cout << "Heap is empty!";
        return;
    }
    return H->Elements[1];
}

T Delete(PriorityQueue H)
{
    if (isEmpty(H))
    {
        cout << "Heap is empty!";
        return;
    }
    int i, child;
    T MinElement, LastElement;
    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为该双亲值最小的孩子的位置
            child++;
        if (LastElement > H->Elements[child])   // 若不符合判定条件,继续下滤操作
            H->Elements[i] = H->Elements[child];
        else
            break;
    }
    H->Elements[i] = LastElement;
    return MinElement;
}

三、增加/减小元素优先度操作

Increase(i, x, H)操作将增加在位置i处元素的权值(正增加),这可以使用下滤操作来实现。同样若想减小某处元素的权值,可以定义相应的Decrease函数并使用上滤操作来实现。

void Increace(int p, T increase, PriorityQueue H)
{
    if (isEmpty(H))
    {
        cout << "Heap is empty!";
        return;
    }
    int i, child;
    T MinElement, LastElement;
    MinElement = H->Elements[p];
    LastElement = H->Elements[p] + increase;
    for (i = 1; i * 2 <= H->size; i = child)    // 开始下滤操作
    {
        child = i * 2;
        if (child != H->size && H->Elements[child + 1] < H->Elements[child])//child为该双亲值最小的孩子的位置
            child++;
        if (LastElement > H->Elements[child])   // 若不符合判定条件,继续下滤操作
            H->Elements[i] = H->Elements[child];
        else
            break;
    }
    H->Elements[i] = LastElement;
}

void Decreace(int p, T increase, PriorityQueue H)
{
    if (isEmpty(H))
    {
        cout << "Heap is empty!";
        return;
    }
    int i, child;
    T MinElement, LastElement;
    MinElement = H->Elements[p];
    LastElement = H->Elements[p] - increase;     //  
    for (i = ++H->size; H->Elements[i / 2] > LastElement; i /= 2)   // 开始上滤
        H->Elements[i] = H->Elements[i / 2];    // 双亲结点大于X,则将其移入空穴,并将空穴上滤。
    H->Elements[i] = LastElement;
}

四、删除操作

定义Delete(i,H)函数删除位置i处的元素,这可以通过首先调用Decrease函数大幅减小i处元素的权值(等同于大幅增加该元素优先度)确保其通过上滤到达首位置,再调用Delete函数即可实现删除操作。

五、输入关键字构建堆

这可以通过循环调用Insert来实现;但更为一般的方法是将N个关键字放入二叉树中,保持其结构性;然后再通过一系列下滤操作即可实现堆序性。

void BuildHeap(PriorityQueue H)
{
    if (isEmpty(H))
    {
        cout << "Heap is empty!";
        return;
    }
    int i, child;
    T Element;
    for (i = H->size / 2; i > 0; i--)
    {
        child = i * 2;
        if (child != H->size && H->Elements[child + 1] < H->Elements[child])//child为该双亲值最小的孩子的位置
            child++;
        if (H->Elements[i] > H->Elements[child])    // 若不符合判定条件,继续下滤操作
        {
            Element = H->Elements[i];
            H->Elements[i] = H->Elements[child];
            H->Elements[child] = Element;
        }
    }
}

六、注意事项

上述代码中的Elemennt默认是按照int类型来编写的代码,如果想要使用其他类型,可能需要通过重载运算符函数重新定义适用于上述操作(比如加法、乘法、以及比较运算等操作)。

添加两个基础函数

bool isFull(PriorityQueue H)
{
    return H->size + 1 == H->Capacity;
}

bool isEmpty(PriorityQueue H)
{
    return H->size == 0;
}

声明


上述代码仅供参考,读者应该根据具体问题进行自己的思考与设计。

本文部分整理自《数据结构与算法分析》-C语言描述版.(美)Mark Allen weiss 著. 冯舜玺 译

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值