





























































Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
solutions manual for Data Structures and Algorithm Analysis in C (Second Edition) Mark Allen Weiss
Typology: Lecture notes
1 / 69
This page cannot be seen from the preview
Don't miss anything!






























































On special offer
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.
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 Separate O 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 ProcessFile O 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 < X O ≤ 1, since it is true for X O = 1, and for X O < 1, log X O is negative. It is also easy to see that the theorem holds for 1 < X O ≤ 2, since it is true for X O = 2, and for X O < 2, log X O is at most 1. Suppose the theorem is true for p O < X O ≤ 2 p O (where p O is a positive integer), and consider any 2 p O < Y O ≤ 4 p O ( p O ≥ 1). Then log Y O = 1 + log ( Y O / 2) < 1 + Y O / 2 < Y O / 2 + Y O / 2 ≤ Y O, where the first ine- quality follows by the inductive hypothesis. (b) Let 2 X O^ = A O. Then A O B O^ = (2 X O) B O^ = 2 XB O. Thus log A O B O^ = XB O. Since X O = log A O, the theorem is proved.
1.6 (a) The sum is 4 / 3 and follows directly from the formula.
(b) S O = 4
+...^. Subtracting the first equation from
the second gives 3 S O = 1 + 4
+...^. By part (a), 3 S O = 4 / 3 so S O = 4 / 9.
(c) S O = 4
+...^. Subtracting the first equa-
tion from the second gives 3 S O = 1 + 4
+...^. Rewriting, we get
3 S O = 2 i O= 0
∞ 4 i O
_____^ i (^) + i O= 0
∞ 4 i O
_____^1. Thus 3 S O = 2(4 / 9) + 4 / 3 = 20 / 9. Thus S O = 20 / 27.
(d) Let SN O = i O= 0
∞
4 i O
_____^ i O N O
. Follow the same method as in parts (a) - (c) to obtain a formula for S (^) N O
in terms of SN O− 1 , S (^) N O− 2 , ..., S O 0 and solve the recurrence. Solving the recurrence is very difficult.
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.
i O=OI N O / 2 OK
N i
i O= 1
N i
i O= 1
OI N O / 2 − 1 OK i
∼ ln^ N O^ −^ ln^ N O /^^2 ∼∼ ln^ 2.
2.1 2 /N O, 37, √MM N OO, N O, N Olog log N O, N Olog N O, N Olog ( N O^2 ), N Olog^2 N O, N O1.5, N O^2 , N O^2 log N O, N O^3 , 2 N O /^^2 , 2 N O. N Olog N O and N Olog ( N O^2 ) grow at the same rate.
2.2 (a) True.
(b) False. A counterexample is T O 1 ( N O) = 2 N O, T O 2 ( N O) = N O, and P f OO( N O) = N O. (c) False. A counterexample is T O 1 ( N O) = N O^2 , T O 2 ( N O) = N O, and P f OO( N O) = N O^2. (d) False. The same counterexample as in part (c) applies.
2.3 We claim that N Olog N O is the slower growing function. To see this, suppose otherwise. Then, N Oε /^ √MMMMMlog^ N OO^ would grow slower than log N O. Taking logs of both sides, we find that, under this assumption, ε / √MlogMMMMM N OOlog N O grows slower than log log N O. But the first expres- sion simplifies to ε √MlogMMMMM N OO. If L O = log N O, then we are claiming that ε √MM L OO grows slower than log L O, or equivalently, that ε^2 L O grows slower than log^2 L O. But we know that log^2 L O = ο ( L O), so the original assumption is false, proving the claim.
2.4 Clearly, log k O^1 N O = ο(log k O^2 N O) if k O 1 < k O 2 , so we need to worry only about positive integers. The claim is clearly true for k O = 0 and k O = 1. Suppose it is true for k O < i O. Then, by L’Hospital’s rule,
N O→∞
lim N
N O→∞
lim i N
_________^ log i O−^1 N
The second limit is zero by the inductive hypothesis, proving the claim.
2.5 Let P f OO( N O) = 1 when N O is even, and N O when N O is odd. Likewise, let g O( N O) = 1 when N O is odd, and N O when N O is even. Then the ratio P f OO( N O) / g O( N O) oscillates between 0 and ∞.
2.6 For all these programs, the following analysis will agree with a simulation:
(I) The running time is O O( N O). (II) The running time is O O( N O^2 ). (III) The running time is O O( N O^3 ). (IV) The running time is O O( N O^2 ). (V) P j O can be as large as i O^2 , which could be as large as N O^2. k O can be as large as P j O, which is N O^2. The running time is thus proportional to N O .N O^2 .N O^2 , which is O O( N O^5 ). (VI) The if O statement is executed at most N O^3 times, by previous arguments, but it is true only O O( N O^2 ) times (because it is true exactly i O times for each i O). Thus the innermost loop is only executed O O( N O^2 ) times. Each time through, it takes O O(P j O^2 ) = O O( N O^2 ) time, for a total of O O( N O^4 ). 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 algorithm, due to R. Floyd, is not as obvious; the correctness can be proved by induction.
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 N O = 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 A O[ i O] has not been used earlier is O O( i O). The expected number of random numbers that need to be tried is N O / ( N O − i O). This is obtained as follows: i O of the N O numbers would be duplicates. Thus the probability of success is ( N O − i O) / N O. Thus the expected number of independent trials is N O / ( N O − i O). The time bound is thus
i O= 0
N O− 1 N O− i
_____Ni_ < i O= 0
N O− 1 N O− i
i O= 0
N O− 1 N O− i
P j O= 1
N P j
= O O( N O^2 log N O)
The second algorithm saves a factor of i O for each random number, and thus reduces the time bound to O O( N Olog N O) 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 N O. (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 T O. 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 N O = 10,000, 14.2 years for N O = 100,000 and 140 centuries for N O = 1,000,000. Algorithm 2 would take about 3 hours for N O = 100,000 and about 2 weeks for N O = 1,000,000. Algorithm 3 would use 1 ⁄^1 2 minutes for N O = 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) O O( N O^2 ).
(b) O O( N Olog N O).
2.10 (c) The algorithm is linear.
2.11 Use a variation of binary search to get an O O(log N O) solution (assuming the array is preread).
2.13 (a) Test to see if N O is an odd number (or 2) and is not divisible by 3, 5, 7, ..., √MM N OO.
(b) O O(√MM N OO), assuming that all divisions count for one unit of time. (c) B O = O O(log N O). (d) O O(2 B O /^^2 ). (e) If a 20-bit number can be tested in time T O, then a 40-bit number would require about T O^2 time. (f) B O is the better measure because it more accurately represents the size O of the input.
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 O O( L O + P O).
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 Intersect O is shown on page 9.
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 Pow O function in Chapter 2, adapted for polynomial multiplication. If P O is small, a standard method that uses O O( P O) multiplies instead of O O(log P O) 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 M' O = M O mod O N O, so that the hot potato never goes around the circle more than once, and
then if M' O > N O / 2, passing the potato appropriately in the alternative direction. This requires a doubly linked list. The worst-case running time is clearly O O( N O min O( M O, N O)), although when these heuristics are used, and M O and N O are comparable, the algorithm might be significantly faster. If M O = 1, the algorithm is clearly linear. The VAX/VMS C compiler’s memory management routines do poorly with the particular pattern of P free Os in this case, causing O O( N Olog N O) behavior.
3.12 Reversal of a singly linked list can be done nonrecursively by using a stack, but this requires O O( N O) extra space. The solution in Fig. 3.5 is similar to strategies employed in gar- bage collection algorithms. At the top of the while O loop, the list from the start to Pre- viousPos O is already reversed, whereas the rest of the list, from CurrentPos O 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) Delete O takes O O( N O) and is in two nested for loops each of size N O, giving an obvious O O( N O^3 ) bound. A better bound of O O( N O^2 ) is obtained by noting that only N O elements can be deleted from a list of size N O, hence O O( N O^2 ) is spent performing deletes. The remainder of the routine is O O( N O^2 ), so the bound follows. (d) O O( N O^2 ).
/* 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->Front O 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.
4.1 (a) A O.
(b) G O, H O, I O, L O, M O, and K O.
4.2 For node B O:
(a) A O. (b) D O and E O. (c) C O. (d) 1. (e) 3.
4.3 4.
4.4 There are N O nodes. Each node has two pointers, so there are 2 N O pointers. Each node but the root has one incoming pointer from its parent, which accounts for N O−1 pointers. The rest are NULL. O
4.5 Proof is by induction. The theorem is trivially true for H O = 0. Assume true for H O = 1, 2, ..., k O. A tree of height k O+1 can have two subtrees of height at most k O. These can have at most 2 k O+^1 −1 nodes each by the induction hypothesis. These 2 k O+^2 −2 nodes plus the root prove the theorem for height k O+1 and hence for all heights.
4.6 This can be shown by induction. Alternatively, let N O = number of nodes, F O = number of full nodes, L O = number of leaves, and H O = number of half nodes (nodes with one child). Clearly, N O = F O + H O + L O. Further, 2 F O + H O = N O − 1 (see Exercise 4.4). Subtracting yields L O − F O = 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 k O nodes. Consider any tree with k O+1 nodes. Such a tree consists of an i O node left subtree and a k O − i O 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 1 ⁄ 2 with respect to the root. Similar logic implies that the sum for leaves in the right subtree is at most 1 ⁄ 2 , 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 -.
subtree, eventually forming a perfectly balanced right subtree of height H O−1. This follows by the induction hypothesis because the right subtree may be viewed as being formed from the successive insertion of 2 H O−^1 + 1 through 2 H O^ + 2 H O−^1 − 1. The next insertion forces an imbalance at the root, and thus a single rotation. It is easy to check that this brings 2 H O^ to the root and creates a perfectly balanced left subtree of height H O−1. The new key is attached to a perfectly balanced right subtree of height H O−2 as the last node in the right path. Thus the right subtree is exactly as if the nodes 2 H O^ + 1 through 2 H O^ + 2 H O−^1 were inserted in order. By the inductive hypothesis, the subsequent successive insertion of 2 H O^ + 2 H O−^1 + 1 through 2 H O+^1 − 1 will create a perfectly balanced right subtree of height H O−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 2 H O+^1 − 1 nodes is perfectly balanced (and has height H O).
4.18 The two remaining functions are mirror images of the text procedures. Just switch Right O and Left O 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) O O(log log N O).
(b) The minimum AVL tree of height 255 (a huge tree).
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,
After accessing 9,