In this section, we discuss various methods we can use to analyze the efficiency of algorithms. Here, we discuss two main methods of analysis - Big O Notation, and Recurrence Relations.

Big-O Notation

Motivation

Suppose we have 2 different algorithms operating on a list of length . Now suppose we run these algorithms on various size lists, and obtain the following runtime data:

1061
20126
301817
402425
502840
603063
703882

We could use this table to compare the two algorithms based on their time values. For example, we can see that is approximately better than for .

Is there a better way to formalize this comparison?

Well, yes! Suppose we know these functions stay within the bounds

These bounds could be used to more formally compare the two algorithms!

The purpose of Big-O Notation is to provide a more formalized argument for performing these comparisons.

Big-O Notation

Suppose we have a function .

Big-O

We say to mean:

In other words, is a function such that for large values (), is less than some multiple of !

We call the Big-O of . We can intuitively think of as an upper bound for .

Given this definition, to prove that a function , we need to find some and and explicitly show it satisfies this definition. See the below examples.

Non-Uniqueness of Big-O Functions

Note that Big-O functions are not unique! There are a multitude of functions that can satisfy the Big-O of .

For example, suppose . If this is the case, then we can also say that

Big-Omega

Additionally, we can say to mean:

In other words, is a function such for large values (), is greater than some multiple of for all !

We call the Big-Omega of . We can intuitively think of as a lower bound for .

Similar to Big-O, we can prove by finding values satisfying our definition. See the example below.

Big-Theta

Note that while Big-O provides an upper bound, and Big-Omega provides a lower bound, a variety of functions can serve as these upper / lower bounds! Thus, Big-O and Big-Theta may not necessarily provide a good benchmark for a function.

To address this, we define to mean

In other words, is a function that serves as both an upper and lower bound for !

We call the Big-Theta of . To prove , we simply show that and .

We should generally prefer to use Big-Theta when possible.

If we cannot easily find explicit values satisfying our definition, we can also employ limits to prove .

Theorem: Limit Theorem, Big-O

Let and be functions such that

exist.

Then, the following are true.

Thus, if both hold true, then !

We apply the limit theorem in an example below.

Code Analysis with Big-O

Nice Functions

Big-O notation is especially important in computer science, as it serves as a benchmark for comparing algorithms with one another.

Typically we’ll have some algorithm that depends on some varying , where can be the length of a list, loop iterations, etc. Based on this algorithm, we can form a function representing its runtime complexity.

How can we compare these functions, when there are potentially limitless functions to compare?

Well, computer scientists have settled on a collection of nice functions which easily be compared with one another. A non-comprehensive list of these functions is given below, in order of slowest to fastest growing:

Use of over does not typically matter, but we tend to prefer when we’re restricted to integers.

Choosing the respective Big-Theta function for our time function, , will give us an idea on how fast (or slow) our algorithm runs!

Big-O Algorithm Analysis: Intuition

Apart from using definitions and limit theorems, we can often intuitively find a function’s Big-Theta! For a given function , the is often the fastest growing term.

For example,

Note that a slow-growing for an algorithm does not necessarily mean its good for all values !

Determining

Say we have a block of code with some input whose size is , and we want to know .

Note that every statement in a block of code has overhead, and takes time. However, some statements “matter more” than others, as they have more meaningful contributions to the time complexity than others.

We can generalize this in the following rules:

  1. Precedence: We don’t need to include a time function () provided there is a faster growing function in the analysis.
  2. Loops: We don’t need to include the time it takes for a loop iteration to occur, provided the loop body takes time.
  3. Conditionals: We don’t need ot include the time it takes for a conditional to evaluate, provided the conditional body takes time.

See the below example.

Example: Algorithm Analysis

Suppose we have the following, with each statement taking some amount of time:

sum = 0 # a time
for i to n: # b time for each iteration
    sum += i # c time

We would find total time . However, if all we want is the most relevant time complexity, then

  1. We can ignore , as it has an insignificant effect on the time complexity compared to the loop.
  2. We can ignore , as the body of the loop takes time ().

This gives us .

Auxilliary Space

Oftentimes, alongside time complexity analysis, it may be helpful to analyze the amount of extra space (auxilliary space) the algorithm uses, in terms of the input size .

When calculating auxilliary space, we do not count the memory used by the input.

Recurrence Relations

While Big-O Notation is powerful, we sometimes will have difficulty termining the of an algorithm, which will prevent us from determining a Big-O. Many of these cases, in fact, turn out to be in recursive functions.

Here, we propose a method of determining the time complexity of reursive functions, using recurrence relations.

A recurrence relation is an equation defining the value of a function (often ) in terms of earlier values.

Typically, we’ll have one (or more) base cases as well.

Example: Recurrence Relations

Where are constants.

This is in fact the recurrence relation for binary search, as the time required to process a list of length equals the check of the list’s center, plus the time required to process the subsequent list of length .

Given these recurrence relations, we can solve for specific values of ! We can do this by recursively plugging in into our recurrence relation, until we reach a base case.

Example: Solving for Specific Values

Consider the previous recurrence relation.

We can find various times given specific input sizes .

Note that recurrence relations often will need ceilings or floors to ensure that our values remain as integers (otherwise things don’t make sense).

Technique 1: Digging Down

We can use these recurrence relations to determine of a function!

  1. Using the relation to expand itself, we can continuously expand it until we find some general pattern.
  2. Then, we solve for when this pattern ends with regard to the base case.
  3. Finally, we sub in this base case to find our final time complexity.

This technique is known as digging down.

Note that this technique only works for simple relations which have an easily identifiable pattern.

Example: Digging Down

Consider the previous recurrence relation.

Notice that

Note that by our base case, we will stop this expansion when

Giving us a final value of

This gives us a time complexity of !

Technique 2: Recurrence Trees

Another method of solving recurrence relations involves the use of a recurrence tree. See the example below.

Consider the recurrence relation given by

Say we want to find some value . We can do this by drawing a tree to represent our summation, where every level (and its leaves) represents the result of . The combination of all the nodes in this tree will therefore sum to .

graph TD
      r[n] -.-> t1[n/2] & t2[n/2];

      t1 -.-> t3[n/4] & t4[n/4];
      t2 -.-> t5[n/4] & t6[n/4];
      
      t3 -.-> t7[n/8] & t8[n/8];
      t4 -.-> t9[n/8] & t10[n/8];
      t5 -.-> t11[n/8] & t12[n/8];
      t6 -.-> t13[n/8] & t14[n/8];

Note that every level represents the result of . Thus, we can find when our levels end by finding when

We can then sum up all our results to find our total time. We make a table to represent this for us.

LevelNum NodesTime / NodeLevel Total

Using this table, we find total sum as the sum of the level totals (rightmost column).

Master Theorem

Note that in many of the above recurrence relation examples, there’s a common pattern among them. It appears as though we can generalize most recurrence relations in the form:

Given this general form, can we build a general rule for solving such recurrence relations?

Well, yes! Such a rule is given by the master theorem, described below.

Note that the master theorem provides one of the lowest level generalizations for recurrence relations. There are numerous other higher-level theorems we do not cover!

Theorem: Master Theorem

Suppose we have recurrence relation given by , with and .

Then, we have 3 possible cases:

  1. If and then .

  2. If and then

  3. If and then .

Note: Case (3) requires a regularity condition on , which generally will be satisfied by most functions (and thus will not be our primary focus).

Example: Master Theorem (1)

First, observe that . Furthermore, observe that .

We can see that , and furthermore, . Thus, we can use property (2) of the master theorem.

By property (2) of the master theorem, we have