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 n. Now suppose we run these algorithms on various size lists, and obtain the following runtime data:
n
A1(n)
A2(n)
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 A1(n) is approximately better than A2(n) for n≤40.
Is there a better way to formalize this comparison?
Well, yes! Suppose we know these functions stay within the bounds
0.4n≤0.01n2≤A1(n)≤0.6nA2(n)≤0.02n2
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 f(x).
Big-O
We say f(x)=O(g(x)) to mean:
∃x0,Csuch thatx≥x0→f(x)≤Cg(x)
In other words, O(g(x)) is a function such that for large values x (x>x0), f(x) is less than some multiple of g(x)!
We call O(g(x)) the Big-O of f(x). We can intuitively think of O(g(x)) as an upper bound for f(x).
Example: Big-O Intuition
Here, f(x)=O(x2) with C=2 and x0 as shown.
Note that f(x) does not necessarily have to be below Cx2 for all x - only past some cutoff x0.
Given this definition, to prove that a function f(x)=O(g(x)), we need to find some x0 and C and explicitly show it satisfies this definition. See the below examples.
Example: Proving Big-O Functions (1)
Prove that 2xlog(x)−1=O(xlog(x))
Observe that for all x>0,
2xlog(x)−1≤2xlog(x)
Now, let C=2 and x0=1. Because for x>x0,
2xlog(x)−1≤Cxlog(x)
By definition, we’ve shown that 2xlog(x)−1=O(xlog(x)).
Example: Proving Big-O Functions (2)
Prove that 4x2+xlog(x)−1=O(x2)
Observe that for all x>0,
4x2+xlog(x)−1≤4x2+x2=5x2≤4x2+xlog(x)
Thus, we’ve found a Big-O function O(x2) with C=5 and x0=1.
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 f(x).
For example, suppose f(x)=O(x2). If this is the case, then we can also say that
f(x)f(x)=O(x3)=O(x4)⋮
Big-Omega
Additionally, we can say Ω(g(x)) to mean:
∃x0,B>0 such that ∀x≥x0→f(x)≥Bg(x)
In other words, Ω(g(x)) is a function such for large values x (x>x0), f(x) is greater than some multiple of g(x) for all x!
We call Ω(g(x)) the Big-Omega of f(x). We can intuitively think of Ω(g(x)) as a lower bound for f(x).
Example: Big-Omega Intuition
Here, f(x)=Ω(x2) with B=21 and x0 as shown.
Similar to Big-O, we can prove f(x)=Ω(g(x)) by finding values x0,B satisfying our definition. See the example below.
Example: Big-Omega Proof (1)
Prove that 5xlg(x)−x=Ω(xlg(x)).
Observe that x≤xlg(x) when lg(x)≥1 which occurs at x≥2. Then, for x≥2,
5xlg(x)−x≥5xlg(x)−xlg(x)≥4xlg(x)
Thus, choosing B=4 and x0=2, we have shown that 5xlg(x)−x=Ω(xlg(x))!
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 Θ(g(x)) to mean
∃B>0,C>0,x0 such that x≥x0→Bg(x)≤f(x)≤Cg(x)
In other words, Θ(g(x)) is a function that serves as both an upper and lower bound for f(x)!
We call Θ(g(x)) the Big-Theta of f(x). To prove Θ(g(x)), we simply show that f(x)=O(g(x)) and f(x)=Ω(g(x)).
We should generally prefer to use Big-Theta when possible.
If we cannot easily find explicit values x0,B,C satisfying our definition, we can also employ limits to prove Θ(g(x)).
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 n, where n can be the length of a list, loop iterations, etc. Based on this algorithm, we can form a function T(n) representing its runtime complexity.
How can we compare these functions, when there are potentially limitless T(n) 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:
1,lg(n),n,nlg(n),n2,n2lg(n),…
Use of n over x does not typically matter, but we tend to prefer n when we’re restricted to integers.
Choosing the respective Big-Theta function for our time function, T(n)=Θ(g(n)), 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 T(n), the Θ(g(n)) is often the fastest growing term.
For example,
n2lg(n)+nlg(n)−n+lg(n)−7=Θ(n2lg(n))
Note that a slow-growing Θ(g(n)) for an algorithm does not necessarily mean its good for all values n!
Example: Big-O Misconceptions
Suppose we have two algorithms
T1(n)=Θ(n)T2(n)=Θ(n2)
We may intuitively be led to think that T1 is automatically better. However, this is not true - this only tells us that T1(n) is better for sufficiently large n, but does not tell us which is better for small values n.
We’ll have algorithms later in the course where they may be efficient for large values n, but terribly inefficient for small values!
Determining T(n)
Say we have a block of code with some input whose size is n, and we want to know T(n)=Θ(g(n)).
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 (n,n2,…) 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 timefor i to n: # b time for each iteration sum += i # c time
We would find total time T(n)=n⋅(b+c)+a. However, if all we want is the most relevant time complexity, then
We can ignore a, as it has an insignificant effect on the time complexity compared to the loop.
We can ignore n⋅b, as the body of the loop takes time (n⋅c).
This gives us T(n)=n⋅c.
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 n.
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 T(n) 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 T(n)) in terms of earlier values.
Typically, we’ll have one (or more) base cases as well.
Example: Recurrence Relations
T(n)=T(n/2)+cT(1)=d
Where c,d are constants.
This is in fact the recurrence relation for binary search, as the time required to process a list of length n equals the check of the list’s center, plus the time required to process the subsequent list of length n/2.
Given these recurrence relations, we can solve for specific values of T(n)! We can do this by recursively plugging in n0 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 n0.
Another method of solving recurrence relations involves the use of a recurrence tree. See the example below.
Consider the recurrence relation given by
T(n)=2T(n/2)+nT(1)=3
Say we want to find some value T(n). We can do this by drawing a tree to represent our summation, where every level i (and its leaves) represents the result of T(n/2i). The combination of all the nodes in this tree will therefore sum to T(n).
Looking at the leaves of this tree, we find that they are, progressively, T(n/4i), where i≥0 indicates the level in the tree. Given our base case T(1), we find our leaf level at i=log4(n).
Summing up all our results to find our total time, we have the following table.
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:
T(n)=aT(n/b)+f(n)
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 T(n)=aT(n/b)+f(n), with a,b∈Z+ and b>1.
Then, we have 3 possible cases:
If f(n)=O(nc) and logba>c then T(n)=Θ(nlogb(a)).
If f(n)=Θ(nclgk(n)) and logb(a)=c then T(n)=Θ(nlogb(a)lgk+1(n))
If f(n)=Ω(nc) and logb(a)<c then T(n)=Θ(f(n)).
Note: Case (3) requires a regularity condition on f(n), which generally will be satisfied by most functions (and thus will not be our primary focus).
Example: Master Theorem (1)
T(n)=4T(n/2)+n2+lg(n)
First, observe that a=4,b=2,log2(4)=2. Furthermore, observe that f(n)=n2+lg(n).
We can see that f(n)=Θ(n2), and furthermore, logba=lg(2)=2=c. Thus, we can use property (2) of the master theorem.
By property (2) of the master theorem, we have
T(n)=Θ(n2lg(n))
Example: Master Theorem (2)
T(n)=3T(n/4)+nlg(n)+1
First, observe that a=3,b=4,andlog4(3)>0. Furthermore, observe that
f(n)=nlg(n)+1=Θ(nlg(n))
We see that log4(3)=c=1. Thus, we cannot apply property (2) of the master theorem.
However, if f(n)=Θ(nlg(n)), then it is also f(n)=Ω(n)! Because log4(3)<1, we find (by property 3)