Algorithm analysis is the theoretical study of program performance and resource usage.[1]
D: What resources do programs consume?
Q: Why is algorithm analysis important?
Because it allows us to determine what is possible to do given a problem.
def fib(x):
if x == 0: return 0
elif x == 1: return 1
else: return fib(x - 2) + fib(x - 1)
D: How many steps will this algorithm need to make to calculate fib(5)
?
fib
What we observe is that fib
takes a number of steps that is proportional to its argument.
fib(2)
requires 2 stepsfib(3)
requires steps(fib(2))
+ 1 = 3 stepsfib(4)
requires steps(fib(3))
+ steps(fib(2))
+ 1 = 6 stepsThe question that algorithm analysis will enable us answer is:
How many steps does fib(n)
require?
Worst-case: \(T(n)\) The maximum steps/time/memory taken by an algorithm to complete the solution for inputs of size \(n\)
Average-case: The expected time over all inputs with the same characteristics (e.g. size).
Best-case: The performance in ideal situations. Rarely used.
When analysing algorithmic performance, we ignore computational resources ( e.g. CPU speed); we only care about the growth of \(T(n)\) as \(n \rightarrow \infty\)
This is called the asymptotic behaviour
The \(\Theta\) notation defines its exact asymptotic beviour of a function \(f(n)\) by restricting its upper and lower bounds in terms of another function:
\(f(n) = \Theta(g(n))\) means that: \(\forall n > n_0: \exists c_1 > 0, c_2 > 0, n_0:\) \(0 \le c_1 g(n) \le f(n) \le c_2 g(n)\)
This means that there exist positive constants \(c_1\), \(c_2\), and \(n_0\) such that if \(f(n)\) is \(\Theta(g(n))\), then \(f(n)\) execution steps/time are bound by the equation above.
In practice, this means there exists a value \(n_0\) such as a \(\Theta(n^2)\) algorithm will behave worse that \(\Theta(n)\) algorithm
Most times we only care about the worse case behaviour. This is what \(O\) defines:
\(f(n) = O(g(n))\) means that:
\(\forall n > n_0, \exists c > 0, n_0 > 0: 0 \le f(n) \le cg(n)\)
Q: Is \(2n^2 = O(n^3)\)?
We take \(c = 1\) and \(n_0 = 2\) and replace in the “constructor:”
\(0 \le 2n^2 \le n^3\). The equation holds \(\forall n\)
This also means that we can easily overestimate our compexity scale; we need to be precise.
Asymptotic growth analysis allows to estimate the (usually, upper) bounds of functions as a function of problem size.
It also enables us to compare algorithm implementations: an \(O(n^2)\) algorithm is always worse than an \(O(log(n))\) one.
We usually determine the asymptotic growth in terms of the following functions. Each one is asymptotically better than the following:
Growth of important functions for \(x \in (0, 1)\)
Growth of the same functions for \(x \in (0, 2)\)
Growth of the same functions for \(x \in (0, 5)\)
Growth of sane functions for \(x \in (0, 10)\)
If the time to execute for input size \(n = 1\) is 1 sec, then for input size \(n = 1000\)
\(f(x) = 5x + 30\) is \(O(n)\), because intuitively it is bound by a linear function (e.g. \(g(x) = 6x\))
What is the asymptotic complexity of the following polynomials?
\(f(x) = 5x + 40\)
\(f(x) = O(x)\), because we can prove that there exists a \(c_0 = 10\) for which \(g(x) = 10x\) bounds \(f(x)\)
\(f(x) = 5x^2 + 3x +2\)
\(f(x) = O(x^2)\)
We can now estimate the asymptotic complexity of algorithms, but what we are missing is a way to evaluate the number of “steps” an algorithm takes as a function of the input size, i.e., the \(f(x)\) function.
What is a “step”?
4 + 2
x = x + 2
a[1]
x = foo(1,bar)
x = Person("foo")
In general, any operation that consumes computing resources can be considered as a step.
def foo(x):
if x == 0:
print x
else:
print x[0]
Steps:
None of the above actions depend on the size of x
. Therefore this algorithm is \(O(1)\). This means that no matter how big x
is, it will run in constant time.
def foo(a):
for i in a:
y = y + i
return y
Per item of a:
a
in i
then, also a return statement
Our complexity function is \(T(x) = 3x + 1\), which can be upper bounded by \(g(x) = 5x\). Therefore, the compexity class is \(O(n)\)
# a is (n x m) and b is (m x p). The result is (n x p)
def matrix_multiply(a, b):
result = [[0 for x in range(len(a))] for y in range(len(b[0]))]
for i in range(len(a[0])):
for j in range(len(b[0])):
sum = 0
for k in range(len(a)):
sum = sum + a[i][k] * b[k][j]
result[i][j] = sum
return result
Ignoring initialization, for each element in a
, we need:
len(b[0])
times all elements in b
len(a)
times all lines in a
\(f(a,b) = len(a[0]) \times len(b[0]) \times len(a) + 1\) or \(f(a,b) = n \times m \times p + 1\). If we take \(a = max(n, m, p)\), we can prove that \(f\) is bounded by \(g(n) = n^3\). Therefore \(f\) is \(O(n^3)\)
Our discussion up to now assumes that all steps have the same computational cost. However, this is not true.
Latency Comparison Numbers
L1 cache reference 0.5 ns
Branch mispredict 5 ns
L2 cache reference 7 ns
Mutex lock/unlock 25 ns
Main memory reference 100 ns
Compress 1K bytes with Zippy 3,000 ns
Send 1K bytes over 1 Gbps network 10,000 ns
Read 4K randomly from SSD* 150,000 ns
Read 1 MB sequentially from memory 250,000 ns
Round trip within same datacenter 500,000 ns
Read 1 MB sequentially from SSD* 1,000,000 ns
Disk seek 10,000,000 ns
Read 1 MB sequentially from disk 20,000,000 ns
Send packet CA->Netherlands->CA 150,000,000 ns
By Jonas Boner, available here
Humans are very bad at appreciating very small or very big numbers. Let’s convert those numbers to something more relevant to humans: seconds
Latency Comparison Numbers
CPU time -> Human time
L1 cache reference 0.5 ns -> 0.5 secs
Branch mispredict 5 ns -> 5 secs
L2 cache reference 7 ns -> 7 secs
Mutex lock/unlock 25 ns -> 25 secs
Main memory reference 100 ns -> 100 secs
Compress 1K bytes with Zippy 3,000 ns -> 50 mins
Send 1K bytes over 1 Gbps network 10,000 ns -> 2.7 hrs
Read 4K randomly from SSD* 150,000 ns -> 1.7 days
Read 1 MB sequentially from memory 250,000 ns -> 2.9 days
Round trip within same datacenter 500,000 ns -> 5.7 days
Read 1 MB sequentially from SSD* 1,000,000 ns -> 11.5 days
Disk seek 10,000,000 ns -> 3.8 months
Read 1 MB sequentially from disk 20,000,000 ns -> 7.6 months
Send packet CA->Netherlands->CA 150,000,000 ns -> 4.8 years
It should be obvious now that when calculating complexity, not all steps are of the same cost.
# cost times
def foo(a): #
for i in a: # c1 len(a)
y = y + i # c2 + c3 len(a)
return y #
Associated costs:
c1
Main memory reference or L2 cache referencec2
(Loading operands) L1 cache reference (x2), perhaps branch misspredictionc3
(Storing result) Main memory referenceCalculating costs is very complicated and depends on the computer architecture. This is why algorithm analysis usually avoids it.
fib
def fib(x):
if x == 0: return 0
elif x == 1: return 1
else: return fib(x - 2) + fib(x - 1)
fib
s compexity function depends on the size of x
. Given a large enough \(c\)
\[ \begin{aligned} T(n) &= T(n-1) + T(n-2) + C\\ &= T(n-2) + T(n-3) + T(n-3) + T(n-4)\\ &= T(n-3) + T(n-4) + T(n-4) + T(n-5) + T(n-4) + T(n-5) + T(n-5) + T(n-6)\\ & \dots \\ &= 2 \times T(n-2) + \dots + 2^n \times T(n - len(x) - 2)\\ &< c \times 2^n \times T(n - len(x) - 2) \end{aligned} \]
Therefore, a (relatively loose) upper bound for fib
is \(O(n^2)\)
fib
complexity graphicallyAs we can see, \(2^4\) is an over approximation for the last step, therefore \(O(2^n)\) is a not too tight bound.
A tighter bound for fib
is \(O(\frac{1 + \sqrt 5}{2}^n) \approx O(1.618^{n})\)
\(\phi = \frac{1 + \sqrt 5}{2}\) is otherwise known as the golden ratio
What is the complexity of Towers of Hanoi?
def hanoi(n, source, tmp, target):
if n == 0:
return
# move tower of size n - 1 to helper
hanoi(n - 1, source, target, tmp)
print "Move disk %d from %s to %s" %(n, source, target)
# move tower of size n-1 from helper to target
hanoi(n - 1, tmp, source, target)
[1] C. Leiserson and E. Demaine, “MIT open courseware: Introduction to algorithms,” 2005. [Online]. Available: https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-046j-introduction-to-algorithms-sma-5503-fall-2005/.
[2] T. H. Cormen, C. E. Leiserson, Ronald L. Rivest, and C. Stein, Introduction to algorithms (3rd ed.). MIT press, 2009.
This work is (c) 2017 - onwards by TU Delft, Georgios Gousios and his co-authors and licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International license.