十四、堆
优先级队列是一个抽象数据类型(ADT),它提供了删除最大(或最小)关键字值的数据项的方法,插入数据项的方法,有时还有一些其他操作的方法。配合不同的ADT,优先级队列可以用不同的内部结构来实现。优先级队列可以用有序数组来实现,但是这种作法的问题是,尽管删除最大数据项的时间复杂度为O(1),但是插入还是需要比较长的O(N)时间。这是因为必须移动数组中平均一半的数据项以插入新数据项,并在完成插入后,数组依然有序。实现优先级队列的另一种结构:堆。堆是一种树,由它实现的优先级队列的插入和删除的时间复杂度都是(O(logN))。尽管这样删除的时间变慢了一些,但是插入的时间快得多了。当速度非常重要,且有很多插入操作时,可以选择堆来实现优先级队列。
堆的特点:
它是完全二叉树。除了树的最后一层节点不需要是满的,其他的每一层从左到右都完全是满的。
它常常用一个数组实现。
堆中的每一个节点都满足堆的条件,也就是说每一个节点的关键字都大于(或等于)这个节点的子节点的关键字。

// to run this program: C>java HeapApp import java.io.*; class Node { private int iData; // data item (key) public Node(int key) // constructor { iData = key; } public int getKey() { return iData; } public void setKey(int id) { iData = id; } } class Heap { private Node[] heapArray; private int maxSize; // size of array private int currentSize; // number of nodes in array public Heap(int mx) // constructor { maxSize = mx; currentSize = 0; heapArray = new Node[maxSize]; // create array } public boolean isEmpty() { return currentSize==0; } public boolean insert(int key) { if(currentSize==maxSize) return false; Node newNode = new Node(key); heapArray[currentSize] = newNode; trickleUp(currentSize++); return true; } public void trickleUp(int index) { int parent = (index-1) / 2; Node bottom = heapArray[index]; while( index > 0 && heapArray[parent].getKey() < bottom.getKey() ) { heapArray[index] = heapArray[parent]; // move it down index = parent; parent = (parent-1) / 2; } heapArray[index] = bottom; } public Node remove() // delete item with max key { // (assumes non-empty list) Node root = heapArray[0];//移走根 heapArray[0] = heapArray[--currentSize];//把最后一个节点移动到根的位置 trickleDown(0); return root; } public void trickleDown(int index) { int largerChild; Node top = heapArray[index]; // save root while(index < currentSize/2) // while node has at { // least one child, int leftChild = 2*index+1; int rightChild = leftChild+1; // find larger child if(rightChild < currentSize && // (rightChild exists?) heapArray[leftChild].getKey() < heapArray[rightChild].getKey()) largerChild = rightChild; else largerChild = leftChild; // top >= largerChild? if( top.getKey() >= heapArray[largerChild].getKey() ) break; // shift child up heapArray[index] = heapArray[largerChild]; index = largerChild; // go down } heapArray[index] = top; // root to index } public boolean change(int index, int newValue)//关键字的更改 { if(index<0 || index>=currentSize) return false; int oldValue = heapArray[index].getKey(); // remember old heapArray[index].setKey(newValue); // change to new if(oldValue < newValue) // if raised, trickleUp(index); // trickle it up else // if lowered, trickleDown(index); // trickle it down return true; } public void displayHeap() { System.out.print("heapArray: "); // array format for(int m=0; m<currentSize; m++) if(heapArray[m] != null) System.out.print( heapArray[m].getKey() + " "); else System.out.print( "-- "); System.out.println(); // heap format int nBlanks = 32; int itemsPerRow = 1; int column = 0; int j = 0; // current item String dots = "..............................."; System.out.println(dots+dots); // dotted top line while(currentSize > 0) // for each heap item { if(column == 0) // first item in row? for(int k=0; k<nBlanks; k++) // preceding blanks System.out.print(' '); // display item System.out.print(heapArray[j].getKey()); if(++j == currentSize) // done? break; if(++column==itemsPerRow) // end of row? { nBlanks /= 2; // half the blanks itemsPerRow *= 2; // twice the items column = 0; // start over on System.out.println(); // new row } else // next item on row for(int k=0; k<nBlanks*2-2; k++) System.out.print(' '); // interim blanks } System.out.println("\n"+dots+dots); // dotted bottom line } } class HeapApp { public static void main(String[] args) throws IOException { int value, value2; Heap theHeap = new Heap(31); // make a Heap; max size 31 boolean success; theHeap.insert(70); // insert 10 items theHeap.insert(40); theHeap.insert(50); theHeap.insert(20); theHeap.insert(60); theHeap.insert(100); theHeap.insert(80); theHeap.insert(30); theHeap.insert(10); theHeap.insert(90); while(true) // until [Ctrl]-[C] { System.out.print("Enter first letter of "); System.out.print("show, insert, remove, change: "); int choice = getChar(); switch(choice) { case 's': // show theHeap.displayHeap(); break; case 'i': // insert System.out.print("Enter value to insert: "); value = getInt(); success = theHeap.insert(value); if( !success ) System.out.println("Can't insert; heap full"); break; case 'r': // remove if( !theHeap.isEmpty() ) theHeap.remove(); else System.out.println("Can't remove; heap empty"); break; case 'c': // change System.out.print("Enter current index of item: "); value = getInt(); System.out.print("Enter new key: "); value2 = getInt(); success = theHeap.change(value, value2); if( !success ) System.out.println("Invalid index"); break; default: System.out.println("Invalid entry\n"); } } } public static String getString() throws IOException { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readLine(); return s; } public static char getChar() throws IOException { String s = getString(); return s.charAt(0); } public static int getInt() throws IOException { String s = getString(); return Integer.parseInt(s); } }
堆数组的扩展
在程序运行的过程中,如果插入太多的数据项,超出了堆数组的容量,可以创建一个新的数组,把数据从旧数组中复制到新的数组中。(和哈希表中的情况不同,改变堆的大小不需要重新排列数据。)执行复制操作的时间是线性的,但是增大数组容量的操作并不会经常发生,特别是当每次扩展数组容量时,数组的容量都充分地增大了。
堆排序
堆排序的基本思想是使用普通的insert()例程在堆中插入全部无序的数据项,然后重复用remove()例程,就可以按序移除所有数据项。因为insert()和remove()方法操作的时间复杂度都是O(logN),并且每个方法必须都要执行N次,所以整个排序操作需要O(N*logN)时间,这和快速排序一样。但是,它不如快速排序快,部分原因是trickleDown()里while循环中的操作比快速排序里循环中的操作要多。有几个技巧可以是堆排序更有效。其一是节省时间,其二是节省内存。
1、如果在堆中插入N个新数据项,则需要应用trickleUp()方法N次。但是如果现将无序的数据项依次插入数组中,再对节点N/2-1至节点0(排除最后一层的叶子节点)应用trickleUp(),那么只需要N/2次。
2、使用同一个数组。原始代码片段显示了数组中的无序数据。然后把数据插入到堆中,最后从堆中移除它并把它有序地写会到数组中。这个过程需要两个大小为N的数组:初始数组和用于堆的数组。事实上,堆和初始数据可以使用同一个数组。这样堆排序所需的存储空间减少了一半;除了初始数组之外不需要额外的存储空间。每从堆顶移除一个数据项,堆数组的末端单元就变为空的,堆减少一个节点。可以把最近一次移除的节点放到这个新空出的单元中。移除的节点越来越多,堆数据就越来越小,但是有序数据的数组却越来越大。因此,有序数组和堆数组就可以共同使用一块存储空间。
// to run this program: C>java HeapSortApp
import java.io.*;
class Node
{
   private int iData;             // data item (key)
   public Node(int key)           // constructor
   { iData = key; }
   public int getKey()
   { return iData; }
} 
class Heap
{
   private Node[] heapArray;
   private int maxSize;           // size of array
   private int currentSize;       // number of items in array
   public Heap(int mx)            // constructor
   {
      maxSize = mx;
      currentSize = 0;
      heapArray = new Node[maxSize];
   }
   public Node remove()           // delete item with max key
   {                           // (assumes non-empty list)
      Node root = heapArray[0];
      heapArray[0] = heapArray[--currentSize];
      trickleDown(0);
      return root;
   } 
   public void trickleDown(int index)
   {
      int largerChild;
      Node top = heapArray[index];        // save root
      while(index < currentSize/2)        // not on bottom row
      {
         int leftChild = 2*index+1;
         int rightChild = leftChild+1;
                                          // find larger child
         if(rightChild < currentSize &&   // right ch exists?
                             heapArray[leftChild].getKey() <
                             heapArray[rightChild].getKey())
            largerChild = rightChild;
         else
            largerChild = leftChild;
                                          // top >= largerChild?
         if(top.getKey() >= heapArray[largerChild].getKey())
            break;
                                          // shift child up
         heapArray[index] = heapArray[largerChild];
         index = largerChild;             // go down
      } 
      heapArray[index] = top;             // root to index
   }  
   public void displayHeap()
   {
      int nBlanks = 32;
      int itemsPerRow = 1;
      int column = 0;
      int j = 0;                          // current item
      String dots = "...............................";
      System.out.println(dots+dots);      // dotted top line
      while(currentSize > 0)              // for each heap item
      {
         if(column == 0)                  // first item in row?
            for(int k=0; k<nBlanks; k++)  // preceding blanks
               System.out.print(' ');
                                          // display item
         System.out.print(heapArray[j].getKey());
         if(++j == currentSize)           // done?
            break;
         if(++column==itemsPerRow)        // end of row?
         {
            nBlanks /= 2;                 // half the blanks
            itemsPerRow *= 2;             // twice the items
            column = 0;                   // start over on
            System.out.println();         //    new row
         }
         else                             // next item on row
            for(int k=0; k<nBlanks*2-2; k++)
               System.out.print(' ');     // interim blanks
      }  
      System.out.println("\n"+dots+dots); // dotted bottom line
   } 
   public void displayArray()
   {
      for(int j=0; j<maxSize; j++)
         System.out.print(heapArray[j].getKey() + " ");
      System.out.println("");
   }
   public void insertAt(int index, Node newNode)
   { heapArray[index] = newNode; }
   public void incrementSize()
   { currentSize++; }
   public void heapify(int index)
   {
     if(index>currentSize/2-1)
    return;
     heapify(index*2+2);
     heapify(index*2+1);
     trickleDown(index);
   }
} 
class HeapSortApp
{
   public static void main(String[] args) throws IOException
   {
      int size, j;
      System.out.print("Enter number of items: ");
      size = getInt();
      Heap theHeap = new Heap(size);
      for(j=0; j<size; j++)       // fill array with
      {                        //    random nodes
         int random = (int)(java.lang.Math.random()*100);
         Node newNode = new Node(random);
         theHeap.insertAt(j, newNode);
         theHeap.incrementSize();
      }
      System.out.print("Random: ");
         theHeap.displayArray();  // display random array
     /* for(j=size/2-1; j>=0; j--)  // 从非最后一层节点开始make random array into heap
         theHeap.trickleDown(j);*/
      theHeap.heapify(0); //使用递归将数组生成堆  
      System.out.print("Heap:   ");
      theHeap.displayArray();     // dislay heap array
      theHeap.displayHeap();      // display heap
      for(j=size-1; j>=0; j--)    // remove from heap and
      {                        //    store at array end
         Node biggestNode = theHeap.remove();
         theHeap.insertAt(j, biggestNode);
      }   
      System.out.print("Sorted: ");
      theHeap.displayArray();     // display sorted array
   }  
   public static String getString() throws IOException
   {
      InputStreamReader isr = new InputStreamReader(System.in);
      BufferedReader br = new BufferedReader(isr);
      String s = br.readLine();
      return s;
   }
   public static int getInt() throws IOException
   {
      String s = getString();
      return Integer.parseInt(s);
   }
}
堆排序的效率:堆排序运行的时间复杂度为O(N*logN)。尽管它比快速排序略慢,但它比快速排序优越的一点是它对初始数据的分布不敏感。在关键字值按某种排列顺序的情况下,快速排序运行的时间复杂度可以降到O(N2)级,然而堆排序对任意排列的数据,其排序的时间复杂度都是O(N*logN)。
小结:
优先级队列是提供了数据插入和移除最大(或者最小)数据项方法的抽象数据类型(ADT)。
堆是优先级队列ADT的有效的实现形式。
堆提供移除最大数据项和插入的方法,时间复杂度都为O(logN)。
最大数据项总是在根的位置上。
堆不能有序地遍历所有的数据,不能找到特定关键字数据项的位置,也不能移除特定关键字的数据项。
堆通常用数组来实现,表现为一颗完全二叉树。根节点的下标为0,最后一个节点的下标为N-1。
每个节点的关键字都小于它的父节点,大于它的子节点。
要插入的数据项总是先被存放到数组第一个空的单元中,然后再向上筛选它至适当的位置。
当从根移除一个数据项时,用数据中最后一个数据项取代它的位置,然后再向下筛选这个节点至适当的位置。
向上筛选和向下筛选算法可以被看作是一系列的交换,但更有效的作法是进行一系列复制。
可以更改任一个数据项的优先级。首先,更改它的关键字。如果关键字增加了,数据项就向上筛选,而如果关键字减小了,数据项就向下筛选。
在概念上堆排序的过程包括现在堆中插入N次,然后再作N次移除。
通过对无序数组中的N/2个数据项施用向下筛选算法,而不作N次插入,可以使堆排序的运行速度更快。
可以使用同一个数组来存放初始无序的数据,堆以及最后有序的数据。因此,堆排序不需要额外的存储空间。
                    
                
                
            
        
浙公网安备 33010602011771号