Data Structures
and
Algorithm Analysis in C
Second Edition
Solutions Manual
Mark Allen Weiss
Florida International University
Preface
Included in this manual are answers to most of the exercises in the textbook Data Structures and Algorithm Analysis in C, second edition, published by Addison-Wesley. These answers reflect the state of the book in the first printing.
Specifically omitted are likely programming assignments and any question whose solu- tion is pointed to by a reference at the end of the chapter. Solutions vary in degree of complete- ness; generally, minor details are left to the reader. For clarity, programs are meant to be pseudo-C rather than completely perfect code.
Errors can be reported to [email protected]. Thanks to Grigori Schwarz and Brian Harvey for pointing out errors in previous incarnations of this manual.
Table of Contents
1. Chapter 1: Introduction ... 1
2. Chapter 2: Algorithm Analysis ... 4
3. Chapter 3: Lists, Stacks, and Queues ... 7
4. Chapter 4: Trees ... 14
5. Chapter 5: Hashing ... 25
6. Chapter 6: Priority Queues (Heaps) ... 29
7. Chapter 7: Sorting ... 36
8. Chapter 8: The Disjoint Set ADT ... 42
9. Chapter 9: Graph Algorithms ... 45
10. Chapter 10: Algorithm Design Techniques ... 54
11. Chapter 11: Amortized Analysis ... 63
12. Chapter 12: Advanced Data Structures and Implementation ... 66
Chapter 1: Introduction
1.3 Because of round-off errors, it is customary to specify the number of decimal places that should be included in the output and round up accordingly. Otherwise, numbers come out looking strange. We assume error checks have already been performed; the routine SeparateO is left to the reader. Code is shown in Fig. 1.1.
1.4 The general way to do this is to write a procedure with heading void ProcessFile( const char *FileName );
which opens FileName,O does whatever processing is needed, and then closes it. If a line of the form
#include SomeFile is detected, then the call
ProcessFile( SomeFile );
is made recursively. Self-referential includes can be detected by keeping a list of files for which a call to ProcessFileO has not yet terminated, and checking this list before making a new call to ProcessFile.O
1.5 (a) The proof is by induction. The theorem is clearly true for 0 < XO ≤ 1, since it is true for XO = 1, and for XO < 1, log XO is negative. It is also easy to see that the theorem holds for 1 < XO ≤ 2, since it is true for XO = 2, and for XO < 2, log XO is at most 1. Suppose the theorem is true for pO < XO ≤ 2pO (where pO is a positive integer), and consider any 2pO < YO ≤ 4pO (pO ≥ 1). Then log YO = 1 + log (YO / 2) < 1 + YO / 2 < YO / 2 + YO / 2 ≤ YO, where the first ine- quality follows by the inductive hypothesis.
(b) Let 2XO = AO. Then AOBO = (2XO)BO = 2XBO. Thus log AOBO = XBO. Since XO = log AO, the theorem is proved.
1.6 (a) The sum is 4/3 and follows directly from the formula.
(b) SO = 4 __1 +
42 ___2 +
43
___3 + . . . . 4SO = 1+
4 __2 +
42
___3 + . . . . Subtracting the first equation from the second gives 3SO = 1 +
4 __1 +
42
___2 + . . . . By part (a), 3SO = 4/ 3 so SO = 4/ 9.
(c) SO = 4 __1 +
42 ___4 +
43
___9 + . . . . 4SO = 1 + 4 __4 +
42 ___9 +
43
___16 + . . . . Subtracting the first equa- tion from the second gives 3SO = 1+
4 __3 +
42 ___5 +
43
___7 + . . . . Rewriting, we get 3SO = 2
iO=0
Σ
∞ ___4iiO +iO=0
Σ
∞ ___41iO. Thus 3SO = 2(4/ 9) + 4/ 3 = 20/ 9. Thus SO = 20/ 27.(d) Let SNO =
Σ
∞___i4ONiOO. Follow the same method as in parts (a) - (c) to obtain a formula for SNO_______________________________________________________________________________
_______________________________________________________________________________
double RoundUp( double N, int DecPlaces ) {
int i;
double AmountToAdd = 0.5;
for( i = 0; i < DecPlaces; i++ ) AmountToAdd /= 10;
return N + AmountToAdd;
}
void PrintFractionPart( double FractionPart, int DecPlaces ) {
int i, Adigit;
for( i = 0; i < DecPlaces; i++ ) {
FractionPart *= 10;
ADigit = IntPart( FractionPart );
PrintDigit( Adigit );
FractionPart = DecPart( FractionPart );
} }
void PrintReal( double N, int DecPlaces ) {
int IntegerPart;
double FractionPart;
if( N < 0 ) {
putchar(’-’);
N = -N;
}
N = RoundUp( N, DecPlaces );
IntegerPart = IntPart( N ); FractionPart = DecPart( N );
PrintOut( IntegerPart ); /* Using routine in text */
if( DecPlaces > 0 ) putchar(’.’);
PrintFractionPart( FractionPart, DecPlaces );
}
Fig. 1.1.
_______________________________________________________________________________
_______________________________________________________________________________
1.7
iO=OINO/ 2OK
Σ
N __1i =iO=1
Σ
N __1i −iO=1
Σ
OINO/ 2 − 1OK
i
__1 ∼∼ ln NO − ln NO/ 2 ∼∼ ln 2.
1.8 24 = 16 ≡ 1 (modO 5). (24)25 ≡ 125 (modO 5). Thus 2100 ≡ 1 (modO 5).
1.9 (a) Proof is by induction. The statement is clearly true for NO = 1 and NO = 2. Assume true for NO = 1, 2, ..., kO. Then
i
Σ
O=1 kO+1FiO =
i
Σ
O=1k FiO+FkO+1. By the induction hypothesis, the value of the sum on the right is FkO+2 − 2 + FkO+1 = FkO+3 − 2, where the latter equality follows from the definition of the Fibonacci numbers. This proves the claim for NO = kO + 1, and hence for all NO.(b) As in the text, the proof is by induction. Observe that φ + 1 = φ2. This implies that φ−1 + φ−2 = 1. For NO = 1 and NO = 2, the statement is true. Assume the claim is true for NO = 1, 2, ..., kO.
FkO+1 = FkO + FkO−1
by the definition and we can use the inductive hypothesis on the right-hand side, obtaining FkO+1 < φkO + φkO−1
< φ−1φkO+1 + φ−2φkO+1 FkO+1 < (φ−1 + φ−2)φkO+1 < φkO+1 and proving the theorem.
(c) See any of the advanced math references at the end of the chapter. The derivation involves the use of generating functions.
1.10 (a)
iO=1
Σ
N(2iO−1) = 2iO=1
Σ
NiO −iO=1
Σ
N1 = NO(NO+1) − NO = NO2.(b) The easiest way to prove this is by induction. The case NO = 1 is trivial. Otherwise,
iO=1
Σ
NO+1
iO3 = (NO+1)3 +
iO=1
Σ
NiO3= (NO+1)3 + 4 NO2(NO+1)2 _________
= (NO+1)2OH AI 4
NO2
___ + (NO+1)OJ AK
= (NO+1)2OH
AI 4
NO2 + 4NO + 4 ___________ OJ
AK
= 22 (NO+1)2(NO+2)2 _____________
= OH
AI 2
(NO+1)(NO+2) ___________ OJ
AK
2
= OH AINO+1i
Σ
O=1iOJAK2Chapter 2: Algorithm Analysis
2.1 2/NO, 37, √MMNOO, NO, NOlog log NO, NOlog NO, NOlog (NO2), NOlog2NO, NO1.5, NO2, NO2log NO, NO3, 2NO/ 2, 2NO. NOlog NO and NOlog (NO2) grow at the same rate.
2.2 (a) True.
(b) False. A counterexample is TO1(NO) = 2NO, TO2(NO) = NO, and PfOO(NO) = NO.
(c) False. A counterexample is TO1(NO) = NO2, TO2(NO) = NO, and PfOO(NO) = NO2. (d) False. The same counterexample as in part (c) applies.
2.3 We claim that NOlog NO is the slower growing function. To see this, suppose otherwise.
Then, NOε/ √MMMMMlog NOO would grow slower than log NO. Taking logs of both sides, we find that, under this assumption, ε/ √MMMMMMlog NOOlog NO grows slower than log log NO. But the first expres- sion simplifies toε√MMMMMMlog NOO. If LO = log NO, then we are claiming thatε√MMLOO grows slower than log LO, or equivalently, that ε2LO grows slower than log2 LO. But we know that log2 LO = ο (LO), so the original assumption is false, proving the claim.
2.4 Clearly, logkO1NO = ο(logkO2NO) if kO1 < kO2, so we need to worry only about positive integers.
The claim is clearly true for kO = 0 and kO = 1. Suppose it is true for kO < iO. Then, by L’Hospital’s rule,
NlimO→∞
N logiON ______ =
Nlim iO→∞
N logiO−1N _______
The second limit is zero by the inductive hypothesis, proving the claim.
2.5 Let PfOO(NO) = 1 when NO is even, and NO when NO is odd. Likewise, let gO(NO) = 1 when NO is odd, and NO when NO is even. Then the ratio PfOO(NO) / gO(NO) oscillates between 0 and∞.
2.6 For all these programs, the following analysis will agree with a simulation:
(I) The running time is OO(NO).
(II) The running time is OO(NO2).
(III) The running time is OO(NO3).
(IV) The running time is OO(NO2).
(V)PjO can be as large as iO2, which could be as large as NO2. kO can be as large as PjO, which is NO2. The running time is thus proportional to NO.NO2.NO2, which is OO(NO5).
(VI) The ifO statement is executed at most NO3 times, by previous arguments, but it is true only OO(NO2) times (because it is true exactly iO times for each iO). Thus the innermost loop is only executed OO(NO2) times. Each time through, it takes OO(PjO2) = OO(NO2) time, for a total of OO(NO4). This is an example where multiplying loop sizes can occasionally give an overesti- mate.
2.7 (a) It should be clear that all algorithms generate only legal permutations. The first two algorithms have tests to guarantee no duplicates; the third algorithm works by shuffling an array that initially has no duplicates, so none can occur. It is also clear that the first two algorithms are completely random, and that each permutation is equally likely. The third
See
J. Bentley, "Programming Pearls," Communications of the ACM 30 (1987), 754-757.
Note that if the second line of algorithm 3 is replaced with the statement Swap( A[i], A[ RandInt( 0, N-1 ) ] );
then not all permutations are equally likely. To see this, notice that for NO = 3, there are 27 equally likely ways of performing the three swaps, depending on the three random integers.
Since there are only 6 permutations, and 6 does not evenly divide 27, each permutation cannot possibly be equally represented.
(b) For the first algorithm, the time to decide if a random number to be placed in AO[iO] has not been used earlier is OO(iO). The expected number of random numbers that need to be tried is NO/ (NO − iO). This is obtained as follows: iO of the NO numbers would be duplicates.
Thus the probability of success is (NO − iO) / NO. Thus the expected number of independent trials is NO/ (NO − iO). The time bound is thus
i
Σ
O=0 NO−1NO−i ____Ni <
i
Σ
O=0 NO−1NO−i NO2 ____ < NO2
i
Σ
O=0 NO−1NO−i ____1 < NO2
PjO=1
Σ
N __1Pj = OO(NO2log NO)The second algorithm saves a factor of iO for each random number, and thus reduces the time bound to OO(NOlog NO) on average. The third algorithm is clearly linear.
(c, d) The running times should agree with the preceding analysis if the machine has enough memory. If not, the third algorithm will not seem linear because of a drastic increase for large NO.
(e) The worst-case running time of algorithms I and II cannot be bounded because there is always a finite probability that the program will not terminate by some given time TO. The algorithm does, however, terminate with probability 1. The worst-case running time of the third algorithm is linear - its running time does not depend on the sequence of random numbers.
2.8 Algorithm 1 would take about 5 days for NO = 10,000, 14.2 years for NO = 100,000 and 140 centuries for NO = 1,000,000. Algorithm 2 would take about 3 hours for NO = 100,000 and about 2 weeks for NO = 1,000,000. Algorithm 3 would use 1 ⁄12 minutes for NO = 1,000,000.
These calculations assume a machine with enough memory to hold the array. Algorithm 4 solves a problem of size 1,000,000 in 3 seconds.
2.9 (a) OO(NO2).
(b) OO(NOlog NO).
2.10 (c) The algorithm is linear.
2.11 Use a variation of binary search to get an OO(log NO) solution (assuming the array is preread).
2.13 (a) Test to see if NO is an odd number (or 2) and is not divisible by 3, 5, 7, ..., √MMNOO.
(b) OO(√MMNOO), assuming that all divisions count for one unit of time.
(c) BO = OO(log NO).
(d) OO(2BO/ 2).
(e) If a 20-bit number can be tested in time TO, then a 40-bit number would require about TO2 time.
(f) BO is the better measure because it more accurately represents the sizeO of the input.
2.14 The running time is proportional to NO times the sum of the reciprocals of the primes less than NO. This is OO(NOlog log NO). See Knuth, Volume 2, page 394.
2.15 Compute XO2, XO4, XO8, XO10, XO20, XO40, XO60, and XO62.
2.16 Maintain an array PowersOfXO that can be filled in a for loop. The array will contain XO, XO2, XO4, up to XO2OIlog NOK. The binary representation of NO (which can be obtained by testing even or odd and then dividing by 2, until all bits are examined) can be used to multiply the appropriate entries of the array.
2.17 For NO = 0 or NO = 1, the number of multiplies is zero. If bO(NO) is the number of ones in the binary representation of NO, then if NO > 1, the number of multiplies used is
OIlog NOK + bO(NO) − 1 2.18 (a) AO.
(b) BO.
(c) The information given is not sufficient to determine an answer. We have only worst- case bounds.
(d) Yes.
2.19 (a) Recursion is unnecessary if there are two or fewer elements.
(b) One way to do this is to note that if the first NO−1 elements have a majority, then the last element cannot change this. Otherwise, the last element could be a majority. Thus if NO is odd, ignore the last element. Run the algorithm as before. If no majority element emerges, then return the NOthOelement as a candidate.
(c) The running time is OO(NO), and satisfies TO(NO) = TO(NO/ 2) + OO(NO).
(d) One copy of the original needs to be saved. After this, the BO array, and indeed the recur- sion can be avoided by placing each BiO in the AO array. The difference is that the original recursive strategy implies that OO(log NO) arrays are used; this guarantees only two copies.
2.20 Otherwise, we could perform operations in parallel by cleverly encoding several integers into one. For instance, if A = 001, B = 101, C = 111, D = 100, we could add A and B at the same time as C and D by adding 00A00C + 00B00D. We could extend this to add NO pairs of numbers at once in unit cost.
2.22 No. If LowO = 1, HighO = 2, then MidO = 1, and the recursive call does not make progress.
2.24 No. As in Exercise 2.22, no progress is made.
Chapter 3: Lists, Stacks, and Queues
3.2 The comments for Exercise 3.4 regarding the amount of abstractness used apply here. The running time of the procedure in Fig. 3.1 is OO(LO + PO).
_______________________________________________________________________________
_______________________________________________________________________________
void
PrintLots( List L, List P ) {
int Counter;
Position Lpos, Ppos;
Lpos = First( L );
Ppos = First( P );
Counter = 1;
while( Lpos != NULL && Ppos != NULL ) {
if( Ppos->Element == Counter++ ) {
printf( "%? ", Lpos->Element );
Ppos = Next( Ppos, P );
}
Lpos = Next( Lpos, L );
} }
Fig. 3.1.
_______________________________________________________________________________
_______________________________________________________________________________
3.3 (a) For singly linked lists, the code is shown in Fig. 3.2.
_______________________________________________________________________________
_______________________________________________________________________________
/* BeforeP is the cell before the two adjacent cells that are to be swapped. */
/* Error checks are omitted for clarity. */
void
SwapWithNext( Position BeforeP, List L ) {
Position P, AfterP;
P = BeforeP->Next;
AfterP = P->Next; /* Both P and AfterP assumed not NULL. */
P->Next = AfterP->Next;
BeforeP->Next = AfterP;
AfterP->Next = P;
}
Fig. 3.2.
_______________________________________________________________________________
_______________________________________________________________________________
(b) For doubly linked lists, the code is shown in Fig. 3.3.
_______________________________________________________________________________
_______________________________________________________________________________
/* P and AfterP are cells to be switched. Error checks as before. */
void
SwapWithNext( Position P, List L ) {
Position BeforeP, AfterP;
BeforeP = P->Prev;
AfterP = P->Next;
P->Next = AfterP->Next;
BeforeP->Next = AfterP;
AfterP->Next = P;
P->Next->Prev = P;
P->Prev = AfterP;
AfterP->Prev = BeforeP;
}
Fig. 3.3.
_______________________________________________________________________________
_______________________________________________________________________________
3.4 IntersectO is shown on page 9.
_______________________________________________________________________________
_______________________________________________________________________________
/* This code can be made more abstract by using operations such as */
/* Retrieve and IsPastEnd to replace L1Pos->Element and L1Pos != NULL. */
/* We have avoided this because these operations were not rigorously defined. */
List
Intersect( List L1, List L2 ) {
List Result;
Position L1Pos, L2Pos, ResultPos;
L1Pos = First( L1 ); L2Pos = First( L2 );
Result = MakeEmpty( NULL );
ResultPos = First( Result );
while( L1Pos != NULL && L2Pos != NULL ) {
if( L1Pos->Element < L2Pos->Element ) L1Pos = Next( L1Pos, L1 );
else if( L1Pos->Element > L2Pos->Element ) L2Pos = Next( L2Pos, L2 );
else {
Insert( L1Pos->Element, Result, ResultPos );
L1 = Next( L1Pos, L1 ); L2 = Next( L2Pos, L2 );
ResultPos = Next( ResultPos, Result );
} }
return Result;
}
_______________________________________________________________________________
_______________________________________________________________________________
3.5 Fig. 3.4 contains the code for Union.O
3.7 (a) One algorithm is to keep the result in a sorted (by exponent) linked list. Each of the MNO multiplies requires a search of the linked list for duplicates. Since the size of the linked list is OO(MNO), the total running time is OO(MO2NO2).
(b) The bound can be improved by multiplying one term by the entire other polynomial, and then using the equivalent of the procedure in Exercise 3.2 to insert the entire sequence.
Then each sequence takes OO(MNO), but there are only MO of them, giving a time bound of OO(MO2NO).
(c) An OO(MNOlog MNO) solution is possible by computing all MNO pairs and then sorting by exponent using any algorithm in Chapter 7. It is then easy to merge duplicates afterward.
(d) The choice of algorithm depends on the relative values of MO and NO. If they are close, then the solution in part (c) is better. If one polynomial is very small, then the solution in part (b) is better.
_______________________________________________________________________________
_______________________________________________________________________________
List
Union( List L1, List L2 ) {
List Result;
ElementType InsertElement;
Position L1Pos, L2Pos, ResultPos;
L1Pos = First( L1 ); L2Pos = First( L2 );
Result = MakeEmpty( NULL );
ResultPos = First( Result );
while ( L1Pos != NULL && L2Pos != NULL ) { if( L1Pos->Element < L2Pos->Element ) {
InsertElement = L1Pos->Element;
L1Pos = Next( L1Pos, L1 );
}
else if( L1Pos->Element > L2Pos->Element ) { InsertElement = L2Pos->Element;
L2Pos = Next( L2Pos, L2 );
} else {
InsertElement = L1Pos->Element;
L1Pos = Next( L1Pos, L1 ); L2Pos = Next( L2Pos, L2 );
}
Insert( InsertElement, Result, ResultPos );
ResultPos = Next( ResultPos, Result );
}
/* Flush out remaining list */
while( L1Pos != NULL ) {
Insert( L1Pos->Element, Result, ResultPos );
L1Pos = Next( L1Pos, L1 ); ResultPos = Next( ResultPos, Result );
}
while( L2Pos != NULL ) {
Insert( L2Pos->Element, Result, ResultPos );
L2Pos = Next( L2Pos, L2 ); ResultPos = Next( ResultPos, Result );
}
return Result;
}
Fig. 3.4.
_______________________________________________________________________________
_______________________________________________________________________________
3.8 One can use the PowO function in Chapter 2, adapted for polynomial multiplication. If PO is small, a standard method that uses OO(PO) multiplies instead of OO(log PO) might be better because the multiplies would involve a large number with a small number, which is good for the multiplication routine in part (b).
3.10 This is a standard programming project. The algorithm can be sped up by setting
then if M'O > NO/ 2, passing the potato appropriately in the alternative direction. This requires a doubly linked list. The worst-case running time is clearly OO(NO minO(MO, NO)), although when these heuristics are used, and MO and NO are comparable, the algorithm might be significantly faster. If MO = 1, the algorithm is clearly linear. The VAX/VMS C compiler’s memory management routines do poorly with the particular pattern of PfreeOs in this case, causing OO(NOlog NO) behavior.
3.12 Reversal of a singly linked list can be done nonrecursively by using a stack, but this requires OO(NO) extra space. The solution in Fig. 3.5 is similar to strategies employed in gar- bage collection algorithms. At the top of the whileO loop, the list from the start to Pre- viousPosO is already reversed, whereas the rest of the list, from CurrentPosO to the end, is normal. This algorithm uses only constant extra space.
_______________________________________________________________________________
_______________________________________________________________________________
/* Assuming no header and L is not empty. */
List
ReverseList( List L ) {
Position CurrentPos, NextPos, PreviousPos;
PreviousPos = NULL;
CurrentPos = L;
NextPos = L->Next;
while( NextPos != NULL ) {
CurrentPos->Next = PreviousPos;
PreviousPos = CurrentPos;
CurrentPos = NextPos;
NextPos = NextPos->Next;
}
CurrentPos->Next = PreviousPos;
return CurrentPos;
}
Fig. 3.5.
_______________________________________________________________________________
_______________________________________________________________________________
3.15 (a) The code is shown in Fig. 3.6.
(b) See Fig. 3.7.
(c) This follows from well-known statistical theorems. See Sleator and Tarjan’s paper in the Chapter 11 references.
3.16 (c) DeleteO takes OO(NO) and is in two nested for loops each of size NO, giving an obvious OO(NO3) bound. A better bound of OO(NO2) is obtained by noting that only NO elements can be deleted from a list of size NO, hence OO(NO2) is spent performing deletes. The remainder of the routine is OO(NO2), so the bound follows.
(d) OO(NO2).
_______________________________________________________________________________
_______________________________________________________________________________
/* Array implementation, starting at slot 1 */
Position
Find( ElementType X, List L ) {
int i, Where;
Where = 0;
for( i = 1; i < L.SizeOfList; i++ ) if( X == L[i].Element ) {
Where = i;
break;
}
if( Where ) /* Move to front. */
{
for( i = Where; i > 1; i-- )
L[i].Element = L[i-1].Element;
L[1].Element = X;
return 1;
} else
return 0; /* Not found. */
}
Fig. 3.6.
_______________________________________________________________________________
_______________________________________________________________________________
(e) Sort the list, and make a scan to remove duplicates (which must now be adjacent).
3.17 (a) The advantages are that it is simpler to code, and there is a possible savings if deleted keys are subsequently reinserted (in the same place). The disadvantage is that it uses more space, because each cell needs an extra bit (which is typically a byte), and unused cells are not freed.
3.21 Two stacks can be implemented in an array by having one grow from the low end of the array up, and the other from the high end down.
3.22 (a) Let EO be our extended stack. We will implement EO with two stacks. One stack, which we’ll call SO, is used to keep track of the PushOand PopO operations, and the other, MO, keeps track of the minimum. To implement Push(X,E), we perform Push(X,S). If XO is smaller than or equal to the top element in stack MO, then we also perform Push(X,M). To imple- ment Pop(E), we perform Pop(S). If XO is equal to the top element in stack MO, then we also Pop(M). FindMin(E) is performed by examining the top of MO. All these operations are clearly OO(1).
(b) This result follows from a theorem in Chapter 7 that shows that sorting must take Ω(NOlog NO) time. OO(NO) operations in the repertoire, including DeleteMinO, would be
_______________________________________________________________________________
_______________________________________________________________________________
/* Assuming a header. */
Position
Find( ElementType X, List L ) {
Position PrevPos, XPos;
PrevPos = FindPrevious( X, L );
if( PrevPos->Next != NULL ) /* Found. */
{
XPos = PrevPos ->Next;
PrevPos->Next = XPos->Next;
XPos->Next = L->Next;
L->Next = XPos;
return XPos;
} else
return NULL;
}
Fig. 3.7.
_______________________________________________________________________________
_______________________________________________________________________________
3.23 Three stacks can be implemented by having one grow from the bottom up, another from the top down, and a third somewhere in the middle growing in some (arbitrary) direction. If the third stack collides with either of the other two, it needs to be moved. A reasonable strategy is to move it so that its center (at the time of the move) is halfway between the tops of the other two stacks.
3.24 Stack space will not run out because only 49 calls will be stacked. However, the running time is exponential, as shown in Chapter 2, and thus the routine will not terminate in a rea- sonable amount of time.
3.25 The queue data structure consists of pointers Q->FrontO and Q->Rear,O which point to the beginning and end of a linked list. The programming details are left as an exercise because it is a likely programming assignment.
3.26 (a) This is a straightforward modification of the queue routines. It is also a likely program- ming assignment, so we do not provide a solution.
Chapter 4: Trees
4.1 (a) AO.
(b) GO, HO, IO, LO, MO, and KO.
4.2 For node BO:
(a) AO.
(b) DO and EO.
(c) CO.
(d) 1.
(e) 3.
4.3 4.
4.4 There are NO nodes. Each node has two pointers, so there are 2NO pointers. Each node but the root has one incoming pointer from its parent, which accounts for NO−1 pointers. The rest are NULL.O
4.5 Proof is by induction. The theorem is trivially true for HO = 0. Assume true for HO = 1, 2, ..., kO. A tree of height kO+1 can have two subtrees of height at most kO. These can have at most 2kO+1−1 nodes each by the induction hypothesis. These 2kO+2−2 nodes plus the root prove the theorem for height kO+1 and hence for all heights.
4.6 This can be shown by induction. Alternatively, let NO = number of nodes, FO = number of full nodes, LO = number of leaves, and HO = number of half nodes (nodes with one child).
Clearly, NO = FO + HO + LO. Further, 2FO + HO = NO − 1 (see Exercise 4.4). Subtracting yields LO − FO = 1.
4.7 This can be shown by induction. In a tree with no nodes, the sum is zero, and in a one-node tree, the root is a leaf at depth zero, so the claim is true. Suppose the theorem is true for all trees with at most kO nodes. Consider any tree with kO+1 nodes. Such a tree consists of an iO node left subtree and a kO − iO node right subtree. By the inductive hypothesis, the sum for the left subtree leaves is at most one with respect to the left tree root. Because all leaves are one deeper with respect to the original tree than with respect to the subtree, the sum is at most ⁄12 with respect to the root. Similar logic implies that the sum for leaves in the right subtree is at most ⁄12, proving the theorem. The equality is true if and only if there are no nodes with one child. If there is a node with one child, the equality cannot be true because adding the second child would increase the sum to higher than 1. If no nodes have one child, then we can find and remove two sibling leaves, creating a new tree. It is easy to see that this new tree has the same sum as the old. Applying this step repeatedly, we arrive at a single node, whose sum is 1. Thus the original tree had sum 1.
4.8 (a) - * * a b + c d e.
(b) ( ( a * b ) * ( c + d ) ) - e.
(c) a b * c d + * e -.
4.9
1 2
3 4
5 6
7 9
1 2
4
5 6
7 9
4.11 This problem is not much different from the linked list cursor implementation. We maintain an array of records consisting of an element field, and two integers, left and right. The free list can be maintained by linking through the left field. It is easy to write the CursorNewO and CursorDisposeO routines, and substitute them for malloc and free.
4.12 (a) Keep a bit array BO. If iO is in the tree, then BO[iO] is true; otherwise, it is false. Repeatedly generate random integers until an unused one is found. If there are NO elements already in the tree, then MO − NO are not, and the probability of finding one of these is (MO − NO) / MO.
Thus the expected number of trials is MO / (MO−NO) = α / (α − 1).
(b) To find an element that is in the tree, repeatedly generate random integers until an already-used integer is found. The probability of finding one is NO / MO, so the expected number of trials is MO / NO = α.
(c) The total cost for one insert and one delete isα / (α − 1) + α = 1 + α + 1 / (α − 1). Set- tingα = 2 minimizes this cost.
4.15 (a) NO(0) = 1, NO(1) = 2, NO(HO) = NO(HO−1) + NO(HO−2) + 1.
(b) The heights are one less than the Fibonacci numbers.
4.16
1
2
3
4
5
6
7
9
4.17 It is easy to verify by hand that the claim is true for 1 ≤ kO ≤ 3. Suppose it is true for kO = 1, 2, 3, ... HO. Then after the first 2HO − 1 insertions, 2HO−1is at the root, and the right subtree is a balanced tree containing 2HO−1 + 1 through 2HO − 1. Each of the next 2HO−1 insertions, namely, 2HO through 2HO + 2HO−1 − 1, insert a new maximum and get placed in the right
subtree, eventually forming a perfectly balanced right subtree of height HO−1. This follows by the induction hypothesis because the right subtree may be viewed as being formed from the successive insertion of 2HO−1 + 1 through 2HO + 2HO−1 − 1. The next insertion forces an imbalance at the root, and thus a single rotation. It is easy to check that this brings 2HO to the root and creates a perfectly balanced left subtree of height HO−1. The new key is attached to a perfectly balanced right subtree of height HO−2 as the last node in the right path. Thus the right subtree is exactly as if the nodes 2HO + 1 through 2HO + 2HO−1 were inserted in order. By the inductive hypothesis, the subsequent successive insertion of 2HO + 2HO−1 + 1 through 2HO+1 − 1 will create a perfectly balanced right subtree of height HO−1. Thus after the last insertion, both the left and the right subtrees are perfectly bal- anced, and of the same height, so the entire tree of 2HO+1 − 1 nodes is perfectly balanced (and has height HO).
4.18 The two remaining functions are mirror images of the text procedures. Just switch RightO and LeftO everywhere.
4.20 After applying the standard binary search tree deletion algorithm, nodes on the deletion path need to have their balance changed, and rotations may need to be performed. Unlike inser- tion, more than one node may need rotation.
4.21 (a) OO(log log NO).
(b) The minimum AVL tree of height 255 (a huge tree).
4.22_______________________________________________________________________________
_______________________________________________________________________________
Position
DoubleRotateWithLeft( Position K3 ) {
Position K1, K2;
K1 = K3->Left;
K2 = K1->Right;
K1->Right = K2->Left;
K3->Left = K2->Right;
K2->Left = K1;
K2->Right = K3;
K1->Height = Max( Height(K1->Left), Height(K1->Right) ) + 1;
K3->Height = Max( Height(K3->Left), Height(K3->Right) ) + 1;
K2->Height = Max( K1->Height, K3->Height ) + 1;
return K3;
}
_______________________________________________________________________________
_______________________________________________________________________________
4.23 After accessing 3,
1 2
3
4
5 6
7 8
9 10
11 12
13
After accessing 9,
1 2
3 4
5 6
7 8
9 10
11 12
13
After accessing 1,
1
2 3
4
5 6
7 8
9 10
11 12
13
After accessing 5,
1 2
3 4
5
6
7 8
9 10
11 12
13
4.24
1 2
3 4
5
7 8
9 10
11 12
13
4.25 (a) 523776.
(b) 262166, 133114, 68216, 36836, 21181, 13873.
(c) After FindO(9).
4.26 (a) An easy proof by induction.
4.28 (a-c) All these routines take linear time.
_______________________________________________________________________________
_______________________________________________________________________________
/* These functions use the type BinaryTree, which is the same */
/* as TreeNode *, in Fig 4.16. */
int
CountNodes( BinaryTree T ) {
if( T == NULL ) return 0;
return 1 + CountNodes(T->Left) + CountNodes(T->Right);
} int
CountLeaves( BinaryTree T ) {
if( T == NULL ) return 0;
else if( T->Left == NULL && T->Right == NULL ) return 1;
return CountLeaves(T->Left) + CountLeaves(T->Right);
}
_______________________________________________________________________________
_______________________________________________________________________________
_______________________________________________________________________________
_______________________________________________________________________________
/* An alternative method is to use the results of Exercise 4.6. */
int
CountFull( BinaryTree T ) {
if( T == NULL ) return 0;
return ( T->Left != NULL && T->Right != NULL ) + CountFull(T->Left) + CountFull(T->Right);
}
_______________________________________________________________________________
_______________________________________________________________________________
4.29 We assume the existence of a function RandInt(Lower,Upper),O which generates a uniform random integer in the appropriate closed interval. MakeRandomTreeO returns NULL if NO is not positive, or if NO is so large that memory is exhausted.
_______________________________________________________________________________
_______________________________________________________________________________
SearchTree
MakeRandomTree1( int Lower, int Upper ) {
SearchTree T;
int RandomValue;
T = NULL;
if( Lower <= Upper ) {
T = malloc( sizeof( struct TreeNode ) );
if( T != NULL ) {
T->Element = RandomValue = RandInt( Lower, Upper );
T->Left = MakeRandomTree1( Lower, RandomValue - 1 );
T->Right = MakeRandomTree1( RandomValue + 1, Upper );
} else
FatalError( "Out of space!" );
} return T;
}
SearchTree
MakeRandomTree( int N ) {
return MakeRandomTree1( 1, N );
}
_______________________________________________________________________________
_______________________________________________________________________________
4.30_______________________________________________________________________________
_______________________________________________________________________________
/* LastNode is the address containing last value that was assigned to a node */
SearchTree
GenTree( int Height, int *LastNode ) {
SearchTree T;
if( Height >= 0 ) {
T = malloc( sizeof( *T ) ); /* Error checks omitted; see Exercise 4.29. */
T->Left = GenTree( Height - 1, LastNode );
T->Element = ++*LastNode;
T->Right = GenTree( Height - 2, LastNode );
return T;
} else
return NULL;
}
SearchTree
MinAvlTree( int H ) {
int LastNodeAssigned = 0;
return GenTree( H, &LastNodeAssigned );
}
_______________________________________________________________________________
_______________________________________________________________________________
4.31 There are two obvious ways of solving this problem. One way mimics Exercise 4.29 by replacing RandInt(Lower,Upper) with (Lower+Upper) / 2. This requires computing 2HO+1−1, which is not that difficult. The other mimics the previous exercise by noting that the heights of the subtrees are both HO−1. The solution follows:
_______________________________________________________________________________
_______________________________________________________________________________
/* LastNode is the address containing last value that was assigned to a node. */
SearchTree
GenTree( int Height, int *LastNode ) {
SearchTree T = NULL;
if( Height >= 0 ) {
T = malloc( sizeof( *T ) ); /* Error checks omitted; see Exercise 4.29. */
T->Left = GenTree( Height - 1, LastNode );
T->Element = ++*LastNode;
T->Right = GenTree( Height - 1, LastNode );
} return T;
}
SearchTree
PerfectTree( int H ) {
int LastNodeAssigned = 0;
return GenTree( H, &LastNodeAssigned );
}
_______________________________________________________________________________
_______________________________________________________________________________
4.32 This is known as one-dimensional range searching. The time is OO(KO) to perform the inorder traversal, if a significant number of nodes are found, and also proportional to the depth of the tree, if we get to some leaves (for instance, if no nodes are found). Since the average depth is OO(log NO), this gives an OO(KO + log NO) average bound.
_______________________________________________________________________________
_______________________________________________________________________________
void
PrintRange( ElementType Lower, ElementType Upper, SearchTree T ) {
if( T != NULL ) {
if( Lower <= T->Element )
PrintRange( Lower, Upper, T->Left );
if( Lower <= T->Element && T->Element <= Upper ) PrintLine( T->Element );
if( T->Element <= Upper )
PrintRange( Lower, Upper, T->Right );
} }
_______________________________________________________________________________
_______________________________________________________________________________
4.33 This exercise and Exercise 4.34 are likely programming assignments, so we do not provide code here.
4.35 Put the root on an empty queue. Then repeatedly DequeueO a node and EnqueueO its left and right children (if any) until the queue is empty. This is OO(NO) because each queue operation is constant time and there are NO EnqueueO and NO DequeueO operations.
4.36 (a)
0,1
2 : 4
2, 3
6 : -
4, 5
8 : -
6, 7 8, 9
(b)
1,2,3
4 : 6
4, 5 6,7,8
4.39
A B
D
H I
E J
C F
L O
K M
Q
P R
G N
4.41 The function shown here is clearly a linear time routine because in the worst case it does a traversal on both TO1 and TO2.
_______________________________________________________________________________
_______________________________________________________________________________
int
Similar( BinaryTree T1, BinaryTree T2 ) {
if( T1 == NULL || T2 == NULL )
return T1 == NULL && T2 == NULL;
return Similar( T1->Left, T2->Left ) && Similar( T1->Right, T2->Right );
}
_______________________________________________________________________________
_______________________________________________________________________________
4.43 The easiest solution is to compute, in linear time, the inorder numbers of the nodes in both trees. If the inorder number of the root of T2 is xO, then find xO in T1 and rotate it to the root.
Recursively apply this strategy to the left and right subtrees of T1 (by looking at the values in the root of T2’s left and right subtrees). If dNO is the depth of xO, then the running time satisfies TO(NO) = TO(iO) + TO(NO−iO−1) + dNO, where iO is the size of the left subtree. In the worst case, dNO is always OO(NO), and iO is always 0, so the worst-case running time is quadratic.
Under the plausible assumption that all values of iO are equally likely, then even if dNO is always OO(NO), the average value of TO(NO) is OO(NOlog NO). This is a common recurrence that was already formulated in the chapter and is solved in Chapter 7. Under the more reason- able assumption that dNO is typically logarithmic, then the running time is OO(NO).
4.44 Add a field to each node indicating the size of the tree it roots. This allows computation of its inorder traversal number.
4.45 (a) You need an extra bit for each thread.
(c) You can do tree traversals somewhat easier and without recursion. The disadvantage is that it reeks of old-style hacking.
Chapter 5: Hashing
5.1 (a) On the assumption that we add collisions to the end of the list (which is the easier way if a hash table is being built by hand), the separate chaining hash table that results is shown here.
4371
1323 6173
4344
4199 9679 1989
0 1 2 3 4 5 6 7 8 9
(b)
9679 4371 1989 1323 6173 4344
4199 0
1 2 3 4 5 6 7 8 9
(c)
9679 4371
1323 6173 4344
1989 4199 0
1 2 3 4 5 6 7 8 9
(d) 1989 cannot be inserted into the table because hashO2(1989)=6, and the alternative locations 5, 1, 7, and 3 are already taken. The table at this point is as follows:
4371
1323 6173 9679
4344
4199 0
1 2 3 4 5 6 7 8 9
5.2 When rehashing, we choose a table size that is roughly twice as large and prime. In our case, the appropriate new table size is 19, with hash function hO(xO) = xO(modO 19).
(a) Scanning down the separate chaining hash table, the new locations are 4371 in list 1, 1323 in list 12, 6173 in list 17, 4344 in list 12, 4199 in list 0, 9679 in list 8, and 1989 in list 13.
(b) The new locations are 9679 in bucket 8, 4371 in bucket 1, 1989 in bucket 13, 1323 in bucket 12, 6173 in bucket 17, 4344 in bucket 14 because both 12 and 13 are already occu-
(c) The new locations are 9679 in bucket 8, 4371 in bucket 1, 1989 in bucket 13, 1323 in bucket 12, 6173 in bucket 17, 4344 in bucket 16 because both 12 and 13 are already occu- pied, and 4199 in bucket 0.
(d) The new locations are 9679 in bucket 8, 4371 in bucket 1, 1989 in bucket 13, 1323 in bucket 12, 6173 in bucket 17, 4344 in bucket 15 because 12 is already occupied, and 4199 in bucket 0.
5.4 We must be careful not to rehash too often. Let pO be the threshold (fraction of table size) at which we rehash to a smaller table. Then if the new table has size NO, it contains 2pNO ele- ments. This table will require rehashing after either 2NO − 2pNO insertions or pNO deletions.
Balancing these costs suggests that a good choice is pO = 2/ 3. For instance, suppose we have a table of size 300. If we rehash at 200 elements, then the new table size is NO = 150, and we can do either 100 insertions or 100 deletions until a new rehash is required.
If we know that insertions are more frequent than deletions, then we might choose pO to be somewhat larger. If pO is too close to 1.0, however, then a sequence of a small number of deletions followed by insertions can cause frequent rehashing. In the worst case, if pO = 1.0, then alternating deletions and insertions both require rehashing.
5.5 (a) Since each table slot is eventually probed, if the table is not empty, the collision can be resolved.
(b) This seems to eliminate primary clustering but not secondary clustering because all ele- ments that hash to some location will try the same collision resolution sequence.
(c, d) The running time is probably similar to quadratic probing. The advantage here is that the insertion can’t fail unless the table is full.
(e) A method of generating numbers that are not random (or even pseudorandom) is given in the references. An alternative is to use the method in Exercise 2.7.
5.6 Separate chaining hashing requires the use of pointers, which costs some memory, and the standard method of implementing calls on memory allocation routines, which typically are expensive. Linear probing is easily implemented, but performance degrades severely as the load factor increases because of primary clustering. Quadratic probing is only slightly more difficult to implement and gives good performance in practice. An insertion can fail if the table is half empty, but this is not likely. Even if it were, such an insertion would be so expensive that it wouldn’t matter and would almost certainly point up a weakness in the hash function. Double hashing eliminates primary and secondary clustering, but the compu- tation of a second hash function can be costly. Gonnet and Baeza-Yates [8] compare several hashing strategies; their results suggest that quadratic probing is the fastest method.
5.7 Sorting the MNO records and eliminating duplicates would require OO(MNOlog MNO) time using a standard sorting algorithm. If terms are merged by using a hash function, then the merging time is constant per term for a total of OO(MNO). If the output polynomial is small and has only OO(MO + NO) terms, then it is easy to sort it in OO((MO + NO)log (MO + NO)) time, which is less than OO(MNO). Thus the total is OO(MNO). This bound is better because the model is less restrictive: Hashing is performing operations on the keys rather than just com- parison between the keys. A similar bound can be obtained by using bucket sort instead of a standard sorting algorithm. Operations such as hashing are much more expensive than comparisons in practice, so this bound might not be an improvement. On the other hand, if the output polynomial is expected to have only OO(MO + NO) terms, then using a hash table saves a huge amount of space, since under these conditions, the hash table needs only
OO(MO + NO) space.
Another method of implementing these operations is to use a search tree instead of a hash table; a balanced tree is required because elements are inserted in the tree with too much order. A splay tree might be particularly well suited for this type of a problem because it does well with sequential accesses. Comparing the different ways of solving the problem is a good programming assignment.
5.8 The table size would be roughly 60,000 entries. Each entry holds 8 bytes, for a total of 480,000 bytes.
5.9 (a) This statement is true.
(b) If a word hashes to a location with value 1, there is no guarantee that the word is in the dictionary. It is possible that it just hashes to the same value as some other word in the dic- tionary. In our case, the table is approximately 10% full (30,000 words in a table of 300,007), so there is a 10% chance that a word that is not in the dictionary happens to hash out to a location with value 1.
(c) 300,007 bits is 37,501 bytes on most machines.
(d) As discussed in part (b), the algorithm will fail to detect one in ten misspellings on aver- age.
(e) A 20-page document would have about 60 misspellings. This algorithm would be expected to detect 54. A table three times as large would still fit in about 100K bytes and reduce the expected number of errors to two. This is good enough for many applications, especially since spelling detection is a very inexact science. Many misspelled words (espe- cially short ones) are still words. For instance, typing themO instead of thenO is a misspelling that won’t be detected by any algorithm.
5.10 To each hash table slot, we can add an extra field that we’ll call WhereOnStack,O and we can keep an extra stack. When an insertion is first performed into a slot, we push the address (or number) of the slot onto the stack and set the WhereOnStackO field to point to the top of the stack. When we access a hash table slot, we check that WhereOnStackO points to a valid part of the stack and that the entry in the (middle of the) stack that is pointed to by the WhereOn- StackO field has that hash table slot as an address.
5.14
(2) 00000010 00001011 00101011
(2) 01010001 01100001 01101111 01111111
(3) 10010110 10011011 10011110
(3) 10111101 10111110
(2) 11001111 11011011 11110000 000 001 010 011 100 101 110 111
Chapter 6: Priority Queues (Heaps)
6.1 Yes. When an element is inserted, we compare it to the current minimum and change the minimum if the new element is smaller. DeleteMinO operations are expensive in this scheme.
6.2
1
3 2
6 7 5 4
15 14 12 9 10 11 13 8
1
3 2
12 6 4 8
15 14 9 7 5 11 13 10
6.3 The result of three DeleteMins,O starting with both of the heaps in Exercise 6.2, is as fol- lows:
4
6 5
13 7 10 8
15 14 12 9 11
4
6 5
12 7 10 8
15 14 9 13 11
6.4
6.5 These are simple modifications to the code presented in the text and meant as programming exercises.
6.6 225. To see this, start with iO=1 and position at the root. Follow the path toward the last node, doubling iO when taking a left child, and doubling iO and adding one when taking a right child.
6.7 (a) We show that HO(NO), which is the sum of the heights of nodes in a complete binary tree of NO nodes, is NO − bO(NO), where bO(NO) is the number of ones in the binary representation of NO. Observe that for NO = 0 and NO = 1, the claim is true. Assume that it is true for values of kO up to and including NO−1. Suppose the left and right subtrees have LO and RO nodes, respectively. Since the root has heightOIlog NOK, we have
HO(NO) = OIlog NOK + HO(LO) + HO(RO)
= OIlog NOK + LO − bO(LO) + RO − bO(RO)
= NO − 1 + (OIlog NOK − bO(LO) − bO(RO))
The second line follows from the inductive hypothesis, and the third follows because LO + RO = NO − 1. Now the last node in the tree is in either the left subtree or the right sub- tree. If it is in the left subtree, then the right subtree is a perfect tree, and bO(RO) = OIlog NOK − 1. Further, the binary representation of NO and LO are identical, with the exception that the leading 10 in NO becomes 1 in LO. (For instance, if NO = 37 = 100101, LO = 10101.) It is clear that the second digit of NO must be zero if the last node is in the left sub- tree. Thus in this case, bO(LO) = bO(NO), and
HO(NO) = NO − bO(NO)
If the last node is in the right subtree, then bO(LO) = OIlog NOK. The binary representation of RO is identical to NO, except that the leading 1 is not present. (For instance, if NO = 27 = 101011, LO = 01011.) Thus bO(RO) = bO(NO) − 1, and again
HO(NO) = NO − bO(NO)
(b) Run a single-elimination tournament among eight elements. This requires seven com- parisons and generates ordering information indicated by the binomial tree shown here.
a b c
d e
f g
h
The eighth comparison is between bO and cO. If cO is less than bO, then bO is made a child of cO.
Otherwise, both cO and dO are made children of bO.
(c) A recursive strategy is used. Assume that NO = 2kO. A binomial tree is built for the NO elements as in part (b). The largest subtree of the root is then recursively converted into a binary heap of 2kO−1 elements. The last element in the heap (which is the only one on an extra level) is then inserted into the binomial queue consisting of the remaining binomial trees, thus forming another binomial tree of 2kO−1elements. At that point, the root has a sub- tree that is a heap of 2kO−1 − 1 elements and another subtree that is a binomial tree of 2kO−1 elements. Recursively convert that subtree into a heap; now the whole structure is a binary heap. The running time for NO = 2kO satisfies TO(NO) = 2TO(NO/ 2) + log NO. The base case is
6.8 Let DO1, DO2, ..., DkO be random variables representing the depth of the smallest, second smal- lest, and kOthO smallest elements, respectively. We are interested in calculating EO(DkO). In what follows, we assume that the heap size NO is one less than a power of two (that is, the bottom level is completely filled) but sufficiently large so that terms bounded by OO(1 / NO) are negligible. Without loss of generality, we may assume that the kOthOsmallest element is in the left subheap of the root. Let pPjO,kO be the probability that this element is the PjOthOsmal- lest element in the subheap.
Lemma: For kO>1, EO(DkO) =
PjO=1
Σ
kO−1
pPjO,kO(EO(DPjO) + 1).
Proof: An element that is at depth dO in the left subheap is at depth dO + 1 in the entire subheap. Since EO(DPjO + 1) = EO(DPjO) + 1, the theorem follows.
Since by assumption, the bottom level of the heap is full, each of second, third, ..., kO−1thO smallest elements are in the left subheap with probability of 0.5. (Technically, the probabil- ity should be ⁄12 − 1/(NO−1) of being in the right subheap and ⁄12 + 1/(NO−1) of being in the left, since we have already placed the kOthOsmallest in the right. Recall that we have assumed that terms of size OO(1/NO) can be ignored.) Thus
pPjO,kO = pkO−PjO,kO = 2kO−2 _1
____
(
PjO−1kO−2)
Theorem: EO(DkO) ≤ log kO.
Proof: The proof is by induction. The theorem clearly holds for kO = 1 and kO = 2. We then show that it holds for arbitrary kO > 2 on the assumption that it holds for all smaller kO. Now, by the inductive hypothesis, for any 1 ≤ PjO ≤ kO−1,
EO(DPjO) + EO(DkO−PjO) ≤ log PjO + log kO−PjO SincePfOO(xO) = log xO is convex for xO > 0,
log PjO + log kO−PjO ≤ 2log (kO/ 2) Thus
EO(DPjO) + EO(DkO−PjO) ≤ log (kO/ 2) + log (kO/ 2) Furthermore, since pPjO,kO = pkO−PjO,kO,
pPjO,kOEO(DPjO) + pkO−PjO,kOEO(DkO−PjO) ≤pPjO,kOlog (kO/ 2) + pkO−PjO,kOlog (kO/ 2) From the lemma,
EO(DkO) =
PjO=1
Σ
kO−1
pPjO,kO(EO(DPjO) + 1)
= 1 +
PjO=1
Σ
kO−1
pPjO,kOEO(DPjO) Thus
EO(D O) ≤ 1 + k
Σ
O−1p Olog (kO/ 2)≤ 1 + log (kO/ 2)
PjO=1
Σ
kO−1
pPjO,kO
≤ 1 + log (kO/ 2)
≤ log kO
completing the proof.
It can also be shown that asymptotically, EO(DkO) ∼∼ log (kO−1) − 0.273548.
6.9 (a) Perform a preorder traversal of the heap.
(b) Works for leftist and skew heaps. The running time is OO(KdO) for dO-heaps.
6.11 Simulations show that the linear time algorithm is the faster, not only on worst-case inputs, but also on random data.
6.12 (a) If the heap is organized as a (min) heap, then starting at the hole at the root, find a path down to a leaf by taking the minimum child. The requires roughly log NO comparisons. To find the correct place where to move the hole, perform a binary search on the log NO ele- ments. This takes OO(log log NO) comparisons.
(b) Find a path of minimum children, stopping after log NO − log log NO levels. At this point, it is easy to determine if the hole should be placed above or below the stopping point. If it goes below, then continue finding the path, but perform the binary search on only the last log log NO elements on the path, for a total of log NO + log log log NO comparisons. Other- wise, perform a binary search on the first log NO − log log NO elements. The binary search takes at most log log NO comparisons, and the path finding took only log NO − log log NO, so the total in this case is log NO. So the worst case is the first case.
(c) The bound can be improved to log NO + log*NO + OO(1), where log*NO is the inverse Ack- erman function (see Chapter 8). This bound can be found in reference [16].
6.13 The parent is at position OI(iO + dO − 2)/dOK. The children are in positions (iO − 1)dO + 2, ..., idO + 1.
6.14 (a) OO((MO + dNO)logdONO).
(b) OO((MO + NO)log NO).
(c) OO(MO + NO2).
(d) dO= max (2, MO / NO).
(See the related discussion at the end of Section 11.4.)
6.16
31 18
9 10
4
15 8
5
21 11
6 2
12 11
18 17
6.17
1
2 3
7 6 5 4
8 9 10 11 12 13 14 15
6.18 This theorem is true, and the proof is very much along the same lines as Exercise 4.17.
6.19 If elements are inserted in decreasing order, a leftist heap consisting of a chain of left chil- dren is formed. This is the best because the right path length is minimized.
6.20 (a) If a DecreaseKeyO is performed on a node that is very deep (very left), the time to per- colate up would be prohibitive. Thus the obvious solution doesn’t work. However, we can still do the operation efficiently by a combination of DeleteO and InsertO. To DeleteO an arbi- trary node xO in the heap, replace xO by the MergeO of its left and right subheaps. This might create an imbalance for nodes on the path from xO’s parent to the root that would need to be fixed by a child swap. However, it is easy to show that at most log NO nodes can be affected, preserving the time bound. This is discussed in Chapter 11.
6.21 Lazy deletion in leftist heaps is discussed in the paper by Cheriton and Tarjan [9]. The gen- eral idea is that if the root is marked deleted, then a preorder traversal of the heap is formed, and the frontier of marked nodes is removed, leaving a collection of heaps. These can be merged two at a time by placing all the heaps on a queue, removing two, merging them, and placing the result at the end of the queue, terminating when only one heap remains.
6.22 (a) The standard way to do this is to divide the work into passes. A new pass begins when the first element reappears in a heap that is dequeued. The first pass takes roughly