十四、堆
优先级队列是一个抽象数据类型(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次插入,可以使堆排序的运行速度更快。
可以使用同一个数组来存放初始无序的数据,堆以及最后有序的数据。因此,堆排序不需要额外的存储空间。