Skip to content

Commit

Permalink
ct0371-2: Add notes
Browse files Browse the repository at this point in the history
  • Loading branch information
alek3y committed Dec 7, 2023
1 parent 829f68d commit d85da83
Show file tree
Hide file tree
Showing 6 changed files with 400 additions and 0 deletions.
92 changes: 92 additions & 0 deletions src/ct0371-2/02/05/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,95 @@ Dato che la maggior parte delle operazioni sono $O(h)$ dove $h$ è l'**altezza**
return r
```
con $T(n) = \Theta(n)$ perchè $T(n) = 2T\left(\frac{n}{2}\right) + d$ se $n > 0$ per il [teorema master](../../../ct0371-1/01/03/README.md#teorema-master).

## Esempi

- Restituire il numero massimo di ripetizioni in un albero binario di ricerca in $\Theta(n)$.

```cpp
int massimo_ripetizioni(Tree t) {
if (t.root == nullptr) {
return 0;
}

int max = 1, count = 1;
PNode iter = minimum(t.root); // O(h)
int value = iter->key;
iter = successor(iter);
while (iter != nullptr) {
if (iter->key == value) {
count++;
} else {
if (count > max) {
max = count;
}
count = 1;
value = iter->key;
}
iter = successor(iter); // O(h)
}

if (count > max) {
max = count;
}
return max;
}
```
con $T(n) = \Theta(n)$ perchè chiamando `minimum` e poi $n$ volte `successor` si ha una visita in-order.

- Verificare che un albero sia di ricerca.

```cpp
bool is_bst(PNode r) {
if (r == nullptr) {
return true;
}
int min, max;
return is_bst_aux(r, min, max);
}

