首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

12.2  ADT优先队列:ADT表的变体

ADT表按查找关键字组织数据,便于在给定了查找关键字的情况下检索某个项。这样,如果数据库需要按值来维护和查找,如本章前面描述的城市表,则应使用ADT表。此处分析与ADT表相关的另一种ADT,对一些应用程序而言,它更适用。

假设有病人到医院急诊室就诊。当病人进院时,工作人员将病人的记录输入数据库,供护士、医生和收费处检索。工作人员还必须跟踪急诊室病人,并确定每位病人接受治疗的时间。

ADT表适用于医院的普通数据库。急诊室工作人员应为病人使用什么ADT呢?使用ADT表,可按病人姓名的字母表顺序,或按ID号码的顺序诊治病人;使用队列,可按病人的到达顺序诊治病人。在这两种情况下,若Zither女士因急性阑尾炎到急诊室候诊,就必须等到Able先生将手上的一根小刺拔掉才能就诊。很明显,急诊室工作人员应为候诊患者指定某个紧急级别,或优先级。下一位医生应诊治优先级最高的病人。急诊室工作人员需要一个能根据优先级找出下一位候诊患者的ADT。

每日或每周的任务列表也是一个使用优先级的例子。设本周的工作安排如下:

●       将生日卡送给Mabel姑妈。

●       开始编写有关世界历史的研究论文。

●       读完《墙和镜子》一书的第12章。

●       制订周六晚上的计划。

在查阅列表时,一般会选择先处理优先级最高的任务。

优先级值可以表示病人的诊治优先级,或完成任务的优先级。应当为优先级值使用什么参量?这有很多可能,例如从1到10的简单数字。假设最大的优先级值指示最高的优先级。优先级值是表示数据项的记录的一部分。将各个项插入ADT,然后向ADT请求具有最高优先级的项。

这样的ADT称为优先队列(priority queue)。其正式定义是:优先队列是一个提供以下操作的ADT:

(1) 构建空优先队列。

(2) 确定优先队列是否为空。

(3) 将新项插入优先队列。

(4) 检索然后删除优先队列中具有最高优先级值的项。

图12-8是优先队列类的UML图。下面的伪码详细指定了ADT优先队列的操作:

PriorityQueue

items

createPQueue()

pqIsEmpty()

pqInsert()

pqDelete()

图12-8  优先队列类的UML图

// PQItemType is the type of the items

// stored in the priority queue.

+createPQueue()

// Creates an empty priority queue.

 

