infIILecture5.en.handout
infIILecture5.en.handout
Computer Science II
Course at D-MAVT, ETH Zurich
Spring 2024
Announcement: Exercise Scalar Product
90
Announcement: bonus exercise 1
91
Algorithm design
every-day problem
abstraction
computational problem
design
algorithm 1 algorithm 2
comparison
92
Preview
Sorting coins
Sorting numbers
93
4.1 Selection sort
[Bhargava Ch. 2]
94
Example: Selection Sort
5 6 2 8 4 1 (i = 0) def sort(a):
n = len(a)
1 6 2 8 4 5 (i = 1)
for i in range(n):
1 2 6 8 4 5 (i = 2) # find minimum
mini = i
1 2 4 8 6 5 (i = 3) for j in range(i+1, n):
if a[j] < a[mini]:
1 2 4 5 6 8 (i = 4)
mini = j
1 2 4 5 6 8 (i = 5) # swap minimum
a[mini],a[i] = a[i],a[mini]
1 2 4 5 6 8
95
Example: Selection Sort
Invariant:
before iteration i, a[:i]
5 6 2 8 4 1 (i = 0) contains the smallest i
elements of a sorted in
1 6 2 8 4 5 (i = 1)
ascending order.
1 2 6 8 4 5 (i = 2)
Selection of the smallest
1 2 4 8 6 5 (i = 3) element a[mini] in the
unsorted part a[i:n].
1 2 4 5 6 8 (i = 4)
Swap a[mini] with the first
1 2 4 5 6 8 (i = 5) element a[i] of the unsorted
part.
1 2 4 5 6 8
Repeat until everything is
sorted (i = n). 96
Invariants
97
Invariants
98
4.2 Merge Sort
99
divide et impera
Divide and Conquer
Divide the problem recursively into smaller/simpler subproblems that
contribute to the simplified computation of the overall problem.
P11 L11
P1 L1
P12 L12
Problem P Solution L
P21 L21
P2 L2
P22 L22
100
Merge sort
Divide and Conquer!
1. Divide: Divide array into two halves of (roughly) equal size.
2. Recurse: Sort both halves reursively.
3. Conquer: Combine sorted halves by merging.
7 4 1 16 9 11 10 2 12 3
71 4 71 9
16 9
16 11
2 10
3 10
2 12
11 3
12
1 2 3 4 7 9 10 11 12 16
101
Merge (Conquer): Overview
Assumption: two halves of the array a are already sorted.
1 4 7 9 16 2 3 10 11 12
b: 1 2 3 4 7 9 10 11 12 16
102
Merge (Conquer): Beispiel
1 4 7 9 16 2 3 10 11 12
1 2 3 4 7 9 10 11 12 16
103
Algorithm merge
104
Algorithm merge_sort
def merge_sort(a):
if len(a) <= 1:
return a
else:
sorted_a1 = merge_sort(a[:len(a) // 2])
sorted_a2 = merge_sort(a[len(a) // 2:])
return merge(sorted_a1, sorted_a2)
105
Merge Sort: Step by step (after every merge)
9 8 6 7 6 4 1 2
8 9 6 7 6 4 1 2
8 9 6 7 6 2 1 4
6 7 8 9 6 2 1 4
6 7 8 9 2 6 1 4
6 7 8 9 2 6 1 4
6 7 8 9 1 2 4 6
1 2 4 6 6 7 8 9
106
5. Efficiency of algorithms
107
Comparison of Algorithms
Natural questions:
How efficient is our algorithm?
Is it as efficient as a different algorithm?
Is it the most efficient algorithm?
108
Comparison of Algorithms by time measurements?
Goals
Understand dependence on the input size.
Quantify the running time behavior of an algorithm independent of the
machine.
Compare running time behavior of algorithms.
110
Running Time Analysis: Dependence on input
111
Computation Model
Unit cost model: operations on fundamental data types have cost 1.
Fundamental operations: arithmetic computations, comparisons,
assignment, flow control (jumps), . . .
Fundamental data types: e.g., bounded integer or floating point number.
Memory model
Memory access has cost 1.
Bounded-size Objects can be dynamically allocated with cost 1.
Fields of the objects can be accessed with cost 1.
113
Asymptotic behavior: idea
The exact running time of an algorithm usually cannot be predicted.
Idea: general trend for large inputs, e.g.,:
If the input size doubles, the running time doubles.
If the input size doubles, the running time quadruples.
n 2·n 3·n
A1 n 2·n 3·n
A2 2·n 4·n 6·n
A3 4·n+2 8·n+2 12 · n + 2
A4 n2 4 · n2 9 · n2
A5 2 · n2 8 · n2 18 · n2
A6 4 · n2 + 3 · n + 2 16 · n2 + 6 · n + 2 36 · n2 + 9 · n + 2
⇒ We only consider the asymptotic behavior of the algorithm.
⇒ We ignore all constant additive and multiplicative factors. 114
Asymptotic behavior: idea
The algorithms with running time
n, 2 · n, 4 · n + 2 all behave the same way:
⇒ If the input size doubles, the running time doubles.
⇒ Their running times behave like the function n 7→ n.
⇒ We say that their running time is in Θ(n).
More general: The asymptotic notation Θ(g(n)) for the running time means
that the algorithm behaves for large n like g(n).
115
Asymptotic behavior: idea
Precise: Θ(n2 )
The running time behaves like n 7→ n2 .
problem size doubled ⇒ running time quadruples
116
Definition: asymptotic upper bound
provided: a function g : N→R
g(n) = n2
N→R|
n
O(g) = f :
∃ c > 0, ∃n0 ∈ N:
∀ n ≥ n0 :
o
0 ≤ f (n) ≤ c · g(n)
N R
Set of all functions f : → that satisfy: there is some c ∈ R+ and some
N
n0 ∈ such that 0 ≤ f (n) ≤ c · g(n) for all n ≥ n0 . 117
Definition: asymptotic lower bound
provided: a function g : N→R
N→R|
n
Ω(g) = f :
∃ c > 0, ∃n0 ∈ N:
g(n) = n
∀ n ≥ n0 :
o
0 ≤ c · g(n) ≤ f (n)
N R
Set of all functions f : → that satisfy: there is some c ∈ R+ and some
N
n0 ∈ such that 0 ≤ f (n) ≤ c · g(n) for all n ≥ n0 .
118
Definition: asymptotic tight bound
119
About the Notation
120
Example: Selection Sort
n−1
X n−1
X
1+ 1 + 2 + 1
i=0 j=i+1
n(n − 1)
= = Θ(n2 )
2
121
Growth in every-day life
122
Small n
2n
n4
60
40 n2
20
n
ln n
2 3 4 5 6
123
Larger n
·106
2n
1
0.8
0.6
0.4 n4
0.2
n2
n
5 10 15 20 log n
124
“Large” n
·1020
1
2n
0.8
0.6
0.4
0.2
n4 n2
n
20 40 60 80 100 log n
125
Logarithms
1,000 n2
800
600
400 n3/2
n log n
200
n
log n
10 20 30 40 50
126
Time Consumption
127
Example
128
Example
129
Example
def zeros(n):
matrix = []
for i in range(n):
row = []
Runtime: Θ(n2 ).
for j in range(n):
row.append(0)
matrix.append(row)
return matrix
130
Example
def zeros(n):
matrix = []
for i in range(n):
row = []
Runtime: Θ(n2 ).
for j in range(i):
row.append(0)
matrix.append(row)
return matrix
131
Example
def faux_nested_loop(a):
i = 0
while i < len(a):
j = i
while j < len(a) and a[j] == a[i]:
j += 1
i = j
132
Example
133
Example
134
Summary
every-day problem
abstraction
computational problem
design
algorithm 1 algorithm 2
comparison
136
Summary: runtimes (2/3)
Try to derive a simplified formula T (n) that sums up the cost of your
algorithm.
Usually, T (n) is a sum of terms. Take the dominant term f (n) without its
coefficient. The dominant term is the one for which limn→∞ fg(n)
(n)
= 0, for
any other term g(n) in T (n).
Conclude that T (n) = Θ(f (n)).
If it’s too hard to derive T (n), compute at least an upper bound U (n).
Conclude that T (n) = O(f (n)), where f (n) is U (n)’s dominant term.
137
Summary: runtime (3/3)
Note that T (n) = O(f (n)) only means that asymptotically, the runtime of
your algorithm’s runtime is eventually bounded above by a factor of
f (n). T (n) = Θ(f (n)) is more precise. It means that your algorithm runs
in time proportional to f (n).
If you manage to derive a lower bound for T (n), say L(n). Then you can
conclude that T (n) = Ω(g(n)), where g(n) is L(n)’s dominant term.
If T (n) is O(f (n)) and also Ω(f (n)), then T (n) is also Θ(f (n))! This trick
can sometimes be useful for algorithms that are hard to analyze.
138