Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Programmazione e Strutture dati, dispensa completa, Dispense di Algoritmi E Strutture Di Dati

Array, operazioni su di essi e algoritmi di ordinamento (selection sort, insertion sort, bubble sort, mergesort, quicksort). Testing dei programmi e uso dei file. Abstract Data Types e Pseudo-Generics. Ricorsione. Complessità computazionale. ADT: - Punto, - Lista, - Stack (implementazioni con Array e con liste), - Queue (implementazioni con Array e con liste), - Albero binario - Albero binario di ricerca, Alberi AVL, Heap - Tabelle Hash

Tipologia: Dispense

2023/2024

In vendita dal 18/09/2024

kaxhz
kaxhz 🇮🇹

5

(1)

23 documenti

1 / 30

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
PROGRAMMAZIONE E
STRUTTURE DATI
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e

Anteprima parziale del testo

Scarica Programmazione e Strutture dati, dispensa completa e più Dispense in PDF di Algoritmi E Strutture Di Dati solo su Docsity!

PROGRAMMAZIONE E

STRUTTURE DATI

Sommario

L00) Sviluppo di programmi

Fasi dello sviluppo:

  • Analisi: Specifica di cosa fa il programma. Individua i dati di input e di output ed i loro vincoli, definiti in termini di Precondizione (condizione sui dati di ingresso che deve essere soddisfatta aƯinché la funzione sia applicabile) e Postcondizione (condizione su dati di uscita e di ingresso che deve essere soddisfatta al termine dell’esecuzione del programma). Utilizza un dizionario dei dati, ovvero una tabella il cui schema è: Identificatore, Tipo, Descrizione.
  • Progettazione: Definizione di come il programma eƯettua la trasformazione specificata. Progettazione dell’algoritmo per raƯinamenti successivi, che consiste nel definire degli step in base ai quali eƯettuare una decomposizione funzionale.
  • Implementazione: Codifica dell’algoritmo nel linguaggio scelto, Testing del programma (Scelta dei casi di prova, esecuzione, verifica dei risultati rispetto ai risultati attesi).
  • Esecuzione

L01) Array

Algoritmi di ordinamento:

Elencare gli elementi di un insieme secondo una sequenza stabilita da una relazione d'ordine. Possiamo classificare gli algoritmi di ordinamento in base ad alcune proprietà: Un algoritmo è Stabile se due elementi con la medesima chiave mantengono lo stesso ordine con cui si presentavano prima dell'ordinamento. Un algoritmo ordina in loco se in ogni dato istante al più è allocato un numero costante di variabili, oltre all'array da ordinare(es. se uso array aggiuntivo per salvare gli elementi da ordinare non è in loco) Un algoritmo è Adattivo se il numero di operazioni eƯettuate dipende dall’input. Infine, un algoritmo può essere Interno, se i dati sono contenuti nella memoria RAM, o Esterno se i dati sono residenti su disco o su nastro. Tutti gli algoritmi che vedremo ordinano per confronti. Algoritmi semplici: Numero di operazioni quadratico rispetto alla taglia dell’input: O(n^2 )

  • selection sort. (non adattivo)
  • insertion sort
  • bubble sort Algoritmi avanzati. Più eƯicienti.
  • Merge sort. O(n log n). Non ordina in loco.
  • Quicksort. O(n log n) operazioni nel caso medio, quadratico nel caso peggiore.

Selection Sort

Analisi:

Progettazione, definiamo funzioni corrispondenti agli step individuati e, per ognuna, seguiamo gli step di specifica, progettazione, codifica e verifica:  input_array(a, n) Input sequenza s in un array a di dimensione n  ordina_array(a, n) Ordina array a di dimensione n (per motivi di eƯicienza l’array di input e di

. output sono lo stesso array)  output_array(a, n) Output sequenza s1 contenuta in array a di dimensione n Progettazione (funzione ordina_array): Il Selection Sort è un algoritmo di ordinamento di una sequenza di interi: eƯettua una visita totale delle posizioni dell’array, per ogni posizione visitata individua l’elemento che dovrebbe occupare quella posizione, in questo modo, se i è la posizione corrente (0<=i<n), tutti gli elementi nelle posizioni comprese tra 0 ed i-1 rispettano l’ordinamento. L’elemento che deve occupare la posizione i sarà il minimo tra quelli nelle posizioni comprese tra i ed n-1. RaƯinamento del programma principale: • minimo(a, n) • scambia(i, j). Analisi della funzione minimo:

Insertion Sort

Visita totale: ad ogni passo gli elementi che precedono l’elemento corrente sono tra di loro ordinati, pur non occupando ancora la loro posizione definitiva. Si inserisce l’elemento corrente nella posizione che garantisce il mantenimento dell’ordinamento, dunque gli elementi precedenti maggiori sono spostati in avanti. Il primo elemento è già un sotto-array ordinato. Progettazione: for(i = 1; i < n; i++), inserisci l’elemento in posizione i nell’array ordinato a[0..i-1] RaƯinamento del programma principale: definiamo una sotto-funzione che inserisca un elemento in un array già ordinato.

Bubble Sort

Algoritmo iterativo: Si eƯettuano n-1 visite dell’array. Alla visita i-esima, si confrontano elementi adiacenti dal primo al (n-i)-esimo elemento (visito sempre in elemento in meno rispetto alla visita precedente). Elementi adiacenti che non risultano ordinati vengono scambiati. Ad ogni passo l’elemento più grande viene portato nella sua posizione finale… Dopo il passo i-esimo, gli elementi tra le posizioni n-i ed n-1 risultano ordinati e nelle loro posizioni finali.

  • Visita finalizzata: termina quando un elemento dell’array verifica una certa condizione. Usa due condizioni di uscita: una sull’indice di scansione (visitati tutti gli elementi si esce comunque dal ciclo), l’altra che dipende dal problema specifico.

Ricerca di un elemento in un array qualsiasi

Analisi: Progettazione: Si scorre l’array di input finché non si trova l’elemento o non si raggiunge la fine dell’array: se l’elemento è stato trovato allora si restituisce la posizione corrente, altrimenti si restituisce -1. Si utilizzano un indice per scorrere l’array (int i=0;) e una variabile booleana ci dice se abbiamo trovato l’elemento (preferibile rispetto all’uso dell’istruzione break). La condizione di permanenza nel ciclo sarà: (i<n && !trovato). Short circuit evaluation Invece dello schema precedente, i programmatori C spesso usano il seguente: while( i<n && a[i]!=elem) i++; Infatti quando i=n la condizione a sinistra dell’operatore && è falsa e la condizione a destra dell’operatore non viene valutata (se venisse valutata a[i] sarebbe indefinito). Nota che in molti linguaggi di programmazione non c’è la short circuit evaluation.

Ricerca lineare in un array ordinato

Se l’array è ordinato in senso crescente, non è necessario arrivare alla fine dell’array per stabilire che l’elemento non è stato trovato, ci si può fermare appena si trova un elemento maggiore (o uguale) di quello dato.

Ricerca binaria in un array ordinato

Consiste nel dividere l’array in due metà e confrontare l’elemento da cercare con l’elemento centrale.

  • uguali -> trovato (e ci si ferma)
  • elemento da cercare minore -> continuare la ricerca nella prima metà dell’array
  • elemento da cercare maggiore -> continuare la ricerca nella seconda metà dell’array Se l’elemento non è presente, l’array si ridurrà ad un solo elemento, non divisibile in due. Nel caso peggiore si visitano log 2 n elementi dell’array.

L02) Testing dei programmi e uso dei file

