Measuring Runtime

Evaluating algorithms

There are many possible ways to measure how successful an algorithm is. Correctness, efficiency or simplicity are all valid aspects to evaluate. In this course, we will be looking in-depth at the runtime of an algorithm.

The runtime can vary widely based on several factors such as the input, the code implementation and the machine used to execute the program. We will be looking at different algorithms at a high level; to account for different languages and implementations we examine these algorithms in pseudocode.

Worst case vs average

When measuring runtime, there are many different perspectives that we can take.

The worst case of an algorithm specifies the maximum number of computations performed on an input of size n. This can be good at providing an upper bound but it is not very accurate when trying to predict real-world statistics.

The average of an algorithm specifies the average number of computations performed on an input of size n. This can be a more realistic estimation of an algorithm but there is debate about what ‘average’ actually means, as well as this calculation being much more complicated or even sometimes impossible.

Big-O notation

The official definition is:

Let f,g:NR be functions. We say that f is O(g) if there is some n0N and some c>0 from R such that for all nn0 we have:\text{Let } f, g: \Bbb{N} \rightarrow \Bbb{R} \text{ be functions. We say that } f \text{ is } O(g) \text{ if there is some } n_0 \in \Bbb{N} \text{ and some } c \gt 0 \text{ from } \Bbb{R} \text{ such that for all } n \geq n_0 \text{ we have:}

0f(n)cg(n)0 \leq f(n) \leq cg(n)

What this translates to, is that by ‘f is O(g)f \text{ is } O(g)’ we are saying that ‘for sufficiently large n’, fO(g)f \in O(g). This tells us the growth rate of the runtime of an algorithm ff is no greater than that of gg.

When writing this in practice, we use the form f=O(g)f = O(g). For example:

  • 3n3=O(n3)3n^3 = O(n^3) with c=3c = 3 and n0=0n_0 = 0

  • 3n2+6=O(n2)3n^2 + 6 = O(n^2) with c=9c = 9 and n0=1n_0 = 1

  • lg(n)=O(n)lg(n) = O(n) with c=1c = 1 and n0=1n_0 = 1

  • 8n2+150n+10000=O(n2)8n^2 + 150n + 10000 = O(n^2) with c=10158c = 10158 and n0=1n_0 = 1

There may be more than one correct set of values for cc and n0n_0 in each case.

Laws of big-O

  1. For any constant a>0a \gt 0 in R:f1=O(g1)af1O(g1)\Bbb{R}: f_1 = O(g_1) \Rightarrow af_1 \in O(g_1)

  2. f1=O(g1)f_1 = O(g_1) and f2=O(g2)f1+f2=O(g1+g2)f_2 = O(g_2) \Rightarrow f_1 + f_2 = O(g_1 + g_2)

  3. f1=O(g1)f_1 = O(g_1) and f2=O(g2)f1f2=O(g1g2)f_2 = O(g_2) \Rightarrow f_1 f_2 = O(g_1 g_2)

  4. f1=O(g1)f_1 = O(g_1) and g1=O(g2)f1=O(g2)g_1 = O(g_2) \Rightarrow f_1 = O(g_2)

  5. For any dN:d \in \Bbb{N}: if f1f_1 is polynomial of degree dd with positive leading coefficient then f1=O(nd)f_1 = O(n^d)

  6. For any constants a>0a \gt 0 and b>1b \gt 1 in R:na=O(bn)\Bbb{R}: n^a = O(b^n)

  7. For any constants a>0a \gt 0 in R:lg(na)=O(lg(n))\Bbb{R}: lg(n^a) = O(lg(n))

  8. For any constants a>0a \gt 0 and b>0b \gt 0 in R:lga(n)=O(nb)\Bbb{R}: lg^a(n) = O(n^b)

Big-Ω notation

The official definition is:

Let f,g:NR be functions. We say that f is Ω(g) if there is some n0N and some c>0 from R such that for all nn0 we have:\text{Let } f, g: \Bbb{N} \rightarrow \Bbb{R} \text{ be functions. We say that } f \text{ is } \Omega(g) \text{ if there is some } n_0 \in \Bbb{N} \text{ and some } c \gt 0 \text{ from } \Bbb{R} \text{ such that for all } n \geq n_0 \text{ we have:}

0cg(n)f(n)0 \leq cg(n) \leq f(n)

Similar to big-O setting an upper bound, this tells us the growth rate of the runtime of an algorithm ff is no less than that of gg. Big-Ω\Omega sets a lower bound.

Big-Θ notation

Now that you can work out the upper and lower bounds of a given algorithm, there is one more notation to be aware of. An algorithm ff is said to be Θ(g)\Theta(g) if f=O(g)f = O(g) and $f = \Omega(g)$.

The Master Theorem

The Master Theorem is used for asymptotic analysis of algorithms in the form of recurrence relations. The theorem is given below:

Let n0N, kN0 and a, bR with a>0 and b>1, and let T : NN satisfy the following condition: \text{Let } n_0 \in \N, \text{ } k \in \N_{0} \text{ and a, b} \in \R \text{ with } a \gt 0 \text{ and } b \gt 1, \text{ and let } \newline \text{T : } \N \rightarrow \N \text{ satisfy the following condition: }

T(n) = {Θ(1), if n<n0aT(nb)+Θ(nk), if n n0\text{T(n) = } \begin{cases} \Theta(1), &\text{ if n} \lt n_{0} \\\\ aT(\frac{n}{b}) + \Theta(n^{k}), &\text{ if n } \geq n_{0} \end{cases}

Let e=logb(a); we call e the critical exponent. Then: \text{Let } e = \log_{b}(a) \text{; we call } e \text{ the critical exponent. Then: }

T(n) = {Θ(ne), if k <eΘ(nelg(n)), if k =eΘ(nk), if k >e\text{T(n) = } \begin{cases} \Theta(n^{e}), &\text{ if k } \lt e \\\\ \Theta(n^{e}lg(n)), &\text{ if k } = e \\\\ \Theta(n^{k}), &\text{ if k } \gt e \end{cases}

In-Place

An algorithm is said to be in-place if it can implement on the input array with only O(1)O(1) extra space. For example, Quick Sort and Insertion Sort.

Stable

An algorithm is said to be stable if for every pair of indices with A[i].key=A[j].keyA[i].key = A[j].key and $i \lt j, $ the entry A[i]A[i] comes before A[j]A[j] in the output array. For example, Insertion Sort and Merge Sort.