1.42k likes | 1.61k Vues
Dott. Ing. Leonardo Rigutini Dipartimento Ingegneria dell’Informazione Università di Siena Via Roma 56 – 53100 – SIENA Uff. 0577233606 rigutini@dii.unisi.it http://www.dii.unisi.it/ ~ rigutini/. Introduzione alla programmazione. Algoritmo. Che cos’è l’informatica.
E N D
Dott. Ing. Leonardo Rigutini Dipartimento Ingegneria dell’Informazione Università di Siena Via Roma 56 – 53100 – SIENA Uff. 0577233606 rigutini@dii.unisi.it http://www.dii.unisi.it/~rigutini/ Introduzione alla programmazione
Che cos’è l’informatica L’informatica è lo studio sistematico degli algoritmi che descrivono e trasformano l’informazione: la loro teoria, analisi, progetto, efficienza, realizzazione (ACM Association for Computing Machinery) Nota:È possibile svolgere un’attività concettualmente di tipo informatico senza l’ausilio del calcolatore, per esempio nel progettare ed applicare regole precise per svolgere operazioni aritmetiche con carta e penna; l’elaboratore, tuttavia, è uno strumento di calcolo potente, che permette la gestione di quantità di informazioni altrimenti intrattabili
Algoritmo Algoritmo: sequenza di istruzioni attraverso le quali un operatore umano è capace di risolvere ogni problema di una data classe; non è direttamente eseguibile dall’elaboratore Programma: sequenza di operazioni atte a predisporre l’elaboratore alla soluzione di una determinata classe di problemi Il programma è la descrizione di un algoritmo in una forma comprensibile all’elaboratore L’elaboratore è una macchina universale:cambiando il programma residente in memoria, è in grado di risolvere problemi di natura diversa (una classe di problemi per ogni programma)
Algoritmi nella realtà Come abbiamo già detto un algoritmo può essere descritto in maniera informale come “una sequenza di passi, definiti con precisione, che portano alla realizzazione di un compito” Es: Le istruzioni di montaggio di un elettrodomestico Il calcolo del MCD (massimo comune divisore) Il prelievo di denaro dal bancomat: Inserimento tessera Inserimento codice segreto .ecc… Molte attività umane sono “algoritmi”
Algoritmo Un algoritmo deve essere: comprensibile Altrimenti l’esecutore non può capirlo non-ambiguo: Altrimenti vi potrebbero essere diverse valide interpretazioni dello stesso algoritmo corretto Ovviamente dovrebbe effettivamente fare quello per cui è stato pensato efficiente (utile ma non vincolante) Dovrebbe eseguire il suo compito nel modo più veloce possibile
Codifica di un algoritmo Quindi per risolvere un problema è necessario esprimerlo sotto forma di algoritmo: Una volta descritto in termini di passi più o meno elementari è possibile codificare questi passi utilizzando il linguaggio di programmazione prescelto Ricordiamo anche che per un dato problema possono esistere molteplici algoritmi che lo risolvono e che possono distinguersi per la migliore o peggiore complessità L’uso di un linguaggio di alto livello permette di utilizzare nomi per le variabili e per le funzioni molto vicini alla loro funzione: tassoInteresse per una variabile che indica il tasso d’interesse, ecc…
Schema dell’algoritmo Solitamente la codifica di un problema in algoritmo avviene utilizzando i “diagrammi di flusso”, ovvero diagrammi grafici che permettono di visualizzare il “flusso” delle operazioni da compiere per arrivare alla soluzione Un diagramma di flusso schematizza l’ordine delle operazioni più o meno semplici richieste per risolvere un problema Ogni operazione di assegnamento è racchiusa in un rettangolo mentre le operazioni di verifica sono visualizzate in rombi
Diagrammi di flusso Esempio di codifica di un banale algoritmo con un diagramma di flusso: Rappresentare un grosso programma tramite un unico diagramma di flusso diventa comunque impossibile: Solitamente un algoritmo viene scomposto in unità funzionali distinte (funzioni, procedure o moduli) Ogni unità dovrebbe essere così semplice da poter essere facilmente schematizzabile tramite un diagramma di flusso leggi x e memorizzalo in a a > 100 Fai qualcosa Stampa “errore” true false
Linguaggi Il linguaggio utilizzato per descrivere l’algoritmo deve essere interpretabile dall’esecutore dell’algoritmo: Se abbiamo le istruzioni di montaggio di un elettrodomestico in cinese, non siamo in grado di seguire i passi necessari a terminare il lavoro (a meno che non si conosca il cinese!!) In informatica: I calcolatori sono “esecutori di algoritmi” Gli algoritmi sono descritti tramite programmi, cioè sequenze di istruzioni scritte in un opportuno linguaggio, comprensibile al calcolatore Compito dell’esperto informatico: Produrre algoritmi per un dato problema Codificarli in programmi (renderli comprensibili al calcolatore)
Precisione Il calcolatore esegue un programma passo-passo, in modo preciso, veloce e potente, senza deviare dal flusso delle istruzioni anche se esso è palesemente sbagliato: Questo perché il calcolatore è privo di “intelligenza”, non può decidere se una istruzione è sbagliata, se c’è, la esegue. Questo richiede quindi che le istruzioni siano puntuali e che l’algoritmo sia chiaro e puntuale: “per fare la torta di mele occorrono 3 Kg di mele, 3 uova e 0,5 Kg di farina” è sufficientemente precisa “legate l’arrosto e salatelo” non è sufficientemente precisa poiché per esempio non specifica la quantità di sale da utilizzare
Correttezza Un algoritmo è corretto se esso perviene alla soluzione del compito per cui è stato sviluppato, senza difettare di alcun passo. Se nell’algoritmo di montaggio di un mobile viene omessa l’istruzione di stringere una vite, il risultato finale può non essere “corretto” perché il mobile molto probabilmente non starà in piedi (dipende dalla importanza della vite!!) Sarà necessario individuare l’errore e ripararlo per avere il risultato corretto Un algoritmo non deve tralasciare nessun passo per giungere alla soluzione, altrimenti il suo risultato potrebbe essere non corretto.
Efficienza Un algoritmo dovrebbe raggiungere la soluzione nel minor tempo possibile e/o utilizzando la minima quantità di risorse, compatibilmente con la sua correttezza. efficienza in tempo e spazio (risorse) Es. Non è efficiente un algoritmo in cui prima viene regolato un dispositivo e poi, nel passo successivo, tale impostazione viene modificata, se poi deve essere riportata a come era prima Un buon informatico cercherà di ottimizzare un algoritmo il più possibile per cercare di arrivare alla soluzione il più veloce possibile e con l’utilizzo di minor risorse possibili
Complessità La bontà di un programma viene valutata analizzando due fattori: Quanto è veloce il programma (complessità in tempo) Quante risorse richiede il programma (complessità in spazio) Solitamente, a meno di richieste esagerate, la complessità spaziale non è un problema, quindi si “guarda” molto più a quanto tempo ci vuole per eseguire un compito
Complessità La complessità temporale di un programma però non è di semplice valutazione Tempo di esecuzione: dipende dalla potenza della macchina su cui è eseguito dipende dalla dimensione dell’input (molti dati molto più tempo) La soluzione è stata di assegnare un costo alle operazioni e calcolare quindi la complessità totale come costo complessivo: L’operazione di confronto ha costo uno Tutte le altre hanno costo nullo (non è vero, ma hanno un costo irrisorio rispetto al confronto)
Complessità Inoltre non è possibile dare una misura puntuale del costo di un algoritmo, poiché molte volte esso dipende dallo spazio dell’input: Si calcola quindi il costo in funzione della dimensione dell’input costo = f(n) , dove n è la dimensione dei dati su cui andiamo a lavorare Infine è necessario calcolare il costo nel caso migliore e nel caso peggiore : o(f(n)) per la complessità nel caso migliore (costo minimo) O(f(n)) per la complessità nel caso peggiore Il limite inferiore (o() ) non ha molto significato
Complessità Es. Problema: “Dati n numeri, trovare il più grande” Algoritmo: MAXVAL = primo numero Per ogni numero seguente, se è maggiore di quello in MAXVAL, memorizzalo in MAXVAL Complessità: nel caso peggiore, il valore massimo si trova in ultima posizione; in questo caso sono necessari n-1 confronti. Quindi la complessità dell’algoritmo è O(n). Tale funzione (f(n)=n) è detta anche “lineare”, dato che cresce linearmente con n.
Complessità In realtà, l’esempio precedente, avrebbe complessità O(n-1), ma poiché per n >>1 O(n-1)~O(n), viene utilizzato O(n) come valore di complessità Questo vale per tutte le funzioni f(n) indicanti la complessità di un algoritmo: O(2n+4) , per n>>1 O(n) O(2n2+n+4), per n>>1 O(n2) Ecc.. cioè viene individuata la funzione maggiorante
Complessità complessità n • La complessità dà un’idea della “fattibilità” dell’algoritmo. Supponiamo il costo di una operazione pari ad 1 secondo, il tempo necessario per un algoritmo con le varie complessità è il seguente: • Come si vede, un problema con complessità polinomiale diventa molto velocemente “intrattabile”
Esempio: un algoritmo più complicato Gestione di una biblioteca: Ogni libro si trova su uno scaffale e può essere preso in prestito ed in seguito restituito. La biblioteca è dotata di uno schedario ed ogni scheda contiene: Nome,cognome dell’autore (o autori) Titolo Data di pubblicazione Numero scaffale in cui si trova Numero d’ordine della posizione Le schede sono disposte in ordine alfabetico in base all’autore ed al titolo.
La biblioteca Algoritmo di ricerca di un libro in biblioteca: Ricerca della scheda nello schedario Lettura del numero di scaffale e posizione del libro Ricerca dello scaffale indicato Prelievo del libro e scrittura sulla scheda delle informazioni sul prestito: data,nome ecc.. Ogni passo può (deve) essere descritto più dettagliatamente: sotto-algoritmo Passo1: 1.1 Lettura della prima scheda dello schedario 1.2 Se il nome dell’autore ed il titolo coincidono, ricerca conclusa 1.3 Si ripete il passo 2 fino trovare la scheda cercata o a terminare le schede 1.4 Se non viene trovata la scheda, la ricerca termina in modo infruttuoso
La biblioteca Il sotto-algoritmo per il passo 1, è corretto ma non efficiente: Ha una “complessità” lineare con il numero di schede, ossia nel caso peggiore tutte le schede nello schedario verranno analizzate E’ possibile definire un altro algoritmo per il passo 1 più efficiente: 1.1 Se lo schedario è vuoto ricerca infruttuosa 1.2 Lettura scheda centrale 1.3 Se è quella cercata, ricerca conclusa con successo 1.4 Se il nome dell’autore e/o titolo è precedente, si ripete l’algoritmo sulla prima metà dello schedario 1.5 Se il nome dell’autore e/o titolo è successivo, si ripete l’algoritmo sulla seconda metà dello schedario Nel caso peggiore, questo algoritmo analizza log2(n) schede: 16000 schede alg1 = 16000 confronti 16000 schede alg2 = 14 confronti
La biblioteca Nel secondo algoritmo presentato per automatizzare il passo 1, l’algoritmo richiama se stesso su un differente set di dati fino ad una terminazione positiva o negativa Un algoritmo di questo tipo si dice “ricorsivo”: ovvero il problema viene scomposto in sottoproblemi uguali ma su set di dati più piccoli, fino ad arrivare o alla soluzione o alla impossibilità di proseguire condizioni di stop: Il sottoinsieme su cui viene richiamato l’algoritmo è vuoto (1.1) La ricerca è andata a buon fine (1.3) Vedremo più avanti in dettaglio quali tipi di algoritmi esistono
Livelli di astrazione e storia dei linguaggi di programmazione
Programmazione di basso livello Al livello più basso le istruzioni di un computer sono estremamente elementari. Il processore esegue le istruzioni macchina: Sequenze di bytes i codici delle operazioni e le locazioni in memoria Le CPU di fornitori diversi (Intel, SUN, Mac) hanno insiemi differenti di istruzioni macchina: Instruction set
Linguaggio binario Scrivere programmi direttamente in linguaggio macchina però risulta essere molto complicato: Sequenze di numeri con poca espressività Es • Carica il contenuto della posizione di memoria 40 • Carica il valore • Se il primo valore è maggiore del secondo continua con l’istruzione in posizione 240 21 40 16 100 163 240
Assembler Il primo passo fu di assegnare nomi abbreviati ai comandi: iLoad “carica un numero intero” bipush “inserisci una costante numerica” if_icmpgt “se il numero intero è maggiore” L’esempio può essere riscritto così: iLoad 40 bipush 100 if_icmpgt 240 Similmente in seguito furono assegnati dei nomi anche alle locazioni di memoria (variabili): iLoad intRate bipush 100 if_icmpgt intError ASSEMBLER
Assemblatore Tale metodo di programmazione richiedeva quindi un software che traducesse il codice dal linguaggio ASSEMBLER al linguaggio macchina: ASSEMBLATORE La nuova forma di programmazione continua ad avere corrispondenza uno-a-uno con il linguaggio macchina Ad ogni istruzione ASSEMBLER corrisponde una istruzione del Instruction Set Rimane però molto difficile sviluppare grandi programmi utilizzando istruzioni di così basso livello: If_cmpgt, iLoad, ecc… lavorano su indirizzi in memoria, su registri della CPU ed anche un semplice confronto tra due variabili richiede una sequenza di alcune istruzioni assembler
Linguaggi di alto livello Furono teorizzati allora linguaggi di programmazione che astraevano dalla architettura del calcolatore (indipendenza dall’ Instruction Set) e permettevano costrutti molto più vicini al “pensiero umano”: “se EXPR allora FAI QUESTO, altrimenti FAI QUEST’ALTRO” “ripeti L1 fino a che NON ACCADE QUESTO” Questi costrutti permettevano di racchiudere una sequenza di istruzioni macchina in una “descrizione” molto più vicina all’essere umano
La compilazione Questi linguaggi richiedono l’uso di un compilatore che traduca il codice di alto livello in codice macchina: Ogni linguaggio ha quindi il suo compilatore (o più di uno) Inoltre, dato che ciò che il compilatore genera è il codice macchina e quest’ultimo è strettamente dipendente dalla macchina (Hardware/SO) su cui gira, la compilazione traduce il codice da un linguaggio astratto in una forma dedicata e strettamente dipendente dalla macchina che si sta adoperando
La compilazione Quindi: È il compilatore che si occupa di tradurre un dato linguaggio (es. C) nel codice macchina adatto alla macchina su cui dovrà essere eseguito Se un programma C viene compilato su ambiente Windows, sarà creato un programma che “girerà” solo su ambienti windows; similarmente, se lo stesso codice viene compilato su Linux, sarà possibile eseguire l’applicazione solo su una macchina Linux Un linguaggio di programmazione di alto livello deve rispettare RIGOROSE convenzioni affinché sia interpretabile dal compilatore
Linguaggi alto livello se il tasso di interesse è maggiore di 100 scrivi a video un messaggio di errore Sviluppo If (intRate>100) printf(“Error”) C++ Compilazione 21 40 16 100 163 240
La compilazione Il processo di compilazione però “peggiora” le prestazioni rispetto ad un programma scritto direttamente in linguaggio macchina: Il tempo richiesto dal traduttore per generare il codice macchina Il codice oggetto generato dal compilatore tipicamente è meno “ottimizzato” rispetto a quello generato direttamente dall’essere umano In realtà: Anche considerando il tempo di traduzione del compilatore, il tempo necessario allo sviluppo del programma rimane comunque decisamente inferiore a quello necessario a sviluppare direttamente in codice oggetto Il codice generato dal compilatore è sì difficile che sia “ottimizzato”, ma comunque la perdita di prestazione risulta essere accettabile: Oggi inoltre tutti i compilatori hanno la possibilità di ottimizzare il codice E comunque non è detto che un essere umano riuscirebbe a generare codice ottimizzato per programmi molto complicati come quelli che si possono scrivere utilizzando linguaggi di alto livello
Un po’ di storia dei linguaggi di alto livello A seguito di queste teorie nacquero i primi linguaggi di alto livello: FORTRAN(FORmula TRANslator): il primo linguaggio di alto livello sviluppato per descrivere formule matematiche COBOL (Common Business Oriented Language): il primo linguaggio orientato alle applicazioni gestionali Per molto tempo questi due linguaggi rimasero i linguaggi più diffusi fino a quando non fu studiata una categoria di linguaggi più rigorosamente basati su uno studio dei principi della programmazione: Il capostipite di questa famiglia fu ALGOL60 (ALGOrithmic Language) che pur non essendo mai stato utilizzato nella pratica, è famoso per questo motivo.
Un po’ di storia dei linguaggi di alto livello Dopo l’ALGOL60, furono sviluppati molti nuovi linguaggi di programmazione “orientati” ad una programmazione generale : PASCAL, oggi molto diffuso per la didattica C ,tra i più potenti linguaggi di programmazione, divenuto molto popolare grazie al fatto che il sistema Unix fu interamente sviluppato utilizzando questo linguaggi di programmazione ADA, Basic, Perl, Phyton, ecc…. Anche questi però ad un certo punto furono superati da un nuovo paradigma: la programmazione ad oggetti C++, C# e Java derivati dal C Eiffel e Delphi derivati dal Pascal VisualBasic ecc…
Linguaggio procedurale La programmazione “classica” segue il paradigma di programmazione procedurale o imperativo Cosa significa: Il problema è scomposto in molti sottoproblemi che possono essere risolti in maniera semi-indipendente per poi essere aggregati per ottenere il risultato finale Ogni sotto-problema viene assegnato ad un sottoprogramma che si occupa di eseguire le operazioni per restituire il risultato (o effettuare il compito per cui è stato creato) L’applicazione stessa è vista come un sottoproblema: Assemblare i sotto-problemi individuati in maniera da ottenere il risultato
Linguaggio procedurale Quindi seguendo questa idea, il programma è costituito da diverse funzioni (procedure o subroutines) ognuna disegnata per uno scopo Il corpo del programma è esso stesso una funzione, con la particolarità però di avere una forma (prototipo) standard: la funzione main() Quando un programma procedurale è compilato ed avviato, ciò che viene lanciato è la procedura main: Tutto ciò che deve essere eseguito dal programma deve essere implementato nella procedura main, ricorrendo a tutte le chiamate di funzione e dati necessari
Linguaggio procedurale Dato A funzione1 Dato B funzione2 int main(int,int) … … Dato N funzioneM avvio dell’applicazione
Variabili Una variabile rappresenta un’area di memoria riservata dal compilatore per memorizzare dati Quest’area di memoria è riferita tramite un nome univoco: Questo nome permette di accedere alla memoria per leggere/scrivere informazioni In fase di programmazione, quando viene definita ed utilizzata la variabile non ha un valore, ma rappresenta tutti i valori che ciò che essa rappresenta potrà assumere Il valore è assegnato a run-time
Nomi di variabili Ogni variabile (e funzione che vedremo in seguito) è riferita tramite un nome Un nome in un linguaggio di programmazione è inteso come una etichetta che rispetta alcune regole fissate dal linguaggio stesso: Deve essere costituita da un singolo token. Etichette formate da più parole separate non sono accettate (il compilatore non può sapere che le due parole vanno interpretate insieme!!) Non può essere una delle parole riservate del linguaggio (if, else, while, return ecc…) Deve contenere solo caratteri alfa-numerici più il carattere ‘_’: questa condizione implica la prima poiché il carattere spazio ‘ ‘ non è permesso Non può comunque iniziare con un carattere numerico
Dichiarazione di variabili Dichiarare una variabile significa avvertire il compilatore che riservi un’area di memoria perché in futuro essa verrà utilizzata con quel nome: Processo di allocazione della memoria Ogni variabile, prima di essere utilizzata, deve essere dichiarata: In caso contrario il compilatore genera un errore, non trovando la corrispondenza del nome con un area di memoria nella tabella simboli Quando una variabile viene dichiarata, deve perciò essere specificato sia il nome della variabile, sia il tipo di dato che tale variabile potrà assumere: Se una variabile viene dichiarata di tipo T, necessariamente essa potrà memorizzare valori di tipo T
Tipi di dato I tipi di dato si distinguono in tipi semplici e tipi strutturati: tipi di dato semplici – sono tipi di dato a cui può essere associato un singolo valore (numerico o stringa) ed un riferimento alla variabile è un riferimento al contenuto tipi di dato strutturati – sono tipi di dato composti da più “campi”, da cioè uno o più altri tipi di dato a loro volta semplici o strutturati Ogni linguaggio di programmazione mette a disposizione una serie di tipi di dati predefiniti: Tipi di dato semplici bool, int, char, double, ecc… Tipi di dato strutturati array
Tipi semplici predefiniti Tutti i linguaggi di programmazione mettono a disposizione un numero di tipi di dato semplice “built-in”:
Operatori • Per questi tipi di dato semplici sono forniti dal linguaggio anche gli operatori: • Assegnamento (=) • Addizione (+) • Sottrazione (–) • Moltiplicazione (*) • Divisione (/) • Modulo (%) , ritorna il resto della divisione. Es. 5%3 = 2 • Uguaglianza (==) e disuguaglianza (!=), ritornano vero o falso • Minore o maggiore (<, <=, >, >=) , ritornano vero o falso
Il tipo char • Il tipo char è un tipo semplice particolare: • Il nome deriva dall’abbreviazione di “character” ed infatti è utilizzato per rappresentare l’insieme finito dei caratteri (la tabella ASCII) • Ogni carattere ha un codice numerico compreso tra 0 e 127 (7 bit), più un “extended set” compreso tra 128 e 255 (8° bit)
Il tipo char e le sequenze di escape Alcuni codici sono riservati per i caratteri di controllo e solitamente sono indicati utilizzando il carattere “\” come carattere di escape: Molte volte è necessario utilizzare particolari sequenze di caratteri per rappresentare caratteri che altrimenti non sarebbe possibile visualizzare Es: Le stringhe sono sequenze di caratteri delimitate da “. Se vogliamo inserire un carattere “ all’interno di una stringa dobbiamo utilizzare la sequenza di escape \” per informare il compilatore che “ non delimita la fine della stringa Di solito le sequenze di escape sono utilizzate per i caratteri di controllo: “new line” \n, “tab” \t, ecc….
Caratteri di controllo La rappresentazione dei caratteri tramite un codice numerico permette un ordinamento tra gli stessi caratteri e quindi la possibilità di operazioni