bool is_bst_aux(PNode u, int& min, int& max) {
int min_sx, min_dx, max_sx, max_dx;
bool is_sx, is_dx;
if (u->left == nullptr) {
is_sx = true;
min_sx = max_sx = u->key;
} else {
is_sx = is_bst_aux(u->left, min_sx, max_sx);
}
if (u->right == nullptr) {
is_dx = true;
min_dx = max_dx = u->key;
} else {
is_dx = is_bst_aux(u->right, min_dx, max_dx);
}

min = min_sx;
max = max_dx;
return is_sx && is_dx && max_sx <= u->key && min_dx >= u->key;
}
```
con $T(n) = \Theta(n)$ per la [decomposizione](../04/README.md).

- Verificare che dato un albero $T$ _binario di ricerca_ $\forall k, k \in T \land k + 2 \in T \Rightarrow k+1 \in T$.

```cpp
bool check(Tree t) {
if (t.root == nullptr) {
return true;
}

PNode iter = minimum(t.root), succ;
bool valid = true;
while (iter != nullptr && valid) {
succ = successor(iter);
if (succ != nullptr && succ->key >= iter->key+2) {
valid = false;
} else {
iter = succ;
}
}
return valid;
}
```
con $T(n) = O(n)$ dato che la visita in-order può terminare in anticipo se l'albero non è valido.
43 changes: 43 additions & 0 deletions src/ct0371-2/02/06/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Altri tipi

Tra gli [alberi binari di ricerca](../04/README.md) ci sono anche altri tipi come:
- **Alberi AVL**

Sono alberi [**bilanciati**](../README.md#alberi-k-ari) i cui nodi contengono un **fattore di bilanciamento** che è minore o uguale ad $1$ per ogni nodo, e rappresenta la differenza dell'altezza del sottoalbero sinistro con quella del destro.

L'_inserimento_ e _cancellazione_ sono più complesse dato che bisogna mantenere l'albero bilanciato.

- **B-Alberi**

Sono alberi **bilanciati** che hanno **almeno** due figli, dove:
- Tutte le foglie hanno la stessa profondità
- Ogni nodo $v$ (_radice_ esclusa) contiene $\mathrm{grado}(v)-1 \leq K(v) \leq 2\mathrm{grado}(v)-1$ chiavi **ordinate**
- La radice $r$ contiene $1 \leq K(r) \leq 2\mathrm{grado}(r)-1$ chiavi **ordinate**
- Ogni nodo interno $v$ ha $K(v)+1$ figli
- Le chiavi separano gli intervalli delle chiavi nei sottoalberi

Per esempio,
```dot process
graph {
node [shape=record]
0 [label="46"]
1 [label="27 | 37"]
2 [label="66 | 79"]
3 [label="10 | 15 | 25"]
4 [label="30 | 35"]
5 [label="40 | 45"]
6 [label="50 | 55 | 65"]
7 [label="68 | 74"]
8 [label="80 | 99"]

0 -- 1, 2
1 -- 3, 4, 5
2 -- 6, 7, 8
}
```

- **Alberi rossi e neri**

Contengono oltre alla chiave il **colore** del nodo, che può essere **rosso** o **nero**.

L'albero viene vincolato in base al _colore_ in un modo che garantisce che l'albero sia **bilanciato**.
26 changes: 26 additions & 0 deletions src/ct0371-2/03/01/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Insertion sort

L'**insertion sort** consiste nell'estendere una parte di $k$ elementi ordinati al $(k+1)$-esimo elemento.

```c
insertion(Array A)
for j = 2 to A.length
key = A[j]
i = j - 1
while i >= 1 and key < A[i]
A[i + 1] = A[i]
i = i - 1
A[i + 1] = key
```
L'algoritmo è **corretto** per la sua [invariante](../../01/02/README.md#analisi-della-correttezza):
> L'array `A[1, ..., j-1]` contiene gli elementi **ordinati** che originariamente erano in `A[1, ..., j-1]`
e quando il `for` termina `j = A.length+1` quindi l'_invariante_ vale per `A[1, ..., n+1-1]`, cioè l'intero array.
Nel caso **migliore** è $\Theta(n)$ e nel **peggiore** $\Theta(n^2)$ perchè il `for` itera $n - 1$ volte ed il numero di confronti è:
$$
\left(\sum_{j = 2}^n j - 1\right) = \sum_{k = 1}^{n - 1} k = \frac{n(n - 1)}{2} = \Theta(n^2)
$$
L'algoritmo è **stabile** ed **in loco**.
53 changes: 53 additions & 0 deletions src/ct0371-2/03/02/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Merge sort

Il **merge sort** divide a metà l'array e riordina ricorsivamente le due parti per poi riunirle.

```c
mergesort(Array A, int p, int r)
if p < r
med = (p + r)/2
mergesort(A, p, med)
mergesort(A, med+1, r)
merge(A, p, med, r)

merge(Array A, int p, int med, int r)
L = []
left_len = med - p + 1
for i = 1 to left_len
push(L, A[p + i - 1])

R = []
right_len = r - med
for i = 1 to right_len
push(R, A[q + j])

push(L, Infinity)
push(R, Infinity)

