Homework 4 Solutions Problem 1 (a) Suppose we are given a sequence S of n elements, each of which is an integer in the range [0, n2 − 1]. Describe a simple method for sorting S in O(n) time. (Hint: think of alternative ways of viewing the elements.) Solution Let k be an integer in the range [0, n2 − 1]. If we represent in in n-ary form, it would consist of two numbers (l, f ) s.t. k = l ∗ n + f where each l and f are in the range [0, n − 1]. Conviniently, the lexicographical ordering of the n-ary representation corresponds to the ordering of the numbers. Radix-sort as defined on page 284 of the course packet sorts pairs of numbers like this lexicographically. Radix-sort is a linear time algorithm, so this is obviously a linear time algorithm too. Algorithm LimitedSort(S, n) Input: a sequence S of n number in the range [0, n2 − 1] Output: sorted S { change the representation } for i ← 0 to n − 1 do S[i] ← ((S[i] − S[i] mod n)/n, S[i] mod n) radix-sort(A) { restore the representation } for i ← 0 to n − 1 do S[i] ← S[i][0] ∗ n + S[i][1] (b) Does the running time of straight radix-sort depend on the order of keys in Solution The running time of the radix-sort algorithm that uses bucket-sort as an internal stable sort (as described on page 284 of the course packet) does not depend on the order of keys, because bucket-sort starts by removing every element from the array and inserting it in a bucket (single insertion takes constant time), and then reads each element from the buckets back into the array all at a constant time cost per element. (c) Does the running time of radix-exchange-sort depend on the order of keys in the input? Solution Asymptotic running time will always be O(bn) for an array of n numbers with b bits each, because for every bit we scan through every element of the array exaclty once. On the other hand, depending on the number of swap operations we have to perform, the constant factors might differ. For example, if given a sorted sequence, we will just end up performing nb comparisons. But given a sequence where at each bth interation of the radix-exchange-sort all elements with 1s in bth place end up at the beginning, we can end up with as many as nb/2 swaps. 101 100 111 110 001 000 011 010 010 011 000 001 110 111 100 101 001 000 011 010 101 100 111 110 000 001 010 011 100 101 110 111 CS 16 — Introduction to Algorithms and Data Structures Semester II, 99–00 2 Problem 2 A forensic lab receives a delivery of n samples. They look identical, but in fact, some of them have different chemical composition. There is a device that can be applied to two samples and tells whether they are different or not. It is known in advance that most of the samples (more then 50% ) are identical. Find one of those identical samples making no more than n comparisons. (Beware: it is possible that two samples are identical but do not belong to the majority of identical samples.) Solution The basic idea is that if two samples are different, they may be discarded, because one of them does not belong to the majority and the majority remains if we do this. The algorithm processes samples on by one, and keeps track of the current “candidate” for the majority sample, c, and its “multiplicity”, k. The invariant is that if we add k copies of the c-th sample to the yet unrpocessed samples, the majority sample of the initial array and the majority sample of this new array would be the same. We will use 6= and = to compare the samples (as there is no total or partial order on the samples). Algorithm FindMajority(S) Input: array S of samples Output: the majority element of S k←0 for i ← 0 to n − 2 do if k = 0 then k←1 c ← S[i + 1] else if S[i + 1] = c then k ←k+1 else k ←k−1 return c Alternative solutions would be to go through the array taking each consecutive pair of samples and comparing it. If a pair contains distinct samples, throw them out. If a pair of two “equal” samples, keep one of them. Then perform the same procedure on the remaining samples. At each iteration like this, we will end up “throwing out” at least half of the samples. Let us assume without loss of generality, that n = 2k , Then the number of comparisons we would have to perform will be at most n/2 + n/4 + n/8 + . . . + 1 = 2k−1 + . . . + 1 = 2k − 1 = n − 1. Problem 3 Entries in a phone book have multiple keys: last name, first name, address, and telephone number. They are sorted first by last name, then by first name, and finally by address. Design a variation of radix sort for sorting the entries in a phone book, assuming that you have available a procedure that sorts records on a single key. The running time of your algorithm should be of the same order as the running time of the procedure that sorts on a single key. (Beware: this procedure may not be stable.) Solution Let single-sort(S,k) be the sorting procedure that sorts S according to the key k. If single-sort is stable, the following algorithm can be used: Algorithm sort(S): Input: a sequence S of length n CS 16 — Introduction to Algorithms and Data Structures Semester II, 99–00 3 Output: S, sorted let S 0 be a copy of S { to avoid changing S } single-sort(S 0 ,address) single-sort(S 0 ,firstname) single-sort(S 0 ,lastname) If single-sort isn’t stable, however, sorting by first name may scramble the results of the addresssorting step, and sorting by last name may scramble the results of the first-name-sorting step. In this case, it is necessary to first sort by last name, then sort each resulting group by first name, and then finally sort each resulting group by address. Let single-sort(S,k,i,j) be the sorting procedure that sorts S[i . . . j] according to the key S. Algorithm sort(S): Input: a sequence S of length n Output: S, sorted single-sort(S,lastname,0,S.size()−1) lef t ← 0 right ← 0 while (right < S.size())do current ← S[lef t].lastname() while (right 6= S.size() and S[right].lastname() = current) do right ← right + 1 single-sort(S, firstname, lef t, right − 1) blef t ← lef t bright ← lef t while (bright < right)do current ← S[blef t].firstname() while (bright 6= right and S[bright].firstname() = current) do bright ← bright + 1 single-sort(S, address, blef t, bright − 1) blef t ← bright lef t ← right CS 16 — Introduction to Algorithms and Data Structures Semester II, 99–00 4 Problem 4 (a) Insert into an initially empty binary search tree items with the following keys (in this order): 30, 40, 23, 58, 48, 26, 11, 13. Draw the tree after each insertion. Solution A 30 B 30 C 30 40 D 30 23 40 E 30 23 40 23 40 58 58 G 30 48 23 F 40 H 30 30 11 23 58 26 40 23 40 48 58 26 11 58 26 13 48 48 (b) Remove from the binary search tree in figure 8.2 the following keys (in this order): 32, 65, 76, 88, 97. Draw the tree after each removal. Solution A 44 B 44 17 88 28 65 29 54 C 44 17 97 88 28 82 76 29 76 54 97 80 28 29 54 CS 16 — Introduction to Algorithms and Data Structures 54 82 E 44 17 80 28 82 97 80 29 80 17 88 28 97 82 D 44 80 17 54 82 29 Semester II, 99–00 5 (c) A different binary search tree results when we try to insert the same sequence into an empty binary tree in a different order. Give an example of this with at least 5 elements. Solution Consider the elements 10, 20, 30, 40, 50. Inserting them in this order yields a rightheavy tree because each new element is inserted as the right child of the previously-inserted element. Inserting them in reverse order yields a left-heavy tree because each new element is inserted as the left child of the previously-inserted element. 50 10 40 20 30 30 20 40 50 (a) inserting in order 10, 20, 30, 40, 50 10 (b) inserting in order 50, 40, 30, 20, 10 Problem 5 (a) Let T be a binary search tree, and let x be a key. Give an efficient algorithm for finding the smallest key y in T such that y > x. Note that x may or may not be in T . Explain why your algorithm has the running time it does. Solution For each node v of a binary search tree, the keys in v’s left subtree are less than v and the keys in v’s right subtree are greater than v. Thus, at each node a choice can be made: • if v ≤ x then y is the smallest thing larger than x in v’s right subtree (if v = x then y is the smallest thing in the right subtree, but this is just a special case of y being in the right subtree and does not need to be treated separately) • if v > x then y is the smaller of v and the smallest thing greater than x in v’s left subtree (it is not necessary to consider v’s right subtree because while everything there is larger than x, it is also larger than v) This leads to the following algorithm: Algorithm findFloor(T ,x): Input: a binary search tree T and a key x Output: the smallest y in T such that y > x if such a y exists, and ∞ otherwise current ← T .root() best ← null while (T .isInternal(current)) do if (current.key() ≤ x) then current ← T.rightChild(current) { y is in right subtree } else best ← current.key() { current node is a candidate for y } current ← T.leftChild(current) { y is in left subtree } CS 16 — Introduction to Algorithms and Data Structures Semester II, 99–00 6 return best Note that when best is updated we can use best ← current.key() instead of best ← min(best, current.key()) because as soon as best is updated with the current node’s value, we go to the left subtree and all of the keys in that subtree are less than the current node (and the new value of best). The running time is O(h), where h is the height of the tree, because in each iteration of the while loop, current advances one level down the tree. Note that in the worst case this is O(n) and in the best case it is O(log n). (b) Write a nonrecursive program to print out the keys from a binary search tree in order. Solution Since the key of a node v is always greater then or equal to the keys in its left subtree, and less then or equal to the keys in its right subtree, our algorithm is just a glorified inorder traversal. The problem is making this traversal non-recursive. Note that if we are moving up and down the tree (as in Firutre 4.16), it is enough to know where we came from, to know where we are going and whether we should print the key or not (we need to know where we came from since a node is visited several times). Two algorithms that implement this non-recursive inorder traversal follow. The second one might be easier to read since it uses algortihm inorderNext that appeared in the second homework, but it has more complicated running time analysis. Consider the following picture of a finite state automaton that describes the algorithm. Node v is initialized to the root of T . DownLeft UpFromLeft Down Right P P P L R L R L UpFromRight P R L R begin v is internal GO DownLeft end v is external v is root v is a left child v is a right child print v GO UpFromRight GO UpFromLeft v is internal print v print v v is external GO DownRight CS 16 — Introduction to Algorithms and Data Structures Semester II, 99–00 7 The following algorithm is basically the diagram above in pseudo-code: Algorithm PrintBST(T ) Input: binary search tree T Output: keys of T printed in order v ← T .root() if (T .isExternal(v)) then return cmd ← DownLeft while(true) do if (cmd = DownLeft) then v ← T .leftChild(v) if (T .isExternal(v)) then print v cmd ←UpFromLeft else cmd ←DownLeft else if (cmd = UpFromLeft) then v ← T .parent(v) print v cmd ←DownRight else if (cmd = DownRight) then v ← T .rightChild(v) if (T .isExternal(v)) then print v cmd ←UpFromRight else cmd ←DownLeft else { case when command is UpFromRight } v ← T .parent() if (T .isRoot(v)) then return { the end of the traversal } if (T .leftChild(T .parent(v)) = v) then { v is a left child } cmd ←UpFromLeft else cmd ←UpFromRight Note that the only times we do print is when we are at leaves or when we just came up from the left child. Since any tree has n − 1 edges and all we do here is traverse each edge once down and once up, this is an O(n) time algorithm. The second version of this algortihm uses inorderNext algorithm that appeared in homework 2. Algorithm PrintBST(T ) Input: binary search tree T Output: keys of T printed in order { find the left-most node in the tree } v ← T .root() while (T .isInternal(v)) do v ← T .leftChild(v) CS 16 — Introduction to Algorithms and Data Structures Semester II, 99–00 8 while (v 6= null) do print v v ← inorderNext(v) This routine goes down and up the tree basically in the same way as the algorithm above. We either go from a leaf to its parent (if such exists), or go to the leftmost node in the right subtree of v. But note that if we combine all the paths followed by inorderNext for every v, we just follow the contour of T , same as in the first version of the algorithm. So amortized application of inorderNext on every v is also O(n) because, in the end, it just traverses every edge once up and once down. CS 16 — Introduction to Algorithms and Data Structures Semester II, 99–00