119 lines
4.8 KiB
Markdown
119 lines
4.8 KiB
Markdown
|
---
|
|||
|
title: Caratterizzazione della complessità di un algoritmo per l'ordinamento da file
|
|||
|
author: Raffaele Mignone
|
|||
|
subject: Ordinamento da file
|
|||
|
keywords:
|
|||
|
- Complessità
|
|||
|
- Ordinamento da file
|
|||
|
- Kotlin
|
|||
|
papersize: a4
|
|||
|
lang: it-IT
|
|||
|
---
|
|||
|
|
|||
|
# Ordinamento da file
|
|||
|
|
|||
|
## Tracia
|
|||
|
|
|||
|
Scrivere un programma per l’ordinamento di un file di grandi dimensioni, senza caricarlo in memoria, mediante distribuzioni e fusioni successive. Nel passo di distribuzione i valori vengono distribuiti in due file, passando dall’uno l’altro ogni volta che si incontro un valore minore dell’ultimo trattato; i due file vengono poi fusi prendendo ogni volta il valore minore fra i due non ancora trattati.
|
|||
|
|
|||
|
## Soluzione
|
|||
|
|
|||
|
L'algoritmo si compone di due parti principali: la parte di distribuzione (@lst:branch) e la parte fusione (@lst:merge) dei valori.
|
|||
|
I due algoritmi vengono infine combinati nello snippet -@lst:run.
|
|||
|
|
|||
|
|
|||
|
```{#lst:branch .kotlin caption="Algoritmo per la distribuzione dei valori"}
|
|||
|
private fun branch(
|
|||
|
source: Scanner,
|
|||
|
destination1: PrintStream,
|
|||
|
destination2: PrintStream
|
|||
|
) {
|
|||
|
var destination = destination1
|
|||
|
var current: T
|
|||
|
var last: T? = null
|
|||
|
|
|||
|
while (source.hasNextLine()) {
|
|||
|
current = fromJson(source.nextLine())
|
|||
|
|
|||
|
if (isLess(current, last))
|
|||
|
destination = switchDestination(
|
|||
|
destination, destination1, destination2
|
|||
|
)
|
|||
|
|
|||
|
destination.println(toJson(current))
|
|||
|
last = current
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
```{#lst:merge .kotlin caption="Algoritmo per la fusione dei valori"}
|
|||
|
private fun merge(
|
|||
|
source1: Scanner,
|
|||
|
source2: Scanner,
|
|||
|
destination: PrintStream
|
|||
|
) {
|
|||
|
var source = source1
|
|||
|
var current: T
|
|||
|
var cache: T? = null
|
|||
|
|
|||
|
while (source1.hasNextLine() && source2.hasNextLine()) {
|
|||
|
if (cache == null)
|
|||
|
cache = fromJson(
|
|||
|
switchSource(source, source1, source2).nextLine()
|
|||
|
)
|
|||
|
|
|||
|
current = fromJson(source.nextLine())
|
|||
|
|
|||
|
if (!isLess(current, cache)) {
|
|||
|
source = switchSource(source, source1, source2)
|
|||
|
current = cache!!.also { cache = current }
|
|||
|
}
|
|||
|
|
|||
|
destination.println(toJson(current))
|
|||
|
}
|
|||
|
|
|||
|
destination.println(toJson(cache!!))
|
|||
|
|
|||
|
mergeTail(source1, source2, destination)
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
```{#lst:run .kotlin caption="Algoritmo di sorting"}
|
|||
|
fun run (source: File) {
|
|||
|
tryBranch(source)
|
|||
|
|
|||
|
while (!isSorted()) {
|
|||
|
tryMerge(source)
|
|||
|
tryBranch(source)
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
## Caratterizzazione della complessità
|
|||
|
|
|||
|
Per la la caratterizzazione della complessità si è scelto di usare il metodo dell'ordine di grandezza.
|
|||
|
Per poter applicare questo metodo la prima operazione da compiere è l'individuazione dell'operazione caratteristica.
|
|||
|
Trattandosi di un algoritmo che ordina valori letti da file come operazioni caratteristica sono state scelte le operazioni di lettura e scrittura.
|
|||
|
Nel caso della funzione -@lst:branch sono presenti un'operazione di scrittura e una di lettura all'interno del ciclo `while`; quest'ultimo viene eseguito finché sono presenti linee nel file quindi $n$ volte.
|
|||
|
|
|||
|
Con un ragionamento analogo si può stimare pari ad $n$ anche la complessità della funzione -@lst:merge.
|
|||
|
|
|||
|
Passando all'analisi della funzione -@lst:run notiamo immediatamente un ulteriore ciclo `while` che ha come condizione d'arresto il file ordinato.
|
|||
|
Da ciò possiamo evincere come l'algoritmo sia sensibile all'input e che quindi abbia una complessità che varia in base ad esso.
|
|||
|
Inoltre notiamo anche che la funzione `tryBranch`[^nota] viene eseguita almeno una volta anche se i dati contenuti nel file già sono ordinati.
|
|||
|
Avendo caratterizzato la complessità della funzione -@lst:branch pari a $n$ possiamo asserire che l'algoritmo di distribuzione e fusione ha una complessità di *best case* lineare.
|
|||
|
|
|||
|
[^nota]: Le funzioni `tryBranch` e `tryMerge` fanno da wrapper alle funzioni `branch` e `merge` ne gestiscono le eccezioni.
|
|||
|
|
|||
|
Per la complessità di *worst case* e media dobbiamo introdurre il concetto di numero di inversioni ovvero il numero di coppie di valori non ordinati.
|
|||
|
Nel caso peggiore (valori ordinati in ordine inverso) si ha un numero di inversioni $i = \frac{n(n-1)}{2} \approx \frac{n^2}{2}$ dove $n$ indica la dimensione del problema.
|
|||
|
Ipotizzando di poter rimuovere ad ogni *giro di while* un numero di inversioni proporzionale ad $n$ il ciclo `while` verrà eseguito circa $n$ volte.
|
|||
|
Avendo un ciclo `while` eseguito $n$ volte che richiama delle funzioni con all'interno un altro ciclo `while` che viene eseguito $n$ volte, possiamo stimare la complessità *worst case* come quadratica.
|
|||
|
|
|||
|
Nei casi intermedi ci aspettiamo un numero di inversioni compreso tra $1$ e $\frac{n^2}{2}$ quindi sempre di ordine $n^2$.
|
|||
|
Ripetendo gli stessi ragionamenti fatti per il *worst case* otteniamo un complessità media pari a $n^2$.
|
|||
|
|
|||
|
| | *best case* | *average* | *worst case* |
|
|||
|
| :-: | :-: | :-: | :-: |
|
|||
|
| Complessità | $n$ | $n^2$ | $n^2$ |
|