A Systematic Approach to Algorithms

Vijay K. Garg · The University of Texas at Austin

Chapter 1. Introduction to Algorithms

Foundations: what an algorithm is, how to measure its efficiency, and the data structures we will rely on.

What is in this chapter

Computers are fast, but algorithms are what make them useful. The same problem can admit algorithms whose running times differ by factors of millions on inputs that already fit comfortably in memory: sorting a billion records takes minutes with a good algorithm but centuries with a bad one. This chapter assembles the preliminaries that the rest of the book relies on:

Euclid's GCD

Euclid's GCD repeatedly subtracts the smaller of two integers from the larger until the two are equal. This is the classical imperative form. In Chapter 2, the same idea is expressed as a search for the least vector in a lattice, yielding the LLP algorithm LLP-GCD.

Time complexity: $O(\log \min(a, b))$, where $a$ and $b$ are the two inputs.

int GCD(int a, int b) {
  while (a != b) {
    if (a > b) { a = a - b; };
    if (b > a) { b = b - a; }
  };
  return a;
}

Factorial

Recursive factorial; the textbook example for the recursion form $T(n) = n \cdot T(n-1)$.

Time complexity: $O(n)$.

int compute(int n) {
  if (n == 0) { return 1; };
  return n * compute(n - 1);
}

LinearSearch

Linear scan for key in array $A$. Returns the first matching index, or $-1$. Worst case $O(n)$.

Time complexity: $O(n)$, where $n$ is the size of the array.

int find(int[] A, int key) {
  int i = 0;
  while (i < A.length) {
    if (A[i] == key) { return i; };
    i = i + 1;
  };
  return -1;
}

MaxElement

One-pass scan for the maximum element.

Time complexity: $O(n)$, where $n$ is the size of the array.

int find(int[] A) {
  int max = A[0];
  int i = 1;
  while (i < A.length) {
    if (A[i] > max) { max = A[i]; };
    i = i + 1;
  };
  return max;
}

BinarySearch

Recursive binary search on a sorted array, $O(\log n)$.

Time complexity: $O(\log n)$, where $n$ is the size of the array.

int find(int[] A, int key, int low, int high) {
  if (low > high) { return -1; };
  int mid = (low + high) / 2;
  if (A[mid] == key) { return mid; };
  if (A[mid] > key) { return find(A, key, low, mid - 1); };
  return find(A, key, mid + 1, high);
}

Towers of Hanoi

The canonical $T(n) = 2T(n-1) + 1$ recurrence; the body captures the recursive structure (the actual disk move appears as a comment in the base case).

Time complexity: $O(2^n)$ moves to relocate $n$ disks.

void solve(int n, int source, int aux, int target) {
  if (n == 1) {
    // Move disk from `source` to `target`.
  } else {
    solve(n - 1, source, target, aux);
    // Move disk from `source` to `target`.
    solve(n - 1, aux, source, target);
  }
}

NestedSum

A textbook $O(n^2)$ double loop, used to motivate Big-O analysis.

Time complexity: $O(n^2)$.

int compute(int n) {
  int total = 0;
  int i = 0;
  while (i < n) {
    int j = 0;
    while (j < n) {
      total = total + i * j;
      j = j + 1;
    };
    i = i + 1;
  };
  return total;
}

PairSum

Counts pairs $(i, j)$ with $i < j$ such that $A[i] + A[j] = $ target. Naive $O(n^2)$; the classic two-pointer / hash-map improvement is left as an exercise in the book.

Time complexity: $O(n^2)$, where $n$ is the size of the array.

int count(int[] A, int target) {
  int c = 0;
  int i = 0;
  while (i < A.length) {
    int j = i + 1;
    while (j < A.length) {
      if (A[i] + A[j] == target) { c = c + 1; };
      j = j + 1;
    };
    i = i + 1;
  };
  return c;
}

MergeSort

Recursive divide-and-conquer sort, $O(n \log n)$. Uses an auxiliary Merge aux method declared in the same class — an early example of LL's aux-method support.

Time complexity: $O(n \log n)$, where $n$ is the size of the array.

void sort(int[] A, int low, int high) {
  if (low < high) {
    int mid = (low + high) / 2;
    sort(A, low, mid);
    sort(A, mid + 1, high);
    Merge(A, low, mid, high);
  }
}

void Merge(int[] A, int low, int mid, int high) {
  int[] B = new int[A.length];
  forall k in [low..high] : B[k] = A[k];
  int i = low;  int j = mid + 1;  int k = low;
  while (i <= mid && j <= high) {
    if (B[i] <= B[j]) { A[k] = B[i]; i = i + 1; }
    else              { A[k] = B[j]; j = j + 1; };
    k = k + 1;
  };
  while (i <= mid)  { A[k] = B[i]; i = i + 1; k = k + 1; };
  while (j <= high) { A[k] = B[j]; j = j + 1; k = k + 1; }
}

Heap

Min-heap operations on a flat array: heapify, build, insert, extractMin. Heaps anchor Dijkstra and Prim later in the book.

Time complexity: build $O(n)$; heapify, insert, and extractMin are $O(\log n)$, where $n$ is the heap size.

class Heap {
  void build(int[] A, int n) {
    int i = n / 2 - 1;
    while (i >= 0) { heapify(A, i, n); i = i - 1; }
  }

  void heapify(int[] A, int i, int n) {
    int smallest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    if (left < n && A[left] < A[smallest])  { smallest = left; };
    if (right < n && A[right] < A[smallest]) { smallest = right; };
    if (smallest != i) {
      swap(A[i], A[smallest]);
      heapify(A, smallest, n);
    }
  }

  int insert(int[] A, int key, int n) {
    n = n + 1;
    int i = n - 1;
    while (i > 0 && A[(i - 1) / 2] > key) {
      A[i] = A[(i - 1) / 2];
      i = (i - 1) / 2;
    };
    A[i] = key;
    return n;
  }

  int extractMin(int[] A, int n) {
    if (n < 1) { return -2147483648; };
    int min = A[0];
    A[0] = A[n - 1];
    n = n - 1;
    heapify(A, 0, n);
    return min;
  }
}

Looking ahead

The asymptotic vocabulary introduced in this chapter is used pervasively throughout the book. The data structures play their part as well: heaps anchor Dijkstra's algorithm in Chapter 9(Shortest Paths), union-find drives Kruskal in Chapter 10(MST), linked lists and arrays are the canonical input formats for the LLP algorithms of Chapter 2.