A49. Comprendere e implementare un algoritmo
Moltissime volte ricevo richieste di aiuto o consigli su come eseguire una certa azione, su come far in modo che il programma processi
in una certa maniera dei dati. Tali domande hanno il più delle volte risposte molto semplici. La difficoltà insita in ogni problema di
informatica, sia esso atto a calcolare la media aritmetica di un insieme di misure o finalizzato all'analisi di un file di dati, consiste
nell'individuare un algoritmo efficace per risolverlo.
Non deve spaventare questa parola, che, al contrario di quel che si possa pensare, non differisce dalle tecniche usate per risolvere un problema di matematica. Per chi non ne conoscesse il significato, dò una semplice definizione, ripresa da Wikipedia: "Insieme di istruzioni elementari (univocamente interpretabili) che, eseguite in un ordine stabilito, permettono la soluzione di un problema in un numero finito di passi". Vorrei innanzitutto porre l'attenzione sull'inizio della definizione, ossia "insieme di istruzioni elementari". Infatti, quando il cervello elabora un problema, più per abitudine che per istinto, è portato a considerare più operazioni simultaneamente, ad esempio: per calcolare la media aritmetica di più numeri è necessario dividere la somma per la numerosità dell'insieme dei dati. Si è implicitamente richiesto di calcolare una somma, quindi di eseguire prima un dato numero di addizioni e in seguito una divisione!
Altra caratteristica importante è data dall'ordine: istruzioni uguali, se eseguite in sequenze diverse possono portare a risultati anche assai diversi. Eccone un esempio. Nelle applicazioni Windows Form (che vedremo nella prossima lezione), si può ricorrere all'uso di un timer per eseguire istruzioni a intervalli di tempo prefissati. Si potrebbe voler visualizzare un messaggio quando viene soddisfatta una certa condizione e parallelamente fermare il timer. Il codice che più spesso si scrive è:
Il computer, tuttavia, è un "asino veloce", e non è capace di eseguire operazioni complesse al di fuori di quelle basilari. Bisogna quindi scomporre il problema in più sottoproblemi facilmente risolvibili individualmente. In questo capitolo farò due esempi, uno semplice e un altro più complesso, che richiedono di pensare prima all'algoritmo da utilizzare.
Non si sono riscontrati problemi fino al punto in cui è stato necessario calcolare la frequenza. La difficoltà non esiste se si scompone il quesito in più operazioni semplici. Per far questo, risulta molto utile utilizzare i diagrammi di flusso, oppure uno schema disegnato a penna su un foglio di carta. Ad esempio, ecco uno schema del flusso del problema:
Non deve spaventare questa parola, che, al contrario di quel che si possa pensare, non differisce dalle tecniche usate per risolvere un problema di matematica. Per chi non ne conoscesse il significato, dò una semplice definizione, ripresa da Wikipedia: "Insieme di istruzioni elementari (univocamente interpretabili) che, eseguite in un ordine stabilito, permettono la soluzione di un problema in un numero finito di passi". Vorrei innanzitutto porre l'attenzione sull'inizio della definizione, ossia "insieme di istruzioni elementari". Infatti, quando il cervello elabora un problema, più per abitudine che per istinto, è portato a considerare più operazioni simultaneamente, ad esempio: per calcolare la media aritmetica di più numeri è necessario dividere la somma per la numerosità dell'insieme dei dati. Si è implicitamente richiesto di calcolare una somma, quindi di eseguire prima un dato numero di addizioni e in seguito una divisione!
Altra caratteristica importante è data dall'ordine: istruzioni uguali, se eseguite in sequenze diverse possono portare a risultati anche assai diversi. Eccone un esempio. Nelle applicazioni Windows Form (che vedremo nella prossima lezione), si può ricorrere all'uso di un timer per eseguire istruzioni a intervalli di tempo prefissati. Si potrebbe voler visualizzare un messaggio quando viene soddisfatta una certa condizione e parallelamente fermare il timer. Il codice che più spesso si scrive è:
Nulla di sbagliato nella sintassi e, a prima vista neanche nella logica, ma... se il timer ha un intervallo impostato a valori minori di 2000 (e in genere è sempre sull'ordine delle centinaia), viene visualizzata una serie di decine se non centinaia di messaggi a schermo. Questo perchè, dato che l'istruzione usata per visualizzare i messaggi in questo tipo di applicazioni ferma l'esecuzione del codice fino alla pressione di uno dei pulsanti proposti, la condizione viene soddisfatta un gran numero di volte prima che il codice prosegua al passo successivo, ossia fermare il timer. Quindi il codice più corretto è:If [Condizione]Then '> Visualizza il messaggio '> Ferma il timer End If
Ecco come un errore apparentemente semplice ha compromesso irreversibilmente il programma!If [Condizione]Then '> Ferma il timer '> Visualizza il messaggio End If
Il computer, tuttavia, è un "asino veloce", e non è capace di eseguire operazioni complesse al di fuori di quelle basilari. Bisogna quindi scomporre il problema in più sottoproblemi facilmente risolvibili individualmente. In questo capitolo farò due esempi, uno semplice e un altro più complesso, che richiedono di pensare prima all'algoritmo da utilizzare.
Problema 1: La frequenza assoluta
Ecco un piccolo problema di programmazione che nella mia classe, a suo tempo, ha riscosso una totale incapacità di risoluzione:
La frequenza assoluta
Generare un array di x numeri casuali, con x inserito da testiera, compresi tra y e z, anch'essi inseriti da tastiera. Quindi clacolare la
frequenza assoluta di ogni numero presente.Non si sono riscontrati problemi fino al punto in cui è stato necessario calcolare la frequenza. La difficoltà non esiste se si scompone il quesito in più operazioni semplici. Per far questo, risulta molto utile utilizzare i diagrammi di flusso, oppure uno schema disegnato a penna su un foglio di carta. Ad esempio, ecco uno schema del flusso del problema:
- Creare una variabile x, quindi richiederne il valore da testiera
- Creare due variabili y e z, quindi richiederne il valore da tastiera
- Controllare che y sia minore o uguale a z. In caso contrario, invertire i valori
- Creare un array di x elementi
- Iterare x volte sull'array, inizializzando ogni cella ordinatamente
- Creare un randomizzatore temporaneo (classe Random)
- Assegnare alla cella il valore casuale estratto tra y e z
- L'array è pieno. Ora creare una lista dictionary a tipizzazione forte di (Int32, Int32). La lista conterrà una chiave (il numero) e un valore (la frequenza di quel numero), che verrà incrementata ogni volta che si trova un valore uguale
- Iterare x volte sull'array
- Controllare che il valore della cella dell'array esista: in questo caso, aumentare di 1 la frequenza del valore nella lista che ha per chiave questo numero; in caso contrario, aggiungere tale numero alla lista e impostarne la frequenza ad 1
- Scrivere in output tutte le frequenze
Module Module1Sub Main()Dim ArraySizeAs Int32 'Legge la dimensione Console.WriteLine("Inserire il numero di elementi: ") ArraySize = Console.ReadLine 'Se è minore di 0, esce dal programma If ArraySize < 0Then Console.WriteLine("Dimensione non valida!") Console.ReadKey()Exit Sub End If 'Crea l'array di un dato numero di elementi Dim Values(ArraySize - 1)As Int32Dim Min, MaxAs Int32 'Legge valore minimo e massimo Console.WriteLine("Inserire minimo e massimo:") Console.Write("Minimo: ") Min = Console.ReadLine Console.Write("Massimo: ") Max = Console.ReadLine 'Se il minimo è maggiore del massimo, inverte i 'valori, onde evitare errori in seguito If Min > MaxThen Dim TempAs Int32 = Min Min = Max Max = TempEnd If 'Una nuova classe per generare numeri casuali Dim RndAs New RandomFor IAs Int32 = 0To Values.Length - 1 'Assegna a Values(I) un numero casuale intero tra Min 'e Max, estremi inclusi. Il "+1" è stato messo perchè 'il secondo parametro rappresenta il limite superiore 'escluso. Values(I) = Rnd.Next(Min, Max + 1)Next 'Nuovo dizionario di frequenze Dim FrequencesAs New Dictionary(Of Int32, Int32)For IAs Int32 = 0To Values.Length - 1 'Se Values(I) è un numero già rilevato... If Frequences.ContainsKey(Values(I))Then 'Incrementa la sua frequenza di 1 Frequences(Values(I)) += 1Else 'Altrimenti lo aggiunge, impostandone la 'frequenza a 1 Frequences.Add(Values(I), 1)End If Next 'Scrive ogni valore e rispettiva frequenza a schermo For Each KeyAs Int32In Frequences.Keys Console.WriteLine("Il valore {0} compare {1} volte.", _ Key, Frequences(Key))Next Console.ReadKey()End Sub End Module
Problema 2: Ordinamento
Il secondo problema tratta dell'ordinamento crescente di un insieme di elementi. Non sono molte le occasioni in cui si prospetta un quesito
del genere, dato che il .Net Framework offre metodi di ordinamento automatico per quasi tutti i tipi di collezione. Tuttavia, talvolta è
utile sapere come avviene l'ordinamento e come implementarne uno sotto forma di algoritmo. Ecco un esempio:
Il ragionamento è semplice:Module Module1 'Notare il vincolo di interfaccia: se non è possibile comparare due 'elementi, non è neanche possibile ordinarli Sub SortArray(Of TAs IComparable)(ByVal Values()As T) 'Il numero di sostituzioni effettuate Dim OccurrencesAs Int32 = 0Do Occurrences = 0 'Itera su ogni elemento For IAs Int32 = 0To Values.Length - 1If I = Values.Length - 1Then 'Se è l'ultimo elemento non si può comparare 'con l'elemento successivo in quanto non esiste, 'quindi continua fino alla fine Continue For End If 'Se l'elemento corrente è maggiore del successivo, 'li scambia di posto, poichè l'ordine deve essere 'crescente, e non decrescente If Values(I).CompareTo(Values(I + 1)) = 1Then 'La funzione Swap è stata definita del capitolo 'suoi Generics: scambia due valori Swap(Values(I), Values(I + 1)) 'È stata effettuata una sostituzione: 'aumenta Occurrences di 1 Occurrences += 1End If Next 'Se non sono state effettuate sostituzioni, significa 'che l'array è ordinato, quindi termina Loop Until Occurrences = 0End Sub Sub Main()Dim A()As Int32 = {0, 90, 23, 89, 1098, 26, 478} SortArray(A) 'Il metodo ForEach è un metodo statico della classe astratta 'Array e permette di eseguire una data azione per ogni 'elemento dell'array. Accetta come primo parametro l'array in 'questione e come secondo un delegate di tipo Action generic 'Esso rappresenta una procedura che accetta un solo parametro 'di tipo T. In questo caso, scrive a schermo ogni numero A.ForEach(A,New Action(Of Int32)(AddressOf Console.WriteLine)) Console.ReadKey()End Sub End Module
- Si devono eseguire alcune operazioni sull'array. Partendo dal presupposto che ogni operazione cambi l'ordine di almeno due elementi, una volta che la routine non sostituirà più niente, la lista sarà ordinata
- Sotto questa condizione, si itera sugli elementi dell'array
- Dato che l'ordine è crescente, un elemento nella posizione N deve avere un valore per certo minore o tutt'al più uguale rispetto a ogni elemento successivo. Si esegue quindi un controllo tra l'elemento N-esimo e l'(N+1)-esimo: se il primo è maggiore del secondo, allora li si scambia di posto
- Dopo aver eseguito un'operazione del genere, si incrementa il contatore delle sostituzioni di uno
- Se il contatore delle sostituzioni è zero, ossia tutti gli elementi sono ordinati, esce automaticamente dal ciclo, altrimenti lo ripete un'altra volta
In conclusione
Per "trasformare" un qualsiasi problema in un algoritmo, occorre prima individuare i procedimenti logici che portano alla sua risoluzione
e successivamente scomporli in singole e semplici operazioni eseguibili dal computer. Tutti i costrutti, gli operatori, le classi, e i
metodi analizzati fino ad ora non sono altro che degli strumenti in grado di aiutarvi nella scrittura di un programma: quello che è necessario
fare è saper scegliere quali usare e quando usarli per raggiungere la soluzione nel modo più efficace.
The Totem's Lair - Copyright (C) 2009
È vietata la riproduzione sia totale che parziale del sito.