La fase di testing consiste nell’esercitare il programma con dati di test per verificare che il suo comportamento sia conforme a quello atteso. Oracolo: output atteso, quello che ci si aspetta il programma produca Debugging: Individuazione e correzione del difetto che ha causato il malfunzionamento Test suite: un insieme di casi di test per un programma, individuati per classi (array vuoto, ordinato…). Più alta è la fase in cui si introduce il difetto, maggiore è la diƯicoltà nel rimuoverlo.

Automatizzare il testing

Per automatizzare il testing si possono usare i file per leggere dati di input e scrivere i dati di output. Usiamo 2 file per dati di input: un file “input.txt”, contenente gli elementi di input, e un file “oracle.txt” contenente l’output atteso. Un file “output.txt” risultante dall’esecuzione del programma, che contiene l’output eƯettivo e una indicazione dell’esito del test (PASS 1 / FAIL 0). In C, il termine stream indica una sorgente di input o una destinazione per l’output. Molti programmi piccoli ottengono il loro input da uno stream (ad es. la tastiera) e lo inviano ad un altro stream (ad esempio il video). Gli stream rappresentano file memorizzati da qualche parte (hard disk o altri tipi di memoria a lungo termine) e sono associati a periferiche (schede di rete, stampanti, etc).

Aprire (e chiudere) un File

FILE *fopen(const char *filename, const char *mode) int fclose(FILE *stream) La funzione fopen() prende come parametri due tringhe: filename è il nome del file da aprire, può contenere informazioni riguardanti la sua posizione, e mode è una stringa di modalità che specifica quali operazioni abbiamo intenzione di compiere sul file. Stringhe mode per file di testo: "r" Apre il file in lettura (il file deve esistere) "w" Apre il file in scrittura (non è necessario che il file esista) "a" Apre il file in accodamento (non è necessario che il file esista) "r+" Apre il file in lettura e scrittura (il file deve esistere) "w+" Apre il file in lettura e scrittura - tronca il file se esiste "a+" Apre il file in lettura e scrittura - accoda se il file esiste Il valore di ritorno è un puntatore a file, che verrà dato come parametro a tutte le funzioni che opereranno su quel file. fclose() prende come unico parametro il file da chiudere e ritorna 0 in caso di successo, o un valore costante EOF in caso di fallimento. Il carattere EOF cambia in base al sistema operativo.

I/O da stream

char *fgets(char *s, int size, FILE *stream); int fscanf(FILE *stream, const char *format, ...); int fprintf(FILE *stream, const char *format, ...) Tutte e tre le funzioni precedenti prendono come parametro un puntatore a file. fgets() prende come parametro il file (lo stream) da cui leggere, la stringa s in cui scrivere quello che viene letto dallo stream fino al carattere newline e un intero size che indica che la funzione può leggere fino a size-1 caratteri. Ritorna la stringa s in caso di successo, NULL in caso di errore o se si raggiunge la fine del file senza aver letto alcun carattere. fscanf() prende come parametro il file da leggere, legge da stream fino ad un carattere di spazio e non lo memorizza. Restituisce il numero di dati letti e scritti con successo. fprintf() scrive su stream. Restituisce il numero di caratteri scritti; -1 in caso di errore.

I/O su stringhe

int sprintf(char *restrict buƯer, const char *restrict format, ... ); int sscanf(const char *str, const char *format, ...) Le funzioni di sopra possono leggere e scrivere dati usando una stringa come se fosse un flusso. sprintf() scrive caratteri in una stringa. Prende come primo argomento il buƯer (stringa) in cui andare a scrivere. Restituisce il numero di caratteri memorizzati

Implementazione: Codifica di quanto definito nella specifica, è spesso nascosta al

programmatore, seguendo il principio dell’Incapsulamento (information hiding).

ADT: Punto................................................................................................................................