i = 1, j = 1
for k = p to r
if L[i] <= R[j]
A[k] = L[i]
i++
else
A[k] = R[j]
j++
```
L'algoritmo è **corretto** per l'[invariante](../../01/02/README.md#analisi-della-correttezza) dell'ultimo `for`:
> In `A[p, ..., k-1]` sono **ordinati** elementi che, come `L[i]` e `R[j]`, sono minori del resto di `L` e `R`
e quando il `for` termina `k = r + 1` quindi l'_invariante_ vale per `A[p, ..., r+1-1]` cioè l'intero array.
La complessità di `merge` si può ricavare, sapendo che $n = r - p + 1$, da:
$$
\begin{split}
T(n) &= \Theta(\texttt{left\_len}) + \Theta(\texttt{right\_len}) + \Theta(r - p + 1) = \\
&= \Theta(\texttt{left\_len} + \texttt{right\_len}) + \Theta(n) = \\
&= \Theta(\texttt{med} - p + 1 + r - \texttt{med}) + \Theta(n) = \\
&= \Theta(r - p + 1) + \Theta(n) = \Theta(n)
\end{split}
$$
mentre quella di `mergesort` è $T(n) = 2T(\frac{n}{2}) + \Theta(n) = \Theta(n \log n)$ per il [teorema master](../../../ct0371-1/01/03/README.md#teorema-master).
L'algoritmo è **stabile** ma non _in loco_, ed è migliorabile con l'[insertion sort](../01/README.md) su array piccoli.
175 changes: 175 additions & 0 deletions src/ct0371-2/03/03/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Quick sort

Il **quick sort** sceglie un elemento **pivot** spostando gli elementi minori a sinistra per **partizionare** l'array in `A[p, ..., q-1]` e `A[q+1, ..., r]` e poi riordinarli ricorsivamente, dove `A[q]` è il valore del _pivot_.

```c
quicksort(Array A, int p, int r)
if p < r
q = partition(A, p, r)
quicksort(A, p, q-1)
quicksort(A, q+1, r)

