堆Heap,一堆苹果,为了卖相好,越好看的越往上放,就是大顶堆;为了苹果堆的稳定,质量越小越往上放,就是小顶堆;堆首先是完全二叉树,但只确保父节点和子节点大小逻辑,不关心左右子节点的大小关系,通常是一个可以被看做一棵树的数组对象,是个很常见的结构,比如BST对象,都与堆有关系。
arbitrary element insertion but supports removal of elements in order of priority
A priority queue
is an abstract data type for storing a collection of prioritized elements that supports arbitrary element insertion but supports removal of elements in order of priority
, that is, the element with first priority can be removed at any time. This ADT is fundamentally different from the position-based
data structures such as stacks, queues, deques, lists, and even trees, we discussed in previous chapters. These other data structures store elements at specific positions, which are often positions in a linear arrangement of the elements determined by the insertion and deletion operations performed. The priority queue ADT stores elements according to their priorities
, and has no external notion of “position.”
Keys, Priorities, and Total Order Relations
Applications commonly require comparing and ranking objects according to parameters or properties, called “keys,” that are assigned to each object in a collection. Formally, we define a key to be an object that is assigned to an element as a specific attribute for that element and that can be used to identify, rank, or weigh that element
. Note that the key is assigned to an element, typically by a user or application; hence, a key might represent a property that an element did not originally possess.
The key an application assigns to an element is not necessarily unique, however, and an application may even change an element’s key if it needs to. For example, we can compare companies by earnings or by number of employees; hence, either of these parameters can be used as a key for a company, depending on the information we wish to extract. Likewise, we can compare restaurants by a critic’s food quality rating or by average entrée price. To achieve the most generality then, we allow a key to be of any type that is appropriate for a particular application.
As in the examples above, the key used for comparisons is often more than a single numerical value, such as price, length, weight, or speed. That is, a key can sometimes be a more complex property that cannot be quantified with a single number. For example, the priority of standby passengers is usually determined by taking into account a host of different factors, including frequent-flyer status, the fare paid, and check-in time. In some applications, the key for an object is data extracted from the object itself (for example, it might be a member variable storing the list price of a book, or the weight of a car). In other applications, the key is not part of the object but is externally generated by the application (for example, the quality rating given to a stock by a financial analyst, or the priority assigned to a standby passenger by a gate agent).
A priority queue needs a comparison rule
that never contradicts itself. In order for a comparison rule, which we denote by ≤
, to be robust in this way, it must define a total order relation
, which is to say that the comparison rule is defined for every pair of keys and it must satisfy the following properties:
• Reflexive property : k ≤ k
• Antisymmetric property: if k1 ≤ k2 and k2 ≤ k1, then k1 = k2
• Transitive property: if k1 ≤ k2 and k2 ≤ k3, then k1 ≤ k3
The Priority Queue ADT
Sorting with a Priority Queue
Another important application of a priority queue is sorting, where we are given a collection L of n elements that can be compared according to a total order relation
, and we want to rearrange them in increasing order (or at least in nondecreasing order if there are ties). The algorithm for sorting L with a priority queue Q, called PriorityQueueSort
, is quite simple and consists of the following two phases
:
- In the first phase, we put the elements of L into an initially empty priority queue P through a series of n insert operations, one for each element.
- In the second phase, we extract the elements from P in nondecreasing order by means of a series of n combinations of min and removeMin operations, putting them back into L in order.
The algorithm works correctly for any priority queue P, no matter how P is implemented. However, the running time of the algorithm is determined by the running times of operations insert, min, and removeMin, which do depend on how P is implemented. Indeed, PriorityQueueSort should be considered more a sorting “scheme” than a sorting “algorithm,” because it does not specify how the priority queue P is implemented.
The PriorityQueueSort scheme
is the paradigm of several popular sorting algorithms, including selection-sort
, insertion-sort
, and heap-sort
, which we discuss in this chapter.
The STL priority_queue Class
The priority queue class is templated with three parameters: the base type of the elements
, the underlying STL container in which the priority queue is stored
, and the comparator object
. Only the first template argument is required. The second parameter (the underlying container) defaults to the STL vector
. The third parameter (the comparator) defaults to using the standard C++ less-than operator (“<”)
. The STL priority queue uses comparators in the same manner as we defined in Section 8.1.2. In particular, a comparator is a class that overrides the “()
” operator in order to define a boolean function that implements the less-than operator.
#include <queue>
using namespace std; // make std accessible
// a priority queue of integers
priority_queue<int> p1;
// a priority queue of points with left-to-right order
priority_queue<Point2D, vector<Point2D>, LeftRight> p2;
p2.push( Point2D(8.5, 4.6) ); // add three points to p2
p2.push( Point2D(1.3, 5.7) );
p2.push( Point2D(2.5, 0.6) );
cout << p2.top() << endl; p2.pop(); // output: (8.5, 4.6)
cout << p2.top() << endl; p2.pop(); // output: (2.5, 0.6)
cout << p2.top() << endl; p2.pop(); // output: (1.3, 5.7)
Implementation with an Unsorted List
Implementation with a Sorted List
Selection-Sort
If we implement the priority queue P with an unsorted list, then the first phase of PriorityQueueSort takes O(n) time, since we can insert each element in constant time. In the second phase, the running time of each min and removeMin operation is proportional to the number of elements currently in P. Thus, the bottleneck computation in this implementation is the repeated “selection” of the minimum element from an unsorted list in the second phase. For this reason, this algorithm is better known as selection-sort
.
Insertion-Sort
If we implement the priority queue P using a sorted list, then we improve the running time of the second phase to O(n), because each operation min and removeMin on P now takes O(1) time. Unfortunately, the first phase now becomes the bottleneck for the running time, since, in the worst case, each insert operation takes time proportional to the size of P. This sorting algorithm is therefore better known as insertion-sort (see Figure 8.2), for the bottleneck in this sorting algorithm involves the repeated “insertion” of a new element at the appropriate position in a sorted list.
Heaps
The two implementations of the PriorityQueueSort
scheme presented in the previous section suggest a possible way of improving the running time for priority-queue sorting. One algorithm (selection-sort) achieves a fast running time for the first phase, but has a slow second phase, whereas the other algorithm (insertion-sort) has a slow first phase, but achieves a fast running time for the second phase. If we could somehow balance the running times of the two phases, we might be able to significantly speed up the overall running time for sorting.
This approach is, in fact, exactly what we can achieve using the priority-queue implementation discussed in this section.
An efficient realization of a priority queue uses a data structure called a heap
. This data structure allows us to perform both insertions and removals in logarithmic time, which is a significant improvement over the list-based implementations discussed in Section 8.2. The fundamental way the heap achieves this improvement is to abandon the idea of storing elements and keys in a list and take the approach of storing elements and keys in a binary tree
instead.
The Heap Data Structure
A heap is a binary tree T that stores a collection of elements with their associated keys at its nodes and that satisfies two additional properties:
- a
relational property
, defined in terms of the way keys are stored in T, and - a
structural property
, defined in terms of the nodes of T itself.
We assume that a total order relation on the keys is given, for example, by a comparator.
Heap-Order Property
Complete Binary Tree Property
The Height of a Heap
Proposition 8.5 has an important consequence. It implies that if we can perform update operations on a heap in time proportional to its height, then those operations will run in logarithmic time.
Therefore, let us turn to the problem of how to efficiently perform various priority queue functions using a heap.
Complete Binary Trees and Their Representation
The simplifications that come from representing a complete binary tree T with a vector aid in the implementation of functions add and remove. Assuming that no array expansion is necessary, functions add and remove can be performed in O(1) time because they simply involve adding or removing the last element of the vector. Moreover, the vector associated with T has n + 1 elements (the element at index 0 is a placeholder
). If we use an extendable array that grows and shrinks for the implementation of the vector (for example, the STL vector class), the space used by the vector-based representation of a complete binary tree with n nodes is O(n) and operations add and remove take O(1) amortized time.
Implementing a Priority Queue with a Heap
insert
remove
Down-Heap Bubbling after a Removal
C++ 实现
A heap-based implementation of a priority queue.
We present an implementation of the insert
operation. As outlined in the previous section, this works by adding the new element to the last position of the tree and then it performs up-heap bubbling by repeatedly swapping this element with its parent until its parent has a smaller key value.
Finally, let us consider the removeMin
operation. If the tree has only one node, then we simply remove it. Otherwise, we swap the root’s element with the last element of the tree and remove the last element. We then apply down-heap bubbling to the root. Letting u
denote the current node, this involves determining u
’s smaller child, which is stored in v
. If the child’s key is smaller than u
’s, we swap u
’s contents with this child’s.
template<typename E, typename C>
class HeapPriorityQueue {
public:
int size() const; // number of elements
bool empty() const; // is the queue empty?
void insert(const E &e); // insert element
const E &min(); // minimum element
void removeMin(); // remove minimum
private:
VectorCompleteTree <E> T; // priority queue contents
C isLess; // less-than comparator
// shortcut for tree position
typedef typename VectorCompleteTree<E>::Position Position;
};
// number of elements
template<typename E, typename C>
int HeapPriorityQueue<E, C>::size() const { return T.size(); }
// is the queue empty?
template<typename E, typename C>
bool HeapPriorityQueue<E, C>::empty() const { return size() == 0; }
// minimum element
template<typename E, typename C>
const E &HeapPriorityQueue<E, C>::min() { return *(T.root()); }
// insert element
template<typename E, typename C>
void HeapPriorityQueue<E, C>::insert(const E &e) {
T.addLast(e); // add e to heap
Position v = T.last(); // e’s position
while (!T.isRoot(v)) { // up-heap bubbling
Position u = T.parent(v);
if (!isLess(*v, *u)) {
break; // if v in order, we’re done
}
T.swap(v, u); // else swap with parent
v = u;
}
}
// remove minimum
template<typename E, typename C>
void HeapPriorityQueue<E, C>::removeMin() {
if (size() == 1) {// only one node?
T.removeLast(); // remove it
} else {
Position u = T.root(); // root position
T.swap(u, T.last()); // swap last with root
T.removeLast(); // . . .and remove last
while (T.hasLeft(u)) { // down-heap bubbling
Position v = T.left(u);
if (T.hasRight(u) && isLess(*(T.right(u)), *v)) {
v = T.right(u); // v is u’s smaller child
}
if (isLess(*v, *u)) { // is u out of order?
T.swap(u, v); // . . .then swap
u = v;
} else {
break; // else we’re done
}
}
}
}