Strutture: tipo di dati composito che include un elenco di variabili fisicamente raggruppate in un unico blocco di memoria. Vantaggio: migliore leggibilità dei programmi. Per rendere più leggibile il codice, puoi definire una struct e creare un alias per essa utilizzando typedef: typedef struct point Point; Oppure usare in combinazione il typedef e la definizione della struttura: Puntatori È possibile allocare memoria per una struttura attraverso le funzioni malloc o calloc: Point *p = malloc(sizeof(Point)); In questo modo allochiamo spazio per una struttura di tipo Point e assegniamo un puntatore alla variabile p. In questo caso, per accedere ai campi della struttura usiamo l’operatore freccia -> : Implementazione – nel file punto.h

L’implementazione della struttura del tipo punto è nell’header file, visibile quindi al modulo client, che potrebbe accedere direttamente ai campi della struct senza usare gli operatori dell’ADT. Per “nascondere” la rappresentazione della struttura del tipo di dato, nell’header file punto.h dichiariamo: typedef struct punto *Punto; Non posiamo evitarlo perché all’atto della compilazione del modulo client, il compilatore non saprebbe quanta memoria allocare per una dichiarazione del tipo: Punto p; Invece, essendo il tipo punto un puntatore, il compilatore sa quanta memoria deve allocare per una variabile di quel tipo, indipendentemente dalla dimensione dell’elemento puntato. Il tipo Punto viene poi implementato come un puntatore alla struttura nel file punto.c in modo da non renderla visibile al client tramite l’include dell’header file punto.h :

L03) Pseudo-generics

I generics sono uno strumento che permette la definizione di un tipo parametrizzato, che viene esplicitato successivamente in fase di compilazione (o linkaggio). Obiettivo: Implementare algoritmi e ADT che siano in grado di funzionare, di volta in volta, con il tipo di dati desiderato. Esempio: problema dell’ordinamento

  1. Interi: ordinare una sequenza di numeri
  2. Stringhe: mettere un elenco di nomi in ordine alfabetico
  3. Dati strutturati: ordinare i record di studenti secondo la matricola Come procedere:
    • Creare un tipo generico Item (interfaccia) che supporti input, output e confronto
    • Modificare le librerie in modo che operino sul tipo Item
    • Realizzare Item in 3 file .c che supportano le 3 varianti intero, stringa e struttura
    • Linkare ed eseguire separatamente le 3 varianti (es. make int, make string…) Nell’header file Item.h dichiaro un puntatore ad un tipo non specificato (void): typedef void Item; Il puntatore a void non può essere dereferenziato (non si sa che tipo sia e quanto spazio occupi), quindi prima di usarlo bisogna assegnarlo al tipo desiderato: o int p_int = item; o char* p_char = item; o struct studente{ char nome[20]; int matricola; };

Istanziare un nodo: man mano che costruiamo la lista, creiamo dei nuovi nodi da aggiungere alla lista. I passi per creare un nodo sono:

  1. Allocare la memoria necessaria
  2. Memorizzare i dati nel nodo
  3. Inserire il nodo nella lista Per creare un nodo ci serve un puntatore temporaneo che punti al nodo: struct node *new_node; Possiamo usare malloc per allocare la memoria necessaria e salvare l’indirizzo in new_node: new_node = malloc(sizeof(struct node)); new_node adesso punta ad un blocco di memoria che contiene la struttura di tipo node. Calcolo della size: scorriamo i nodi incrementando un contatore. Ottimizzazione: mantenere un contatore da incrementare ad ogni inserimento e decrementare ad ogni cancellazione.

Inserimento in testa

Il modo più semplice per inserire un nuovo elemento in una lista L è inserire l’elemento in un nuovo nodo da aggiungere in testa alla lista.

  1. si alloca il nuovo nodo N
  2. si aggiunge il collegamento con il record iniziale della lista
  3. si aggiorna L facendolo puntare a N

Eliminazione in testa

Il modo più semplice per eliminare un elemento in una lista L è eliminarlo in testa alla lista.

  1. Si crea un puntatore temporaneo T, copia di L
  2. Si aggiorna L facendolo puntare al successivo di L
  3. Si elimina il nodo puntato da T, liberando la memoria Operatori aggiuntivi:

