Divide & Conquer1 Introduction (top-down merge_sort)Here is the merge_sort algorithm from the Sorts notes (Ch 13 Slide 13): void merge_sort(A,p,r) { if (r - p > 1) { q = (p+r)/2; // n = r-p = size of input merge_sort(A,p,q); // q-p = n/2 = size of subrange merge_sort(A,q,r); // r-q = n/2 = size of subrange merge(A,p,q,r); // Θ(n) } } void merge(T* A, size_t p, size_t q, size_t r) { T B [r-p]; // temp space for merged copy of A g_set_merge(A+p, A+q, A+q, A+r, B); // merge the two parts of A to B g_copy(B, B+(r-p), A+p); // copy B back to A[p,r) } By splitting the sort problem into halves [the "divide" step] we reduce the problem to two sub-problems to be solved recursively [the "conquer" step] plus a merge operation [the "combine" step]:
Letting T(n) denote the runtime of the algorithm with input size n, we have just shown that T satisfies the recursion:
Let b(n) = B×n = Bn represent Θ(n) for some positive constant B. Note that k×b(n/k) = k×B×n/k = B×n = b(n). We can solve this recursion by substitution: T(n) = 2×T(n/2) + b(n) = 2×(2×T(n/4) + b(n/2)) + b(n) = 4×T(n/4) + 2×b(n/2) + b(n) = 4×T(n/4) + b(n) + b(n) = 4×T(n/4) + 2b(n) = ... = 2k×T(n/2k) + kb(n) Assume for simplcity that n = 2k and we have: T(n) = 2k×T(n/2k) + b(n)×k = n×T(1) + b(n)×log n = A×n + B×n×log n // where A = T(1) = An + B n log n = Θ(n log n) We can also test a "guess" for compliance: Lemma. The function f(n) = n log2 n satisfies the recursion T(n) = 2×T(n/2) + n. Proof. Calculate the right-hand side of the recursion: 2 f(n/2) + n = 2(n/2 log (n/2)) + n = n (log n/2 + 1) = n (log n - log 2 + 1) = n (log n - 1 + 1) = n log n = f(n) which is the left-hand side. ∎ Conclusion. The asymptotic runtime of top-down merge sort is O(n log n). 2 Sidebar (bottom-up merge_sort)void merge_sort_bu (A, n) { if (n < 2) return; for (size_t i = 1; i < n; i = i+i) { for (size_t j = 0; j < n - i; j += i+i) { if (n < j+i+i) // possible at last step merge(A + j, A + j+i, A+n); else merge(A + j, A + j+i, A + j+i+i); } } } Proposition. The asymptotic runtime of bottom-up merge_sort is O(n log n). Proof. The inner loop body is a call to merge on ranges of size i (at a cost of 2i). The inner loop runs n/2i times. Thus the cost of the inner loop is n, independent of i. (Another argument is that the inner loop touches each array element one time.) The update rule for the outer loop doubles i and the stop rule terminates execution when i >= n. Thus the outer loop runs for each positive integer k until 2k ≥ n, or equivalently k ≥ log n. Therefore the outer loop runs log n times. Therefore the runtime is n log n. ∎ Optimizations. Some very simple modifications to the straight algorithms can be made that reduce the runtime of the merge sorts on certain data sets. The simplest is the observation that if the last (largest) item in the first range is ≤ the first (smallest) item in the second range then the Θ(k) merge operation can be replaced with concatenation. (Here k is the length of the segments.) Depending on the data container, that operation is a straight data copy (vector - still Θ(k) but without any further comparisons) or a concatenation of link segments (list - just splice the entire pair of segments forward and skip the merge entirely - Θ(1)). This is why we stated the runtimes in terms of O rather than Θ. 3 FFTThe fast fourier transform (here in the context of multiplying two polynomials) is given by: FFT (a, n) // a is an array of size n (or smaller) { if (n <= 1) return a; // base case for recursion wn = exp(2*pi*i/n); // wn = principal nth root of unity w = 1; // w is maintained as wn^k, updated in post-processing loop b = (a[0], a[2], ... , a[n-2]); // even indexed values c = (a[1], a[3], ... , a[n-1]); // odd indexed values yb = FFT(b, n/2); // recursive call yc = FFT(c, n/2); // recursive call for (k = 0; k < n/2; ++k) { // loop invariant: w = wn^k at beginning of loop body ya[k] = yb[k] + w*yc[k]; // see (1) below ya[k+n/2] = yb[k] - w*yc[k]; // see (2) below w = w*wn; // w = wn^(k+1) for next iteration } return ya; } This is clearly another example of a divide-and-conquer algorithm design. Looking at cost:
We see the same recursion as for merge_sort. Therefore the same conclusion on runtime holds: Proposition. The asymptotic runtime of FFT(n) is Θ(n log n). (See [FFT Notes] and/or Chapter 30 of [Cormen] for more details.) Exercise 1. Consider the two recursions:
Use substitution to show that R(n) = Θ(n) and S(n) = Θ(n2). (You may assume n = 2k.) 4 Recurrences and the Master TheoremRecurrences of the form
(where a ≥ 1 and b > 1 are real number constants) arise fairly often in the analysis of divide and conquer algorithms. Many of these are difficult to solve exactly in closed form. But there are results about the asymptotic behavior of solutions which are usually enough for the analysis. These are summarized by the: Master Theorem. Assume the recurrence T as above and let d = logb(a).
(See Section 4.5 of [Cormen] for a proof.) Exercise 2. Use the Master Theorem and the results of Exercise 1 to show that the three recurrences:
illustrate the 3 cases of the theorem. 5* Generating FunctionsWe will now illustrate the use of generating functions to solve some recurrences. This powerful technique is in general use in discrete mathematics as well as analysis of algorithms at the research level. The illustration here derives a closed form solution for the famous Fibonacci recursion (and is treated as a series of exercises in Chapter 4 of [Cormen]). Given a sequence of numbers s0, s1, s2, ... the (ordinary) generating functions for the sequence is the infinite sum
(Such sums are usually called formal power series.)* Two such generating functions are equal iff their coefficients are equal. Let F denote the generating function for the Fibonacci sequence
where the coefficients fk are given by the recursion:
Straighforward calculation verifies that F satisfies the following:
To verify, look at the coefficients of zk on the right-hand side. Now we can work some powerful mathematical magic, starting with algebra. Solving for F(z) we have:
The quadratic denominator has real roots r1, r2 and hence can be factored as:
Letting φ,ψ = (1 ± √5)/2 we can re-write as
Applying the technique of partial fractions we can obtain
The final leap uses the identity
applied to each of the two fractions to obtain
Equating coefficients we have proved: Proposition. The kth Fibonacci number is given by the formula fk = (φk - ψk)/√5. The number φ = 1.61803... is called the golden ratio. Da Vinci used φ as the optimally esthetic length/width ratio for rectangles in architecture. Cormen observes that, because |ψ| < 1, fk is equal to φk/√5 rounded to the nearest integer for k > 1. *Throughout this section ∑k is taken to mean the sum over all non-negative integers k. |