使用二叉堆设计基于.NET的泛型优先级队列
2019-01-23黄明志
黄明志
(仲恺农业工程学院信息科学与技术学院,广州 510225)
0 引言
优先级队列类(PriorityQueue
1 基本算法
1.1 二叉堆
二叉堆是一棵完全二叉树(Complete Binary Tree),对于每个父节点,它的值均小于等于(或大于等于)其子节点。而对于具有n个节点的完全二叉树,可以按照从上到下(从根节点到叶节点)、从左到右的规则将各个节点映射到一个大小为n的一维数组中。
最小堆是指父节点的值均小于或等于其子节点的堆。而最大堆则是指父节点的值均大于或等于其子节点的堆。
二叉堆具有的特性:一是根节点的优先级最高;二是当新增节点或移除根节点后能够快速地重新建堆。因此,使用堆来存放元素是非常适合优先级队列这种数据结构的。
1.2 计算子节点的索引
计算某个节点的子节点在数组中基于0的索引的方法如下:对于某个索引为i的节点,左子节点的索引为2*i+1;而右子节点的索引则为2*(i+1),当然,也可以表示为左节点的索引加1。
1.3 堆化
堆化是指将数组变为最小堆(或最大堆)数组的操作。对于存储有n个节点的堆数组,堆化操作依次地从n/2-1开始,直到编号为0的节点。如果要将整个堆堆化为最小堆,则每次操作就是要比较当前节点与其子节点的值,确保当前节点的值比子节点的要小;否则,就将当前节点与其子节点进行交换,使其满足最小堆的性质。例如,假设数组共有9个元素,初始时处于无序状态(如图1);堆化操作将从索引为3(计算方法为:9/2-1,取整后为3)的节点开始,通过比较,发现其右子节点的值较小,故需对调其位置(如图2);同理,当堆化索引为1的节点时,首先是值为1和4的两个节点的位置对调(箭头1,如图3),然后是值为4和3的两个节点的位置也对调(箭头2);整棵二叉树完全堆化后如图4所示。
图1 初始时的无序状态
图2 堆化索引为3的节点
图3 堆化索引为1的节点
图4 堆化完成后的状态
2 具体设计
PriorityQueue
类的开始处的程序代码如下:
图5 PriorityQueue
基于性能的考虑,在PriorityQueue
Comparer用于确定元素的优先级的大小。其值可通过构造函数传递过来,如果从构造函数中传递过来的比较器为null,则使用相应类型比较器的默认值(即Comparer
modified标志供迭代时若发现队列中有任何元素被更改时作适当的处理。
2.1 构造函数
PriorityQueue
在实例化对象时,如果collection参数为非null,则需要进行堆化操作。下述的Heapify方法建立最小堆:
对于最小堆来说,就像上述基本算法所介绍的那样,SiftDown的作用是将一个节点和它的子节点进行比较,保证它比其所有的子节点都要小,否则,就通过交换两个节点来满足这个要求。这个调整或交换的顺序是从当前节点向下,一直到叶节点为止:
2.2 入队
元素入队时,若发现数组已满,则将数组的容量扩大一倍的操作由Array.Resize实施。
其中,SiftUp方法是将入队元素调整到恰当的位置,以确保整个堆仍然处于最小堆的状态。
入队的元素可以为null,这样的设计考虑使得PriorityQueue
如果不考虑自动扩展数组容量的操作,入队操作的时间复杂度主要由SiftUp方法来决定,即其时间复杂度为 O(logN)。
2.3 出队
由于堆处于最小堆状态,因此,出队的操作就是返回数组中的第一个元素。当然,返回并移除优先级最高的元素时,需要进行必要的调整,以确保堆再次处于最小堆的状态。另外,当队列为空(无元素)时抛出InvalidOperationException异常(这与.NET中Queue
出队操作的时间复杂度主要由SiftDown方法来决定,出队操作的时间复杂度O(logN)。
显而易见,Peek操作无需调用SiftDown方法,其时间复杂度为 O(1)。
2.4 实现枚举数与简单迭代
为了实现在泛型的优先级队列中进行简单迭代操作,同时让优先级队列类的设计具有良好的封装性,在PriorityQueue
(1)MoveNext方法
Enumerator
(2)Current属性
通过Current属性,获取集合中的当前元素。public E Current
(3)获取枚举数并实现IEnumerable
通过上述的设计,就令PriorityQueue
实现IEnumerable
2.5 实现ICollection及相关接口
(1)实现接口中的基本成员
ICollection接口主要有CopyTo和GetEnumerator方法以及Count、IsSynchronized和SyncRoot属性。例如,获取可用于同步ICollection访问的对象的代码为“public Object SyncRoot{get{return heap;}}”;而 IsSynchronized属性总是返回false。
(2)实现显式接口
优先级队列类实现了若干个显式接口。例如,获取一个循环访问集合的枚举数就有IEnumerable.GetE-numerator和IEnumerable
而显式接口ICollection.CopyTo的实现则如下:void ICollection.CopyTo(Array array,int arrayIndex)
而公共的CopyTo是通过显式将this对象转换为ICollection接口后调用上述显式方法实现的,也就是说,其方法体中仅需“((ICollection)this).CopyTo(array,arrarIndex)”这样一行语句即可。
为了更简单起见,上述代码中实例化各个异常时所传入的用于描述异常信息的字符串仅以参数名表示,而实际上应为可更明确地描述异常的文本。
3 测试
3.1 以实现IComparerable的类作为元素的测试
对于实现IComparerable
如果要按从大到小的顺序出队,就需要设计一个比较器,将比较操作反转。下面设计一个将优先级反转的类,以简化这类常见操作的使用:
这样,在构造上述整型优先级队列时,使用以下的比较器实例作为实参来构造队列即可实现将出队顺序由使用默认比较器时的从小到大反转为从大到小了:Comparer
3.2 以没有或非直接使用内置IComparerable接口的类作为元素的测试
当元素类并没有实现IComparerable
4 结语
优先级队列在宽带管理等应用场景中被广泛使用,而.NET中并没有相应的类。因PriorityQueue