partition(Array A, int p, int r) -> int
x = A[r] // Pivot
i = p - 1
for j = p to r - 1
if A[j] <= x
i = i + 1
swap(A[i], A[j])
swap(A[i+1], A[r])
return i+1
```
L'algoritmo è **corretto** per l'[invariante](../../01/02/README.md#analisi-della-correttezza) del `for`:
> Ogni elemento tra `p` ed `i` è **minore o uguale** al _pivot_ e tra `i+1` e `j-1` è **maggiore** del _pivot_
e quando il `for` termina `j = r` quindi l'_invariante_ vale con gli elementi tra `i+1` e `r-1` maggiori del _pivot_.
La complessità di `partition` è $\Theta(n)$ dato che impiega `r - p + 1 = n` iterazioni, mentre di `quicksort`:
$$
T(n) = \begin{cases}
O(1) & \text{se } n = 1 \\
T(k) + T(n - k - 1) + \Theta(n) & \text{se } n > 1
\end{cases}
$$
che si può risolvere separando nei diversi casi:
- **Peggiore**
In questo caso una _partizione_ è grande $n - 1$ e l'altra è vuota, quindi:
$$
\begin{split}
T(n) &= T(n - 1) + T(0) + \Theta(n) = T(n - 1) + \Theta(n) = \\
&= T(n - 1) + cn = \\
&= T(n - 2) + c(n - 1) + cn = \\
&= \sum_{i = 1}^n ci = c\frac{n(n + 1)}{2} = \Theta(n^2)
\end{split}
$$
- **Migliore**
In questo caso le _partizioni_ sono grandi $\lfloor\frac{n}{2}\rfloor$ e $\lfloor\frac{n}{2}\rfloor - 1$, quindi:
$$
T(n) = 2T\left(\frac{n}{2}\right) + \Theta(n) = \Theta(n \log n)
$$
per il [teorema master](../../../ct0371-1/01/03/README.md#teorema-master).
- **Medio**
In questo caso una _partizione_ è grande $\frac{9}{10}n$ e l'altra $\frac{1}{10}n$, quindi:
$$
T(n) = T\left(\frac{9n}{10}\right) + T\left(\frac{n}{10}\right) + cn
$$
risolvibile con l'_albero delle ricorsioni_:
```dot process
graph {
node [shape=box]
0 [label="cn"]
1 [label="¹⁄₁₀cn"]
2 [label="⁹⁄₁₀cn"]
3 [label="¹⁄₁₀₀cn"]
4 [label="⁹⁄₁₀₀cn"]
5 [label="⁹⁄₁₀₀cn"]
6 [label="⁸¹⁄₁₀₀cn"]
0 -- 1, 2
1 -- 3, 4
2 -- 5, 6
{
node [shape=point width=0]
7, 8, 9, 10, 11, 12, 13, 14
}
{
edge [style=dashed]
3 -- 7, 8
4 -- 9, 10
5 -- 11, 12
6 -- 13, 14
}
}
```
in cui ogni livello somma al più $cn$ dato che alcuni cammini **termineranno prima**, fino a:
$$
\frac{n}{10^i} = 1\ \lor\ \left(\frac{9}{10}\right)^i n = 1
$$
da cui si ricava che il cammino **più corto** è alto $\log_{10} n$ e quello **più lungo** $\log_{\frac{10}{9}} n$, quindi:
$$
T(n) \leq cn \cdot \log_{\frac{10}{9}} n = O(n \log n)
$$
Questo processo si può **generalizzare** per ogni $0 < \alpha < 1$ e $c > 0$:
$$
T(n) = T(\alpha n) + T((1 - \alpha)n) + cn = O(n \log n)
$$
Se però il caso medio provenisse dall'**alternarsi** del caso migliore $L(n)$ e peggiore $U(n)$ definiti come:
$$
\begin{split}
L(n) &= 2U\left(\frac{n}{2}\right) + \Theta(n) \\
U(n) &= L(n - 1) + \Theta(n)
\end{split}
$$
si avrebbe che:
$$
\begin{split}
L(n) &= 2\left(L\left(\frac{n}{2} - 1\right) + \Theta\left(\frac{n}{2}\right)\right) + \Theta(n) = \\
&= 2L\left(\frac{n}{2} - 1\right) + 2\Theta\left(\frac{n}{2}\right) + \Theta(n) = \\
&= 2L\left(\frac{n}{2} - 1\right) + \Theta(n) = \Theta(n \log n)
\end{split}
$$
che è comunque $O(n \log n)$.
## Pivot casuale
Scegliere un _pivot_ **casuale** al posto di `A[r]` diminuisce le probabilità che capiti il caso peggiore.
```c
random_quicksort(Array A, int p, int r)
if p < r
q = random_partition(A, p, r)
random_quicksort(A, p, q-1)
random_quicksort(A, q+1, r)
random_partition(Array A, int p, int r) -> int
i = random(p, r)
swap(A[i], A[r])
return partition(A, p, r)
```

Come per il [merge sort](../02/README.md), è possibile migliorare le prestazioni sfruttando l'[insertion sort](../01/README.md) su array piccoli, oppure scegliendo il _pivot_ come **mediana** di tre elementi equidistanti.

## Chiavi duplicate

Nel caso fossero presenti chiavi **duplicate** si rischierebbe di avere array **sbilanciati** (e.g. nel caso di chiavi tutte uguali), che si possono evitare creando una _partizione_ per gli elementi uguali al _pivot_ `A[r]`:
```c
quicksort(Array A, int p, int r)
if p < r
<q, t> = partition(A, p, r)
quicksort(A, p, q-1)
quicksort(A, t+1, r)

partition(Array A, int p, int r) -> <int, int>
x = A[r]
min = eq = p
mag = r
while eq < mag
if A[eq] < x
swap(A[eq], A[min])
min++
eq++
else if A[eq] == x
eq++
else
mag--
swap(A[eq], A[mag])
swap(A[r], A[mag])
return <min, mag>
```
che è **corretto** perchè il `while` ha _invariante_:
> Tra `p` e `min` sono **minori**, tra `min` ed `eq` sono **uguali** e tra `mag` e `r` sono **maggiori** del _pivot_
assicurando che quando il `while` termina `eq = mag` e quindi ci sono solo tre _partizioni_.
Nel caso **medio** `quicksort` è $O(n \log n)$ e nel **peggiore** $O(n^2)$ con `partition` da $\Theta(n)$.
L'algoritmo è **in loco**, ma non è _stabile_.
Loading

0 comments on commit d85da83

Please sign in to comment.