Recitation 8

Introduction

In lecture, we have talked about the runtimes of various sorting algorithms. This was also the first time we analyzed recursive algorithms' runtimes. In this recitation, you will review the runtime analysis of Quick Sort and Merge Sort, and have the chance to analyze the runtimes of other recursive algorithms.

Review

In recitation, the TA will walk through the runtime analysis of Merge Sort and Quick Sort. The algorithms for both are shown below. The algorithms come from TextMergeQuick.java (for Merge Sort) and Quick.java (for Quick Sort), but are modified to remove generics (to make the code easier to read).

Merge Sort

public static void mergeSort(Integer[] a, int first, int last)
{
    Integer[] tempArray = new Integer[a.length];
    mergeSort(a, tempArray, first, last);
}

private static void mergeSort(Integer[] a, Integer[] tempArray, int first, int last)
{
    if (first < last)
    {
        // sort each half
        int mid = (first + last)/2;// index of midpoint
        mergeSort(a, tempArray, first, mid); // sort left half array[first..mid]
        mergeSort(a, tempArray, mid + 1, last); // sort right half array[mid+1..last]

        merge(a, tempArray, first, mid, last); // merge the two halves
    }
}

private static void merge(Integer[] a, Integer[] tempArray, int first, int mid, int last)
{
    // Two adjacent subarrays are a[beginHalf1..endHalf1] and a[beginHalf2..endHalf2].
    int beginHalf1 = first;
    int endHalf1 = mid;
    int beginHalf2 = mid + 1;
    int endHalf2 = last;

    // while both subarrays are not empty, copy the smaller item into the temporary array
    int index = beginHalf1; // next available location in tempArray
    for (; (beginHalf1 <= endHalf1) && (beginHalf2 <= endHalf2); index++)
    {
        // Invariant: tempArray[beginHalf1..index-1] is in order

        if (a[beginHalf1].compareTo(a[beginHalf2]) < 0)
        {
            tempArray[index] = a[beginHalf1];
            beginHalf1++;
        }
        else
        {
            tempArray[index] = a[beginHalf2];
            beginHalf2++;
        }
    }

    // finish off the nonempty subarray

    // finish off the first subarray, if necessary
    for (; beginHalf1 <= endHalf1; beginHalf1++, index++)
    {
        // Invariant: tempArray[beginHalf1..index-1] is in order
        tempArray[index] = a[beginHalf1];
    }

    // finish off the second subarray, if necessary
    for (; beginHalf2 <= endHalf2; beginHalf2++, index++)
    {
        // Invariant: tempa[beginHalf1..index-1] is in order
        tempArray[index] = a[beginHalf2];
    }
    
    // copy the result back into the original array
    for (index = first; index <= last; index++)
    {
        a[index] = tempArray[index];
    }
}

Quick Sort

public static void quickSort(Integer[] array, int first, int last)
{
    if (first < last)
    {
        // create the partition: Smaller | Pivot | Larger
        int pivotIndex = partition(array, first, last);

        // sort subarrays Smaller and Larger
        quickSort(array, first, pivotIndex-1);
        quickSort(array, pivotIndex+1, last);
    }
}

private static int partition(Integer[] a, int first, int last)
{
    int pivotIndex = last;
    T pivot = a[pivotIndex];

    // determine subarrays Smaller = a[first .. endSmaller]
    //                 and Larger = a[endSmaller+1 .. last-1]
    // such that elements in Smaller are <= pivot and
    // elements in Larger are >= pivot; initially, these subarrays are empty

    int indexFromLeft = first;
    int indexFromRight = last - 1;

    boolean done = false;
    while (!done)
    {
        // starting at beginning of array, leave elements that are < pivot;
        // locate first element that is >= pivot
        while (a[indexFromLeft].compareTo(pivot) < 0)
        {
            indexFromLeft++;
        }

        // starting at end of array, leave elements that are > pivot;
        // locate first element that is <= pivot
        while (a[indexFromRight].compareTo(pivot) > 0 && indexFromRight > first)
        {
            indexFromRight--;
        }

        // Assertion: a[indexFromLeft] >= pivot and
        //            a[indexFromRight] <= pivot.
        if (indexFromLeft < indexFromRight)
        {
            swap(a, indexFromLeft, indexFromRight);
            indexFromLeft++;
            indexFromRight--;
        }
        else
        {
            done = true;
        }
    }

    // place pivot between Smaller and Larger subarrays
    swap(a, pivotIndex, indexFromLeft);
    pivotIndex = indexFromLeft;

    // Assertion:
    // Smaller = a[first..pivotIndex-1]
    // Pivot = a[pivotIndex]
    // Larger = a[pivotIndex + 1..last]

    return pivotIndex;
}

private static void swap(Integer [] a, int i, int j)
{
    Integer temp = a[i];
    a[i] = a[j];
    a[j] = temp;
}

Practice Problems and Questions

Recursive Algorithms

Now that you have reviewed in detail how to analyze recursive algorithms, try determining the runtimes of these algorithms:

  1. Recursive Factorial: public long factorial (int N)
    {
        if (N < 0)
            throw new IllegalArgumentException();
        if (N <= 1)
            return 1;
        return N * factorial(N-1);
    }
  2. Recursive Sequential Search (non-generic form of SeqSArray.java, see also a Linked List version): public static int recSeqSearch(Integer [] a, Integer key, int first)
    {
        if (first >= a.length) // base case not found
            return -1;
        else if (a[first].compareTo(key) == 0) // base case found
            return first;
        else return recSeqSearch(a, key, first+1); // recursive case
    }
  3. Memoized Integer Exponentiation public long power(long X, long N)
    {
        if (N == 0) //base case
            return 1;
        else if (N == 1) //base case
            return X;
        else //recursive case
        {
            if (N % 2 == 0)
            {
                long result = power(X, N/2); //memoize
                return result * result;
            }
            else
            {
                long result = power(X, N/2); //memoize
                return result * result * X;
            }
        }
    }

Additional Questions

  1. The code for Merge Sort and Quick Sort above no longer uses generics. How would the analysis change if you were analysizing generic code instead?
  2. The recursive sequential search algorithm above has O(N). We saw earlier in the semester that the iterative sequential search also has O(N). So, does it matter which version you use in your programs? Why?

Final Note

Many recursive algorithms, especially divide and conquer algorithms, can be solved using Recurrence Relations. In fact, this is how you would need to analyze the non-memoized integer exponentiation algorithm. The Master Theorem makes solving many recurrence relations very quick and relatively easy. These are topics that will be covered in CS/COE 1501 (Algorithm Implementation), but it might be helpful to start learning them before CS/COE 1501.

Submission and Grading

This is for your practice. The final runtimes are provided for you so you can check your answer. If you have any questions about your analysis or your answers to the additional questions, please talk to the instructor or the TA.

Answers for Recursive Runtimes

  1. Recursive Factorial: O(N)
  2. Recursive Sequential Search: O(N)
  3. Memoized Integer Exponentiation: O(log(N))