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:
10 | 6 | 1 |
20 | 12 | 6 |
30 | 18 | 17 |
40 | 24 | 25 |
50 | 28 | 40 |
60 | 30 | 63 |
70 | 38 | 82 |
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 .
Example: Big-O Intuition
Here, with and as shown.
Note that does not necessarily have to be below for all - only past some cutoff .
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.
Example: Proving Big-O Functions (1)
Prove that
Observe that for all ,
Now, let and . Because for ,
By definition, we’ve shown that .
Example: Proving Big-O Functions (2)
Prove that
Observe that for all ,
Thus, we’ve found a Big-O function with and .
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 .
Example: Big-Omega Intuition
Here, with and as shown.
Similar to Big-O, we can prove by finding values satisfying our definition. See the example below.
Example: Big-Omega Proof (1)
Prove that .
Observe that when which occurs at . Then, for ,
Thus, choosing and , we have shown that !
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.
Example: Big-O, Proof by Limits
Observe that
Thus, .
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 !
Example: Big-O Misconceptions
Suppose we have two algorithms
We may intuitively be led to think that is automatically better. However, this is not true - this only tells us that is better for sufficiently large , but does not tell us which is better for small values .
We’ll have algorithms later in the course where they may be efficient for large values , but terribly inefficient for small 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:
- Precedence: We don’t need to include a time function () provided there is a faster growing function in the analysis.
- Loops: We don’t need to include the time it takes for a loop iteration to occur, provided the loop body takes time.
- 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
- We can ignore , as it has an insignificant effect on the time complexity compared to the loop.
- 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!
- Using the relation to expand itself, we can continuously expand it until we find some general pattern.
- Then, we solve for when this pattern ends with regard to the base case.
- 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 !
Example: Digging Down (2)
We expand it to find the following pattern:
We find this pattern stops when we have
Giving us final time complexity .
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.
Level | Num Nodes | Time / Node | Level Total |
---|---|---|---|
Using this table, we find total sum as the sum of the level totals (rightmost column).
Example: Recurrence Tree
Consider the recurrence tree given by
We find its time complexity by drawing a recurrence tree.
graph TD r[sqrt n + 1]; n1[sqrt n/4 + 1]; n2[sqrt n/4 + 1]; r -.-> n1 & n2; n3[T n/8]; n4[T n/8]; n5[T n/8]; n6[T n/8]; n1 -.-> n3 & n4; n2 -.-> n5 & n6;
Looking at the leaves of this tree, we find that they are, progressively, , where indicates the level in the tree. Given our base case , we find our leaf level at .
Summing up all our results to find our total time, we have the following table.
Level Num Nodes Time / Node Level Total We find our total time as
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:
If and then .
If and then
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
Example: Master Theorem (2)
First, observe that . Furthermore, observe that
We see that . Thus, we cannot apply property (2) of the master theorem.
However, if , then it is also ! Because , we find (by property 3)