From d85da83ca9c1d48b736633436eee7a46cb3f4be4 Mon Sep 17 00:00:00 2001 From: alek3y Date: Thu, 7 Dec 2023 23:05:15 +0100 Subject: [PATCH] ct0371-2: Add notes --- src/ct0371-2/02/05/README.md | 92 ++++++++++++++++++ src/ct0371-2/02/06/README.md | 43 +++++++++ src/ct0371-2/03/01/README.md | 26 ++++++ src/ct0371-2/03/02/README.md | 53 +++++++++++ src/ct0371-2/03/03/README.md | 175 +++++++++++++++++++++++++++++++++++ src/ct0371-2/03/README.md | 11 +++ 6 files changed, 400 insertions(+) create mode 100644 src/ct0371-2/02/06/README.md create mode 100644 src/ct0371-2/03/01/README.md create mode 100644 src/ct0371-2/03/02/README.md create mode 100644 src/ct0371-2/03/03/README.md create mode 100644 src/ct0371-2/03/README.md diff --git a/src/ct0371-2/02/05/README.md b/src/ct0371-2/02/05/README.md index cc510cef..e1e096ba 100644 --- a/src/ct0371-2/02/05/README.md +++ b/src/ct0371-2/02/05/README.md @@ -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. diff --git a/src/ct0371-2/02/06/README.md b/src/ct0371-2/02/06/README.md new file mode 100644 index 00000000..b3632fe2 --- /dev/null +++ b/src/ct0371-2/02/06/README.md @@ -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**. diff --git a/src/ct0371-2/03/01/README.md b/src/ct0371-2/03/01/README.md new file mode 100644 index 00000000..a1bbb6d2 --- /dev/null +++ b/src/ct0371-2/03/01/README.md @@ -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**. diff --git a/src/ct0371-2/03/02/README.md b/src/ct0371-2/03/02/README.md new file mode 100644 index 00000000..70184c59 --- /dev/null +++ b/src/ct0371-2/03/02/README.md @@ -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. diff --git a/src/ct0371-2/03/03/README.md b/src/ct0371-2/03/03/README.md new file mode 100644 index 00000000..fb63c104 --- /dev/null +++ b/src/ct0371-2/03/03/README.md @@ -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 + = partition(A, p, r) + quicksort(A, p, q-1) + quicksort(A, t+1, r) + +partition(Array A, int p, int r) -> + 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 +``` +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_. diff --git a/src/ct0371-2/03/README.md b/src/ct0371-2/03/README.md new file mode 100644 index 00000000..ccfdb96a --- /dev/null +++ b/src/ct0371-2/03/README.md @@ -0,0 +1,11 @@ +# Ordinamento + +Gli algoritmi trattati saranno basati sul **confronto** attraverso una tecnica incrementale, cioè utilizzando la soluzione al problema grande $k$ per risolvere quello grande $k+1$. + +Al momento **non esiste** alcun algoritmo basato _sul confronto_ con complessità inferiore a $O(n \log n)$. + +## Proprietà + +Un algoritmo è detto **in loco** quando lo spazio richiesto dall'algoritmo è **costante**. + +Viene anche detto **stabile** se l'ordine degli elementi con chiavi uguali rimane invariato dopo l'ordinamento.