Divide and Conquer

  • A recursive technique for solving problems
  • Ask yourself: “Can I solve this if I use divide and conquer?”
  • Two steps:
    • Figure out the base case — the simplest possible case
    • Divide and reduce the problem until it becomes the base case
  • Every recursive call must reduce the problem size
  • Base case for array problems is often an empty array or an array with one element
  • Functional languages like Haskell have no loops — recursion is the only option there

Quicksort

  • Much faster than selection sort

Design

  • Base cases — arrays that don’t need sorting:
    • Empty array
    • Array with one element
  • Array with two elements: compare and swap if needed

How It Works

  • Pick an element as the pivot
  • Partition — split remaining elements into two sub-arrays:
    • Elements less than the pivot
    • Elements greater than the pivot
  • Recursively sort both sub-arrays
  • Result: $quicksort(left) + pivot + quicksort(right)$
  • Pick a random element as pivot — don’t fixate on a specific position
  • In-place partitioning avoids extra lists; Lomuto is one common approach

Lomuto Partitioning

int[] array = [5, 3, 4, 7, 8, 0, 1, 10, 30];

var pivot = array[0];
var storeIndex = 1;
for (int i = 1; i < array.Length; i++)
{
    if (array[i] < pivot)
    {
        (array[i], array[storeIndex]) = (array[storeIndex], array[i]);
        storeIndex++;
    }
}
(array[0], array[storeIndex - 1]) = (array[storeIndex - 1], array[0]);

Inductive Proofs

  • A way to prove an algorithm works
  • Two steps: base case and inductive case
  • Prove it works for the base case (size 0, 1, 2)
  • Prove that if it works for size N, it works for size N+1 — and so on

Big O Revisited

  • Quicksort speed depends on pivot choice
  • Always picking the first element as pivot on a sorted array → worst case $O(n^2)$
  • With a random pivot → average case $O(n \log n)$
  • Merge sort is always $O(n \log n)$ in best, average, and worst case
  • Constants are dropped in Big O when comparing different complexities — e.g. $O(n)$ vs $O(\log n)$, the latter always wins at scale
  • Constants matter when complexities are the same — e.g. quicksort vs merge sort both at $O(n \log n)$, quicksort wins due to a smaller constant and hits average case most of the time

Exercises

4.1 Recursive Sum

int Sum(int[] numbers)
{
    if (numbers.Length == 1)
    {
        return numbers[0];
    }
    return numbers[0] + Sum(numbers[1..numbers.Length]);
}

4.2 Recursive Count

int Count(int[] items)
{
    if (items.Length == 1)
    {
        return 1;
    }
    return 1 + Count(items[1..items.Length]);
}

4.3 Recursive Max

int Max(int[] items)
{
    if (items.Length == 1)
    {
        return items[0];
    }

    var restMax = Max(items[1..items.Length]);
    return items[0] > restMax ? items[0] : restMax;
}
static int BinarySearch(int[] space, int key)
{
    return Search(space, key, 0, space.Length - 1);
}

static int Search(int[] space, int key, int lo, int hi)
{
    if (lo > hi)
    {
        return -1;
    }
  
    int mid = lo + (hi - lo) / 2;
  
    if (space[mid] == key)
    {
        return mid;
    }

    if (key > space[mid])
    {
        return Search(space, key, mid + 1, hi);
    }

    return Search(space, key, lo, mid - 1);
}