searchItem

Dati una lista l e un elemento val, restituisce la posizione della lista in cui appare la prima occorrenza dell’elemento, o -1 se l’elemento non è presente. Richiede una visita finalizzata della lista. Possiamo ottenere sia l’Item ricercato (restituendolo), sia la sua posizione (salvandola nella variabile pos, passata come puntatore), implementando la funzione con il seguente prototipo: Item searchItem(List list, Item item, int *pos).

Eliminazione

Per eliminare un elemento in una lista concatenata L:

  1. Si fanno avanzare due puntatori Prev e P fino a che P punta al nodo da eliminare,
  2. Si aggiorna il nodo puntato da Prev, facendo puntare al nodo successivo di P,
  3. Si elimina il nodo puntato da P, liberando la memoria.

Inserimento

Per inserire un elemento in una data posizione della lista concatenata L:

  1. Si fa avanzare un puntatore P, fino a che punta al nodo precedente alla posizione dell’inserimento;
  2. Si crea il nuovo nodo e lo si fa puntare al successivo di P;
  3. Si fa puntare P al nuovo nodo

Reverse

Per invertire una lista concatenata L:

  1. Per ogni nodo (con i nodi fino a Prev già invertiti) si utilizzano due puntatori Prev e P;
    • Si salva il successivo di P in un puntatore temporaneo Temp
    • Si aggiorna il nodo puntato da P, facendolo puntare a Prev
    • Si fanno avanzare P e Prev
  2. Si aggiorna la testa facendola puntare all’ultimo nodo.

CloneList

Diverse scelte progettuali possibili:

  • Clonare solo la struct list: I nodi sono gli stessi. Una modifica su una lista viene riflessa nell’altra
  • Clonare i nodi ma non gli item: Modifiche alla struttura di una lista non vengono riflesse nell’altra, ma se si modifica un item, risulta modificato in entrambe le liste
  • Clonare i nodi e gli item: Le liste, una volta clonate sono totalmente indipendenti

Liste: Implementazioni alternative

  • Array: vantaggi e svantaggi
  • Liste a collegamento singolo o Con puntatore head: le operazioni insertTail sono particolarmente ineƯicienti (bisogna scorrere tutta la lista, numero di operazioni lineare rispetto alla taglia della lista) o Con puntatori head e tail: Inserimenti in coda in tempo costante
  • Liste circolari (ultimo nodo collegato al primo), permettono l’attraversamento della lista partendo da un nodo qualsiasi
  • Liste a collegamento doppio. Svantaggio: operazioni di inserimento e cancellazione più complicate a causa della necessità di aggiornare due puntatori. Vantaggio: Cancellazione e inserimento in tempo costante quando si ha il puntatore al nodo.
  • Combinazioni delle soluzioni viste finora: es. liste doppie con tail o circolari

La coda è una struttura dati lineare a dimensione variabile. Si può accedere direttamente solo alla head della lista. Non è possibile accedere ad un elemento diverso da head, se non dopo aver eliminato tutti gli elementi che lo precedono (cioè quelli inseriti prima). Tra le possibili implementazioni, le più usate sono realizzate tramite: Lista concatenata e Array.

Implementazione con Array

È possibile utilizzare gli operatori di: Rimozione dalla testa (avviene in tempo costante), Aggiunta in coda (richiede un tempo lineare, costante se aggiungi un puntatore all’ultimo elemento della lista). Per motivi di eƯicienza, conviene avere accesso sia al primo elemento sia all’ultimo, modificando il tipo lista come un puntatore ad una struct che contiene:

  • un intero numelem che indica il numero di elementi della coda
  • un puntatore head ad uno struct nodo
  • un puntatore tail ad uno struct nodo Dobbiamo innanzitutto aggiungere il puntatore tail, poi bisogna modificare gli operatori principali:
  • RemoveHead, deve eventualmente aggiornare entrambi i puntatori head e tail.
  • addListTail, grazie alla presenza del puntatore tail, non deve scorrere gli elementi della lista fino all’ultimo e deve eventualmente aggiornare entrambi i puntatori head e tail.

