=========================================================================== CSC 263H Lecture Outline for Week 9 Winter 2004 =========================================================================== [[Q: denotes a question that you should think about and that will be answered during lecture. ]] Note: At both Scarborough and St. George this week's lectures will begin with the completion of the amortized analysis material found in the week 8 lecture notes. --------------- Priority Queues: [ Section 2.4 ] --------------- A "priority queue" is just like a queue except that every item in the queue has a "priority" (usually just a number). More formally, a priority queue consists of a set of elements, where each element has a priority, together with other information (possibly). The operations that can be performed on a priority queue are: ENQUEUE(x,p): insert an element x in the set, with priority value p HEAD(): return an element x with the smallest priority value DEQUEUE(): remove and return an element x with the smallest priority value Note that there is a real possibility for confusion here. When you have two tasks to complete and you say that task A has priority 1 and task B has priority 2, which task is more important? Which has higher priority? General English convention is that task A which has a lower priority _value_ is understood to be more important and we say it has higher priority. For example, think of items on a "top 10" list: number 1 is more important than number 10 (even though its index is smaller). We will use the term priority _value_ to refer to the actual priority number. The term "min-queue" is sometimes used to indicate that smaller priority values represent more important items (it's possible to define a corresponding notion of "max-queue" where this is reversed). [[Q: If your priority queue represents tasks, does HEAD return a task that is most important or least important? ]] Applications of priority queues: - job scheduling in an operating system - printer queues - event-driven simulation algorithms - etc. Data structures for priority queues: [[Q: What possible data structures could be used to implement priority queues? Consider the time-complexity of the operations for various alternatives. ]] ----- Heaps: [2.4] ----- A heap is a simple data structure that can be used to represent a priority queue by storing items into a _complete_ binary tree (i.e., each level of the tree contains the maximum number of nodes, except possibly the last level, and nodes in the last level are "as far left" as possible) in such a way that for every node in the tree, the priority of the element stored in that node is greater than or equal to the priorities of the elements stored in that node's children. Note that this means that _every_ subtree of a heap is also a heap. Traditionally, a heap is stored by using an array A together with an integer "heapsize" that stores the number of elements currently in the heap. The following conventions is used to store the nodes of the tree in the array: the root of the tree is stored at position 1, the two children of the root are stored at positions 2 and 3, the four "grandchildren" of the root are stored at positions 4,5,6,7, etc. [[Q: Given an element x at index i in the array, what will be the index of the left child and right child of x? What will be the index of x's parent? ]] Hence, it is possible to "traverse" paths in the tree by simply updating an index appropriately, without having to store any extra pointers. If the size of the array is close to the size of the heap, this data structure is extremely space-efficient. (in particular, we can use the dynamic array techniques to ensure that the size of the array is always within a constant factor of the number of elements in the heap.) [[Q: Draw the array that 3_ represents the heap / \_ on the right. ]] 8 4 / \ / \ 9 9 5 15 / \ 10 16 How do we perform the various priority queue operations on a heap? - ENQUEUE: Increment 'heapsize' and add the new element at the end of the array. The result might violate the heap property, so "percolate" the element up (exchanging it with its parent) until its priority is no smaller than the priority of its parent. For example, if we perform ENQUEUE(7) on the previous heap, we get the following result (showing both the tree and the array for each step of the operation): _ 3 _ _ 3 _ __/ \__ __/ \_ _8_ 4 _8_ 4 _/ \_ _/ \_ _/ \_ _/ \_ 9 9 5 15 9 7 5 15 / \ / / \ / 10 16 7 10 16 9 [3,8,4,9,9,5,15,10,16,7] [3,8,4,9,7,5,15,10,16,9] _ 3 _ __/ \__ _7_ 4 _/ \_ _/ \_ 9 8 5 15 / \ / 10 16 9 [3,7,4,9,8,5,15,10,16,9] [[Q: How can we be sure when we are "bubbling-up" (switching a node with its parent) that it won't now need switching with its other child? ]] Running time? In the worst-case, Theta(height) = Theta(log n). - HEAD: Simply return A[1] (if heapsize >= 1). Theta(1) time. - DEQUEUE: Decrement 'heapsize' and remove the first element of the array. In order to be left with a valid heap, move the last element in the array to the first position (so the heap now has the right "shape"), and percolate this element _down_ until its priority value is no greater than the priorities of both its children. Do this by exchanging the element with its child of lowest priority value at every step. For example, if we perform DEQUEUE on the previous heap, we get the following result (showing both the tree and the array for each step of the operation): _ _ _ 9 _ __/ \__ __/ \__ _7_ 4 _7_ 4 _/ \_ _/ \_ _/ \_ _/ \_ 9 8 5 15 9 8 5 15 / \ / / \ 10 16 9 10 16 [ ,7,4,9,8,5,15,10,16,9] [9,7,4,9,8,5,15,10,16] _ 4 _ _ 4 _ __/ \__ __/ \__ _7_ 9 _7_ 5 _/ \_ _/ \_ _/ \_ _/ \_ 9 8 5 15 9 8 9 15 / \ / \ 10 16 10 16 [4,7,9,9,8,5,15,10,16] [4,7,5,9,8,9,15,10,16] Running time? As for ENQUEUE, Theta(log n) in the worst-case.