Algorithms_2022-2023
Algorithms_2022-2023
Giacomo Fiumara
[email protected]
1 / 258
Section 1
Introduction
Introduction to Algorithms (1)
3 / 258
Introduction to Algorithms (2)
4 / 258
Introduction to Algorithms (3)
Example: sorting
Input:
A sequence composed of n number a1 , a2 , . . . , an
Output:
A permutation a1′ , a2′ , . . . , an′ of the input sequence such that
5 / 258
Insertion sort
6 / 258
Insertion sort
7 / 258
Insertion sort
def InsertionSort(a):
for j in range(1, len(a)):
key = a[j]
i = j
while i > 0 and a[i-1] > key:
a[i] = a[i-1]
i = i - 1
a[i] = key
print(a)
return a
ins-sort.py
8 / 258
Insertion sort
– At the beginning of the for loop, a[1, ... j-1] contains all
sorted elements, while a[j+1 ... n] contains unsorted
elements
9 / 258
Insertion sort
Algorithm analysis
10 / 258
Insertion sort
Algorithm analysis
Input size
– Number of elements composing the input data
11 / 258
Insertion sort
Algorithm analysis
Computational cost
– Number of elementary instructions to be executed
12 / 258
Insertion sort
Algorithm analysis
n−1
X
T (n) = c1 n + c2 (n − 1) + c3 (n − 1) + c4 tj
j=1
n−1
X n−1
X
+ c5 (tj − 1) + c6 (tj − 1) + c7 (n − 1)
j=1 j=1
13 / 258
Insertion sort
Algorithm analysis
n−1
X
T (n) = c1 n + c2 (n − 1) + c3 (n − 1) + c4 tj
j=1
n−1
X n−1
X
+ c5 (tj − 1) + c6 (tj − 1) + c7 (n − 1)
j=1 j=1
14 / 258
Insertion sort
Algorithm analysis
The equation:
n−1
X
T (n) = c1 n + c2 (n − 1) + c3 (n − 1) + c4 tj
j=1
n−1
X n−1
X
+ c5 (tj − 1) + c6 (tj − 1) + c7 (n − 1)
j=1 j=1
T (n) = c1 n + c2 (n − 1) + c3 (n − 1) + c4 (n − 1) + c7 (n − 1)
= (c1 + c2 + c3 + c4 + c7 )n − (c2 + c3 + c4 + c7 )
15 / 258
Insertion sort
Algorithm analysis
The equation:
T (n) = c1 n + c2 (n − 1) + c3 (n − 1) + c4 (n − 1) + c7 (n − 1)
= (c1 + c2 + c3 + c4 + c7 )n − (c2 + c3 + c4 + c7 )
T (n) = an + b
16 / 258
Insertion sort
Algorithm analysis
n−1 n−1
X X n(n − 1)
tj = j=
2
j=1 j=1
n−1 n−1
X X (n − 1)2
(tj − 1) = (j − 1) =
2
j=1 j=1
17 / 258
Mathematical detour
Arithmetic progression
an = a1 + (n − 1)d
ak = aj + (k − j)d
18 / 258
Mathematical detour
Arithmetic progression
1
Sn = n(a1 + an )
2
Indeed, we can write:
Sn = a1 + a2 + a3 + · · · + an−2 + an−1 + an
Sn = an + an−1 + an−2 + · · · + a3 + a2 + a1
If we sum the LHSs and the RHSs of the two above
expressions:
Quantities in parentheses
2Sn = (a1 + an ) + (a2 + an−1 ) + (a3 + an−2 ) + · · ·
+ (a3 + an−2 ) + (a2 + an−1 ) + (a1 + an )
a2 + an−1 = a1 + d + an − d = a1 + an
a3 + an−2 = a1 + 2d + an − 2d = a1 + an
20 / 258
Mathematical detour
Arithmetic progression
Therefore:
1
Sn = n(a1 + an )
2
21 / 258
Insertion sort
Algorithm analysis
n−1
X (n − 1)(1 + n − 1) n(n − 1)
j= =
2 2
j=1
n−1
X (n − 1)(1 − 1 + n − 1) (n − 1)2
(j − 1) = =
2 2
j=1
22 / 258
Insertion sort
Algorithm analysis
Equation
n−1
X
T (n) = c1 n + c2 (n − 1) + c3 (n − 1) + c4 tj
j=1
n−1
X n−1
X
+ c5 (tj − 1) + c6 (tj − 1) + c7 (n − 1)
j=1 j=1
becomes:
n−1
X
T (n) = c1 n + c2 (n − 1) + c3 (n − 1) + c4 j
j=1
n−1
X n−1
X
+ c5 (j − 1) + c6 (j − 1) + c7 (n − 1)
j=1 j=1
23 / 258
Insertion sort
Algorithm analysis
T (n) = an2 + bn + c
Concluding remark
Divide
Conquer
Combine
26 / 258
Divide and conquer
Merge sort
– Divide
Problems are divided in a number of subproblems, of the same
kind but having a smaller input data size
– Conquer
A recursive approach is adopted for subproblems. When their
input data size is small enough, subproblems are eventually
solved
– Combine
Solutions of subproblems are eventually combined to obtain
the solution of the original problem
27 / 258
Divide and conquer
Merge sort
– Divide
If S contains at least two elements its elements must be
copied in two sequences S1 and S2 containing ⌈n/2⌉ and
⌊n/2⌋ elements respectively
– Conquer
Recursively sort sequences S1 and S2
– Combine
Copy elements in S merging the two sorted sequences S1 ed S2
in a sorted sequence
28 / 258
Divide and conquer
Merge sort
29 / 258
Divide and conquer
Merge sort
def merge(s1,s2,s):
i = j = 0
while i+j < len(s):
if j==len(s2) or (i<len(s1) and s1[i]<s2[j]):
#print("IF: ",i,’\t’,j,’\t’)
s[i+j] = s1[i]
i += 1
else:
#print("ELSE: ",i,’\t’,j,’\t’)
s[i+j] = s2[j]
j += 1
30 / 258
Divide and conquer
Merge sort
def mergesort(s):
n = len(s)
if n < 2:
return
mid = n // 2
s1 = s[0:mid]
s2 = s[mid:n]
print("Prima: ",s1,s2,s)
mergesort(s1)
mergesort(s2)
merge(s1,s2,s)
print(s1,s2,s)
31 / 258
Divide and conquer
Merge sort
Array:
[85, 24, 63, 45, 17, 31, 96, 50]
[24, 85]
[45, 63]
[24, 45, 63, 85]
[17, 31]
[50, 96]
[17, 31, 50, 96]
[17, 24, 31, 45, 50, 63, 85, 96]
32 / 258
Divide and conquer
Merge sort
33 / 258
Divide and conquer
Merge sort
Θ(1) se n ≤ c
T (n) =
aT (n/b) + D(n) + C (n) negli altri casi
Where
34 / 258
Divide and conquer
Merge sort
– T (n/b)
The current problem is divided in two subproblems each
working on n/2 input data. Therefore the Conquer phase
costs 2T (n/2)
– D(n)
In the Division phase the median element of the subarray must
be identified. This operation has a constant computational
cost, D(n) = Θ(1)
– C (n)
merge function costs Θ(n)
35 / 258
Divide and conquer
Merge sort
Therefore:
Θ(1) se n = 1
T (n) =
2T (n/2) + Θ(n) se n > 1
c se n = 1
T (n) =
2T (n/2) + cn se n > 1
36 / 258
Divide and conquer
Merge sort
37 / 258
Divide and conquer
Merge sort
38 / 258
Divide and conquer
Merge sort
39 / 258
Divide and conquer
Merge sort
n = 2i
i = log2 n = lg n
40 / 258
Divide and conquer
Merge sort
41 / 258
Section 3
Growth of Functions
Algorithm Analysis
Θ-notation
A function f (n) belongs to the set Θ(g (n)) if there exist positive
constants c1 and c2 such that it can be “sandwiched” between
c1 g (n) and c2 g (n), for sufficiently large n
43 / 258
Algorithm Analysis
Θ-notation
44 / 258
Algorithm Analysis
Θ-notation
45 / 258
Algorithm Analysis
Θ-notation
f (n) = an2 + bn + c
46 / 258
Algorithm Analysis
Θ-notation
d
X
p(n) = ai ni
i=0
47 / 258
Algorithm Analysis
O-notation
48 / 258
Algorithm Analysis
O-notation
49 / 258
Algorithm Analysis
O-notation
50 / 258
Algorithm Analysis
Ω-notation
51 / 258
Algorithm Analysis
Ω-notation
52 / 258
Algorithm Analysis
Ω-notation
53 / 258
Algorithm Analysis
An example: prefix averages
54 / 258
Algorithm Analysis
An example: prefix averages (first implementation)
def prefix_average1(S):
n = len(S)
A = [0] * n
for j in range(n):
total = 0
for i in range(j+1):
total += S[i]
A[j] = total / (j + 1)
return A
55 / 258
Algorithm Analysis
An example: prefix averages (first implementation)
56 / 258
Algorithm Analysis
An example: prefix averages (first implementation)
57 / 258
Algorithm Analysis
An example: prefix averages (second implementation)
def prefix_average2(S):
n = len(S)
A = [0] * n
for j in range(n):
A[j] = sum(S[0:j+1]) / (j + 1)
return A
58 / 258
Algorithm Analysis
An example: prefix averages (second implementation)
59 / 258
Algorithm Analysis
An example: prefix averages (third implementation)
def prefix_average3(S):
n = len(S)
A = [0] * n
for j in range(n):
total += S[j]
A[j] = total / (j + 1)
return A
60 / 258
Algorithm Analysis
An example: prefix averages (third implementation)
(
1 se n = 0
n! =
n · (n − 1) · (n − 2) · · · 3 · 2 · 1 se n ≥ 1
63 / 258
Recursion
The Factorial Function
(
1 se n = 0
n! =
n · (n − 1)! se n ≥ 1
64 / 258
Recursion
The Factorial Function
def factorial(n):
if n == 0:
return 1
else:
return n*factorial(n-1)
65 / 258
Recursion
The Factorial Function
66 / 258
Recursion
The Factorial Function
67 / 258
Recursion
Binary Search
68 / 258
Recursion
Binary Search
70 / 258
Recursion
Binary Search
Three cases:
72 / 258
Recursion
Binary Search
binarysearch.py
73 / 258
Recursion
Binary Search
74 / 258
Recursion
Binary Search
n
<1 , cioè r = ⌊log n + 1⌋
2r
75 / 258
Section 5
Stack
Stack
77 / 258
Stack: ADT
78 / 258
Stack: ADT
79 / 258
Stack: ADT (1/2)
def create():
return []
def pop(mylist):
return mylist.pop()
def top(mylist):
return mylist[-1]
80 / 258
Stack: ADT (2/2)
def is_empty(mylist):
# return False if mylist
if mylist:
return False
else:
return True
def length(mylist):
return len(mylist)
81 / 258
Stack: ADT
– Computational costs
push(e) O(1)
pop() O(1)
top() O(1)
is_empty() O(1)
length(S) O(1)
82 / 258
Section 6
Queues
Queues
84 / 258
Queues
85 / 258
Queues: ADT
86 / 258
Queues: ADT
87 / 258
Double-ended queues
88 / 258
Double-ended queues: ADT (1/2)
89 / 258
Double-ended queues: ADT (2/2)
90 / 258
Double-ended queues
reverse() rotate(n)
91 / 258
Double-ended queues: ADT (1/2)
def del_first(dq):
if len(dq):
return dq.pop(0)
return False
def del_last(dq):
if len(dq):
return dq.pop()
return False
92 / 258
Double-ended queues: ADT (2/2)
def first(dq):
if dq:
return dq[0]
return False
def last(dq):
if dq:
return dq[-1]
return False
def is_empty(dq):
return len(dq) == 0
93 / 258
Section 7
Linked lists
Linked Lists
95 / 258
Singly Linked Lists
96 / 258
Singly Linked Lists
– The first element of a list is called head, the last is the tail
– By starting at the head and moving from one node to another
by following the references, it is possible to reach the tail of
the list (traversing)
97 / 258
Singly Linked Lists
def create():
llist = dict()
llist[’head’] = ’tail’
llist[’tail’] = 0
return llist
98 / 258
Singly Linked Lists
Inserting an Element at the Head
99 / 258
Singly Linked Lists
Inserting an Element at the Head
def add_first(llist,element):
llist[element] = llist[’head’]
llist[’head’] = element
return llist
100 / 258
Singly Linked Lists
Inserting an Element at the Tail
101 / 258
Singly Linked Lists
Inserting an Element at the Tail
def add_last(llist,element):
curr = ’head’
while curr != ’tail’:
next = llist[curr]
if next == ’tail’:
llist[e] = llist[curr]
llist[curr] = e
break
curr = next
return llist
102 / 258
Singly Linked Lists
Removing an Element from a Singly Linked List
103 / 258
Singly Linked Lists
Removing an Element from a Singly Linked List
def del_first(llist):
x = llist[’head’]
llist[’head’] = llist[x]
del llist[x]
104 / 258
Singly Linked Lists
Removing the Last Element from a Singly Linked List
– Therefore, the only way consists in starting from the head and
search through the list
105 / 258
Singly Linked Lists 1/5
def create():
llist = dict()
llist[’head’] = ’tail’
llist[’tail’] = 0
return llist
def add_first(llist,element):
llist[element] = llist[’head’]
llist[’head’] = element
return llist
106 / 258
Singly Linked Lists 2/5
def add_last(llist,e):
curr = ’head’
while curr != ’tail’:
next = llist[curr]
if next == ’tail’:
llist[e] = llist[curr]
llist[curr] = e
break
curr = next
return llist
def del_first(llist):
x = llist[’head’]
llist[’head’] = llist[x]
del llist[x]
107 / 258
Singly Linked List 3/5
Iterative implementation of traversal
def itraverse(llist):
curr = ’head’
path = []
path.append((0,curr))
k = 1
while curr != ’tail’:
next = llist[curr]
path.append((k,next))
curr = next
k += 1
return path
108 / 258
Singly Linked Lists 4/5
Recursive implementation of traversal
109 / 258
Singly Linked Lists 5/5
def printall(llist):
curr = ’head’
while curr != ’tail’:
next = llist[curr]
print(curr, "\t-->\t", next)
curr = next
110 / 258
Doubly Linked Lists
111 / 258
Doubly Linked Lists
112 / 258
Doubly Linked Lists
Inserting and Removing Elements
113 / 258
Doubly Linked Lists
def create():
llist = dict()
llist[’head’] = {’prev’: None,’next’: ’tail’}
llist[’tail’] = {’prev’: ’head’,’next’: None}
return llist
def printall(llist):
curr = ’head’
while curr != ’tail’:
next = llist[curr][’next’]
print(curr, "\t<==>\t", next)
curr = next
114 / 258
Doubly Linked Lists
115 / 258
Doubly Linked Lists
116 / 258
Section 8
Trees
Trees
118 / 258
Trees
119 / 258
Trees
120 / 258
Trees
121 / 258
Trees
– Two nodes sharing the same parent nodes are said sibling
123 / 258
Trees
Example: hierarchy of Python exceptions
124 / 258
Trees
Example: hierarchy of trees
125 / 258
Trees
Ordered trees
126 / 258
Binary trees
Implementation 1/2
def add_root(e):
tree = dict()
tree[e] = {’P’: None, ’L: None’, ’R’: None}
return tree
127 / 258
Binary trees
Implementation 2/2
if __name__ == ’__main__’:
mytree = add_root(’root’)
add_child(mytree, ’root’, ’lft1’, ’L’)
add_child(mytree, ’root’, ’lft2’, ’L’)
add_child(mytree, ’root’, ’rgt1’, ’R’)
add_child(mytree, ’rgt1’, ’rgt2’, ’R’)
for k,v in mytree.items():
print(k, "\t",v)
128 / 258
Trees
Computing Depth
129 / 258
Trees
Computing Depth
def is_root(tree,node):
return tree[node][’P’] == None
def depth(tree,start):
if is_root(tree,start):
return 1
else:
return 1 + depth(tree, parent(tree, start))
130 / 258
Trees
Computing Depth
131 / 258
Trees
Computing height
132 / 258
Trees: altezza
133 / 258
Binary Trees
134 / 258
Binary Trees
135 / 258
Binary Trees
Decision Tree
136 / 258
Binary Trees
Arithmetic expression
(3 + 1) × 3
− ((3 × (7 − 4)) + 6)
(9 − 5) + 2
137 / 258
Binary Trees
Recursive definition
138 / 258
Binary Trees
Abstract Data Types
139 / 258
Binary Trees
Implementation 1/3
def add_root(e):
tree = dict()
tree[e] = {’P’:None, ’L’:None, ’R’:None}
return tree
140 / 258
Binary Trees
Implementation 2/3
def is_root(tree,node):
return tree[node][’P’] == None
def depth(tree,start):
if is_root(tree,start):
return 1
else:
return 1 + depth(tree,tree[start][’P’])
binarytree.py
141 / 258
Binary Trees
Implementation 3/3
binarytree.py
142 / 258
Binary Trees
Properties
– In a binary tree, level 0 has at most one node (the root), level
1 has at most two nodes, level 2 has at most 4 nodes, and so
on
143 / 258
Binary Trees
Properties
144 / 258
Binary Trees
Properties
1 h + 1 ≤ n ≤ 2h+1 − 1
2 1 ≤ nE ≤ 2h
3 h ≤ nI ≤ 2 h − 1
4 log(n + 1) − 1 ≤ h ≤ n − 1
145 / 258
Binary Trees
Properties
1 2h + 1 ≤ n ≤ 2h+1 − 1
2 h + 1 ≤ nE ≤ 2h
3 h ≤ nI ≤ 2 h − 1
4 log(n + 1) − 1 ≤ h ≤ (n − 1)/2
146 / 258
Binary Trees
Properties
147 / 258
Binary Trees
Properties
148 / 258
Binary Trees
Properties
– This operation can be repeated until the final tree will consist
of a single node, which will eventually be an external node
– Thus, the proposition is justified
149 / 258
Binary Trees
Implementation
150 / 258
Binary Trees
Implementation
151 / 258
Trees: preorder traversal
152 / 258
Trees
Preorder traversal
153 / 258
Trees
Preorder traversal
root node
Root level 1 lft
level 2 lft lft
level 2 lft lft
level 1 rgt
level 2 rgt lft
L1 lft L1 rgt level 2 rgt rgt
154 / 258
Trees
Preorder traversal
def preorder(tree,node,visited):
if node != None:
visited.append(node)
lc = tree[node][’L’]
rc = tree[node][’R’]
if lc != None or rc != None:
preorder(tree, lc, visited)
preorder(tree, rc, visited)
return visited
155 / 258
Trees
Postorder traversal
156 / 258
Trees
Postorder traversal
157 / 258
Trees
Postorder traversal
158 / 258
Trees
Postorder traversal
def postorder(tree,node,visited):
if node != None:
lc = tree[node][’L’]
rc = tree[node][’R’]
if lc != None or rc != None:
postorder(tree, lc, visited)
postorder(tree, rc, visited)
visited.append(node)
return visited
159 / 258
Trees
Traversals Computational Complexity
160 / 258
Trees
Breadth-First Tree Traversal
161 / 258
Trees binari: breadth first traversal
root node
Root level 1 lft
level 1 rgt
level 2 lft lft
level 2 lft rgt
level 2 rgt lft
L1 lft L1 rgt level 2 rgt rgt
162 / 258
Trees binari: breadth first traversal
def bfs(tree,node):
"Breadth First Search"
queue,visited = [],[]
if node == root(tree):
queue.append(node)
while queue:
current = queue.pop(0)
visited.append(current)
if tree[current][’L’]:
queue.append(tree[current][’L’])
if tree[current][’R’]:
queue.append(tree[current][’R’])
return visited
163 / 258
Trees: inorder traversal
164 / 258
Trees: inorder traversal
165 / 258
Trees: inorder traversal
def inorder(tree,node,visited):
if node != None:
print(node,visited)
lc = tree[node][’L’]
rc = tree[node][’R’]
if lc != None:
inorder(tree, lc, visited)
visited.append(node)
if rc != None:
inorder(tree, rc, visited)
return visited
166 / 258
Trees: inorder traversal
level 2 sx sx
Root level 1 sx
level 2 sx dx
root node
level 2 dx sx
level 1 dx
L1 sx L1 dx level 2 dx dx
L2 sx sx L2 sx dx L2 dx sx L2 dx dx
167 / 258
Section 9
169 / 258
Binary Search Trees
170 / 258
Binary Search Trees
171 / 258
Binary Search Trees
172 / 258
Binary Search Trees
def inorder(tree,node,visited):
if node != None:
print(node,visited)
lc = tree[node][’L’]
rc = tree[node][’R’]
if lc != None:
inorder(tree, lc, visited)
visited.append(node)
if rc != None:
inorder(tree, rc, visited)
return visited
173 / 258
Binary Search Trees
Query
def tree_search(tree,root,val):
if root == None:
print("Non existent node")
return root
if root == val:
print("Found")
return root
if val < root:
return tree_search(tree,tree[root][’L’],val)
else:
return tree_search(tree,tree[root][’R’],val)
174 / 258
Binary Search Trees
Query
175 / 258
Binary Search Trees
Query: iterative implementation
def iterative_tree_search(tree,root,val):
while root is not None and root != val:
if val < root:
root = tree[root][’L’]
else:
root = tree[root][’R’]
return root
176 / 258
Binary Search Trees
Searching for min/max
def tree_min(tree,root):
while tree[root][’L’] is not None:
root = tree[root][’L’]
return root
def tree_max(tree,root):
while tree[root][’R’] is not None:
root = tree[root][’R’]
return root
177 / 258
Binary Search Trees
Searching for next/previous
def tree_successor(tree,node):
if tree[node][’R’] is not None:
return tree_min(tree,tree[node][’R’])
par = tree[node][’P’]
while par is not None and node == tree[par][’R’]:
node = par
par = tree[par][’P’]
return par
178 / 258
Binary Search Trees
Inserting an element
179 / 258
Binary Search Trees
Inserting an element
def tree_insert(tree,node):
y = None
x = root(tree)
while x is not None:
y = x
if node < x:
x = tree[x][’L’]
else:
x = tree[x][’R’]
tree[node] = {’P’: y, ’L’:None, ’L’:None}
if y is not None and node < y:
tree[y][’L’] = node
else:
tree[y][’R’] = node
180 / 258
Binary Search Trees
Deleting an element
181 / 258
Binary Search Trees
Deleting an element
def transplant(tree,old,new):
oldparent = tree[old][’P’]
if oldparent == None:
tree[new][’P’] = None
if tree[old][’L’]:
tree[new][’L’] = tree[old][’L’]
if tree[old][’R’]:
tree[new][’R’] = tree[old][’R’]
elif old == tree[oldparent][’L’]:
tree[oldparent][’L’] = new
else:
tree[oldparent][’R’] = new
if new != None:
tree[new][’P’] = tree[old][’P’]
del tree[old]
182 / 258
Binary Search Trees
Deleting an element
183 / 258
Binary Search Trees
Deleting an element
184 / 258
Binary Search Trees
Deleting an element
185 / 258
Section 10
Quicksort
Quicksort
187 / 258
Quicksort: how it works 1/2
188 / 258
Quicksort: how it works 2/2
189 / 258
Quicksort
quicksort.py
190 / 258
Quicksort
quicksort.py
191 / 258
Quicksort
– a) Initial condition
– b) The value 2 is placed in the partition containing numbers
smaller than x
192 / 258
Quicksort
194 / 258
Quicksort
– i) x is placed in t
195 / 258
Mathematical detour
Sum of a finite number of elements of a geometric progression
Sn = a1 + a2 + · · · + an−1 + an
q · Sn = q · a1 + q · a2 + · · · + q · an−2 + q · an−1 + q · an =
= a2 + a3 + · · · + an + q · an
q · Sn − Sn = q · an − a1
Sn · (q − 1) = q · an − a1 =
= q n · a1 − a1 =
= a1 · (q n − 1)
qn − 1
Sn = a1 ·
q−1
196 / 258
Quicksort: computational complexity
197 / 258
Quicksort: computational complexity (worst case)
n
T (n) ≤ 2T ( ) + Θ(n)
2
T (n) = O(nlgn)
199 / 258
Quicksort: computational complexity (average case)
– In this case:
9n n
T (n) ≤ T +T + cn
10 10
200 / 258
Quicksort: computational complexity (average case)
9n n
T (n) ≤ T +T + cn
10 10
201 / 258
Section 11
Graphs
Graphs
Introduction
203 / 258
Graphs
204 / 258
Graphs
Some definitions
206 / 258
Graphs
Some definitions
207 / 258
Graphs
Some definitions
208 / 258
Graphs
Some definitions
209 / 258
Graphs
Some definitions
210 / 258
Graphs
Some definitions
211 / 258
Graphs
Some definitions
212 / 258
Graphs
Some important properties
Proposition 1
If G is a graph with m edges and vertex set V , then
X
deg (V ) = 2m
v in V
213 / 258
Graphs
Some important properties
Proposition 2
If G is a directed graph with m edges and vertex set V , then
X X
indeg (V ) = outdeg (V ) = m
v in V v in V
214 / 258
Graphs
Some important properties
Proposition 3
Let G be a simple graph with n vertices and m edges. If G is
undirected, then
m ≤ n(n − 1)/2
m ≤ n(n − 1)
215 / 258
Graphs
Some important properties
216 / 258
Data Structures for Graphs
Edge list
Adjacency list
Adjacency matrix
217 / 258
Data Structures for Graphs: Edge List
Edge List
218 / 258
Data Structures for Graphs: Edge List
Edge List
In a simpler way:
e ==> u - v
f ==> v - w
g ==> w - u
h ==> w - z
219 / 258
Data Structures for Graphs: Edge List
Edge List
220 / 258
Data Structures for Graphs: Edge List
Edge List: performances
222 / 258
Data Structures for Graphs: Adjacency List
Adjacency List
223 / 258
Data Structures for Graphs: Adjacency List
Adjacency List
224 / 258
Data Structures for Graphs: Adjacency List
Adjacency List
225 / 258
Data Structures for Graphs: Adjacency List
Adjacency List Structure
226 / 258
Data Structures for Graphs: Adjacency List
Adjacency List Structure
228 / 258
Data Structures for Graphs: Adjacency List
Adjacency List
229 / 258
Data Structures for Graphs: Adjacency Matrix
Adjacency Matrix
230 / 258
Data Structures for Graphs: Adjacency Matrix
Adjacency Matrix
231 / 258
Data Structures for Graphs: Adjacency Matrix
Adjacency Matrix
232 / 258
Data Structures for Graphs: Adjacency Matrix
Adjacency Matrix Structure
233 / 258
Data Structures for Graphs: Adjacency Matrix
Ajacency Matrix Structure
234 / 258
Data Structures for Graphs: Adjacency Matrix
Adjacency Matrix Structure
— Advantages
The most significant advantage of an adjacency matrix is that
any edge (u, v ) can be accessed in worst-case O(1) time
— Disadvantages
To find edges incident to vertex v we must examine all n
entries in the row associated with v . An adjacency list can
locate those edges in optimal O(deg (V )) time
Adding or removing vertices from a graph is problematic, as
the matrix must be resized
The O(n2 ) space usage of an adjacency matrix is usually worse
than the O(n + m) space required for adjacency lists, although
the number of edges in a dense graph is proportional to n2
235 / 258
Data Structures for Graphs
236 / 258
Graph ADT: Implementation of the Adjacency List /1
def create_graph():
return dict()
237 / 258
Graph ADT: Implementation of the Adjacency List /2
238 / 258
Graph ADT: Implementation of the Adjacency List /3
Implementation
def degree(graph):
# dizionario con il degree di ogni nodo del grafo
deg = dict()
for node in graph:
deg[node] = len(graph[node])
return deg
def is_complete(graph):
# restituisce True se il grafo e’ completo
n = len(graph)
for node in graph:
if len(graph[node]) < n - 1:
return False
return True
239 / 258
Graph ADT: Implementation of the Adjacency List /4
Implementation
def nodes(graph):
# restituisce una lista dei nodi del grafo
return list(graph.keys())
240 / 258
Graph ADT: Implementation of the Adjacency List /5
Implementation
def edges(graph):
# restituisce una lista degli archi del grafo
Edges = []
for k, v in graph.items():
for u in v:
if (k,u) not in Edges and (u,k) not in Edges:
Edges.append((k,u))
return Edges
def vertex_count(graph):
# restituisce il numero di vertici del grafo
return len(graph)
def edge_count(graph):
# restituisce il numero di archi del grafo
return len(edges(graph))
241 / 258
Graph ADT: Implementation of the Adjacency List /6
Implementation
242 / 258
Graph ADT: Implementation of the Adjacency List /7
Implementation
243 / 258
Graph ADT: Implementation of the Adjacency List /8
Implementation
244 / 258
Graph Traversals
245 / 258
Graph Traversals
246 / 258
Graph Traversals
247 / 258
Depth-First Search: Running Time of DFS
248 / 258
Depth-First Search: Running Time of DFS
...
249 / 258
Breadth-First Search
bfs.py
251 / 258
Breadth-First Search
252 / 258
Breadth-First Search
253 / 258
Breadth-First Search
254 / 258
Breadth-First Search
Glossary
For BFS:
255 / 258
Breadth-First Search
Properties
Proposition
Let G be an undirected or directed graph on which a BFS traversal
starting at vertex s has been performed. Then
The traversal visits all vertices of G that are reachable from s
For each vertex v at level i, the path of the BFS tree T
between s and v has i edges, and any other path of G from s
to v has at least i edges
If (u, v ) is an edge that is not in the BFS tree, then the level
number of v can be at most 1 greater than the level number
of u
256 / 258
Breadth-First Search
257 / 258
Breadth-First Search
258 / 258