数据结构 | 第10章 优先级队列 - 概述

44 阅读5分钟

第10章 优先级队列

此前的搜索树结构和词典结构,都支持覆盖数据全集的访问和操作——其中存储的每一数据对象都可作为查找和访问目标。为此,搜索树结构需要在所有元素之间定义并维护一个显式的全序 (full order) 关系;而词典结构中数据对象之间,尽管不必支持比较大小,但在散列表之类的具体实现中,都从内部强制地在对象的数值与其对应的秩之间,建立起某种关联,从而隐式地定义了一个全序次序。

就对外接口的功能而言,本章将要介绍的优先级队列,较之此前的数据结构反而有所削弱。具体地,这类结构将操作对象限定于当前的全局极值者。这种根据数据对象之间相对优先级对其进行访问的方式,与此前的访问方式有着本质区别,称作循优先级访问 (call-by-priority) 。

当然,“全局极值”本身就隐含了“所有元素可相互比较”这一性质。然而优先级队列并不会也不必忠实地动态维护这个全序,却转而维护一个偏序 (particial order) 关系。其高明之处在于,如此不仅足以高效地支持仅针对极值对象的接口操作,更可有效地控制整体计算成本对于常规的查找、插入或删除等操作,优先级队列的效率并不低于此前的结构;而对于数据集的批量构建及相互合并等操作,其性能却能更胜一筹。

10.1 优先级队列ADT

10.1.1 优先级与优先级队列

现实实例:在决定病人接受治疗次序时,除了他们到达医院的先后次序,更应考虑到病情的轻重缓急,优先治疗病情最为危重的人。

按照事先约定的优先级,可以始终高效查找并访问优先级最高数据项的数据结构,也统称作优先级队列 (priority queue)

10.1.2 关键码、比较器与偏序关系

仿照词典结构,我们也将优先级队列中的数据项称作词条(entry);而与特定优先级相对应的数据属性,也称作关键码(key)。不同应用中的关键码,特点不尽相同,但无论具体形式如何,作为确定词条优先级的依据,关键码之间必须可以比较大小——这与词典结构完全不同,后者仅要求关键码支持判等操作。

因此对于优先级队列,必须以比较器的形式兑现对应的优先级关系。尽管定义了明确的比较器即意味着在任何一组词条之间定义了一个全序关系,但优先级队列并不严格地维护这样一个全序关系(代价不菲),而是维护词条集的一个偏序关系,如此,不仅依然可以支持对最高优先级词条的动态访问,而且可将相应的计算成本控制在足以令人满意的范围之类。

10.1.3 操作接口

image.png

10.1.4 操作实例:选择排序

既便不清楚其具体实现,我们也已可以按照以上ADT接口,基于优先级队列描述和实现各种算法。如实现和改进3.5.3节所介绍的选择排序算法。O(n2) -> O(nlogn)

image.png

10.1.5 接口定义

因为这一组ADT接口可能有不同的实现方式,故这里均以虚函数形式统一描述这些接口,以便在不同的派生类中具体实现。

优先级队列标准接口:

 0001 template <typename T> struct PQ { //优先级队列PQ模板类
 0002    virtual void insert ( T ) = 0; //按照比较器确定的优先级次序插入词条
 0003    virtual T getMax() = 0; //取出优先级最高的词条
 0004    virtual T delMax() = 0; //删除优先级最高的词条
 0005 };

10.1.6 应用实例:Huffman编码树

  • 数据结构

    沿用5.29至5.33定义的Huffman超字符、Huffman树、Huffman森林、Huffman编码表、Huffman二进制编码串等数据结构。

  • 比较器

    若将Huffman森林视作优先级队列,则其中每一颗树(每一个超字符)即是一个词条,为保证词条之间可以相互比较,可如代码5.29所示重载对应的操作符。进一步地,因超字符的优先级可度量为其对应权重的负值,故不妨将大小关系颠倒过来,令小权重超字符的优先级更高,以便于操作接口的统一。

  • 编码算法

     0001 /******************************************************************************************
     0002  * Huffman树构造算法:对传入的Huffman森林forest逐步合并,直到成为一棵树
     0003  ******************************************************************************************
     0004  * forest基于优先级队列实现,此算法适用于符合PQ接口的任何实现方式
     0005  * 为Huffman_PQ_List、Huffman_PQ_ComplHeap和Huffman_PQ_LeftHeap共用
     0006  * 编译前对应工程只需设置相应标志:DSA_PQ_List、DSA_PQ_ComplHeap或DSA_PQ_LeftHeap
     0007  ******************************************************************************************/
     0008 HuffTree* generateTree ( HuffForest* forest ) {
     0009    while ( 1 < forest->size() ) {
     0010       HuffTree* s1 = forest->delMax(); HuffTree* s2 = forest->delMax();
     0011       HuffTree* s = new HuffTree();
     0012       s->insert ( HuffChar ( '^', s1->root()->data.weight + s2->root()->data.weight ) );
     0013       s->attach ( s1, s->root() ); s->attach ( s->root(), s2 );
     0014       forest->insert ( s ); //将合并后的Huffman树插回Huffman森林
     0015    }
     0016    HuffTree* tree = forest->delMax(); //至此,森林中的最后一棵树
     0017    return tree; //即全局Huffman编码树
     0018 }
    
  • 效率分析

    优先级队列的所有ADT操作均可在O(logn)时间内完成,故generateTree()算法也相应地可在O(nlogn)时间内构造出Huffman编码树。

    优先级队列如何使ADT的操作在O(logn)时间内完成呢?实际上,借助无序列表、有序列表、无序向量或有序向量,都难以同时兼顾insert()和delMax()操作的高效率。因此,必须另辟蹊径,寻找更为高效的实现方法。

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 18 天,点击查看活动详情