+pqIsEmpty():boolean{query

// Determines whether a priority queue is empty.

 

+pqInsert(in newItem: PQItemType) throws PQueueException

// Inserts newItem into a priority queue. Throws

// PQueueException if priority queue is full.

 

+pqDelete(): PQItemType

 // Retrieves and then deletes the item in a priority queue

 // with the highest priority value.

这些操作类似于ADT表的一部分操作。显著区别在于pqDelete操作。tableRetrive和tableDelete表操作序列可以检索和删除查找关键字中包含给定值的项。pqDelete操作则可以检索和删除具有最高优先级值的项。注意,与tableRetrive和tableDelete不同,没有为pqDelete传递最高优先级的值。一般情况下,最高优先级的值是未知的,故很难用tableRetrieve和tableDelete完成该任务。另一方面,也不能使用pqDelete来检索和删除包含指定值的项。

由此看来,ADT优先队列和ADT表既有共同点,也有不同点。这可从它们的实现反映出来。开始时,先利用表的一些实现来实现优先队列。若优先队列中的项数很少,则可用有序的线性实现。基于数组的实现方案按优先级值的升序维护数据项,所以,具有最高优先级值的项在数组末端,如图12-9(a)所示。pqDelete只返回items[size-1]中的项,并使size减1。不过,在使用二叉查找确定插入位置后,pqInsert操作必须移动数组元素,为新项腾出空间。

如图12-9(b)所示,基于引用的线性实现按优先级值的降序排列各项,使具有最高优先级值的项处于链表开头。这样,pqDelete仅返回pqHead引用的项,然后将pqHead改为引用下一项。不过,pqInsert操作必须遍历链表,以确定正确的插入位置。这样,与表的线性实现一样,优先队列的线性实现面临着同样的困境。

图12-9  ADT优先队列的一些实现:(a)基于数组;(b)基于引用;(c)二叉查找树

考虑将二叉查找树作为优先队列的实现,如图12-9(c)所示。pqInsert操作与tableInsert操作相同,但在表操作中,并没有与pqDelete操作直接对应的操作。它必须在不知道最高优先级值的情况下查找包含该值的项。这个任务并不难,因为这一项一定是树中的最右节点。这样,只需跟踪rightChild引用,直至找到rightChild引用为null的节点为止(使用类似于二叉查找树的findLeftmost和deleteLeftmost方法,也可完成任务)。从树中删除这个节点也很容易,因为它最多只有一个子节点。

因此,对于表和优先队列而言,采用二叉查找树的实现都是一个不错的选择。但表和优先队列的用法不同。一些表应用程序主要涉及检索和遍历操作,故不影响二叉查找树的平衡。而优先队列不包含检索和遍历操作,它们的应用程序都涉及插入和删除,这将影响二叉查找树的形状。为此,应该使用第13章介绍的二叉查找树的均衡变体。若知道优先队列的最大项数,则堆的基于数组的实现是一个更好选择,详见下一节。采用堆的实现经常是优先队列最适合的选择,但基本不适用于表。

12.2.1  堆

堆是与二叉查找树类似的ADT,但又不同于二叉查找树,主要体现在两个方面。第一,二叉查找树可看作是有序的,而堆在更弱的意义上有序。不过,这种有序足以使优先队列的操作高效执行。第二,二叉查找树有多种不同的形状,而堆总是完全二叉树。

堆是完全二叉树,可以为空,或者:

(1) 根包含的查找关键字大于等于各个子节点的查找关键字。

(2) 根包含作为子树的堆。

在这个堆的定义中,根项包含的查找关键字最大。这样的堆也称为maxheap。相对而言,minheap将包含最小查找关键字的项放在根中。练习题16将进一步分析minheap。

图12-10是类Heap的UML图。下面是堆操作的伪码:

Heap

items

createHeap()

heapIsEmpty()

heapInsert()

heapDelete()

图12-10 类Heap的UML图

// HeapItemType is the type of the items stored in the

// priority queue.

 

+createHeap()

// Creates an empty heap.

 

+heapIsEmpty():Boolean {query}

// Determines whether a heap is empty.

 

+heapInsert(in newItem: HeapItemType) throws HeapException

// Inserts newItem into a heap. Throws HeapException

// if heap is full.

 

+heapDelete(): HeapItemType

// Retrieves and then deletes a heap's root item.

// This item has the largest search key.

堆是完全二叉树,因此,若知道堆的最大项数,则可以用二叉树的基于数组的实现,如第10章所述。例如,图12-11显示堆及其数组表示。堆节点中的查找关键字大于等于其子节点的查找关键字。另外,在堆中,子节点的查找关键字彼此无关;换言之,并不知道哪个子节点包含的查找关键字更大。

图12-11  堆及数组表示

1. 堆的基于数组的实现

用下面的数据字段表示堆:

●       items:堆项数组

●       size:表示堆中项数的整数

数组items对应于树的基于数组的表示。在下面的讨论中,为简化起见,设堆项是整数。

2. heapDelete

首先考虑heapDelete操作。堆中最大的查找关键字在什么位置?因为各个节点的查找关键字都大于等于任一子节点的查找关键字,所以最大的查找关键字一定在树根中。这样,heapDelete操作的第1步为:

// return the item in the root

rootItem=item[0];

这很简单,但也必须删除根。在删除根后,将留下两个分离的堆,如图12-12(a)所示。因此,需要将剩余的节点再转化为堆。在开始转换时,取出树中最后一个节点的项,并将其放在根中,如下所示:

// copy the item from the last node into the root

items[O] = items[size-1]

 

// remove the last node

--size

图12-12  (a)分离堆;(b)半堆

如图12-12(b)所示,该步骤的结果不一定是堆。不过,这是一棵完全二叉树,其左右子树都是堆。唯一的问题是,根项不在正确的位置。这样的结构称为半堆(semiheap)。需要一种方式,将半堆转换为堆。一种策略是使根项逐渐下移,直到进入处于正确位置的节点为止。换言之,遍历树的其余部分,在查找关键字大于等于其子节点的查找关键字的第一个节点处停止,将该项放在这个节点中。为此,首先比较半堆根的查找关键字与其子节点的查找关键字。如果根的查找关键字小于子节点的查找关键字的较大者,则交换根与较大子节点的项。所谓较大的子节点,即查找关键字大于另一子节点的查找关键字的子节点。

图12-13显示了heapDelete操作。这个例子只做了一次交换,值5就下移至正确位置;不过,一般情况都需要多次交换。实际上,一旦交换了根项和较大子节点C的项,C就成为半堆的根(注意,节点C并不移动,更改的是它的值)。该策略推出了下面的迭归算法。

图12-13  从堆中删除

+heapRebuild(inout items:ArrayType, in root:integer,

 in size:integer)

// Converts a semiheap rooted at index root into a heap.

 

   // Recursively trickle the item at index root down to

   // its proper position by swapping it with its larger

   // child, if the child is larger than the item.

   // If the item is at a leaf, nothing needs to be done.

 

   if (the root is not a leaf) {

       // root must have a left child

       child = 2 * root + 1                 // left child index

 

       if (the root has a right child)  {

          rightChild = child + 1          // right child index

          if (items[rightChild].getKey() > items[child].getKey()) {

child = rightChild            // larger child index

            // end if

         // end if

 

   // if the item in the root has a smaller search key

   // than the search key of the item in the larger

   // child, swap items

   if (items[root].getKey() < items[child].getKey()) {

       Swap items[root] and items[child]

 

       // transform semiheap rooted at child into a heap

       heapRebuild(items, child, size)

}         // end if

  }   // end if

// else root is a leaf, so you are done

图12-14演示了heapRebuild的递归调用。

图12-14  heapRebuild的递归调用

heapDelete操作使用heapRebuild,如下所示。

// return the item in the root

rootItem = items[0]

 

// copy the item from the last node into the root

items[0] = items[size-l]

 

// remove the last node

--size    

 

// transform the semiheap back into a heap

heapRebuild(items, O, size)

return rootItem

简要分析一下heapDelete的效率。树存储在数组中,为删除节点,需要交换数组元素。这些交换值得关注,但不意味着算法的效率低下。数组的内容是引用,不必复制对象的数据,故交换速度很快。最多交换多少次数组元素?在heapDelete将树中最后一个节点的项复制到根之后,heapRebuild在树中下移此项,直至适当位置。在最坏的情况下,项从根出发,沿单个路径下移,到达叶子。因此,heapRebuild必须交换的数组项数不大于树的高度。如第11章所述,包含n个节点的完全二叉树的高度为。每个交换需要3次引用更改。因此,heapDelete需要的引用更改次数为:

  3*+1

所以,heapDelete为O(log n),实际上,这非常高效。

3. heapInsert

heapInsert算法的策略与heapDelete正好相反。heapInsert在树底插入一个新项,然后上移到适当位置,如图12-15所示。上移节点很容易,因为item[i]节点的父节点(不是根)总存储在item[(i–1)/2]中。heapInsert操作的伪码如下。

图12-15  在堆中插入

// insert newItem into the bottom of the tree

items[size] = newItem

// trickle new item up to appropriate spot in the tree

place = size

parent = (place-1)/2

while ( (parent >= 0) and

                (items[place] > items[parent]) ) {

    Swap items[place] and items[parent]

   place = parent

   parent = (place-1)/2

}   // end while

Increment size

heapInsert的效率与heapDelete相同。换言之,在最坏的情况下,heapInsert必须交换从叶子到根的路径上的所有数组元素。故交换次数不超过树高。完全二叉树的高度为,因此heapInsert也为O(log n)。

堆的实现若不需要按查找关键字在堆中查找元素的方法,一般就只使用一个数据类型形参。但为了正确安排各个元素,必须提供比较对象的方法。这个实现采用JCF中许多实现都使用的技术,即允许通过Comparable接口对元素进行自然排序,或者为构造函数提供一个Comparator对象。如果没有提供Comparator对象,就把元素的类型用作数据类型实参,来实现Comparable接口。注意数据类型形参没有扩展Comparable接口,否则,在提供了Comparator对象的情况下,元素就不需要实现Comparable接口了。

下面的类是ADT堆的基于数组的实现。

package Heaps;

import java.util.ArrayList;

import java.util.Comparator;

public class Heap<T> {

    private ArrayList<T> items;      // array of heap items

    private Comparator<? super T> comparator; 

    public Heap() {

        items = new ArrayList<T>();

    }   // end default constructor

    public Heap(Comparator<? super T> comparator) {

        items = new ArrayList<T>();

        this.comparator = comparator;

    }   // end default constructor

// heap operations:

    public boolean heapIsEmpty() {

    // Determines whether a heap is empty.

    // Precondition: None.

    // Postcondition: Returns true if the heap is empty;

    // otherwise returns false.

        return items.size() == 0;

     } // end heapIsEmpty

    public void heapInsert(T newItem)

                      throws HeapException, ClassCastException {

     // Inserts an item into a heap.

     // Precondition: newItem is the item to be inserted.

     // Postcondition: If the heap was not full, newItem is

     // in its proper position; otherwise HeapException is

     // thrown.

         if (!items.add(newItem)) {

             // adding element to ArrayList item for heap

     Throws new HeapException(“HeapException: heapInsert failed”);

} else {

        // trickle new item up to its proper position

        int place = items.size()-1;

        int parent = (place - 1)/2;

       while (  (parent >= 0) &&

(compareItems(items.get(place), items.get(parent)))<0){

            // swap items[place] and items[parent]

           T temp = items.get(parent);

           Items.set(parent, items.get(place));

           items.set(place, temp);

           place = parent;

           parent = (place - 1)/2;

}   // end while

    }   // end if

    }   // end if

} // end heapInsert

public T heapDelete() {

// Retrieves and deletes the item in the root of a heap.

// This item has the largest search key in the heap.

// Precondition: None.

// Postcondition: If the heap is not empty, returns the

// item in the root of the heap and then deletes it.

// However, if the heap is empty, removal is impossible

// and the method returns null.

   T rootItem = null;

   int loc;

   if (!heapIsEmpty()) {

       rootItem = items.get(0);

       loc = items.size()-1;

       // if we remove the item first, it may make the ArrayList items

    // empty, then set() won’t work

       items.set(0, items.get(loc));

       items. remove(loc);

    heapRebuild(0);

    }  // end if

   return rootItem;

} // end heapDelete

protected void heapRebuild(int root) {

// if the root is not a leaf and the root's search key

// is less than the larger of the search keys in the

// root's children

    int child = 2 * root + 1;   // index of root's left

                          // child, if any

    if ( child < items.size() ) {

       // root is not a leaf, so it has a left child at child

         int rightChild = child + 1;   // index of right child,

                                  // if any 

         // if root has a right child, find larger child

         if ( (rightChild < items.size()) &&

(compareItems(items.get(rightChild)- items.get(child))) <0) {

             child = rightChild;     // index of larger child

          } // end if

          // if the root's value is smaller than the

          // value in the larger child, swap values

          if (compareItems(items.get(root). items.get(child))>0){

              T temp = items.get(root);

              Items.set(root, items.get(child));

              Items.set(child, temp);

              // transform the new subtree into a heap

              heapRebuild(child);

           }   // end if

       }   // end if

       // if root is a leaf, do nothing

   } // end heapRebuild

Private int compareItems(T item1, T item2) {

     If (comparator == null) {

         return ((Comparable<T>)item1). compareTo (item2);

      } else {

         Return comparator.compare(item1, item2);

       }   // end if

    }   // end compareItems

} // end Heap

12.2.2  ADT优先队列的堆实现

一旦实现了ADT堆,ADT优先队列的实现便迎刃而解,因为优先队列的操作与堆的操作非常相似。优先队列项的优先级值对应于堆项的查找关键字。所以,优先队列的实现可重用Heap类。换言之,PriorityQueue将Heap的实例作为其数据字段。

package PriorityQueues;

import Heaps.Heap;

import Heaps.HeapException;

import java.util.Comparator;

//  *****************************************************

// Assumes that Heap implementation is in package Heaps.   

// PriorityQueue class implementation.

//  ****************************************************

public class PriorityQueue<T> {

    private Heap<T> h;

    public PriorityQueue() {

        h = new Heap<T> ();

    }   // end default constructor

    public PriorityQueue(Comparator<? super T> comparator) {

        h = new Heap<T> (comparator);

    }   // end default constructor

    // priority-queue operations:

   public boolean pqIsEmpty() {

       return h.heapIsEmpty();

    }  // end pqIsEmpty

   public void pqInsert(T newItem)

                            throws PriorityQueueException {

       try {

           h.heapInsert(newItem);

       }   // end try

       catch (HeapException e) { throw new PriorityQueueException(

                    "PQueueException: Problem inserting to Priority queue ");

       }  // end catch

   }   // end pqInsert

   public T pqDelete() {

       return h.heapDelete();

   }  // end pqDelete

}  // end PriorityQueue

与二叉查找树相比,优先队列的堆实现如何?若知道优先队列的最大项数,则堆实现的效果更好。

堆是完全二叉树,因此总是平衡树,这是一个重要优点。若二叉查找树是平衡的,则在平均情况下,对于n个项,二叉查找树和堆实现具有相同的阶,都是O(log n)。不过,二叉查找树的高度在插入和删除操作期间可能发生增长,在最坏的情况下,将大大超过log2n,把实现的效率降低为O(n)。而堆实现避免了性能的下降。

第13章将介绍如何保持二叉查找树的平衡性,但是,为解决这个问题,其操作的复杂性远远超过堆操作。不过,千万不要认为堆可替代二叉查找树,成为表的实现。如前所述,堆不适合扮演这个角色。如果对这个问题认识不清,可试着在堆上执行表操作tableRetrieve,或按查找关键字的顺序遍历堆。

有限、各不相同的优先级值  若优先级值的数量有限,而且各不相同,如从整数1到20,则很多项具有相同的优先级值。可以按出现的先后顺序排列具有相同优先级值的项。

队列堆为每个不同的优先级值安排一个队列,可用来处理上述情况。在队列尚未就位的情况下,为了将项插入优先队列,可在堆上为该项的优先级值建立一个队列,然后将项插入该队列。要从优先队列中删除一项,可在堆中与最高优先级对应的队列中删除队首项。若该删除操作使队列变空,则将该队列从堆中删除。

章末的编程问题7进一步分析处理各不相同的优先级值。

12.2.3  堆排序

顾名思义,堆排序算法使用堆来排序无序的anArray数组。首先,算法将数组转换为堆。为完成转换,一种方法是使用heapInsert方法,将项逐一插入堆。

还有一种方法由数组anArray的项构建堆,这种方法效率更高。设anArray的原始内容如图12-16(a)所示。将数组anArray的项指派给树的节点,从根开始,从左到右,逐渐下移,构建一个二叉树。结果如图12-16(b)所示。此后,反复调用heapRebuild,将该树转换为堆。heapRebuild将半堆转换为堆。半堆的子树都是堆,但其根还未就位。但是,树中有供heapRebuild处理的半堆吗?图12-16(b)的树不是半堆,但查看叶子,可找到半堆;换言之,每个叶子是一个半堆(实际上,每个叶子是一个堆,但为简单起见,忽略了这个事实)。首先从右到左在叶子上调用heapRebuild,接着沿树上移,在到达节点s时,其子树为堆。于是,heapRebuild将以s为根的半堆转换为堆。

图12-16  (a)anArray的原始内容;(b)anArray的相应二叉树

下面的算法将包含n个项的anArray数组转换为堆,这是堆排序算法的第一步。

for (index = n - 1 down to 0)

      // Assertion: the tree rooted at index is a semiheap

     heapRebuild(anArray, index, n)

      // Assertion: the tree rooted at index is a heap

实际上,在前面的for语句中,可用n/2替换n–1。章末编程问题21要求解释原因。图12-17跟踪对图12-16(a)中的数组执行该算法的过程。

图12-17  将anArray数组转换为堆

将数组转换为堆之后,堆排序将数组分为两个区域:Heap(堆)和Sorted(有序),如图12-18所示。Heap区域在anArray[0..last],Sorted区域在anArray[last+1..n–1]。开始时,Heap区域包含anArray的所有内容,而Sorted区域为空,换言之,last= n–1。

图12-18  堆排序将数组分为两个区域

在算法的每一步,将项I从Heap区域移到Sorted区域。堆排序算法的不变式为:

●       在步骤k后,Sorted区域包含anArray的k个最大值,且有序,换言之,anArray[n–1]是最大值,anArray[n–2]是第二个最大值,以此类推。

●       Heap区域中的项构成堆。

只要保持不变式,I必然是Heap区域中的最大值项,故必为堆的根。为完成移动,交换堆的根项与堆的最后一项,换言之,交换anArray[0]与anArray[last],然后将last的值减1。结果,刚从根交换到anArray[last]的项成为Sorted区域的最小项,且在Sorted区域的第一个位置。在移动后,将Heap区域转换为堆,因为新根的位置可能不对。可用heapRebuild下移根的当前项,使Heap区域再一次成为一个堆。

下面的伪码总结这些步骤:

+heapSort(in anArray:ArrayType, in n:integer)

// Sorts anArray[O..n-1].

 

    // build initial heap

    for (index = n - 1 down to O) {

        // Invariant: the tree rooted at index is a semiheap

        heapRebuild(anArray, index, n)

        // Assertion: the tree rooted at index is a heap

     }   // end for

     // Assertion: anArray[O] is the largest item in heap

     // anArray[O..n-1]

 

     // initialize the regions

     last  = n  -  1

 

     // Invariant: anArray[O..last] is a heap,

     // anArray[last+l..n-1] is

      // sorted and contains the largest items of A

      for (step = 1 through n) {

          // move the largest item in the heap region -- that

          // is, the root anArray[0] -- to the beginning of the

          // Sorted region by swapping items

          Swap anArray[0] and anArray[last]

 

          // expand the Sorted region, shrink the Heap region

          Decrement last

 

           // make the Heap region a heap again

          heapRebuild(anArray, 0, last)

       }  // end for

图12-19完成图12-17开始的堆排序算法的跟踪。堆排序的Java实现留作练习。

图12-19  跟踪从图12-17开始的堆排序

堆排序的效率分析与第10章介绍的归并排序相似。在最坏和平均情况下,两种算法均为O(n*log n)。与归并排序相比,堆排序的优势在于不需要第二个数组。快速排序在平均情况下也为O(n*log n),但在最坏的情况下为O(n2)。尽管在最坏情况下的效率不高,但快速排序仍然是首选的排序方法。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论