Implementazione con Array

La coda è implementata come un puntatore ad una struct queue che contiene due elementi:

  • Un array di MAXQUEUE elementi
  • Un intero che indica la posizione head della coda
  • Un intero che indica la posizione tail della coda (prima casella libera) Quando la coda si riempie, non è possibile eseguire l’operazione enqueue… Se l’array viene gestito normalmente, cioè mantenendo head<=tail, ci sono dei problemi.. Risultano disponibili solo le posizioni a destra di tail, ma sono libere anche quelle a sinistra di head… Prima soluzione: ad ogni rimozione si compatta l’array nelle posizioni iniziali, con uno shift degli elementi… ma quest’operazione risulterebbe troppo costosa! Seconda soluzione: gestire l’array in modo circolare: gli elementi della coda si trovano nel segmento head, head+1...tail-1, ma non necessariamente head<=tail. Infatti, dopo aver inserito in posizione N- (ultima posizione dell’array), se c’è ancora spazio, si inseriscono ulteriori elementi a partire dalla posizione 0 (prima posizione dell’array). La tail viene incrementata usando modulo MAXQUEUE. In questo modo si riesce a garantire che ad ogni istante la coda abbia capacità massima di N-1.

Adesso tail<head. In questo ordine circolare il successore di p è (p+1)%N: ogni volta che si inserisce un elemento tail avanza: tail=(tail+1)%N. Ogni volta che si rimuove un elemento head avanza: head=(head+1)%N. La coda è piena (e non si può eseguire enqueue) se il successore di tail in questo ordine circolare è head: (tail+1)%n==head. Quando la coda è vuota, i valori di head e tail coincidono: tail==head. Abbiamo visto due implementazioni diverse dell’ADT coda:

  • Lista o pro: è una implementazione espandibile (unico limite è la capacità della memoria) o contro: la struttura è più complessa
  • Array circolare o pro: gli elementi sono memorizzati in modo contiguo e la struttura è più semplice o contro: dimensione fissata, bisogna conoscere a priori il numero massimo di elementi che la coda deve contenere, parte dello spazio è inutilizzato. Note su array circolare: Potrei usare la realloc per ridimensionare la coda (come fatto per lo stack) e poter quindi sempre inserire elementi? Sì, ma devo considerare che l’ordine degli elementi della coda nell’array non necessariamente va dalla posizione 0 alla posizione n-1…

L06) Ricorsione

Record di attivazione

Ogni volta che viene invocata una funzione viene creata dinamicamente una struttura dati detta record di attivazione: si crea di una nuova istanza della funzione chiamata, viene allocata la memoria per i parametri e per le variabili locali, si eƯettua il passaggio dei parametri, si trasferisce il controllo alla funzione chiamata e si esegue il codice della funzione. È il “mondo della funzione” e contiene:

  • i parametri formali
  • le variabili locali
  • l’indirizzo di ritorno (Return address RA) che indica il punto a cui tornare (nel codice del chiamante) al termine della funzione.
  • un collegamento al record di attivazione del chiamante (Link Dinamico DL)
  • l'indirizzo del codice della funzione (puntatore alla prima istruzione del corpo) Ciclo di Vita: Il record di attivazione associato a una chiamata di una funzione f viene creato al momento della invocazione di f, permane per tutto il tempo in cui f è in esecuzione e viene distrutto (deallocato) al termine dell’esecuzione. La dimensione del record di attivazione varia da una funzione all’altra. Per una data funzione, è fissa e calcolabile a priori (c’è un record per ogni chiamata della funzione, non per ogni funzione). L’area di memoria in cui vengono allocati i record di attivazione è chiamata stack e viene gestita come una lista LIFO nella quale ogni elemento è un record di attivazione.

i calcoli sono fatti prima. La chiamata serve solo, dopo averli fatti, per proseguire la computazione. Questa forma di ricorsione si chiama «Ricorsione Tail» (ricorsione in coda).

Strutture ricorsive

Alcune strutture dati sono inerentemente ricorsive, come le Sequenze o le Strutture ad albero… La formulazione ricorsiva di algoritmi su di esse risulta più naturale. Es. Lista linkata, Def: Una lista è… una lista vuota, oppure… un elemento seguito da una lista.

L07) Complessità computazionale

Analisi della complessità: stima del costo degli algoritmi in termini di risorse di calcolo: tempo di esecuzione, spazio di memoria. Variabili che influenzano il tempo di esecuzione: La macchina usata, La dimensione dei dati, La configurazione dei dati (es. array già ordinato). Vogliamo ottenere un modello astratto per la valutazione del tempo, che:

  • Sia indipendente dalla macchina usata
  • Stimi il tempo in funzione della dimensione dell’input
  • Che abbia un comportamento asintotico
  • Fornisca una stima del caso peggiore di configurazione dei dati Esempio di macchina astratta:
  • Istruzioni e condizioni atomiche hanno costo unitario
  • Le strutture di controllo hanno un costo pari alla somma dei costi dell’esecuzione delle istruzioni interne, più la somma dei costi delle condizioni
  • Le chiamate a funzione hanno un costo pari al costo di tutte le loro istruzioni e condizioni;
  • Il passaggio dei parametri ha costo nullo
  • Istruzioni e condizioni con chiamate a funzioni hanno costo pari alla somma del costo delle funzioni invocate più uno Comportamento asintotico: Nell’analizzare la complessità di tempo di un algoritmo siamo interessati a come aumenta il tempo al crescere della taglia n dell’input.

Suddivisione di algoritmi in classi di complessità

Concentrandoci sul comportamento al crescere della dimensione n dei dai all’infinito (trascurando costanti moltiplicative e termini di ordine inferiore, ad esempio, se il tempo richiesto da un algoritmo su tutti gli input di dimensione n è al massimo 5 n^3 + 3n, la complessità temporale asintotica è O(n^3 )), le classi più frequenti sono:

  • a costante
  • a n+b lineare
  • a n^2 + bn + c quadratica
  • a logg n + h logaritmica
  • an^ esponenziale
  • nn^ esponenziale

Notazioni asintotiche O ed Omega

Considerate f e g funzioni dai naturali ai reali positivi diremo che:

  • f(n) è O di g(n), ( f(n)∈O(g(n)) ), se esistono due costanti positive c ed n 0 tali che se n>=n 0 , f(n)<=c g(n). Applicata alla funzione di complessità f(n), la notazione O ne limita superiormente la crescita e fornisce quindi una indicazione della bontà dell’algoritmo.
  • f(n) è Omega di g(n), ( f(n)∈Ω(g(n)) ), se esistono due costanti positive c ed n 0 tali che se n>=n 0 , c g(n)<= f(n). La notazione Ω limita inferiormente la complessità, indicando così che il comportamento dell’algoritmo non è migliore di un comportamento assegnato. Notazione asintotica O(o grande) (limite superiore asintotico) Not. asintotica Ω (limite inferiore asintotico) Not. asintotica θ (theta) (limite asintotico stretto)

Complessità dei problemi

Studiare la complessità di un problema è diverso dallo studiare la complessità di un algoritmo. Un problema può essere risolto da svariati algoritmi. Per poter dire che un problema ha complessità O(g(n)) (ipotizziamo di parlare del caso peggiore) basta trovare un qualsiasi algoritmo che lo risolva con O(g(n)). Per poter aƯermare che un problema è Ω(g(n)) occorre invece dimostrare matematicamente che tutti i possibili algoritmi (inventati o non) lo risolvano alla meglio come Ω(g(n)).