Overview

Part 2 - Complexity (1)

Introduction

What do we mean by an ‘Algorithmic complexity’, a good (but not fully formal) definition would be - “How much resources does the algorithm use in relation to quantitative properties of its input”

And just to be clear, the definition of an algorithm: any well-defined procedure to solve a given problem.

So what kind of resources could we quantify from an input? Of course the runtime of an algorithm is important. How much memory/space an algorithm requires to operate, is often important as well. These are the main two resources that are looked upon when writing new algorithms - but we could also look at, # of comparisons, # of array accesses, # of arithmetic operations performed etc.

The quantitative properties of the input are important here as well, the time it takes to find the first 10 digits of π\pi is obviously going to be less than the first 100 000 digits of π\pi.

Therefore, we can view the complexity as a mathematical function; Which takes an input of (size) n - maps it to the function T(n)T(n) that outputs the given runtime/space it takes for an input of nn.

In pure mathematics (and Computer Science) this type of functions are called ‘Big O Notation’1

One thing to remember is - that in the real world the only thing that determines the output isn’t just the input. For example an input of nn doesn’t always give T(n)T(n) for a given quicksort algorithm.

That’s why computer scientists use Worst-case, Best-case, and Average-case complexities.

As said before the exact runtime of a program depends on so many things - that’s why we introduce the so-called Asymptotic complexity. Which is the order of growth of the complexity function - which holds true for all complexity functions within the same ‘class’. This is often what computer scientists mean when they say that an algorithm has a complexity of …\dots.

A Computer Scientist Definition of Complexity

Definition 1 (Order of growth)

Let ff and gg be functions. ff has an order of growth of gg if there are constants CC and n0n_0 such that,

f(n)≤Cg(n) for n≥n0.f(n) \leq C g(n) \text{ for } n \geq n_0.

What this means if we decipher the math; f(n)f(n) is eventually bounded by Cg(n)C g(n), after a certain point, n0n_0.

With this in our toolkit we can now use the O-notation:

f(n)∈O(g(n))f(n) \in \mathcal{O}(g(n))
Example 1

Say we have the function f(n)=13n+37f(n) = 13n + 37 - I claim that the (Asymptotic) complexity of this function is O(n)\mathcal{O}(n)

Proof

We need a CC and n0n_0 so that 13n+37≤Cn13n + 37 \leq C n for n≥n0n \geq n_0.

If we pick C=14C = 14 and say n0=37n_0 = 37,

then 13n+37≤13n+n⟺13n+37≤14n13n + 37 \leq 13n + n \Longleftrightarrow 13n + 37 \leq 14n for n≥n0n \geq n_0.

Orders of growth

One with a background in Calculus soon realizes that order of these Big O functions will be the following,

O(1)O(log⁡(n))⋮O(n)O(nlog⁡(n))O(n2)⋮O(2n)⋮\begin{gather*} \mathcal{O}(1) \newline \mathcal{O}(\log(n)) \newline \vdots \newline \mathcal{O}(n) \newline \mathcal{O}(n \log(n)) \newline \mathcal{O}(n^2) \newline \vdots \newline \mathcal{O}(2^n) \newline \vdots \end{gather*}

Rules of (Asymptotic) Complexity

Some arithmetic rules for using the Big O Notation are the following:

Addition

O(f)+O(g)=O(f+g)=O(max⁡(f,g))=max⁡(O(f),O(g))\mathcal{O}(f) + \mathcal{O}(g) = \mathcal{O}(f + g) = \mathcal{O}(\max(f, g)) = \max(\mathcal{O}(f), \mathcal{O}(g))

Multiplication

O(f)×O(g)=O(f×g)\mathcal{O}(f) \times \mathcal{O}(g) = \mathcal{O}(f \times g)

And

C×O(f)=O(f)C \times \mathcal{O}(f) = \mathcal{O}(f)

How to find the Complexity of code

Now that we have understood properly what the Big O notation is and how we can operate with it - the next step is being able to find the complexity in code! How we do this properly is going through each line/operation in an algorithm and see what the complexity of each is. This is very time-consuming though - so, luckily, there are shortcuts.

Sequence of statements (arithmetic and logical operations, function calls, etc.), gives us addition between them.

func whats_the_complexity(int n):
n = n + 5; // This takes O(1)
n = n / 10; // This takes O(1)
n = do_something(n, 100); // Say this take O(n)
return n

The total complexity of whats_the_complexity would in this case be: O(1)+O(1)+O(n)\mathcal{O}(1) + \mathcal{O}(1) + \mathcal{O}(n) and from our first rule that would mean a total (Asymptotic) complexity of O(n)\mathcal{O}(n) for n≥1n \geq 1.

Nested loops gives multiplication

func whats_the_complexity(int[][] arr):
// Suppose arr is a n x n matrix
sum = 0;
for i in arr: // Takes O(n)
for j in i: // Takes O(n)
sum += j;
for i in arr: // Takes O(n)
sum += i;
return sum

The total complexity of whats_the_complexity would in this case be: O(n)×O(n)+O(n)\mathcal{O}(n) \times \mathcal{O}(n) + \mathcal{O}(n) and from our first and second rule that would mean a total (Asymptotic) complexity of O(n2)\mathcal{O}(n^2).

Relatives of O-notation

What we have gone through is only a part of a bigger set of notations. The ‘real’ definitions are the following:

Definition 2 (Relatives of O-notation)

Let ff and gg be functions.

f(n)∈O(g(n))f(n) \in \mathcal{O}(g(n)) if,

f(n)≤Cg(n) for n≥n0.f(n) \leq C g(n) \text{ for } n \geq n_0.

Now let’s introduce,

f(n)∈Ω(g(n))f(n) \in \Omega(g(n)) if,

f(n)≥cg(n) for n≥n0.f(n) \geq c g(n) \text{ for } n \geq n_0.

f(n)∈Θ(g(n))f(n) \in \Theta(g(n)) if,

cg(n)≤f(n)≤Cg(n) for n≥n0.c g(n) \leq f(n) \leq C g(n) \text{ for } n \geq n_0.

This means that f(n)∈Θ(g(n))f(n) \in \Theta(g(n)) means f(n)∈O(g(n))f(n) \in \mathcal{O}(g(n)) and f(n)∈Ω(g(n))f(n) \in \Omega(g(n)).

What this means is,

  • f(n)∈O(g(n))f(n) \in \mathcal{O}(g(n)), the function f(n)f(n) eventually has an upper bound,
  • f(n)∈Ω(g(n))f(n) \in \Omega(g(n)), the function f(n)f(n) eventually has a lower bound,
  • f(n)∈Θ(g(n))f(n) \in \Theta(g(n)), the function f(n)f(n) eventually has both a lower and upper bound.

Conclusion

This concludes the first part of our complexity journey - complexities are, as we can see, a very powerful tool to identify what kind of algorithm we’re working with and to get a somewhat accurate answer about questions one can have about programs, for example, runtime of a program.

Next part will be about dynamic arrays and how we use them - especially how to implement stacks and queues.

Footnotes
  1. Big O notation

    ↩

Search

Essays, lecture notes, and research

Type to search the site.