232 Chapter 5 Hashing values for s, for a total of p(p − 1) possible (r, s) pairs. Notice that the number of (a, b) pairs and the number of (r, s) pairs is identical; thus each (r, s) pair will correspond to exactly one (a, b) pair if we can solve for (a, b) in terms of r and s. But that is easy: As before, subtracting equations yields a(x − y) ≡ (r − s) (mod p), which means that by multiplying both sides by the unique multiplicative inverse of (x − y) (which must exist, since x − y is not zero and p is prime), we obtain a, in terms of r and s. Then b follows. Finally, this means that the probability that x and y collide is equal to the proba- bility that r ≡ s (mod M), and the above analysis allows us to assume that r and s are chosen randomly, rather than a and b. Immediate intuition would place this probability at 1/M, but that would only be true if p were an exact multiple of M, and all possible (r, s) pairs were equally likely. Since p is prime, and r = s, that is not exactly true, so a more careful analysis is needed. For a given r, the number of values of s that can collide mod M is at most p/M −1 (the −1 is because r = s). It is easy to see that this is at most (p − 1)/M. Thus the probability that r and s will generate a collision is at most 1/M (we divide by p − 1, because, as mentioned earlier in the proof, there are only p − 1 choices for s given r). This implies that the hash family is universal. Implementation of this hash function would seem to require two mod operations: one mod p and the second mod M. Figure 5.50 shows a simple implementation in C++, assuming that M is significantly less than 231 − 1. Because the computations must now be exactly as specified, and thus overflow is no longer acceptable, we promote to long long computations, which are at least 64 bits. However, we are allowed to choose any prime p, as long as it is larger than M. Hence, it makes sense to choose a prime that is most favorable for computations. One such prime is p = 231 − 1. Prime numbers of this form are known as Mersenne primes; other Mersenne primes include 25 − 1, 261 − 1 and 289 − 1. Just as a multiplication by a Mersenne prime such as 31 can be implemented by a bit shift and a subtract, a mod operation involving a Mersenne prime can also be implemented by a bit shift and an addition: Suppose r ≡ y (mod p). If we divide y by (p + 1), then y = q (p + 1) + r , where q and r are the quotient and remainder, respectively. Thus, r ≡ q (p + 1) + r (mod p). And since (p + 1) ≡ 1 (mod p), we obtain r ≡ q + r (mod p). Figure 5.51 implements this idea, which is known as the Carter-Wegman trick. On line 8, the bit shift computes the quotient and the bitwise-and computes the remainder when dividing by (p + 1); these bitwise operations work because (p + 1) is an exact power 1 int universalHash( int x, int A, int B, int P, int M ) 2{ 3 return static_cast<int>( ( ( static_cast<long long>( A ) * x ) + B ) % P ) % M; 4} Figure 5.50 Simple implementation of universal hashing
5.9 Extendible Hashing 233 1 const int DIGS = 31; 2 const int mersennep = (1<<DIGS) - 1; 3 4 int universalHash( int x, int A, int B, int M ) 5{ 6 long long hashVal = static_cast<long long>( A ) * x + B; 7 8 hashVal = ( ( hashVal >> DIGS ) + ( hashVal & mersennep ) ); 9 if( hashVal >= mersennep ) 10 hashVal -= mersennep; 11 12 return static_cast<int>( hashVal ) % M; 13 } Figure 5.51 Simple implementation of universal hashing of two. Since the remainder could be almost as large as p, the resulting sum might be larger than p, so we scale it back down at lines 9 and 10. Universal hash functions exist for strings also. First, choose any prime p, larger than M (and larger than the largest character code). Then use our standard string hashing function, choosing the multiplier randomly between 1 and p − 1 and returning an intermediate hash value between 0 and p − 1, inclusive. Finally, apply a universal hash function to generate the final hash value between 0 and M − 1. 5.9 Extendible Hashing Our last topic in this chapter deals with the case where the amount of data is too large to fit in main memory. As we saw in Chapter 4, the main consideration then is the number of disk accesses required to retrieve data. As before, we assume that at any point we have N records to store; the value of N changes over time. Furthermore, at most M records fit in one disk block. We will use M = 4 in this section. If either probing hashing or separate chaining hashing is used, the major problem is that collisions could cause several blocks to be examined during a search, even for a well-distributed hash table. Furthermore, when the table gets too full, an extremely expensive rehashing step must be performed, which requires O(N) disk accesses. A clever alternative, known as extendible hashing, allows a search to be performed in two disk accesses. Insertions also require few disk accesses. We recall from Chapter 4 that a B-tree has depth O(logM/2 N). As M increases, the depth of a B-tree decreases. We could in theory choose M to be so large that the depth of the B-tree would be 1. Then any search after the first would take one disk access, since, presumably, the root node could be stored in main memory. The problem with this strategy is that the branching factor is so high that it would take considerable processing to determine which leaf the data was in. If the time to perform this step could be reduced, then we would have a practical scheme. This is exactly the strategy used by extendible hashing.
234 Chapter 5 Hashing 00 01 10 11 (2) (2) (2) (2) 000100 010100 100000 111000 001000 011000 101000 111001 001010 101100 001011 101110 Figure 5.52 Extendible hashing: original data Let us suppose, for the moment, that our data consists of several 6-bit integers. Figure 5.52 shows an extendible hashing scheme for these data. The root of the “tree” contains four pointers determined by the leading two bits of the data. Each leaf has up to M = 4 elements. It happens that in each leaf the first two bits are identical; this is indi- cated by the number in parentheses. To be more formal, D will represent the number of bits used by the root, which is sometimes known as the directory. The number of entries in the directory is thus 2D. dL is the number of leading bits that all the elements of some leaf L have in common. dL will depend on the particular leaf, and dL ≤ D. Suppose that we want to insert the key 100100. This would go into the third leaf, but as the third leaf is already full, there is no room. We thus split this leaf into two leaves, which are now determined by the first three bits. This requires increasing the directory size to 3. These changes are reflected in Figure 5.53. Notice that all the leaves not involved in the split are now pointed to by two adjacent directory entries. Thus, although an entire directory is rewritten, none of the other leaves is actually accessed. If the key 000000 is now inserted, then the first leaf is split, generating two leaves with dL = 3. Since D = 3, the only change required in the directory is the updating of the 000 and 001 pointers. See Figure 5.54. This very simple strategy provides quick access times for insert and search operations on large databases. There are a few important details we have not considered. First, it is possible that several directory splits will be required if the elements in a leaf agree in more than D + 1 leading bits. For instance, starting at the original example, with D = 2, if 111010, 111011, and finally 111100 are inserted, the directory size must be increased to 4 to distinguish between the five keys. This is an easy detail to take care of, but must not be forgotten. Second, there is the possibility of duplicate keys; if there are more than M duplicates, then this algorithm does not work at all. In this case, some other arrangements need to be made.
5.9 Extendible Hashing 235 000 001 010 011 100 101 110 111 (2) (2) (3) (3) (2) 000100 010100 100000 101000 111000 001000 011000 100100 101100 111001 001010 101110 001011 Figure 5.53 Extendible hashing: after insertion of 100100 and directory split 000 001 010 011 100 101 110 111 (3) (3) (2) (3) (3) (2) 000000 001000 010100 100000 101000 111000 000100 001010 011000 100100 101100 111001 001011 101110 Figure 5.54 Extendible hashing: after insertion of 000000 and leaf split These possibilities suggest that it is important for the bits to be fairly random. This can be accomplished by hashing the keys into a reasonably long integer—hence the name. We close by mentioning some of the performance properties of extendible hashing, which are derived after a very difficult analysis. These results are based on the reasonable assumption that the bit patterns are uniformly distributed. The expected number of leaves is (N/M) log2 e. Thus the average leaf is ln 2 = 0.69 full. This is the same as for B-trees, which is not entirely surprising, since for both data structures new nodes are created when the (M + 1)th entry is added.
236 Chapter 5 Hashing The more surprising result is that the expected size of the directory (in other words, 2D) is O(N1+1/M/M). If M is very small, then the directory can get unduly large. In this case, we can have the leaves contain pointers to the records instead of the actual records, thus increasing the value of M. This adds a second disk access to each search operation in order to maintain a smaller directory. If the directory is too large to fit in main memory, the second disk access would be needed anyway. Summary Hash tables can be used to implement the insert and contains operations in constant average time. It is especially important to pay attention to details such as load factor when using hash tables, since otherwise the time bounds are not valid. It is also important to choose the hash function carefully when the key is not a short string or integer. For separate chaining hashing, the load factor should be close to 1, although perfor- mance does not significantly degrade unless the load factor becomes very large. For probing hashing, the load factor should not exceed 0.5, unless this is completely unavoidable. If linear probing is used, performance degenerates rapidly as the load factor approaches 1. Rehashing can be implemented to allow the table to grow (and shrink), thus maintaining a reasonable load factor. This is important if space is tight and it is not possible just to declare a huge hash table. Other alternatives such as cuckoo hashing and hopscotch hashing can also yield good results. Because all these algorithms are constant time, it is difficult to make strong state- ments about which hash table implementation is the “best”; recent simulation results provide conflicting guidance and suggest that the performance can depend strongly on the types of items being manipulated, the underlying computer hardware, and the programming language. Binary search trees can also be used to implement insert and contains operations. Although the resulting average time bounds are O(log N), binary search trees also support routines that require order and are thus more powerful. Using a hash table, it is not possible to find the minimum element. It is not possible to search efficiently for a string unless the exact string is known. A binary search tree could quickly find all items in a certain range; this is not supported by hash tables. Furthermore, the O(log N) bound is not necessarily that much more than O(1), especially since no multiplications or divisions are required by search trees. On the other hand, the worst case for hashing generally results from an implementa- tion error, whereas sorted input can make binary trees perform poorly. Balanced search trees are quite expensive to implement, so if no ordering information is required and there is any suspicion that the input might be sorted, then hashing is the data structure of choice. Hashing applications are abundant. Compilers use hash tables to keep track of declared variables in source code. The data structure is known as a symbol table. Hash tables are the ideal application for this problem. Identifiers are typically short, so the hash function can be computed quickly, and alphabetizing the variables is often unnecessary.
Exercises 237 A hash table is useful for any graph theory problem where the nodes have real names instead of numbers. Here, as the input is read, vertices are assigned integers from 1 onward by order of appearance. Again, the input is likely to have large groups of alphabetized entries. For example, the vertices could be computers. Then if one particular installation lists its computers as ibm1, ibm2, ibm3, . . . , there could be a dramatic effect on efficiency if a search tree is used. A third common use of hash tables is in programs that play games. As the program searches through different lines of play, it keeps track of positions it has seen by comput- ing a hash function based on the position (and storing its move for that position). If the same position recurs, usually by a simple transposition of moves, the program can avoid expensive recomputation. This general feature of all game-playing programs is known as the transposition table. Yet another use of hashing is in online spelling checkers. If misspelling detection (as opposed to correction) is important, an entire dictionary can be prehashed and words can be checked in constant time. Hash tables are well suited for this, because it is not important to alphabetize words; printing out misspellings in the order they occurred in the document is certainly acceptable. Hash tables are often used to implement caches, both in software (for instance, the cache in your Internet browser) and in hardware (for instance, the memory caches in modern computers). They are also used in hardware implementations of routers. We close this chapter by returning to the word puzzle problem of Chapter 1. If the second algorithm described in Chapter 1 is used, and we assume that the maximum word size is some small constant, then the time to read in the dictionary containing W words and put it in a hash table is O(W). This time is likely to be dominated by the disk I/O and not the hashing routines. The rest of the algorithm would test for the presence of a word for each ordered quadruple (row, column, orientation, number of characters). As each lookup would be O(1), and there are only a constant number of orientations (8) and characters per word, the running time of this phase would be O(R · C). The total running time would be O(R · C + W), which is a distinct improvement over the original O(R · C · W). We could make further optimizations, which would decrease the running time in practice; these are described in the exercises. Exercises 5.1 Given input {4371, 1323, 6173, 4199, 4344, 9679, 1989} and a hash function h(x) = x (mod () 10), show the resulting a. separate chaining hash table b. hash table using linear probing c. hash table using quadratic probing d. hash table with second hash function h2(x) = 7 − (x mod 7) 5.2 Show the result of rehashing the hash tables in Exercise 5.1. 5.3 Write a program to compute the number of collisions required in a long ran- dom sequence of insertions using linear probing, quadratic probing, and double hashing.
238 Chapter 5 Hashing 5.4 A large number of deletions in a separate chaining hash table can cause the table to be fairly empty, which wastes space. In this case, we can rehash to a table half as large. Assume that we rehash to a larger table when there are twice as many elements as the table size. How empty should the table be before we rehash to a smaller table? 5.5 Reimplement separate chaining hash tables using a vector of singly linked lists instead of vectors. 5.6 The isEmpty routine for quadratic probing has not been written. Can you implement it by returning the expression currentSize==0? 5.7 In the quadratic probing hash table, suppose that instead of inserting a new item into the location suggested by findPos, we insert it into the first inactive cell on the search path (thus, it is possible to reclaim a cell that is marked deleted, potentially saving space). a. Rewrite the insertion algorithm to use this observation. Do this by having findPos maintain, with an additional variable, the location of the first inactive cell it encounters. b. Explain the circumstances under which the revised algorithm is faster than the original algorithm. Can it be slower? 5.8 Suppose instead of quadratic probing, we use “cubic probing”; here the ith probe is at hash(x) + i3. Does cubic probing improve on quadratic probing? 5.9 Using a standard dictionary, and a table size that approximates a load factor of 1, compare the number of collisions produced by the hash function in Figure 5.4 and the hash function in Figure 5.55. 5.10 What are the advantages and disadvantages of the various collision resolution strategies? 5.11 Suppose that to mitigate the effects of secondary clustering we use as the collision resolution function f(i) = i · r(hash(x)), where hash(x) is the 32-bit hash value (not yet scaled to a suitable array index), and r(y) = |48271y( mod (231 − 1))| 1 /** 2 * FNV-1a hash routine for string objects. 3 */ 4 unsigned int hash( const string & key, int tableSize ) 5{ 6 unsigned int hashVal = 2166136261; 7 8 for( char ch : key ) 9 hashVal = ( hashVal ˆ ch )* 16777619; 10 11 return hashVal % tableSize; 12 } Figure 5.55 Alternative hash function for Exercise 5.9.
Exercises 239 mod TableSize. (Section 10.4.1 describes a method of performing this calculation without overflows, but it is unlikely that overflow matters in this case.) Explain why this strategy tends to avoid secondary clustering, and compare this strategy with both double hashing and quadratic probing. 5.12 Rehashing requires recomputing the hash function for all items in the hash table. Since computing the hash function is expensive, suppose objects provide a hash member function of their own, and each object stores the result in an addi- tional data member the first time the hash function is computed for it. Show how such a scheme would apply for the Employee class in Figure 5.8, and explain under what circumstances the remembered hash value remains valid in each Employee. 5.13 Write a program to implement the following strategy for multiplying two sparse polynomials P1, P2 of size M and N, respectively. Each polynomial is represented as a list of objects consisting of a coefficient and an exponent. We multiply each term in P1 by a term in P2 for a total of MN operations. One method is to sort these terms and combine like terms, but this requires sorting MN records, which could be expensive, especially in small-memory environments. Alternatively, we could merge terms as they are computed and then sort the result. a. Write a program to implement the alternative strategy. b. If the output polynomial has about O(M + N) terms, what is the running time of both methods? 5.14 Describe a procedure that avoids initializing a hash table (at the expense of memory). 5.15 Suppose we want to find the first occurrence of a string P1P2 · · · Pk in a long input string A1A2 · · · AN. We can solve this problem by hashing the pattern string, obtain- ing a hash value HP, and comparing this value with the hash value formed from A1A2 · · · Ak, A2A3 · · · Ak+1, A3A4 · · · Ak+2, and so on until AN−k+1AN−k+2 · · · AN. If we have a match of hash values, we compare the strings character by character to verify the match. We return the position (in A) if the strings actually do match, and we continue in the unlikely event that the match is false. a. Show that if the hash value of AiAi+1 · · · Ai+k−1 is known, then the hash value of Ai+1Ai+2 · · · Ai+k can be computed in constant time. b. Show that the running time is O(k + N) plus the time spent refuting false matches. c. Show that the expected number of false matches is negligible. d. Write a program to implement this algorithm. e. Describe an algorithm that runs in O(k + N) worst-case time. f. Describe an algorithm that runs in O(N/k) average time. 5.16 A nonstandard C++ extension adds syntax that allows a switch statement to work with the string type (instead of the primitive integer types). Explain how hash tables can be used by the compiler to implement this language addition. 5.17 An (old-style) BASIC program consists of a series of statements numbered in ascend- ing order. Control is passed by use of a goto or gosub and a statement number. Write a program that reads in a legal BASIC program and renumbers the statements so
240 Chapter 5 Hashing that the first starts at number F and each statement has a number D higher than the previous statement. You may assume an upper limit of N statements, but the statement numbers in the input might be as large as a 32-bit integer. Your program must run in linear time. 5.18 a. Implement the word puzzle program using the algorithm described at the end of the chapter. b. We can get a big speed increase by storing, in addition to each word W, all of W’s prefixes. (If one of W’s prefixes is another word in the dictionary, it is stored as a real word.) Although this may seem to increase the size of the hash table drastically, it does not, because many words have the same prefixes. When a scan is performed in a particular direction, if the word that is looked up is not even in the hash table as a prefix, then the scan in that direction can be terminated early. Use this idea to write an improved program to solve the word puzzle. c. If we are willing to sacrifice the sanctity of the hash table ADT, we can speed up the program in part (b) by noting that if, for example, we have just computed the hash function for “excel,” we do not need to compute the hash function for “excels” from scratch. Adjust your hash function so that it can take advantage of its previous calculation. d. In Chapter 2, we suggested using binary search. Incorporate the idea of using prefixes into your binary search algorithm. The modification should be simple. Which algorithm is faster? 5.19 Under certain assumptions, the expected cost of an insertion into a hash table with secondary clustering is given by 1/(1−λ)−λ−ln(1−λ). Unfortunately, this formula is not accurate for quadratic probing. However, assuming that it is, determine the following: a. the expected cost of an unsuccessful search b. the expected cost of a successful search 5.20 Implement a generic Map that supports the insert and lookup operations. The implementation will store a hash table of pairs (key, definition). You will lookup a definition by providing a key. Figure 5.56 provides the Map specification (minus some details). 5.21 Implement a spelling checker by using a hash table. Assume that the dictionary comes from two sources: an existing large dictionary and a second file containing a personal dictionary. Output all misspelled words and the line numbers on which they occur. Also, for each misspelled word, list any words in the dictionary that are obtainable by applying any of the following rules: a. Add one character. b. Remove one character. c. Exchange adjacent characters. 5.22 Prove Markov’s Inequality: If X is any random variable and a > 0, then Pr( |X| ≥ a) ≤ E( |X| )/a. Show how this inequality can be applied to Theorems 5.2 and 5.3. 5.23 If a hopscotch table with parameter MAX_DIST has load factor 0.5, what is the approximate probability that an insertion requires a rehash?
References 241 1 template <typename HashedObj, typename Object> 2 class Pair 3{ 4 HashedObj key; 5 Object def; 6 // Appropriate Constructors, etc. 7 }; 8 9 template <typename HashedObj, typename Object> 10 class Dictionary 11 { 12 public: 13 Dictionary( ); 14 15 void insert( const HashedObj & key, const Object & definition ); 16 const Object & lookup( const HashedObj & key ) const; 17 bool isEmpty( ) const; 18 void makeEmpty( ); 19 20 private: 21 HashTable<Pair<HashedObj,Object>> items; 22 }; Figure 5.56 Dictionary skeleton for Exercise 5.20 5.24 Implement a hopscotch hash table and compare its performance with linear probing, separate chaining, and cuckoo hashing. 5.25 Implement the classic cuckoo hash table in which two separate tables are main- tained. The simplest way to do this is to use a single array and modify the hash function to access either the top half or the bottom half. 5.26 Extend the classic cuckoo hash table to use d hash functions. 5.27 Show the result of inserting the keys 10111101, 00000010, 10011011, 10111110, 01111111, 01010001, 10010110, 00001011, 11001111, 10011110, 11011011, 00101011, 01100001, 11110000, 01101111 into an initially empty extendible hashing data structure with M = 4. 5.28 Write a program to implement extendible hashing. If the table is small enough to fit in main memory, how does its performance compare with separate chaining and open addressing hashing? References Despite the apparent simplicity of hashing, much of the analysis is quite difficult, and there are still many unresolved questions. There are also many interesting theoretical issues.
242 Chapter 5 Hashing Hashing dates to at least 1953, when H. P. Luhn wrote an internal IBM memorandum that used separate chaining hashing. Early papers on hashing are [11] and [32]. A wealth of information on the subject, including an analysis of hashing with linear probing under the assumption of totally random and independent hashing, can be found in [25]. More recent results have shown that linear probing requires only 5-independent hash functions [31]. An excellent survey on early classic hash tables methods is [28]; [29] contains suggestions, and pitfalls, for choosing hash functions. Precise analytic and simulation results for sep- arate chaining, linear probing, quadratic probing, and double hashing can be found in [19]. However, due to changes (improvements) in computer architecture and compilers, simulation results tend to quickly become dated. An analysis of double hashing can be found in [20] and [27]. Yet another collision resolution scheme is coalesced hashing, described in [33]. Yao [37] has shown the uniform hashing, in which no clustering exists, is optimal with respect to cost of a successful search, assuming that items cannot move once placed. Universal hash functions were first described in [5] and [35]; the latter paper intro- duces the “Carter-Wegman trick” of using Mersenne prime numbers to avoid expensive mod operations. Perfect hashing is described in [16], and a dynamic version of perfect hashing was described in [8]. [12] is a survey of some classic dynamic hashing schemes. The (log N/ log log N) bound on the length of the longest list in separate chaining was shown (in precise form) in [18]. The “power of two choices,” showing that when the shorter of two randomly selected lists is chosen, then the bound on the length of the longest list is lowered to only (log log N), was first described in [2]. An early example of the power of two choices is [4]. The classic work on cuckoo hashing is [30]; since the initial paper, a host of new results have appeared that analyze the amount of independence needed in the hash functions and describe alternative implementations [7], [34], [15], [10], [23], [24], [1], [6], [9] and [17]. Hopscotch hashing appeared in [21]. Extendible hashing appears in [13], with analysis in [14] and [36]. Exercise 5.15 (a–d) is from [22]. Part (e) is from [26], and part (f) is from [3]. The FNV-1a hash function described in Exercise 5.9 is due to Fowler, Noll, and Vo. 1. Y. Arbitman, M. Naor, and G. Segev, “De-Amortized Cuckoo Hashing: Provable Worst-Case Performance and Experimental Results,” Proceedings of the 36th International Colloquium on Automata, Languages and Programming (2009), 107–118. 2. Y. Azar, A. Broder, A. Karlin, and E. Upfal, “Balanced Allocations,” SIAM Journal of Computing, 29 (1999), 180–200. 3. R. S. Boyer and J. S. Moore, “A Fast String Searching Algorithm,” Communications of the ACM, 20 (1977), 762–772. 4. A. Broder and M. Mitzenmacher, “Using Multiple Hash Functions to Improve IP Lookups,” Proceedings of the Twentieth IEEE INFOCOM (2001), 1454–1463. 5. J. L. Carter and M. N. Wegman, “Universal Classes of Hash Functions,” Journal of Computer and System Sciences, 18 (1979), 143–154. 6. J. Cohen and D. Kane, “Bounds on the Independence Required for Cuckoo Hashing,” preprint. 7. L. Devroye and P. Morin, “Cuckoo Hashing: Further Analysis,” Information Processing Letters, 86 (2003), 215–219.
References 243 8. M. Dietzfelbinger, A. R. Karlin, K. Melhorn, F. Meyer auf der Heide, H. Rohnert, and R. E. Tarjan, “Dynamic Perfect Hashing: Upper and Lower Bounds,” SIAM Journal on Computing, 23 (1994), 738–761. 9. M. Dietzfelbinger and U. Schellbach, “On Risks of Using Cuckoo Hashing with Simple Universal Hash Classes,” Proceedings of the Twentieth Annual ACM-SIAM Symposium on Discrete Algorithms (2009), 795–804. 10. M. Dietzfelbinger and C. Weidling, “Balanced Allocation and Dictionaries with Tightly Packed Constant Size Bins,” Theoretical Computer Science, 380 (2007), 47–68. 11. I. Dumey, “Indexing for Rapid Random-Access Memory,” Computers and Automation, 5 (1956), 6–9. 12. R. J. Enbody and H. C. Du, “Dynamic Hashing Schemes,” Computing Surveys, 20 (1988), 85–113. 13. R. Fagin, J. Nievergelt, N. Pippenger, and H. R. Strong, “Extendible Hashing—A Fast Access Method for Dynamic Files,” ACM Transactions on Database Systems, 4 (1979), 315–344. 14. P. Flajolet, “On the Performance Evaluation of Extendible Hashing and Trie Searching,” Acta Informatica, 20 (1983), 345–369. 15. D. Fotakis, R. Pagh, P. Sanders, and P. Spirakis, “Space Efficient Hash Tables with Worst Case Constant Access Time,” Theory of Computing Systems, 38 (2005), 229–248. 16. M. L. Fredman, J. Komlos, and E. Szemeredi, “Storing a Sparse Table with O(1) Worst Case Access Time,” Journal of the ACM, 31 (1984), 538–544. 17. A. Frieze, P. Melsted, and M. Mitzenmacher, “An Analysis of Random-Walk Cuckoo Hashing,” Proceedings of the Twelfth International Workshop on Approximation Algorithms in Combinatorial Optimization (APPROX) (2009), 350–364. 18. G. Gonnet, “Expected Length of the Longest Probe Sequence in Hash Code Searching,” Journal of the Association for Computing Machinery, 28 (1981), 289–304. 19. G. H. Gonnet and R. Baeza-Yates, Handbook of Algorithms and Data Structures, 2d ed., Addison-Wesley, Reading, Mass., 1991. 20. L. J. Guibas and E. Szemeredi, “The Analysis of Double Hashing,” Journal of Computer and System Sciences, 16 (1978), 226–274. 21. M. Herlihy, N. Shavit, and M. Tzafrir, “Hopscotch Hashing,” Proceedings of the Twenty-Second International Symposium on Distributed Computing (2008), 350–364. 22. R. M. Karp and M. O. Rabin, “Efficient Randomized Pattern-Matching Algorithms,” Aiken Computer Laboratory Report TR-31-81, Harvard University, Cambridge, Mass., 1981. 23. A. Kirsch and M. Mitzenmacher, “The Power of One Move: Hashing Schemes for Hardware,” Proceedings of the 27th IEEE International Conference on Computer Communications (INFOCOM) (2008), 106–110. 24. A. Kirsch, M. Mitzenmacher, and U. Wieder, “More Robust Hashing: Cuckoo Hashing with a Stash,” Proceedings of the Sixteenth Annual European Symposium on Algorithms (2008), 611–622. 25. D. E. Knuth, The Art of Computer Programming, Vol. 3: Sorting and Searching, 2d ed., Addison- Wesley, Reading, Mass., 1998. 26. D. E. Knuth, J. H. Morris, and V. R. Pratt, “Fast Pattern Matching in Strings,” SIAM Journal on Computing, 6 (1977), 323–350. 27. G. Lueker and M. Molodowitch, “More Analysis of Double Hashing,” Proceedings of the Twentieth ACM Symposium on Theory of Computing (1988), 354–359. 28. W. D. Maurer and T. G. Lewis, “Hash Table Methods,” Computing Surveys, 7 (1975), 5–20.
244 Chapter 5 Hashing 29. B. J. McKenzie, R. Harries, and T. Bell, “Selecting a Hashing Algorithm,” Software—Practice and Experience, 20 (1990), 209–224. 30. R. Pagh and F. F. Rodler, “Cuckoo Hashing,” Journal of Algorithms, 51 (2004), 122–144. 31. M. Pa˘tras¸cu and M. Thorup, “On the k-Independence Required by Linear Probing and Minwise Independence,” Proceedings of the 37th International Colloquium on Automata, Languages, and Programming (2010), 715–726. 32. W. W. Peterson, “Addressing for Random Access Storage,” IBM Journal of Research and Development, 1 (1957), 130–146. 33. J. S. Vitter, “Implementations for Coalesced Hashing,” Communications of the ACM, 25 (1982), 911–926. 34. B. Vöcking, “How Asymmetry Helps Load Balancing,” Journal of the ACM, 50 (2003), 568–589. 35. M. N. Wegman and J. Carter, “New Hash Functions and Their Use in Authentication and Set Equality,” Journal of Computer and System Sciences, 22 (1981), 265–279. 36. A. C. Yao, “A Note on the Analysis of Extendible Hashing,” Information Processing Letters, 11 (1980), 84–86. 37. A. C. Yao, “Uniform Hashing Is Optimal,” Journal of the ACM, 32 (1985), 687–693.
6C H A P T E R Priority Queues (Heaps) Although jobs sent to a printer are generally placed on a queue, this might not always be 245 the best thing to do. For instance, one job might be particularly important, so it might be desirable to allow that job to be run as soon as the printer is available. Conversely, if, when the printer becomes available, there are several 1-page jobs and one 100-page job, it might be reasonable to make the long job go last, even if it is not the last job submitted. (Unfortunately, most systems do not do this, which can be particularly annoying at times.) Similarly, in a multiuser environment, the operating system scheduler must decide which of several processes to run. Generally, a process is allowed to run only for a fixed period of time. One algorithm uses a queue. Jobs are initially placed at the end of the queue. The scheduler will repeatedly take the first job on the queue, run it until either it finishes or its time limit is up, and place it at the end of the queue if it does not finish. This strategy is generally not appropriate, because very short jobs will seem to take a long time because of the wait involved to run. Generally, it is important that short jobs finish as fast as possible, so these jobs should have precedence over jobs that have already been running. Furthermore, some jobs that are not short are still very important and should also have precedence. This particular application seems to require a special kind of queue, known as a priority queue. In this chapter, we will discuss . . . r Efficient implementation of the priority queue ADT. r Uses of priority queues. r Advanced implementations of priority queues. The data structures we will see are among the most elegant in computer science. 6.1 Model A priority queue is a data structure that allows at least the following two operations: insert, which does the obvious thing; and deleteMin, which finds, returns, and removes the minimum element in the priority queue.1 The insert operation is the equivalent of enqueue, and deleteMin is the priority queue equivalent of the queue’s dequeue operation. 1 The C++ code provides two versions of deleteMin. One removes the minimum; the other removes the minimum and stores the removed value in an object passed by reference.
246 Chapter 6 Priority Queues (Heaps) deleteMin Priority Queue insert Figure 6.1 Basic model of a priority queue As with most data structures, it is sometimes possible to add other operations, but these are extensions and not part of the basic model depicted in Figure 6.1. Priority queues have many applications besides operating systems. In Chapter 7, we will see how priority queues are used for external sorting. Priority queues are also impor- tant in the implementation of greedy algorithms, which operate by repeatedly finding a minimum; we will see specific examples in Chapters 9 and 10. In this chapter, we will see a use of priority queues in discrete event simulation. 6.2 Simple Implementations There are several obvious ways to implement a priority queue. We could use a simple linked list, performing insertions at the front in O(1) and traversing the list, which requires O(N) time, to delete the minimum. Alternatively, we could insist that the list be kept always sorted; this makes insertions expensive (O(N)) and deleteMins cheap (O(1)). The former is probably the better idea of the two, based on the fact that there are never more deleteMins than insertions. Another way of implementing priority queues would be to use a binary search tree. This gives an O(log N) average running time for both operations. This is true in spite of the fact that although the insertions are random, the deletions are not. Recall that the only ele- ment we ever delete is the minimum. Repeatedly removing a node that is in the left subtree would seem to hurt the balance of the tree by making the right subtree heavy. However, the right subtree is random. In the worst case, where the deleteMins have depleted the left subtree, the right subtree would have at most twice as many elements as it should. This adds only a small constant to its expected depth. Notice that the bound can be made into a worst-case bound by using a balanced tree; this protects one against bad insertion sequences. Using a search tree could be overkill because it supports a host of operations that are not required. The basic data structure we will use will not require links and will support both operations in O(log N) worst-case time. Insertion will actually take constant time on average, and our implementation will allow building a priority queue of N items in linear time, if no deletions intervene. We will then discuss how to implement priority queues to support efficient merging. This additional operation seems to complicate matters a bit and apparently requires the use of a linked structure.
6.3 Binary Heap 247 6.3 Binary Heap The implementation we will use is known as a binary heap. Its use is so common for priority queue implementations that, in the context of priority queues, when the word heap is used without a qualifier, it is generally assumed to be referring to this implementation of the data structure. In this section, we will refer to binary heaps merely as heaps. Like binary search trees, heaps have two properties, namely, a structure property and a heap- order property. As with AVL trees, an operation on a heap can destroy one of the properties, so a heap operation must not terminate until all heap properties are in order. This turns out to be simple to do. 6.3.1 Structure Property A heap is a binary tree that is completely filled, with the possible exception of the bottom level, which is filled from left to right. Such a tree is known as a complete binary tree. Figure 6.2 shows an example. It is easy to show that a complete binary tree of height h has between 2h and 2h+1 − 1 nodes. This implies that the height of a complete binary tree is log N , which is clearly O(log N). An important observation is that because a complete binary tree is so regular, it can be represented in an array and no links are necessary. The array in Figure 6.3 corresponds to the heap in Figure 6.2. A BC D EF G H IJ Figure 6.2 A complete binary tree ABCDE F GH I J 0 1 2 3 4 5 6 7 8 9 10 11 12 13 Figure 6.3 Array implementation of complete binary tree
248 Chapter 6 Priority Queues (Heaps) 1 template <typename Comparable> 2 class BinaryHeap 3{ 4 public: 5 explicit BinaryHeap( int capacity = 100 ); 6 explicit BinaryHeap( const vector<Comparable> & items ); 7 8 bool isEmpty( ) const; 9 const Comparable & findMin( ) const; 10 11 void insert( const Comparable & x ); 12 void insert( Comparable && x ); 13 void deleteMin( ); 14 void deleteMin( Comparable & minItem ); 15 void makeEmpty( ); 16 17 private: 18 int currentSize; // Number of elements in heap 19 vector<Comparable> array; // The heap array 20 21 void buildHeap( ); 22 void percolateDown( int hole ); 23 }; Figure 6.4 Class interface for priority queue For any element in array position i, the left child is in position 2i, the right child is in the cell after the left child (2i + 1), and the parent is in position i/2 . Thus, not only are links not required, but the operations required to traverse the tree are extremely simple and likely to be very fast on most computers. The only problem with this implementation is that an estimate of the maximum heap size is required in advance, but typically this is not a problem (and we can resize if needed). In Figure 6.3 the limit on the heap size is 13 elements. The array has a position 0; more on this later. A heap data structure will, then, consist of an array (of Comparable objects) and an integer representing the current heap size. Figure 6.4 shows a priority queue interface. Throughout this chapter, we shall draw the heaps as trees, with the implication that an actual implementation will use simple arrays. 6.3.2 Heap-Order Property The property that allows operations to be performed quickly is the heap-order property. Since we want to be able to find the minimum quickly, it makes sense that the smallest element should be at the root. If we consider that any subtree should also be a heap, then any node should be smaller than all of its descendants.
6.3 Binary Heap 249 13 13 21 16 21 16 24 31 19 68 6 31 19 68 65 26 32 65 26 32 Figure 6.5 Two complete trees (only the left tree is a heap) Applying this logic, we arrive at the heap-order property. In a heap, for every node X, the key in the parent of X is smaller than (or equal to) the key in X, with the exception of the root (which has no parent).2 In Figure 6.5 the tree on the left is a heap, but the tree on the right is not (the dashed line shows the violation of heap order). By the heap-order property, the minimum element can always be found at the root. Thus, we get the extra operation, findMin, in constant time. 6.3.3 Basic Heap Operations It is easy (both conceptually and practically) to perform the two required operations. All the work involves ensuring that the heap-order property is maintained. insert To insert an element X into the heap, we create a hole in the next available location, since otherwise, the tree will not be complete. If X can be placed in the hole without violating heap order, then we do so and are done. Otherwise, we slide the element that is in the hole’s parent node into the hole, thus bubbling the hole up toward the root. We continue this process until X can be placed in the hole. Figure 6.6 shows that to insert 14, we create a hole in the next available heap location. Inserting 14 in the hole would violate the heap- order property, so 31 is slid down into the hole. This strategy is continued in Figure 6.7 until the correct location for 14 is found. This general strategy is known as a percolate up; the new element is percolated up the heap until the correct location is found. Insertion is easily implemented with the code shown in Figure 6.8. 2 Analogously we can declare a (max) heap, which enables us to efficiently find and remove the maximum element by changing the heap-order property. Thus, a priority queue can be used to find either a minimum or a maximum, but this needs to be decided ahead of time.
13 13 21 16 21 16 24 31 19 68 24 19 68 65 26 32 65 26 32 31 Figure 6.6 Attempt to insert 14: creating the hole, and bubbling the hole up 13 13 16 14 16 24 21 19 68 24 21 19 68 65 26 32 31 65 26 32 31 Figure 6.7 The remaining two steps to insert 14 in previous heap 1 /** 2 * Insert item x, allowing duplicates. 3 */ 4 void insert( const Comparable & x ) 5{ 6 if( currentSize == array.size( ) - 1 ) 7 array.resize( array.size( ) * 2 ); 8 9 // Percolate up 10 int hole = ++currentSize; 11 Comparable copy = x; 12 13 array[ 0 ] = std::move( copy ); 14 for( ; x < array[ hole / 2 ]; hole /= 2 ) 15 array[ hole ] = std::move( array[ hole / 2 ] ); 16 array[ hole ] = std::move( array[ 0 ] ); 17 } Figure 6.8 Procedure to insert into a binary heap
6.3 Binary Heap 251 We could have implemented the percolation in the insert routine by performing repeated swaps until the correct order was established, but a swap requires three assign- ment statements. If an element is percolated up d levels, the number of assignments performed by the swaps would be 3d. Our method uses d + 1 assignments. If the element to be inserted is the new minimum, it will be pushed all the way to the top. At some point, hole will be 1 and we will want to break out of the loop. We could do this with an explicit test, or we can put a copy of the inserted item in position 0 in order to make the loop terminate. We elect to place X into position 0. The time to do the insertion could be as much as O(log N), if the element to be inserted is the new minimum and is percolated all the way to the root. On average, the percolation terminates early; it has been shown that 2.607 comparisons are required on average to perform an insert, so the average insert moves an element up 1.607 levels. deleteMin deleteMins are handled in a similar manner as insertions. Finding the minimum is easy; the hard part is removing it. When the minimum is removed, a hole is created at the root. Since the heap now becomes one smaller, it follows that the last element X in the heap must move somewhere in the heap. If X can be placed in the hole, then we are done. This is unlikely, so we slide the smaller of the hole’s children into the hole, thus pushing the hole down one level. We repeat this step until X can be placed in the hole. Thus, our action is to place X in its correct spot along a path from the root containing minimum children. In Figure 6.9 the left figure shows a heap prior to the deleteMin. After 13 is removed, we must now try to place 31 in the heap. The value 31 cannot be placed in the hole, because this would violate heap order. Thus, we place the smaller child (14) in the hole, sliding the hole down one level (see Fig. 6.10). We repeat this again, and since 31 is larger than 19, we place 19 into the hole and create a new hole one level deeper. We then place 26 in the hole and create a new hole on the bottom level since, once again, 31 is too large. Finally, we are able to place 31 in the hole (Fig. 6.11). This general strategy is known as a percolate down. We use the same technique as in the insert routine to avoid the use of swaps in this routine. A frequent implementation error in heaps occurs when there are an even number of elements in the heap, and the one node that has only one child is encountered. You must 13 14 16 14 16 19 21 19 68 19 21 19 68 65 26 32 31 65 26 32 31 Figure 6.9 Creation of the hole at the root
252 Chapter 6 Priority Queues (Heaps) 14 14 16 19 16 19 21 19 68 21 19 68 65 26 32 31 65 26 32 31 Figure 6.10 Next two steps in deleteMin 14 14 19 16 19 16 26 21 19 68 26 21 19 68 65 32 31 65 31 32 Figure 6.11 Last two steps in deleteMin make sure not to assume that there are always two children, so this usually involves an extra test. In the code depicted in Figure 6.12, we’ve done this test at line 40. One extremely tricky solution is always to ensure that your algorithm thinks every node has two children. Do this by placing a sentinel, of value higher than any in the heap, at the spot after the heap ends, at the start of each percolate down when the heap size is even. You should think very carefully before attempting this, and you must put in a prominent comment if you do use this technique. Although this eliminates the need to test for the presence of a right child, you cannot eliminate the requirement that you test when you reach the bottom, because this would require a sentinel for every leaf. The worst-case running time for this operation is O(log N). On average, the element that is placed at the root is percolated almost to the bottom of the heap (which is the level it came from), so the average running time is O(log N). 6.3.4 Other Heap Operations Notice that although finding the minimum can be performed in constant time, a heap designed to find the minimum element (also known as a (min)heap) is of no help whatso- ever in finding the maximum element. In fact, a heap has very little ordering information,
1 /** 2 * Remove the minimum item. 3 * Throws UnderflowException if empty. 4 */ 5 void deleteMin( ) 6{ 7 if( isEmpty( ) ) 8 throw UnderflowException{ }; 9 10 array[ 1 ] = std::move( array[ currentSize-- ] ); 11 percolateDown( 1 ); 12 } 13 14 /** 15 * Remove the minimum item and place it in minItem. 16 * Throws UnderflowException if empty. 17 */ 18 void deleteMin( Comparable & minItem ) 19 { 20 if( isEmpty( ) ) 21 throw UnderflowException{ }; 22 23 minItem = std::move( array[ 1 ] ); 24 array[ 1 ] = std::move( array[ currentSize-- ] ); 25 percolateDown( 1 ); 26 } 27 28 /** 29 * Internal method to percolate down in the heap. 30 * hole is the index at which the percolate begins. 31 */ 32 void percolateDown( int hole ) 33 { 34 int child; 35 Comparable tmp = std::move( array[ hole ] ); 36 37 for( ; hole * 2 <= currentSize; hole = child ) 38 { 39 child = hole * 2; 40 if( child != currentSize && array[ child + 1 ] < array[ child ] ) 41 ++child; 42 if( array[ child ] < tmp ) 43 array[ hole ] = std::move( array[ child ] ); 44 else 45 break; 46 } 47 array[ hole ] = std::move( tmp ); 48 } Figure 6.12 Method to perform deleteMin in a binary heap
254 Chapter 6 Priority Queues (Heaps) Figure 6.13 A very large complete binary tree so there is no way to find any particular element without a linear scan through the entire heap. To see this, consider the large heap structure (the elements are not shown) in Figure 6.13, where we see that the only information known about the maximum element is that it is at one of the leaves. Half the elements, though, are contained in leaves, so this is practically useless information. For this reason, if it is important to know where elements are, some other data structure, such as a hash table, must be used in addition to the heap. (Recall that the model does not allow looking inside the heap.) If we assume that the position of every element is known by some other method, then several other operations become cheap. The first three operations below all run in logarithmic worst-case time. decreaseKey The decreaseKey(p, ) operation lowers the value of the item at position p by a positive amount . Since this might violate the heap order, it must be fixed by a percolate up. This operation could be useful to system administrators: They can make their programs run with highest priority. increaseKey The increaseKey(p, ) operation increases the value of the item at position p by a positive amount . This is done with a percolate down. Many schedulers automatically drop the priority of a process that is consuming excessive CPU time. remove The remove(p) operation removes the node at position p from the heap. This is done by first performing decreaseKey(p,∞) and then performing deleteMin(). When a process
6.3 Binary Heap 255 is terminated by a user (instead of finishing normally), it must be removed from the priority queue. buildHeap The binary heap is sometimes constructed from an initial collection of items. This con- structor takes as input N items and places them into a heap. Obviously, this can be done with N successive inserts. Since each insert will take O(1) average and O(log N) worst-case time, the total running time of this algorithm would be O(N) average but O(N log N) worst-case. Since this is a special instruction and there are no other operations intervening, and we already know that the instruction can be performed in linear aver- age time, it is reasonable to expect that with reasonable care a linear time bound can be guaranteed. The general algorithm is to place the N items into the tree in any order, maintaining the structure property. Then, if percolateDown(i) percolates down from node i, the buildHeap routine in Figure 6.14 can be used by the constructor to create a heap-ordered tree. The first tree in Figure 6.15 is the unordered tree. The seven remaining trees in Figures 6.15 through 6.18 show the result of each of the seven percolateDowns. Each dashed line corresponds to two comparisons: one to find the smaller child and one to compare the smaller child with the node. Notice that there are only 10 dashed lines in the entire algorithm (there could have been an 11th—where?) corresponding to 20 comparisons. To bound the running time of buildHeap, we must bound the number of dashed lines. This can be done by computing the sum of the heights of all the nodes in the heap, which is the maximum number of dashed lines. What we would like to show is that this sum is O(N). 1 explicit BinaryHeap( const vector<Comparable> & items ) 2 : array( items.size( ) + 10 ), currentSize{ items.size( ) } 3{ 4 for( int i = 0; i < items.size( ); ++i ) 5 array[ i + 1 ] = items[ i ]; 6 buildHeap( ); 7} 8 9 /** 10 * Establish heap order property from an arbitrary 11 * arrangement of items. Runs in linear time. 12 */ 13 void buildHeap( ) 14 { 15 for( int i = currentSize / 2; i > 0; --i ) 16 percolateDown( i ); 17 } Figure 6.14 buildHeap and constructor
256 Chapter 6 Priority Queues (Heaps) 150 150 80 40 80 40 30 10 70 110 30 10 70 110 100 20 90 60 50 120 140 130 100 20 90 60 50 120 140 130 Figure 6.15 Left: initial heap; right: after percolateDown(7) 150 150 80 40 80 40 30 10 50 110 30 10 50 110 100 20 90 60 70 120 140 130 100 20 90 60 70 120 140 130 Figure 6.16 Left: after percolateDown(6); right: after percolateDown(5) 150 150 80 40 80 40 20 10 50 110 20 10 50 110 100 30 90 60 70 120 140 130 100 30 90 60 70 120 140 130 Figure 6.17 Left: after percolateDown(4); right: after percolateDown(3) Theorem 6.1 For the perfect binary tree of height h containing 2h+1−1 nodes, the sum of the heights of the nodes is 2h+1 − 1 − (h + 1). Proof It is easy to see that this tree consists of 1 node at height h, 2 nodes at height h − 1, 22 nodes at height h − 2, and in general 2i nodes at height h − i. The sum of the heights of all the nodes is then
6.4 Applications of Priority Queues 257 150 10 10 40 20 40 20 60 50 110 30 60 50 110 100 30 90 80 70 120 140 130 100 150 90 80 70 120 140 130 Figure 6.18 Left: after percolateDown(2); right: after percolateDown(1) h (6.1) S = 2i(h − i) i=0 = h + 2(h − 1) + 4(h − 2) + 8(h − 3) + 16(h − 4) + · · · + 2h−1(1) Multiplying by 2 gives the equation 2S = 2h + 4(h − 1) + 8(h − 2) + 16(h − 3) + · · · + 2h(1) (6.2) We subtract these two equations and obtain Equation (6.3). We find that certain terms almost cancel. For instance, we have 2h − 2(h − 1) = 2, 4(h − 1) − 4(h − 2) = 4, and so on. The last term in Equation (6.2), 2h, does not appear in Equation (6.1); thus, it appears in Equation (6.3). The first term in Equation (6.1), h, does not appear in Equation (6.2); thus, −h appears in Equation (6.3). We obtain S = −h + 2 + 4 + 8 + · · · + 2h−1 + 2h = (2h+1 − 1) − (h + 1) (6.3) which proves the theorem. A complete tree is not a perfect binary tree, but the result we have obtained is an upper bound on the sum of the heights of the nodes in a complete tree. Since a complete tree has between 2h and 2h+1 nodes, this theorem implies that this sum is O(N), where N is the number of nodes. Although the result we have obtained is sufficient to show that buildHeap is linear, the bound on the sum of the heights is not as strong as possible. For a complete tree with N = 2h nodes, the bound we have obtained is roughly 2N. The sum of the heights can be shown by induction to be N − b(N), where b(N) is the number of 1s in the binary representation of N. 6.4 Applications of Priority Queues We have already mentioned how priority queues are used in operating systems design. In Chapter 9, we will see how priority queues are used to implement several graph algo- rithms efficiently. Here we will show how to use priority queues to obtain solutions to two problems.
258 Chapter 6 Priority Queues (Heaps) 6.4.1 The Selection Problem The first problem we will examine is the selection problem from Section 1.1. Recall that the input is a list of N elements, which can be totally ordered, and an integer k. The selection problem is to find the kth largest element. Two algorithms were given in Chapter 1, but neither is very efficient. The first algo- rithm, which we shall call algorithm 1A, is to read the elements into an array and sort them, returning the appropriate element. Assuming a simple sorting algorithm, the running time is O(N2). The alternative algorithm, 1B, is to read k elements into an array and sort them. The smallest of these is in the kth position. We process the remaining elements one by one. As an element arrives, it is compared with the kth element in the array. If it is larger, then the kth element is removed, and the new element is placed in the correct place among the remaining k − 1 elements. When the algorithm ends, the element in the kth position is the answer. The running time is O(N·k) (why?). If k = N/2 , then both algorithms are O(N2). Notice that for any k, we can solve the symmetric problem of finding the (N − k + 1)th smallest element, so k = N/2 is really the hardest case for these algorithms. This also happens to be the most interesting case, since this value of k is known as the median. We give two algorithms here, both of which run in O(N log N) in the extreme case of k = N/2 , which is a distinct improvement. Algorithm 6A For simplicity, we assume that we are interested in finding the kth smallest element. The algorithm is simple. We read the N elements into an array. We then apply the buildHeap algorithm to this array. Finally, we perform k deleteMin operations. The last element extracted from the heap is our answer. It should be clear that by changing the heap-order property, we could solve the original problem of finding the kth largest element. The correctness of the algorithm should be clear. The worst-case timing is O(N) to construct the heap, if buildHeap is used, and O(log N) for each deleteMin. Since there are k deleteMins, we obtain a total running time of O(N + k log N). If k = O(N/log N), then the running time is dominated by the buildHeap operation and is O(N). For larger values of k, the running time is O(k log N). If k = N/2 , then the running time is (N log N). Notice that if we run this program for k = N and record the values as they leave the heap, we will have essentially sorted the input file in O(N log N) time. In Chapter 7, we will refine this idea to obtain a fast sorting algorithm known as heapsort. Algorithm 6B For the second algorithm, we return to the original problem and find the kth largest ele- ment. We use the idea from algorithm 1B. At any point in time we will maintain a set S of the k largest elements. After the first k elements are read, when a new element is read it is compared with the kth largest element, which we denote by Sk. Notice that Sk is the smallest element in S. If the new element is larger, then it replaces Sk in S. S will then have a new smallest element, which may or may not be the newly added element. At the end of the input, we find the smallest element in S and return it as the answer. This is essentially the same algorithm described in Chapter 1. Here, however, we will use a heap to implement S. The first k elements are placed into the heap in total time O(k) with a call to buildHeap. The time to process each of the remaining elements is O(1), to test
6.4 Applications of Priority Queues 259 if the element goes into S, plus O(log k), to delete Sk and insert the new element if this is necessary. Thus, the total time is O(k + (N − k) log k) = O(N log k). This algorithm also gives a bound of (N log N) for finding the median. In Chapter 7, we will see how to solve this problem in O(N) average time. In Chapter 10, we will see an elegant, albeit impractical, algorithm to solve this problem in O(N) worst-case time. 6.4.2 Event Simulation In Section 3.7.3, we described an important queuing problem. Recall that we have a sys- tem, such as a bank, where customers arrive and wait in a line until one of k tellers is available. Customer arrival is governed by a probability distribution function, as is the ser- vice time (the amount of time to be served once a teller is available). We are interested in statistics such as how long on average a customer has to wait or how long the line might be. With certain probability distributions and values of k, these answers can be computed exactly. However, as k gets larger, the analysis becomes considerably more difficult, so it is appealing to use a computer to simulate the operation of the bank. In this way, the bank officers can determine how many tellers are needed to ensure reasonably smooth service. A simulation consists of processing events. The two events here are (a) a customer arriving and (b) a customer departing, thus freeing up a teller. We can use the probability functions to generate an input stream consisting of ordered pairs of arrival time and service time for each customer, sorted by arrival time. We do not need to use the exact time of day. Rather, we can use a quantum unit, which we will refer to as a tick. One way to do this simulation is to start a simulation clock at zero ticks. We then advance the clock one tick at a time, checking to see if there is an event. If there is, then we process the event(s) and compile statistics. When there are no customers left in the input stream and all the tellers are free, then the simulation is over. The problem with this simulation strategy is that its running time does not depend on the number of customers or events (there are two events per customer), but instead depends on the number of ticks, which is not really part of the input. To see why this is important, suppose we changed the clock units to milliticks and multiplied all the times in the input by 1,000. The result would be that the simulation would take 1,000 times longer! The key to avoiding this problem is to advance the clock to the next event time at each stage. This is conceptually easy to do. At any point, the next event that can occur is either (a) the next customer in the input file arrives or (b) one of the customers at a teller leaves. Since all the times when the events will happen are available, we just need to find the event that happens nearest in the future and process that event. If the event is a departure, processing includes gathering statistics for the departing customer and checking the line (queue) to see whether there is another customer waiting. If so, we add that customer, process whatever statistics are required, compute the time when that customer will leave, and add that departure to the set of events waiting to happen.
260 Chapter 6 Priority Queues (Heaps) If the event is an arrival, we check for an available teller. If there is none, we place the arrival on the line (queue); otherwise we give the customer a teller, compute the customer’s departure time, and add the departure to the set of events waiting to happen. The waiting line for customers can be implemented as a queue. Since we need to find the event nearest in the future, it is appropriate that the set of departures waiting to happen be organized in a priority queue. The next event is thus the next arrival or next departure (whichever is sooner); both are easily available. It is then straightforward, although possibly time-consuming, to write the simulation routines. If there are C customers (and thus 2C events) and k tellers, then the running time of the simulation would be O(C log(k + 1)) because computing and processing each event takes O(log H), where H = k + 1 is the size of the heap.3 6.5 d-Heaps Binary heaps are so simple that they are almost always used when priority queues are needed. A simple generalization is a d-heap, which is exactly like a binary heap except that all nodes have d children (thus, a binary heap is a 2-heap). Figure 6.19 shows a 3-heap. Notice that a d-heap is much shallower than a binary heap, improving the running time of inserts to O(logd N). However, for large d, the deleteMin operation is more expensive, because even though the tree is shallower, the minimum of d children must be found, which takes d − 1 comparisons using a standard algorithm. This raises the time for this operation to O(d logd N). If d is a constant, both running times are, of course, O(log N). Although an array can still be used, the multiplications and divisions to find children and parents are now by d, which, unless d is a power of 2, seriously increases the running time, because we can no longer implement division by a bit shift. d-heaps are interesting in theory, because there are many algorithms where the number of insertions is much greater than the number of deleteMins (and thus a theoretical speedup is possible). They are also of interest when the priority queue is too large to fit entirely in main memory. 1 23 5 8 17 9 4 7 10 13 15 6 11 9 Figure 6.19 A d-heap (d = 3) 3 We use O(C log(k + 1)) instead of O(C log k) to avoid confusion for the k = 1 case.
6.6 Leftist Heaps 261 In this case, a d-heap can be advantageous in much the same way as B-trees. Finally, there is evidence suggesting that 4-heaps may outperform binary heaps in practice. The most glaring weakness of the heap implementation, aside from the inability to per- form finds, is that combining two heaps into one is a hard operation. This extra operation is known as a merge. There are quite a few ways to implement heaps so that the running time of a merge is O(log N). We will now discuss three data structures, of various complex- ity, that support the merge operation efficiently. We will defer any complicated analysis until Chapter 11. 6.6 Leftist Heaps It seems difficult to design a data structure that efficiently supports merging (that is, pro- cesses a merge in o(N) time) and uses only an array, as in a binary heap. The reason for this is that merging would seem to require copying one array into another, which would take (N) time for equal-sized heaps. For this reason, all the advanced data structures that support efficient merging require the use of a linked data structure. In practice, we can expect that this will make all the other operations slower. Like a binary heap, a leftist heap has both a structural property and an ordering prop- erty. Indeed, a leftist heap, like virtually all heaps used, has the same heap-order property we have already seen. Furthermore, a leftist heap is also a binary tree. The only difference between a leftist heap and a binary heap is that leftist heaps are not perfectly balanced, but actually attempt to be very unbalanced. 6.6.1 Leftist Heap Property We define the null path length, npl(X), of any node X to be the length of the shortest path from X to a node without two children. Thus, the npl of a node with zero or one child is 0, while npl(nullptr) = −1. In the tree in Figure 6.20, the null path lengths are indicated inside the tree nodes. Notice that the null path length of any node is 1 more than the minimum of the null path lengths of its children. This applies to nodes with less than two children because the null path length of nullptr is −1. The leftist heap property is that for every node X in the heap, the null path length of the left child is at least as large as that of the right child. This property is satisfied by only one of the trees in Figure 6.20, namely, the tree on the left. This property actually goes out of its way to ensure that the tree is unbalanced, because it clearly biases the tree to get deep toward the left. Indeed, a tree consisting of a long path of left nodes is possible (and actually preferable to facilitate merging)—hence the name leftist heap. Because leftist heaps tend to have deep left paths, it follows that the right path ought to be short. Indeed, the right path down a leftist heap is as short as any in the heap. Otherwise, there would be a path that goes through some node X and takes the left child. Then X would violate the leftist property. Theorem 6.2 A leftist tree with r nodes on the right path must have at least 2r − 1 nodes.
262 Chapter 6 Priority Queues (Heaps) 1 1 10 1* 0 00 01 00 0 Figure 6.20 Null path lengths for two trees; only the left tree is leftist Proof The proof is by induction. If r = 1, there must be at least one tree node. Otherwise, suppose that the theorem is true for 1, 2, . . . , r. Consider a leftist tree with r + 1 nodes on the right path. Then the root has a right subtree with r nodes on the right path, and a left subtree with at least r nodes on the right path (otherwise it would not be leftist). Applying the inductive hypothesis to these subtrees yields a minimum of 2r − 1 nodes in each subtree. This plus the root gives at least 2r+1 − 1 nodes in the tree, proving the theorem. From this theorem, it follows immediately that a leftist tree of N nodes has a right path containing at most log(N + 1) nodes. The general idea for the leftist heap operations is to perform all the work on the right path, which is guaranteed to be short. The only tricky part is that performing inserts and merges on the right path could destroy the leftist heap property. It turns out to be extremely easy to restore the property. 6.6.2 Leftist Heap Operations The fundamental operation on leftist heaps is merging. Notice that insertion is merely a special case of merging, since we may view an insertion as a merge of a one-node heap with a larger heap. We will first give a simple recursive solution and then show how this might be done nonrecursively. Our input is the two leftist heaps, H1 and H2, in Figure 6.21. You should check that these heaps really are leftist. Notice that the smallest elements are at the roots. In addition to space for the data and left and right pointers, each node will have an entry that indicates the null path length. If either of the two heaps is empty, then we can return the other heap. Otherwise, to merge the two heaps, we compare their roots. First, we recursively merge the heap with the larger root with the right subheap of the heap with the smaller root. In our example, this means we recursively merge H2 with the subheap of H1 rooted at 8, obtaining the heap in Figure 6.22. Since this tree is formed recursively, and we have not yet finished the description of the algorithm, we cannot at this point show how this heap was obtained. However, it is
6.6 Leftist Heaps 263 36 10 8 12 7 21 14 17 18 24 37 18 23 26 H 1 33 H2 Figure 6.21 Two leftist heaps H1 and H2 6 12 7 18 24 8 37 17 18 33 26 Figure 6.22 Result of merging H2 with H1’s right subheap reasonable to assume that the resulting tree is a leftist heap, because it was obtained via a recursive step. This is much like the inductive hypothesis in a proof by induction. Since we can handle the base case (which occurs when one tree is empty), we can assume that the recursive step works as long as we can finish the merge; this is rule 3 of recursion, which we discussed in Chapter 1. We now make this new heap the right child of the root of H1 (see Fig. 6.23). Although the resulting heap satisfies the heap-order property, it is not leftist because the left subtree of the root has a null path length of 1 whereas the right subtree has a null path length of 2. Thus, the leftist property is violated at the root. However, it is easy to see that the remainder of the tree must be leftist. The right subtree of the root is leftist because of the recursive step. The left subtree of the root has not been changed, so it too must still be leftist. Thus, we need only to fix the root. We can make the entire tree leftist by merely swapping the root’s left and right children (Fig. 6.24) and updating the null path length— the new null path length is 1 plus the null path length of the new right child—completing
264 Chapter 6 Priority Queues (Heaps) 3 10 6 21 14 12 7 23 18 24 8 37 33 17 18 26 Figure 6.23 Result of attaching leftist heap of previous figure as H1’s right child the merge. Notice that if the null path length is not updated, then all null path lengths will be 0, and the heap will not be leftist but merely random. In this case, the algorithm will work, but the time bound we will claim will no longer be valid. The description of the algorithm translates directly into code. The node class (Fig. 6.25) is the same as the binary tree, except that it is augmented with the npl (null path length) data member. The leftist heap stores a pointer to the root as its data member. We have seen in Chapter 4 that when an element is inserted into an empty binary tree, 3 6 10 21 14 12 7 23 18 24 8 37 33 17 18 26 Figure 6.24 Result of swapping children of H1’s root
1 template <typename Comparable> 2 class LeftistHeap 3{ 4 public: 5 LeftistHeap( ); 6 LeftistHeap( const LeftistHeap & rhs ); 7 LeftistHeap( LeftistHeap && rhs ); 8 9 ~LeftistHeap( ); 10 11 LeftistHeap & operator=( const LeftistHeap & rhs ); 12 LeftistHeap & operator=( LeftistHeap && rhs ); 13 14 bool isEmpty( ) const; 15 const Comparable & findMin( ) const; 16 17 void insert( const Comparable & x ); 18 void insert( Comparable && x ); 19 void deleteMin( ); 20 void deleteMin( Comparable & minItem ); 21 void makeEmpty( ); 22 void merge( LeftistHeap & rhs ); 23 24 private: 25 struct LeftistNode 26 { 27 Comparable element; 28 LeftistNode *left; 29 LeftistNode *right; 30 int npl; 31 32 LeftistNode( const Comparable & e, LeftistNode *lt = nullptr, 33 LeftistNode *rt = nullptr, int np = 0 ) 34 : element{ e }, left{ lt }, right{ rt }, npl{ np } { } 35 36 LeftistNode( Comparable && e, LeftistNode *lt = nullptr, 37 LeftistNode *rt = nullptr, int np = 0 ) 38 : element{ std::move( e ) }, left{ lt }, right{ rt }, npl{ np } { } 39 }; 40 41 LeftistNode *root; 42 43 LeftistNode * merge( LeftistNode *h1, LeftistNode *h2 ); 44 LeftistNode * merge1( LeftistNode *h1, LeftistNode *h2 ); 45 46 void swapChildren( LeftistNode *t ); 47 void reclaimMemory( LeftistNode *t ); 48 LeftistNode * clone( LeftistNode *t ) const; 49 }; Figure 6.25 Leftist heap type declarations
266 Chapter 6 Priority Queues (Heaps) the node referenced by the root will need to change. We use the usual technique of imple- menting private recursive methods to do the merging. The class skeleton is also shown in Figure 6.25. The two merge routines (Fig. 6.26) are drivers designed to remove special cases and ensure that H1 has the smaller root. The actual merging is performed in merge1 (Fig. 6.27). The public merge method merges rhs into the controlling heap. rhs becomes empty. The alias test in the public method disallows h.merge(h). The time to perform the merge is proportional to the sum of the length of the right paths, because constant work is performed at each node visited during the recursive calls. Thus we obtain an O(log N) time bound to merge two leftist heaps. We can also perform this operation nonrecursively by essentially performing two passes. In the first pass, we create a new tree by merging the right paths of both heaps. To do this, we arrange the nodes on the right paths of H1 and H2 in sorted order, keeping their respective left children. In our example, the new right path is 3, 6, 7, 8, 18 and the resulting tree is shown in Figure 6.28. 1 /** 2 * Merge rhs into the priority queue. 3 * rhs becomes empty. rhs must be different from this. 4 */ 5 void merge( LeftistHeap & rhs ) 6{ 7 if( this == &rhs ) // Avoid aliasing problems 8 return; 9 10 root = merge( root, rhs.root ); 11 rhs.root = nullptr; 12 } 13 14 /** 15 * Internal method to merge two roots. 16 * Deals with deviant cases and calls recursive merge1. 17 */ 18 LeftistNode * merge( LeftistNode *h1, LeftistNode *h2 ) 19 { 20 if( h1 == nullptr ) 21 return h2; 22 if( h2 == nullptr ) 23 return h1; 24 if( h1->element < h2->element ) 25 return merge1( h1, h2 ); 26 else 27 return merge1( h2, h1 ); 28 } Figure 6.26 Driving routines for merging leftist heaps
6.6 Leftist Heaps 267 1 /** 2 * Internal method to merge two roots. 3 * Assumes trees are not empty, and h1’s root contains smallest item. 4 */ 5 LeftistNode * merge1( LeftistNode *h1, LeftistNode *h2 ) 6{ 7 if( h1->left == nullptr ) // Single node 8 h1->left = h2; // Other fields in h1 already accurate 9 else 10 { 11 h1->right = merge( h1->right, h2 ); 12 if( h1->left->npl < h1->right->npl ) 13 swapChildren( h1 ); 14 h1->npl = h1->right->npl + 1; 15 } 16 return h1; 17 } Figure 6.27 Actual routine to merge leftist heaps A second pass is made up the heap, and child swaps are performed at nodes that violate the leftist heap property. In Figure 6.28, there is a swap at nodes 7 and 3, and the same tree as before is obtained. The nonrecursive version is simpler to visualize but harder to code. We leave it to the reader to show that the recursive and nonrecursive procedures do the same thing. 3 10 6 21 14 12 7 23 18 24 37 8 33 17 18 26 Figure 6.28 Result of merging right paths of H1 and H2
268 Chapter 6 Priority Queues (Heaps) 1 /** 2 * Inserts x; duplicates allowed. 3 */ 4 void insert( const Comparable & x ) 5{ 6 root = merge( new LeftistNode{ x }, root ); 7} Figure 6.29 Insertion routine for leftist heaps As mentioned above, we can carry out insertions by making the item to be inserted a one-node heap and performing a merge. To perform a deleteMin, we merely destroy the root, creating two heaps, which can then be merged. Thus, the time to perform a deleteMin is O(log N). These two routines are coded in Figure 6.29 and Figure 6.30. Finally, we can build a leftist heap in O(N) time by building a binary heap (obvi- ously using a linked implementation). Although a binary heap is clearly leftist, this is not necessarily the best solution, because the heap we obtain is the worst possible leftist heap. Furthermore, traversing the tree in reverse-level order is not as easy with links. The buildHeap effect can be obtained by recursively building the left and right subtrees and then percolating the root down. The exercises contain an alternative solution. 1 /** 2 * Remove the minimum item. 3 * Throws UnderflowException if empty. 4 */ 5 void deleteMin( ) 6{ 7 if( isEmpty( ) ) 8 throw UnderflowException{ }; 9 10 LeftistNode *oldRoot = root; 11 root = merge( root->left, root->right ); 12 delete oldRoot; 13 } 14 15 /** 16 * Remove the minimum item and place it in minItem. 17 * Throws UnderflowException if empty. 18 */ 19 void deleteMin( Comparable & minItem ) 20 { 21 minItem = findMin( ); 22 deleteMin( ); 23 } Figure 6.30 deleteMin routine for leftist heaps
6.7 Skew Heaps 269 6.7 Skew Heaps A skew heap is a self-adjusting version of a leftist heap that is incredibly simple to imple- ment. The relationship of skew heaps to leftist heaps is analogous to the relation between splay trees and AVL trees. Skew heaps are binary trees with heap order, but there is no structural constraint on these trees. Unlike leftist heaps, no information is maintained about the null path length of any node. The right path of a skew heap can be arbitrar- ily long at any time, so the worst-case running time of all operations is O(N). However, as with splay trees, it can be shown (see Chapter 11) that for any M consecutive operations, the total worst-case running time is O(M log N). Thus, skew heaps have O(log N) amortized cost per operation. As with leftist heaps, the fundamental operation on skew heaps is merging. The merge routine is once again recursive, and we perform the exact same operations as before, with one exception. The difference is that for leftist heaps, we check to see whether the left and right children satisfy the leftist heap structure property and swap them if they do not. For skew heaps, the swap is unconditional; we always do it, with the one exception that the largest of all the nodes on the right paths does not have its children swapped. This one exception is what happens in the natural recursive implementation, so it is not really a special case at all. Furthermore, it is not necessary to prove the bounds, but since this node is guaranteed not to have a right child, it would be silly to perform the swap and give it one. (In our example, there are no children of this node, so we do not worry about it.) Again, suppose our input is the same two heaps as before, Figure 6.31. If we recursively merge H2 with the subheap of H1 rooted at 8, we will get the heap in Figure 6.32. Again, this is done recursively, so by the third rule of recursion (Section 1.3) we need not worry about how it was obtained. This heap happens to be leftist, but there is no guarantee that this is always the case. We make this heap the new left child of H1, and the old left child of H1 becomes the new right child (see Fig. 6.33). The entire tree is leftist, but it is easy to see that that is not always true: Inserting 15 into this new heap would destroy the leftist property. We can perform all operations nonrecursively, as with leftist heaps, by merging the right paths and swapping left and right children for every node on the right path, with 36 10 8 12 7 21 14 17 18 24 37 18 23 26 H 1 33 H2 Figure 6.31 Two skew heaps H1 and H2
270 Chapter 6 Priority Queues (Heaps) 6 7 12 8 37 18 24 18 17 33 26 Figure 6.32 Result of merging H2 with H1’s right subheap 3 6 10 21 14 7 12 23 8 37 18 24 18 17 33 26 Figure 6.33 Result of merging skew heaps H1 and H2 the exception of the last. After a few examples, it becomes clear that since all but the last node on the right path have their children swapped, the net effect is that this becomes the new left path (see the preceding example to convince yourself). This makes it very easy to merge two skew heaps visually.4 4 This is not exactly the same as the recursive implementation (but yields the same time bounds). If we only swap children for nodes on the right path that are above the point where the merging of right paths terminated due to exhaustion of one heap’s right path, we get the same result as the recursive version.
6.8 Binomial Queues 271 The implementation of skew heaps is left as a (trivial) exercise. Note that because a right path could be long, a recursive implementation could fail because of lack of stack space, even though performance would otherwise be acceptable. Skew heaps have the advantage that no extra space is required to maintain path lengths and no tests are required to determine when to swap children. It is an open problem to determine precisely the expected right path length of both leftist and skew heaps (the latter is undoubtedly more difficult). Such a comparison would make it easier to determine whether the slight loss of balance information is compensated by the lack of testing. 6.8 Binomial Queues Although both leftist and skew heaps support merging, insertion, and deleteMin all effec- tively in O(log N) time per operation, there is room for improvement because we know that binary heaps support insertion in constant average time per operation. Binomial queues support all three operations in O(log N) worst-case time per operation, but insertions take constant time on average. 6.8.1 Binomial Queue Structure Binomial queues differ from all the priority queue implementations that we have seen in that a binomial queue is not a heap-ordered tree but rather a collection of heap-ordered trees, known as a forest. Each of the heap-ordered trees is of a constrained form known as a binomial tree (the reason for the name will be obvious later). There is at most one binomial tree of every height. A binomial tree of height 0 is a one-node tree; a binomial tree, Bk, of height k is formed by attaching a binomial tree, Bk−1, to the root of another binomial tree, Bk−1. Figure 6.34 shows binomial trees B0, B1, B2, B3, and B4. From the diagram we see that a binomial tree, Bk, consists of a root with children B0, B1, . . . , Bk−1. Binomial trees of height k have exactly 2k nodes, and the number of k nodes at depth d is the binomial coefficient d . If we impose heap order on the binomial trees and allow at most one binomial tree of any height, we can represent a priority queue of any size by a collection of binomial trees. For instance, a priority queue of size 13 could be represented by the forest B3, B2, B0. We might write this representation as 1101, which not only represents 13 in binary but also represents the fact that B3, B2, and B0 are present in the representation and B1 is not. As an example, a priority queue of six elements could be represented as in Figure 6.35. 6.8.2 Binomial Queue Operations The minimum element can then be found by scanning the roots of all the trees. Since there are at most log N different trees, the minimum can be found in O(log N) time. Alternatively, we can maintain knowledge of the minimum and perform the operation in O(1) time if we remember to update the minimum when it changes during other operations. Merging two binomial queues is a conceptually easy operation, which we will describe by example. Consider the two binomial queues, H1 and H2, with six and seven elements, respectively, pictured in Figure 6.36.
B0 B1 B2 B3 B4 Figure 6.34 Binomial trees B0, B1, B2, B3, and B4 16 12 H1: 18 21 24 65 Figure 6.35 Binomial queue H1 with six elements 16 12 H1: 18 21 24 65 13 14 23 H2: 26 51 24 65 Figure 6.36 Two binomial queues H1 and H2
6.8 Binomial Queues 273 The merge is performed by essentially adding the two queues together. Let H3 be the new binomial queue. Since H1 has no binomial tree of height 0 and H2 does, we can just use the binomial tree of height 0 in H2 as part of H3. Next, we add binomial trees of height 1. Since both H1 and H2 have binomial trees of height 1, we merge them by making the larger root a subtree of the smaller, creating a binomial tree of height 2, shown in Figure 6.37. Thus, H3 will not have a binomial tree of height 1. There are now three binomial trees of height 2, namely, the original trees of H1 and H2 plus the tree formed by the previous step. We keep one binomial tree of height 2 in H3 and merge the other two, creating a binomial tree of height 3. Since H1 and H2 have no trees of height 3, this tree becomes part of H3 and we are finished. The resulting binomial queue is shown in Figure 6.38. Since merging two binomial trees takes constant time with almost any reasonable implementation, and there are O(log N) binomial trees, the merge takes O(log N) time in the worst case. To make this operation efficient, we need to keep the trees in the binomial queue sorted by height, which is certainly a simple thing to do. Insertion is just a special case of merging, since we merely create a one-node tree and perform a merge. The worst-case time of this operation is likewise O(log N). More precisely, if the priority queue into which the element is being inserted has the property that the smallest nonexistent binomial tree is Bi, the running time is proportional to i + 1. For example, H3 (Fig. 6.38) is missing a binomial tree of height 1, so the insertion will terminate in two steps. Since each tree in a binomial queue is present with probability 1 , it follows that we expect an insertion to terminate in two steps, so the average time 2 is constant. Furthermore, an analysis will show that performing N inserts on an initially empty binomial queue will take O(N) worst-case time. Indeed, it is possible to do this operation using only N − 1 comparisons; we leave this as an exercise. As an example, we show in Figures 6.39 through 6.45 the binomial queues that are formed by inserting 1 through 7 in order. Inserting 4 shows off a bad case. We merge 4 14 16 26 18 Figure 6.37 Merge of the two B1 trees in H1 and H2 13 23 12 H3: 51 24 21 65 24 14 16 65 26 18 Figure 6.38 Binomial queue H3: the result of merging H1 and H2
1 Figure 6.39 After 1 is inserted 1 2 Figure 6.40 After 2 is inserted 31 2 Figure 6.41 After 3 is inserted 1 3 2 4 Figure 6.42 After 4 is inserted 51 3 2 4 Figure 6.43 After 5 is inserted 5 1 3 6 2 4 Figure 6.44 After 6 is inserted 75 1 3 6 2 4 Figure 6.45 After 7 is inserted
6.8 Binomial Queues 275 with B0, obtaining a new tree of height 1. We then merge this tree with B1, obtaining a tree of height 2, which is the new priority queue. We count this as three steps (two tree merges plus the stopping case). The next insertion after 7 is inserted is another bad case and would require three tree merges. A deleteMin can be performed by first finding the binomial tree with the smallest root. Let this tree be Bk, and let the original priority queue be H. We remove the binomial tree Bk from the forest of trees in H, forming the new binomial queue H . We also remove the root of Bk, creating binomial trees B0, B1, . . . , Bk−1, which collectively form priority queue H . We finish the operation by merging H and H . As an example, suppose we perform a deleteMin on H3, which is shown again in Figure 6.46. The minimum root is 12, so we obtain the two priority queues H and H in Figure 6.47 and Figure 6.48. The binomial queue that results from merging H and H is the final answer and is shown in Figure 6.49. For the analysis, note first that the deleteMin operation breaks the original binomial queue into two. It takes O(log N) time to find the tree containing the minimum element and to create the queues H and H . Merging these two queues takes O(log N) time, so the entire deleteMin operation takes O(log N) time. 13 23 12 H3: 51 24 21 65 24 14 16 65 26 18 Figure 6.46 Binomial queue H3 H' : 13 23 51 24 65 Figure 6.47 Binomial queue H , containing all the binomial trees in H3 except B3 H'' : 21 24 14 16 65 26 18 Figure 6.48 Binomial queue H : B3 with 12 removed
276 Chapter 6 Priority Queues (Heaps) 23 13 24 14 51 24 21 65 26 65 16 18 Figure 6.49 Result of applying deleteMin to H3 6.8.3 Implementation of Binomial Queues The deleteMin operation requires the ability to find all the subtrees of the root quickly, so the standard representation of general trees is required: The children of each node are kept in a linked list, and each node has a pointer to its first child (if any). This operation also requires that the children be ordered by the size of their subtrees. We also need to make sure that it is easy to merge two trees. When two trees are merged, one of the trees is added as a child to the other. Since this new tree will be the largest subtree, it makes sense to maintain the subtrees in decreasing sizes. Only then will we be able to merge two binomial trees, and thus two binomial queues, efficiently. The binomial queue will be an array of binomial trees. To summarize, then, each node in a binomial tree will contain the data, first child, and right sibling. The children in a binomial tree are arranged in decreasing rank. Figure 6.51 shows how the binomial queue in Figure 6.50 is represented. Figure 6.52 shows the type declarations for a node in the binomial tree and the binomial queue class interface. 13 23 12 H3: 51 24 21 65 24 14 16 65 26 18 Figure 6.50 Binomial queue H3 drawn as a forest 12 23 13 51 14 24 21 24 16 26 65 65 18 Figure 6.51 Representation of binomial queue H3
1 template <typename Comparable> 2 class BinomialQueue 3{ 4 public: 5 BinomialQueue( ); 6 BinomialQueue( const Comparable & item ); 7 BinomialQueue( const BinomialQueue & rhs ); 8 BinomialQueue( BinomialQueue && rhs ); 9 10 ~BinomialQueue( ); 11 12 BinomialQueue & operator=( const BinomialQueue & rhs ); 13 BinomialQueue & operator=( BinomialQueue && rhs ); 14 15 bool isEmpty( ) const; 16 const Comparable & findMin( ) const; 17 18 void insert( const Comparable & x ); 19 void insert( Comparable && x ); 20 void deleteMin( ); 21 void deleteMin( Comparable & minItem ); 22 23 void makeEmpty( ); 24 void merge( BinomialQueue & rhs ); 25 26 private: 27 struct BinomialNode 28 { 29 Comparable element; 30 BinomialNode *leftChild; 31 BinomialNode *nextSibling; 32 33 BinomialNode( const Comparable & e, BinomialNode *lt, BinomialNode *rt ) 34 : element{ e }, leftChild{ lt }, nextSibling{ rt } { } 35 36 BinomialNode( Comparable && e, BinomialNode *lt, BinomialNode *rt ) 37 : element{ std::move( e ) }, leftChild{ lt }, nextSibling{ rt } { } 38 }; 39 40 const static int DEFAULT_TREES = 1; 41 42 vector<BinomialNode *> theTrees; // An array of tree roots 43 int currentSize; // Number of items in the priority queue 44 45 int findMinIndex( ) const; 46 int capacity( ) const; 47 BinomialNode * combineTrees( BinomialNode *t1, BinomialNode *t2 ); 48 void makeEmpty( BinomialNode * & t ); 49 BinomialNode * clone( BinomialNode * t ) const; 50 }; Figure 6.52 Binomial queue class interface and node definition
278 Chapter 6 Priority Queues (Heaps) 12 12 21 24 21 14 14 24 65 16 26 26 65 16 18 18 Figure 6.53 Merging two binomial trees In order to merge two binomial queues, we need a routine to merge two binomial trees of the same size. Figure 6.53 shows how the links change when two binomial trees are merged. The code to do this is simple and is shown in Figure 6.54. We provide a simple implementation of the merge routine. H1 is represented by the current object and H2 is represented by rhs. The routine combines H1 and H2, placing the result in H1 and making H2 empty. At any point we are dealing with trees of rank i. t1 and t2 are the trees in H1 and H2, respectively, and carry is the tree carried from a previous step (it might be nullptr). Depending on each of the eight possible cases, the tree that results for rank i and the carry tree of rank i + 1 is formed. This process proceeds from rank 0 to the last rank in the resulting binomial queue. The code is shown in Figure 6.55. Improvements to the code are suggested in Exercise 6.35. The deleteMin routine for binomial queues is given in Figure 6.56 (on pages 280–281). We can extend binomial queues to support some of the nonstandard operations that binary heaps allow, such as decreaseKey and remove, when the position of the affected element is known. A decreaseKey is a percolateUp, which can be performed in O(log N) time if we add a data member to each node that stores a parent link. An arbitrary remove can be performed by a combination of decreaseKey and deleteMin in O(log N) time. 1 /** 2 * Return the result of merging equal-sized t1 and t2. 3 */ 4 BinomialNode * combineTrees( BinomialNode *t1, BinomialNode *t2 ) 5{ 6 if( t2->element < t1->element ) 7 return combineTrees( t2, t1 ); 8 t2->nextSibling = t1->leftChild; 9 t1->leftChild = t2; 10 return t1; 11 } Figure 6.54 Routine to merge two equal-sized binomial trees
1 /** 2 * Merge rhs into the priority queue. 3 * rhs becomes empty. rhs must be different from this. 4 * Exercise 6.35 needed to make this operation more efficient. 5 */ 6 void merge( BinomialQueue & rhs ) 7{ 8 if( this == &rhs ) // Avoid aliasing problems 9 return; 10 11 currentSize += rhs.currentSize; 12 13 if( currentSize > capacity( ) ) 14 { 15 int oldNumTrees = theTrees.size( ); 16 int newNumTrees = max( theTrees.size( ), rhs.theTrees.size( ) ) + 1; 17 theTrees.resize( newNumTrees ); 18 for( int i = oldNumTrees; i < newNumTrees; ++i ) 19 theTrees[ i ] = nullptr; 20 } 21 22 BinomialNode *carry = nullptr; 23 for( int i = 0, j = 1; j <= currentSize; ++i, j *= 2 ) 24 { 25 BinomialNode *t1 = theTrees[ i ]; 26 BinomialNode *t2 = i < rhs.theTrees.size( ) ? rhs.theTrees[ i ] 27 : nullptr; 28 int whichCase = t1 == nullptr ? 0 : 1; 29 whichCase += t2 == nullptr ? 0 : 2; 30 whichCase += carry == nullptr ? 0 : 4; 31 32 switch( whichCase ) 33 { 34 case 0: /* No trees */ 35 case 1: /* Only this */ 36 break; 37 case 2: /* Only rhs */ 38 theTrees[ i ] = t2; 39 rhs.theTrees[ i ] = nullptr; 40 break; 41 case 4: /* Only carry */ 42 theTrees[ i ] = carry; 43 carry = nullptr; 44 break; Figure 6.55 Routine to merge two priority queues
45 case 3: /* this and rhs */ 46 carry = combineTrees( t1, t2 ); 47 theTrees[ i ] = rhs.theTrees[ i ] = nullptr; 48 break; 49 case 5: /* this and carry */ 50 carry = combineTrees( t1, carry ); 51 theTrees[ i ] = nullptr; 52 break; 53 case 6: /* rhs and carry */ 54 carry = combineTrees( t2, carry ); 55 rhs.theTrees[ i ] = nullptr; 56 break; 57 case 7: /* All three */ 58 theTrees[ i ] = carry; 59 carry = combineTrees( t1, t2 ); 60 rhs.theTrees[ i ] = nullptr; 61 break; 62 } 63 } 64 65 for( auto & root : rhs.theTrees ) 66 root = nullptr; 67 rhs.currentSize = 0; 68 } Figure 6.55 (continued) 1 /** 2 * Remove the minimum item and place it in minItem. 3 * Throws UnderflowException if empty. 4 */ 5 void deleteMin( Comparable & minItem ) 6{ 7 if( isEmpty( ) ) 8 throw UnderflowException{ }; 9 10 int minIndex = findMinIndex( ); 11 minItem = theTrees[ minIndex ]->element; 12 Figure 6.56 deleteMin for binomial queues
13 BinomialNode *oldRoot = theTrees[ minIndex ]; 14 BinomialNode *deletedTree = oldRoot->leftChild; 15 delete oldRoot; 16 17 // Construct H’’ 18 BinomialQueue deletedQueue; 19 deletedQueue.theTrees.resize( minIndex + 1 ); 20 deletedQueue.currentSize = ( 1 << minIndex ) - 1; 21 for( int j = minIndex - 1; j >= 0; --j ) 22 { 23 deletedQueue.theTrees[ j ] = deletedTree; 24 deletedTree = deletedTree->nextSibling; 25 deletedQueue.theTrees[ j ]->nextSibling = nullptr; 26 } 27 28 // Construct H’ 29 theTrees[ minIndex ] = nullptr; 30 currentSize -= deletedQueue.currentSize + 1; 31 32 merge( deletedQueue ); 33 } 34 35 /** 36 * Find index of tree containing the smallest item in the priority queue. 37 * The priority queue must not be empty. 38 * Return the index of tree containing the smallest item. 39 */ 40 int findMinIndex( ) const 41 { 42 int i; 43 int minIndex; 44 45 for( i = 0; theTrees[ i ] == nullptr; ++i ) 46 ; 47 48 for( minIndex = i; i < theTrees.size( ); ++i ) 49 if( theTrees[ i ] != nullptr && 50 theTrees[ i ]->element < theTrees[ minIndex ]->element ) 51 minIndex = i; 52 53 return minIndex; 54 } Figure 6.56 (continued)
Search
Read the Text Version
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
- 444
- 445
- 446
- 447
- 448
- 449
- 450
- 451
- 452
- 453
- 454
- 455
- 456
- 457
- 458
- 459
- 460
- 461
- 462
- 463
- 464
- 465
- 466
- 467
- 468
- 469
- 470
- 471
- 472
- 473
- 474
- 475
- 476
- 477
- 478
- 479
- 480
- 481
- 482
- 483
- 484
- 485
- 486
- 487
- 488
- 489
- 490
- 491
- 492
- 493
- 494
- 495
- 496
- 497
- 498
- 499
- 500
- 501
- 502
- 503
- 504
- 505
- 506
- 507
- 508
- 509
- 510
- 511
- 512
- 513
- 514
- 515
- 516
- 517
- 518
- 519
- 520
- 521
- 522
- 523
- 524
- 525
- 526
- 527
- 528
- 529
- 530
- 531
- 532
- 533
- 534
- 535
- 536
- 537
- 538
- 539
- 540
- 541
- 542
- 543
- 544
- 545
- 546
- 547
- 548
- 549
- 550
- 551
- 552
- 553
- 554
- 555
- 556
- 557
- 558
- 559
- 560
- 561
- 562
- 563
- 564
- 565
- 566
- 567
- 568
- 569
- 570
- 571
- 572
- 573
- 574
- 575
- 576
- 577
- 578
- 579
- 580
- 581
- 582
- 583
- 584
- 585
- 586
- 587
- 588
- 589
- 590
- 591
- 592
- 593
- 594
- 595
- 596
- 597
- 598
- 599
- 600
- 601
- 602
- 603
- 604
- 605
- 606
- 607
- 608
- 609
- 610
- 611
- 612
- 613
- 614
- 615
- 616
- 617
- 618
- 619
- 620
- 621
- 622
- 623
- 624
- 625
- 626
- 627
- 628
- 629
- 630
- 631
- 632
- 633
- 634
- 635
- 636
- 637
- 638
- 639
- 640
- 641
- 642
- 643
- 644
- 645
- 646
- 647
- 648
- 649
- 650
- 651
- 652
- 653
- 654
- 1 - 50
- 51 - 100
- 101 - 150
- 151 - 200
- 201 - 250
- 251 - 300
- 301 - 350
- 351 - 400
- 401 - 450
- 451 - 500
- 501 - 550
- 551 - 600
- 601 - 650
- 651 - 654
Pages: