Lecture 2: Complexity - CM22008: Algorithms and Complexity

Measuring Program Runtime

  • Stopwatch (Idea 1): Ineffective due to the speed of modern computers.
  • Using the computer's time command (Idea 2): Provides insufficient resolution.
  • Instrumenting the code (Idea 3):
    • Use System.nanoTime() to measure execution time.
    • Example:
      java long start = System.nanoTime(); long end = System.nanoTime(); System.out.println("That took " + (end-start) +" ns.");
    • Inconsistent results may occur due to various factors.
  • Analyzing the Code (Idea 4): Determine the number of operations the code uses.

Analyzing Code Operations

  • Example code:

    for (int i=0; i<n; i++) {
      for (int j=0; j<n; j++) {
        System.out.println("Hello again, world!");
      }
    }
    
  • Outer loop analysis:

    • Initialization: 1
    • Comparison: n+1n + 1
    • Increment: nn
    • Total: 2n+22n + 2
  • Inner loop analysis:

    • Runs nn times.
    • Each time:
      • Initialize j: 1
      • Compare j<n: n+1n + 1
      • Increment j: nn
      • Print: nn
      • Total: 3n+23n + 2
    • Repeated nn times: 3n2+2n3n^2 + 2n
  • Overall total: 3n2+4n+23n^2 + 4n + 2

  • Focus on the dominant term for simplicity.

Graphing Runtime

  • Graphs illustrate the growth of runtime as input size increases.
  • The dominant term dictates the overall shape of the curve.
  • Comparing 3n2+4n+23n^2 + 4n + 2 to 3n23n^2 shows that the dominant term is significant.

Dominant Terms and Constant Factors

  • Dominant terms (e.g., 3x23x^2 in 3x2+4x3x^2 + 4x) dictate the growth rate.
  • Constant factors are less important because:
    • Operation counts do not precisely translate to runtime.
    • Faster computers or optimized compilers can alter the time by a constant factor.

Asymptotic Complexity: Big-O Notation

  • Definition: Given a function g(n)g(n), f(n)f(n) is O(g(n))O(g(n)) if there exist constants c > 0 and n<em>00n<em>0 ≥ 0 such that for all nn</em>0n ≥ n</em>0, 0f(n)cg(n)0 ≤ f(n) ≤ cg(n).
  • f(n)=O(g(n))f(n) = O(g(n)) means that eventually, f(n)f(n) is smaller than g(n)g(n) when g(n)g(n) is scaled by a constant factor.

Big-O Examples

  • n2n^2 is O(n3)O(n^3), O(n4)O(n^4), etc.
  • n2n^2 is O(n2)O(n^2)
  • 3n23n^2 is O(n2)O(n^2) (constant factors don't matter).
  • 3n2+2n+13n^2 + 2n + 1 is O(n2)O(n^2)
  • 1000n21000n^2 is not O(n)O(n)
  • n3n^3 is O(2n)O(2^n), but 2n2^n is not O(n2)O(n^2)
  • logn\log n is O(n)O(n)
  • nlognn \log n is O(n2)O(n^2)
  • O(1)O(1) means bounded by a constant.

Heuristics for Big-O

  • Exponentials grow faster than polynomials.
  • Polynomials grow faster than logarithms.
  • Higher-base exponentials grow faster than lower-base exponentials.
  • Higher-degree polynomials grow faster than lower-degree polynomials.
  • Logarithms are asymptotically the same regardless of their base.

Big-O Growth Comparisons

  • 3n3^n grows faster than 2n2^n.
  • 2n2^n grows faster than n2n^2.
  • n2n^2 grows faster than logn\log n.
  • (logn)2(\log n)^2 is even slower than logn2\log n^2 .
  • When terms are added, only the fastest-growing term matters.
  • Constant factors do not matter.

Terrible Sorting Algorithm

  • Selection Sort:

    • Find the minimum element in a<em>1,,a</em>na<em>1, …, a</em>n and swap it with a1a_1.
    • Find the minimum element in a<em>2,,a</em>na<em>2, …, a</em>n and swap it with a2a_2.
    • Continue this process.
  • Java Implementation:

    for (int i=0; i<a.length; i++){
      int j = findMin(a,i);
      swap(a,i,j);
    }
    
    public static int findMin(int[] a, int i) {
      int m=i;
      for (int j=i; j<a.length; j++) {
        if (a[j]<a[m]) m = j;
      }
      return m;
    }
    

Complexity Analysis of Selection Sort

  • The loop runs nn times.
  • Each time, it computes a minimum and performs constant-time operations (swap, increment).
  • Computing the minimum of nn elements takes nn steps.
  • Total time complexity: T(n)=n+k+(n1)+k++2+k+1+kT(n) = n + k + (n-1) + k + … + 2 + k + 1 + k
  • Simplified: T(n)=n(n+1)2+nkT(n) = \frac{n(n+1)}{2} + nk
  • Therefore, T(n)=O(n2)T(n) = O(n^2)

Big-O: Algorithm Complexity Definition

  • An algorithm has O(g(n))O(g(n)) complexity if the number of operations to compute a solution on input size nn is O(g(n))O(g(n)).
  • Selection sort has O(n2)O(n^2) complexity (quadratic complexity).
  • O(5n2+9)O(5n^2 + 9), O(n2/120314n)O(n^2/1203 - 14n) are equivalent to O(n2)O(n^2).
  • Big-O's robustness to constant factors means that the implementation language (Java, Python) or the speed of the computer does not affect the O(n2)O(n^2) complexity.

Shortcuts in Complexity Calculation

  • Constant-time operations within a loop are dominated by the O(n)O(n) operation (finding the minimum).
  • Running an O(n)O(n) operation nn times gives O(n2)O(n^2) complexity.

Overestimation in Big-O

  • Shortcuts may overestimate the complexity if the O(n)O(n) operation inside the loop is faster than linear time.
  • Concluding that the algorithm is O(n2)O(n^2) remains correct because big-O provides an overestimate.
  • For selection sort, it's not an overestimate.

Big-Omega and Big-Theta

  • Big-O: Upper bound on the rate of growth.
  • Big-Omega (Ω): Lower bound on the rate of growth.
    • Given g(n)g(n), f(n)f(n) is Ω(g(n))Ω(g(n)) if there exist constants c > 0 and n<em>00n<em>0 ≥ 0 such that for all nn</em>0n ≥ n</em>0, 0cg(n)f(n)0 ≤ cg(n) ≤ f(n).
  • Big-Theta (Θ): Exact (asymptotic) complexity.
    • Given g(n)g(n), f(n)f(n) is Θ(g(n))Θ(g(n)) if it is both O(g(n))O(g(n)) and Ω(g(n))Ω(g(n)).
    • There are positive constants c<em>1>0c<em>1 > 0, c2 > 0, and n<em>00n<em>0 ≥ 0 such that for all nn</em>0n ≥ n</em>0, 0c<em>1g(n)f(n)c</em>2g(n)0 ≤ c<em>1g(n) ≤ f(n) ≤ c</em>2g(n).

Big-O, Big-Omega, and Big-Theta Examples

  • Big-O examples work as Big-Omega in reverse. For example, n3n^3 is Ω(n2)Ω(n^2).
  • 2n2^n is Ω(nk)Ω(n^k) for any fixed kk.
  • A polynomial n3+2n+7n^3 + 2n + 7 is Θ(n3)Θ(n^3).
  • Selection sort's running time is Θ(n2)Θ(n^2).

Summary

  • Asymptotic complexity is a more robust measure of runtime than direct measurement.
  • Big-O expresses upper bounds on running time.
  • Big-Omega expresses lower bounds.
  • Big-Theta expresses exact bounds (both upper and lower).
  • Selection sort is a Θ(n2)Θ(n^2) sorting algorithm.

Quiz Questions

Suppose we have two algorithms A and B. A is O(n)O(n); B is O(logn)O(\log n).

  1. What’s the complexity of an algorithm that runs algorithm A exactly three times?
    • O(n)O(n)
  2. What’s the complexity of running A then B?
    • O(n)O(n)
  3. What’s the complexity of running B nn times?
    • O(nlogn)O(n \log n)
  4. What’s the complexity of running A twice, then B nn times?
    • O(nlogn)O(n \log n)
  5. What’s the complexity of running A twice, then B n2n^2 times?
    • O(n2logn)O(n^2 \log n)