
































Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Un'introduzione ai concetti fondamentali dell'informatica, come algoritmi, strutture dati e complessità. Esplora la rappresentazione dei numeri binari, la complessità degli algoritmi e la loro analisi, illustrando esempi pratici come il calcolo del fattoriale e la ricerca di elementi in un vettore. Inoltre, vengono presentate le strutture dati come liste e alberi binari, con esempi di operazioni come l'inserimento, la cancellazione e la ricerca di elementi.
Tipologia: Sintesi del corso
1 / 40
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!

































La logica binaria, ideata da G.Boole oltre cento anni fa, e la logica fondamentale dell’informatica. Essa utilizza due valori, 0 e 1, che possono essere considerati valori di verita, detti bit (0 corrisponde a falso e 1 corrisponde a vero) e tre operazioni fondamentali: AND, OR e NOT. In pratica si attribuisce un valore di verita alle proposizioni (ad esempio secondo l’interpretazione che queste proposizioni hanno nel mondo reale) cioe si indica se ciascuna proposizione e vera oe falsa. Il passo successivo e quello di definire delle operazioni logiche tra proposizioni di modo da costruire propo- sizioni composte da altre proposizioni (ad esempio dalle proposizioni p “io sono giovane” e q “io sono vecchio” costruire la congiunzione p AND q “io sono giovane E io sono vecchio”) indicando quale il valore di verita della proposizione composta a partire dai valori di verita delle proposizioni di cui e composta. Nell’esempio precedente la proposizione p AND qe falsa in quanto non posso essere contemporaneamente giovane e vecchio.
L’operazione AND, o prodotto logico o congiunzione, e un’operazione che dati due bit x e y da come risultato 1 solo quando sia x che y sono 1, altrimenti da 0. La sua tabella di verita `e la seguente.
AND 0 1 0 0 0 1 0 1
L’operazione corrisponde alla congiunzione italiana “e” nel senso che se x denota il valore di verita della frase “A” e y denota il valore di verita della frase “B” allora x AND y e il valore di verita della frase “A e B”. Ad esempio se x e il valore di verita della frase “Siena e in Toscana” (x = 1) e y della frase “Il canee un animale” (y = 1), allora x AND y (che fa 1) e il valore di verita di “Siena e in Toscana e il canee un animale”. Altro esempio: se x e il valore di verita della frase “Siena e in Lombardia” (x = 0) e y della frase “Il canee un animale” (y = 1), allora x AND y (che fa 0) e il valore di verita di “Siena e in Lombardia e il canee un animale”.
L’operazione OR, o somma logica o disgiunzione, e un’operazione che dati due bit x e y da come risultato 1 se almeno uno tra x e y e 1, altrimenti se x e y sono entrambi 0 da 0. La sua tabella di veritae la seguente.
OR 0 1 0 0 1 1 1 1
L’operazione corrisponde alla congiunzione italiana “o” (in senso inclusivo, meglio indicata con e/o): se x denota il valore di verita della frase “A” e y denota il valore di verita della frase “B” allora x OR y e il valore di verita della frase “A e/o B”, che `e vera anche quando sia “A” che “B” sono veri (a differenza del significato
La logica binaria e utilizzata a livello circuitale per costruire tutte le componenti hardware di controllo (unita di controllo, ALU, registri, controllo della memoria, schede, ecc.). Un circuito e costituito da un insieme di porte logiche connesse tra di loro attraverso dei cavi su cui passa una corrente di basso voltaggio. Le porte logiche utilizzate in un circuito sono le porte AND, OR, EXOR e NOT il cui funzionamentoe analogo alle operazioni binarie. In piu sono molto utilizzate le porte NAND (equivalenti alla combinazione di una porta AND e di una porta NOT) e le porte NOR (equivalenti alla combinazione di una porta OR e di una porta NOT). Viene assegnato un voltaggio di corrente diverso a ciascun valore booleano (ad esempio +5V per il valore 1 e +0V per il valore 0). Il funzionamento dell porte puo essere descritti in termini elettrici: una porta AND ha due ingressi e un’uscita, in cui l’uscita produce un voltaggio di +5V solo se entrambi gli ingressi hanno voltaggio +5V , altrimenti produce un voltaggio pari a +0V. Le altre porte hanno un funzionamento analogo. Le tre porte AND, OR e NOT bastano a generare qualsiasi tipo di dispositivo binario secondo il teorema di espansione: Qualsiasi funzione binaria, cioe una funzione f che abbia n argomenti binari x 1 ,... , xn e produce come risultato un valore binario y = f (x 1 ,... , xn), puo essere rappresentata attraverso un’espressione logica binaria composta solo dalle tre operazioni AND, OR e NOT. A sua volta e possibile far vedere che le operazioni AND, OR e NOT possono essere simulate utilizzando solo operazioni NAND (o solo operazioni NOR), per cui un circuito puo essere composto usando solo porte NAND (o porte NOR).
1.2 Numeri binari
Un insieme di otto bit forma un byte, il quale puo servire per memorizzare un numero intero compreso tra 0 e 255. Per capire in che modo sono rappresentati i numeri di questo tipo all’interno di un byte si devono fare alcune considerazioni. I bit all’interno di un byte sono numerati a partire da destra (il primoe il bit meno significativo, bit numero
e il bit piu significativo, bit numero 7). Per vedere qual `e il numero rappresentato da una determinata sequenza di bit si attribuisce un peso numerico a ciascun bit, partendo con il bit meno significativo (l’ottavo bit) che ha peso 1 e assegnando ad ogni altro bit un peso doppio di quello precedente.Bit 7 6 5 4 3 2 1 0 Peso 128 64 32 16 8 4 2 1
I pesi corrispondono alle potenze successive di 2, a partire da 2^0 = 1 fino a 2^7 = 128, di modo che al bit numero K e assegnato il peso 2K^. Si sommano poi i pesi dei bit pari ad uno: il totale ottenutoe il numero rappresentato con quel byte. Ad esempio il byte 00110010 corrisponde al numero 50, infatti si ha
Bit 7 6 5 4 3 2 1 0 Valore 0 0 1 1 0 0 1 0 Peso 128 64 32 16 8 4 2 1
in cui la somma dei pesi e 32 + 16 + 2 = 50. Un altro esempio: il byte 01010101 corrisponde al numero 85. Il numero piu piccolo rappresentabile e 00000000, pari a 0, mentre il piu grande e 11111111, pari a 255. In pratica se b 7 ,... , b 0 sono le cifre binarie allora il numero corrispondentee
∑^7
i=
bi 2 i
Per fare la conversione inversa, cioe passare da decimale a binario, bisogna effettuare una serie di divisioni per due consecutive, fino ad arrivare a zero, e poi segnarsi i resti delle divisioni. Questi scritti nell’ordine inverso formeranno il numero binario. Ad esempio per convertire 25 in binario si procede nel modo seguente 25:2=12 con resto di 1 12:2=6 con resto di 0 6:2=3 con resto di 0 3:2=1 con resto di 1 1:2=0 con resto di 1 Quindi 25 in binarioe 11001. Per ottenere un byte si aggiungono degli zeri avanti fino ad avere 8 bit: 00011001 Un altro esempio: 56 in binario `e 00111000
Per rappresentare un numero intero non negativo si possono usare numeri con piu di 8 bit. Ad esempio un insieme di 16 bite in grado di memorizzare numeri compresi tra 0 e 65535 = 2^16 − 1. In generale un insieme di N bit puo memorizzare numeri tra 0 e 2N^ − 1. I valori piu comuni di N per gli interi sono 16 e 32 bit, che servono a rappresentare numeri compresi tra 0 e 65535 e tra 0 e 4294967295, rispettivamente. La formula per convertire da binario a decimale e la stessa: se bN − 1 ,... , b 0 sono le cifre binarie allora il numero corrispondentee N∑ − 1
i=
bi 2 i
E’ possibile definire sui byte o su qualunque sequenza finita di bit l’equivalente delle funzioni logiche AND, OR, NOT, EXOR, ecc., effettuandole bit a bit. Ad esempio se si prende X = 10110 e Y = 10011 allora X AND Y = 10010, X OR Y = 10111, NOT X = 01001, X XOR Y = 00101. Un’altra operazione fondamentale binaria definibile su sequenze di bit e lo shift o scorrimento. Esistono due tipi fondamentali di shift: lo shift a sinistra e lo shift a destra. Il primo consiste nel far scorrere tutti i bit di una posizione verso sinistra, perdendo il bit piu significativo e mettendo zero come bit meno significativo. Il secondo consiste nel far scorrere tutti i bit di una posizione verso destra, perdendo il il bit meno significativo e mettendo zero come bit piu significativo. Ad esempio lo shift a sinistra del byte X = 00100101e 01001010, mentre quello a destra da 00010010. Si noti che lo shift a sinistra raddoppia un numero intero 1 , dato che ogni cifra binaria avra un peso doppio rispetto a quello di partenza. Allo stesso modo lo shift a destra dimezza un numero intero, trascurando l’eventuale resto.
Le operazioni aritmetiche con i numeri binari sono molto semplici. L’addizione si fa bit per bit tenendo conto che 0 + 0 = 0, 0 + 1 = 1, 1 + 0 = 1, 1 + 1 fa 0 con riporto di 1. Ad esempio 1 1 1 (riporti) 0 1 0 0 1 1 0 0 (76) + 0 1 0 1 1 0 1 0 (90) = 1 0 1 0 0 1 1 0 (176) Si noti che dati due bit x e y, la loro somma coincide con x XOR y, mentre il riporto che si generae dato da x AND y. Anche la moltiplicazione e molto semplice: infatti ogni numero puo essere solo moltiplicato per la cifra 1, che da il numero in questione, o per la cifra 0, che da 0. Per il resto e esattamente come la moltiplicazione decimale. La moltiplicazione tra due bit x e y corrisponde x AND y. (^1) tranne il caso in cui il bit piu significativo sia 1
In pratica la somma tra 100 e 50 darebbe come risultato 150, che e la rappresentazione di -106. Percio se la somma di due numeri positivi da come risultato un numero negativo si ha overflow. Allo stesso modo se la somma di due numeri negativi da come risultato un numero positivo si ha ancora overflow. Anche le altre operazioni possono generare errori di overflow: si pensi ad esempio al prodotto a 8 bit tra 50 e 30.
Nell’elaboratore possono essere rappresentati solo i numeri reali a precisione finita, ossia aventi un numero finito (e fissato a priori) di cifre, sia prima che dopo la virgola. Il motivo e ovvio, in quantoe impossibile memorizzare numeri con infinite cifre ed e difficile trattare con numeri aventi un numero di cifre non fisso. Percio sar`a impossibile rappresentare esattamente sia numeri razionali periodici (ad esempio 1/3 se si usa la base 10) sia numeri reali irrazionali (ad esempio
2 o π). La forma piu semplice per rappresentare numeri realie quella della virgola fissa, cioe un’estensione della rappresentazione dei numeri interi positivi. Si hanno m cifre binarie prima della virgola e n cifre binarie dopo la virgola, in totale quindi m + n cifre complessive. I pesi delle cifre prima della virgola sono le potenze successive di due ad esponente positivo o nullo (esatta- mente come per i numeri interi), mentre i pesi per le cifre dopo la virgola sono 12 , 14 , 18 , 161 ,... , cioe le potenze successsive di due ad esponente negativo in quanto 2−^1 = 12 , 2−^2 = 14 ,.... Un numero avente le cifre bm− 1... b 0 prima della virgola e le cifre b− 1... b−n dopo della virgolavale quindi m∑− 1
i=−n
bi 2 i
Ad esempio con m = 3 e n = 4 il numero binario 011,0101 vale
1 · 21 + 1 · 20 + 1 · 2 −^2 + 1 · 2 −^4 = 2 + 1 + 0.25 + 0. 0625 = 3. 3125
La notazione in virgola fissa non e molto utilizzata e al suo posto i numeri reali vengono usualmente rappresentati con una notazione specifica, detta virgola mobile. Ogni numero reale rappresentabile xe rappresentato con
x = m · 2 e
in cui m (la mantissa di x) e un numero reale con un numero M di cifre binarie compreso tra 1/2 e 1, memorizzato con il metodo a virgola fissa (0 cifre prima della virgola e M dopo), mentre e (l’esponente di x)e un numero intero di E cifre binarie (segno compreso). La richiesta che la mantissa sia maggiore o uguale a 1/2 fa sı che la rappresentazione sia unica. Per avere un’idea della rappresentazione a virgola mobile si possono fare degli esempi usando la base 10 anziche base 2, cio`e con x = m · 10 e
in cui m `e compreso tra 0.1 e 1. Prendendo M = 5 e E = 2 i numeri reali 10, − 4. 5 , 61. 244 , 234171 − 14500000 , 0 .000000834567 sono rappre- sentati con
In pratica la mantissa rappresenta il numero in cui la virgola e portata immediatamente a sinistra della prima cifra (sia intera che frazionaria) e l’esponente rappresenta di quante cifre la virgola deve essere spostata per riottenere il numero originario a partire dalla mantissa (l’esponentee positivo se la virgola va spostata verso destra, altrimenti e negativo). Si noti che alcuni degli esempi riportati sopra non sono rappresentabili esattamente con i parametri scelti, ad esempio l’ultima cifra dell’ultimo esempio. Inoltre si deve tenere conto che n´e i numeri frazionari periodici, n´e i numeri irrazionali potranno mai essere esattamente rappresentati con un numero finito di cifre. Inoltre esistono dei numeri in valore assoluto troppo piccoli da poter essere rappresentati (situazione di underflow), ad esempio 10−^200 o troppo grandi (situazione di overflow), ad esempio 10^300. L’underflowe generalmente rappresentato il numero come se fosse pari a zero, mentre l’overflow genera solitamente una condizione di errore. Valori comuni che si possono trovare per la rappresentazione dei numeri reali in base binaria sono
Dato che la mantissa inizia sempre con la cifra 1, poichee maggiore o uguale a 1/2. quest’ultima non viene memorizzata e al suo posto viene memorizzato il bit di segno (0 se il numero e positivo, 1 see negativo). Le operazioni con i numeri reali a virgola mobile sono pi`u complesse delle operazioni con i numeri interi. In particolare sono necessarie delle operazioni di allineamento delle mantisse. Ad esempio, utilizzando la stessa rappresentazione decimale di prima, la somma 1.0564 + 0.23188 viene effettuata allineando le due mantisse in modo da portare la virgola nella stessa posizione
1 .0564 = 0. 10564 · 10 +01^ = 0. 10564 · 10 +01^ + 0 .23188 = 0. 23188 · 10 +00^ = 0. 02319 · 10 +01^ = risultato = 0. 12883 · 10 +
Come si puo vedere l’allineamento produce un errore di arrotondamento dovuto al fatto che il risultato corretto 1.28828 ha sei cifre complessive e che quindi none rappresentabile esattamente. Tutte le altre operazioni sono suscettibili di questi errori di arrotondamento e per questo non soddisfano le normali proprieta delle usuali operazioni aritmetiche, quali l’associativita e la distribuitivit`a.
Ad esempio nel problema 4 I e l’insieme di tutti i numeri positivi, Se l’insieme {s`ı,no} e
sol(i) =
sı se ie primo no se i non `e primo
Per risolvere un problema con un calcolatore, o in generale con un qualunque dispositivo, e persino a mano, vi e bisogno di una descrizione precisa del procedimento da eseguire. Il procedimento (o algoritmo) deve essere descritto in termini di operazioni elementari (o passi) che l’agente di calcolo conosce bene,e in grado di eseguire senza nessun tipo di abilita, in altre parole meccanicamente, e che puo portare a termine soltanto con le risorse di calcolo disponibili. Uno dei primi algoritmi, se non il primo algoritmo che la storia ricordi `e l’algoritmo di Euclide (Antenaresis) per calcolare il massimo comun divisore di due numeri interi.
Dati due numeri diversi, si sottragga ripetutamente il minore dei due dall’altro fino a che i due numeri non diventino uguali: il numero cosı ottenutoe il massimo comun divisore dei due numeri iniziali
Cio dimostra che l’idea di algoritmoe ben distinta dall’idea di programma e comunque non ´e necessariamente connessa con l’informatica. In generale gli algoritmi possono essere descritti a parole, purch`e si dia una descrizione dettagliata e rigorosa dei passi da utilizzare. Ad esempio due algoritmi per la soluzione del problema dell’elevamento a potenza sono
Esempio 1 (Algoritmo A1) Calcolo della potenza con moltiplicazioni successive
(a) moltiplica P per X
Esempio 2 (Algoritmo A2) Calcolo della potenza con elevamenti al quadrato e dimezzamento dell’esponente
(a) se M `e dispari moltiplica P per Y (b) moltiplica Y per s´e stesso (c) dimezza M (tralasciando il resto)
Gli algoritmi possono anche essere con un meccanismo abbastanza semplice e molto diffuso: i diagrammi di flusso. Essi sono basati su un sistema grafico in cui ogni passo ´e rappresentato da un rettangolo, i punti di scelta dell’algoritmo sono rappresentati con dei rombi contenenti la condizione da controllare ed `e possibile saltare da un punto all’altro del diagramma con delle frecce, si veda l’esempio nella figura seguente.
æ
Inizio
P ← 1 Y ← X M ← N
M=0? Si
No ?
M dispari? Si
No
P ← P · Y
æ ? Y ← Y · Y M ← b M/2 c
æ?
Fine
Un altro sistema abbastanza diffuso in ambito scientifico e quello basato sulla pseudo–codifica in cui l’algo- ritmoe descritto attraverso un linguaggio di programmazione semplificato, con le istruzioni tratte dal linguaggio naturale (del tipo se... allora... altrimenti... fine–se, mentre... ripeti... fine–ripeti) o basate su istruzioni di linguaggi di programmazione esistenti, come l’Algol. Tre proprieta fondamentali che un algoritmo deve possedere sono la finitezza, la correttezza e l’efficienza. Per finitezza si intende che l’algoritmo deve usare sempre una quantita finita di risorse per qualunque istanza del problema tenti di risolvere. Cio vale anche per il tempo di calcolo e percio si richiede che l’algoritmo termini eseguendo sempre un numero finito, anche se non limitato, di passi. Detto in altri termini, un procedimento che in alcuni casi non termina entro un tempo finito, ossia richiede un tempo infinito o comunque un numero infinito di risorse, non e un algoritmo valido. Per correttezza di un algoritmo si intende che l’algoritmo deve essere in grado di risolvere il problema in tutte le sue possibili istanze, ovvero, secondo il formalismo precedente, che ricevendo come input l’istanza i produca come output sol(i). Dimostrare che un algoritmoe corretto non sempre e semplice. Per algoritmi semplici si puo fornire una dimostrazione matematica, basata solitamente sul metodo di induzione. Nella maggior parte dei casi si puo tentare di capire se un algoritmoe corretto attraverso un insieme di test che riescano a coprire in qualche modo l’insieme, solitamente estremamente grande, delle possibili istanze del problema. Un modo molto usato `e quello di trovare degli esempi per diverse classi di istanze. Ad esempio i due algoritmi utilizzati per risolvere il problema dell’elevamento a potenza sono entrambi corretti.
Ovvero TA(n), per un fissato n, e il massimo tempo impiegato da A a risolvere un’istanza di dimensione n. Detto in altri termini TA(n) rappresenta il tempo necessario a risolvere la peggiore istanza possibile di dimensione n. Quest’ultima considerazione spiega il termine di complessita nel caso peggiore. La funzione TA ha molte caratteristiche positive. Innanzitutto non e difficile da calcolare, in quanto ci si deve sempre mettere nell’ottica di cercare il peggior caso possibile, che di solitoe un caso estremo e regolare. Ad esempio in alcuni algoritmi di ordinamento il caso peggiore `e quello di ordinare un insieme ordinato in senso inverso. Inoltre vale l’ovvia relazione che per ogni istanza i
tA(i) ≤ TA(dim(i))
ossia TA fornisce un limite superiore al tempo necessario ad una qualunque istanza, in quanto e il massimo tempo possibile e ogni altra istanza si deve risolvere con minor o ugual tempo. Uno svantaggioe che in certi casi la complessita nel caso peggiore restituisce una stima troppo pessimistica del tempo di calcolo. Un esempio lampantee il celeberrimo algoritmo del simplesso per la programmazione lineare. Nonostante si sappia che nel caso peggiore si comporta veramente male (con tempi di calcolo esponenziali nelle dimensioni del problema), questo algoritmo e molto utilizzato nella pratica perche le istanze comuni vengono risolte molto velocemente. Il motivo sembra essere che il caso peggiore di quel problema e un caso patologico, che non si presenta in pratica. Percio si puo anche considerare la complessita media, ottenuta come valore atteso dei tempi di esecuzione su istanze di dimensione fissata ¯tA(n) = E [tA(i)|dim(i) = n]
In pratica, si deve calcolare per ogni istanza i la probabilit`a p(i) che l’istanza i si presenti e il tempo di calcolo tA(i) dell’algoritmo A su i ed infine calcolare
¯tA(n) =
i:dim(i)=n
p(i)tA(i)
Se la complessita media ovvia al fatto che la complessita del caso peggiore puo essere influenzata da casi patologici e che in media l’algoritmo potrebbe usare molto meno risorse di quelle richieste nel caso peggiore, si ha pero che la complessita mediae molto piu difficile da calcolare che la complessita del caso peggiore in quanto la prima ha bisogno di una distribuzione di probabilita sulle istanze di dimensione n e quasi sempre comporta lo svolgimento di calcoli estremamente complessi. Il calcolo della complessita media e di solito cosı difficile che raramente si trovano nella letteratura scien- tifica risultati medˆı su qualche algoritmo e comunque la complessita media dipende in modo essenziale dalla distribuzione di probabilita p. C’e il rischio di scegliere una distribuzione non “equa” che magari dia piu peso ad istanze facili che ad instanze difficili (o il contrario). In ogni caso la scelta di una distribuzione piuttosto che un’altra potrebbe essere arbitraria: un’ipotesi molto comune e quella della distribuzione uniforme (stessa probabilita per ogni istanza, o almeno stessa probabilita per tutte le istanze di dimensione fissata). Inoltre la complessita media non puo fornire che una stima della quantita di risorse necessaria per risolvere una qualsiasi istanza di dimensione n, senza pero fornire una limitazione sicura, seppure per eccesso, fornita dalla complessita nel caso peggiore. Nell’esempio precedente i due algoritmi presentati per risolvere il problema dell’elevamento a potenza hanno comportamenti diversi. Per calcolare la complessita computazionale calcoliamo il numero delle moltiplicazioni al variare dell’esponente N , poiche il loro numero non varia se cambia X. A1 per calcolare XN^ esegue sempre N moltiplicazioni, quindi il costo e T (N ) = N. Per capire quante operazioni sono svolte da A2 si deve innanzitutto notare che ad ogni passo vengono svolte una moltiplicazione, sempre (punto 3b), ed un’altra moltiplicazione, opzionale (punto 3a), ed una divisione intera, che pero e un’operazione piu veloce delle altre e puo essere trascurata dal computo totale delle operazioni svolte. Il numero di iterazioni che vengono effettuatee la parte intera di log 2 N , poiche questo numero equivale al numero di volte che si puo dimezzare un numero fino a farlo diventare 1, e quindi T (N ) ≤ 2 blog 2 N c. In generale poi non interessa nemmeno una valutazione precisa della complessita computazionale ma basta avere il comportamento asintotico, ad esempio si dice che T (n)e dell’ordine di n^2 per diversi algoritmi di ordinamento di array di n elementi, anzich`e dare espressamente la formula precisa di T (n). Un altro esempio si ha nel confronto dei seguenti due algoritmi per il calcolo del massimo di N numeri.
Il primo algoritmo, chiamato M1, e basato sulla definizione di massimo, cioe di quel numero che e maggiore o uguale di tutti gli altri: basta prendere a turno ciascuno degli N numeri e controllare se essi soddisfano la condizione di massimo, l’algoritmo si ferma quando un siffatto numero viene trovato. In pseudo–codifica l’algortimo M1e
a N − 1, tutti gli altri sono minori o uguali a Ak e quindi il numero pi grandee proprio Ak, altrimenti continuo con il Ak successivoIl secondo algoritmo, chiamato M2, consiste nel costruire un massimo parziale che viene aggiornato ad ogni numero preso in considerazione. Si parte ponendo come massimo parziale il primo elemento dell’insieme. Ad ogni elemento si confronta l’elemento in questione con il massimo parziale e se l’elemento corrente risulta essere maggiore del massimo parziale, il massimo parziale diventa proprio l’elemento corrente. E’ facile vedere che dopo aver passato in rassegna tutti gli elementi dell’insieme si trova come massimo parziale proprio il massimo elemento dell’insieme In pseudo–codifica l’algortimo `e
In termini di efficienza il secondo algoritmo e molto piu efficiente del primo. Infatti contiamo quanti confronti tra numeri usano M1 e M2. Il caso peggiore per lalgoritmo M1 quando il massimo e lultimo elemento: ad ogni ciclo lalgoritmo fa N-1 confronti per contare gli elementi ≤ Ak e deve arrivare allultimo elemento per trovare il massimo, in totale percio fa N · (N − 1) confronti. Invece M2 fa sempre N − 1 confronti. Percio M1e quadratico in N , mentre M2 e lineare in N , e quindi M2e nettamente superiore a M1 in fatto di efficienza. Per inciso si puo vedere che M2 usa il minor numero possibile di confronti e quindie un algoritmo ottimale. La suddivisione netta che si fa nella letteratura scientifica e tra algoritmi utilizzabili in pratica, cioe quelli che hanno un tempo di calcolo polinomiale, e quelli non utilizzabili, cioe quelli che hanno un tempo di calcolo piu che polinomiale. Infatti se si confrontano i tempi di calcolo tra due algoritmi, uno polinomiale (ad esempio con T (n) = n^2 ) e uno esponenziale (con T (n) = 2n) la differenza e ben visibile. Supponendo di avere un computer che fa 1000 operazioni al secondo, si avrebbero i seguenti tempi di calcolo effettivi in secondi (o altre unita di misura) n alg. polin. alg. espon. 10 0,1 sec. 1 sec. 20 0,4 sec. 17 min. 30 0,9 sec. 300 ore 50 2,5 sec. 35000 anni 100 10 sec. 4 · 10 +19^ anni 200 40 sec. 5 · 10 +49^ anni 1000 17 min. 3 , 3 · 10 +290^ anni La ricerca di algoritmi efficienti ha portato alla scoperta di una vasta classe di problemi, detti NP–completi, apparentemente incorrelati ma che sono tutti equivalenti fra di loro nel senso che se fosse possibile risolvere in modo efficiente, cioe attraverso un algoritmo polinomiale, uno di questi allora tutti i problemi di questo insieme sarebbe risolubili in modo efficiente. Poich´e gli sforzi fatti per risolvere in modo efficiente ciascuno di questi problemi non hanno dato risultati positivi,e ritenuto molto plausibile che i problemi NP–completi siano “molto difficili da risolvere” e che non possa esistere una soluzione polinomiale per nessuno di questi. Un esempio semplice di problema NP–completo e il problema del commesso viaggiatore: dato un insieme di N citta costruire un percorso che parte da una determinata citta, passa per tutte le altre citta una singola volta e torna alla citt`a di partenza percorrendo il minor numero di kilometri o utilizzando il minor tempo possibile.
Un tipo di dato `e definito da
Ad esempio il tipo di dato intero e definito dall’insieme dei numeri interi utilizzabili, per esempio tutti i numeri compresi tra -32768 e +32767, e un insieme di operazioni , ad esempio somma, prodotto, differenza, quoziente, resto. I tipi di dati che si trovano negli usuali linguaggi di programmazione si suddividono in dati elementari e dati strutturati. I dati elementari sono costituiti da un unico valore e si suddividono in dati numerici e dati non numerici. I dati strutturati sono costituiti da piu valori, sia elementari, sia strutturati a loro volta. Esistono diverse forme di strutturazione dei dati, le pi`u comuni che si riscontrano nei linguaggi di programmazione sono l’array e il record.
Dati numerici
I dati numerici si suddividono ulteriormente in numeri interi e numeri reali. La suddivisione non e meramente la mancanza della parte frazionaria (numeri interi) o la sua presenza (numeri reali) ma, come sie visto prece- dentemente, corrisponde ad una rappresentazione interna ben diversa: ad esempio complemento a due per i numeri interi e virgola mobile per i numeri reali. Cio comporta che le stesse operazioni (ad esempio le quattro operazioni) sono svolte in modo sostanzialmente diverso nei due tipi: generalmente le operazioni con i numeri interi sono piu veloci.
Dati non numerici
Esistono svariati tipi di dati non numerici, tra i piu importanti troviamo le stringhe e i booleani. Le stringhe consentono di memorizzare informazioni alfanumerici (ad esempio il nome e cognome) e di effettuare alcune operazioni sui caratteri, come l’estrazione di una parte della stringa. I booleani possono assumere solo i valori di verita vero e falso e hanno come possibili operazioni le operazioni logiche gi`a definite nella sezione sulla logica binaria (AND, OR, NOT). Solitamente gli operatori di confronto (uguale, diverso, maggiore, minore) restituiscono come risultato un booleano.
Array
Il tipo di dato strutturato array consente di memorizzare N dati, tutti dello stesso tipo T. Esempi di array possono essere un vettore di 10 componenti intere, un insieme di 40 componenti reali, la lista dei nomi delle 20 regioni italiane. Ogni componente dell’array e numerata e si puo accedere alle singole componenti specificando un indice, cio`e un numero che rappresenta la posizione della componente voluta nell’array (la prima, la seconda,... ). Pochissimi linguaggi hanno operazioni che agiscono direttamente su un intero vettore; negli altri linguaggi per implementare delle operazioni su array bisogna operare su ogni singola componente.
Record
Il tipo di dato strutturato record consente di memorizzare un insieme di dati di tipi diversi T 1 , T 2 ,... , Tn detti campi ai quali, solitamente, e attribuito un nome. Un esempio di recorde fornito dai dati di una persona: nome, cognome, data di nascita, indirizzo, numero di telefono, stipendio. Per accedere ad una delle componenti di un record si indica il nome del campo desiderato.
Altri tipi di dato
Alcuni linguaggi consentono di specificare nuovi tipi di dati. Le modalita piu frequenti sono
I linguaggi orientati agli oggetti consentono di dichiarare dei tipi indicando sia il dominio sia le operazioni ammissibili.
Una costante e un particolare valore di un dominio di un tipo di dati. Ad esempio 5e una costante di tipo intero, “Marco” e una costante di tipo stringa. Molti linguaggi permettono di associare un nome ad una particolare costante in modo da rendere piu leggibile e pi`u facilmente modificabile i programmi, ad esempio num persone=5.
Le variabili sono parti di memoria su cui `e possibile memorizzare un dato appartenente ad un certo tipo di dato. Le variabili utilizzate normalmente sono associate ad un nome che permette di accedere al dato memorizzato, sia per leggere il valore, sia per modificare il valore. Le caratteristiche principali di una variabile sono
nome serve per poter accedere alla variabile;
tipo indica il tipo dei valori che puo memorizzare, in quasi tutti i linguaggie necessario prestabilire il tipo di una variabile (dichiarazione) indicando all’inizio del programma tutte le variabili utilizzate, specificandone il nome e il tipo di dato memorizzato;
durata indica in quale momento la variabile sar presente in memoria
area di visibilita indica la zona del programma in cuie possibile usare la variabile, in molti linguaggi ci sono variabili globali (usabili ovunque) e locali (usabili solo allinterno di un sottoprogramma)
Le istruzioni di un linguaggio imperativo si suddividono in istruzioni elementari e istruzioni strutturate. La principale istruzione elementare e l’assegnamento, che consente di memorizzare un valore, calcolato come risultato di un’espressione, all’interno di una variabile. In pratica questa istruzionee il mattone principale con cui sono costruiti tutti i programmi scritti in un linguaggio imperativi perchee l’istruzione che in grado di spostare i dati e di effettuare calcoli. Le altre istruzioni elementari sono le istruzioni di I/O, lettura e scrittura di dati attraverso le usuali periferiche quali tastiera, video e memorie di massa. Le istruzioni strutturate sono istruzioni composte, costituite da piu istruzioni secondo diverse modalita
sequenza : e una successione di istruzioni I1, I2,... , In che sono eseguite in modo sequenziale, cioe una dopo l’altra secondo l’ordine indicato
scelta : e un punto di scelta del programma, in cui si valuta una condizione 2 e a seconda del risultato si esegue un’istruzione S1 (se la condizionee vera) o un’istruzione S2 (se la condizione `e falsa)
iterazione e un modo per ripetere una o piu istruzioni, si distinguono iterazioni limitate, cioe il numero di ripetizionie fissato a priori (ad esempio ripeti S per 10 volte), e iterazioni illimitate, in cui la ripetizione continua fintantoche una data condizionee vera (ad esempio ripeti S mentre X e diverso da 0) oppuree falsa (ad esempio ripeti S fintantoche A non diventa positivo). (^2) un’espressione il cui risultatoe vero o falso
e possibile scrivere un’espressione matematica, ma ogni operazione deve essere effettuata a se salvando il risultato su un registro, ad esempio (a − b) + b ∗ c ha bisogno di due registri per memorizzare i valori intermedi delle due sottoespressioniUna versione molto semplificata di linguaggio macchina puo essere studiata utilizzando un processore di esempio con una memoria RAM suddivisa in N celle di 32 bit, un registro accumulatore A a 32 bit su cui sono eseguite le operazioni aritmetiche intere, un registro program counter (PC) a 32 bit che indica l’indirizzo dell’istruzione corrente. Codice Istruzione Significato 0 LOAD x copia il valore x su A 10 STORE x memorizza il valore di A in x 20 ADD x addiziona ad A il valore x 30 SUBTRACT x sottrae ad A il valore x 40 MULTIPLY x moltiplica A per il valore x 50 DIVIDE x divide A per il valore x 60 JUMP a salta all’indirizzo a 70 JUMPP a salta all’indirizzo a se A > 0 80 JUMPZ a salta all’indirizzo a se A = 0 90 JUMPN a salta all’indirizzo a se A < 0 100 CALL a salta all’indirizzo a salvando il contenuto corrente del PC 110 RETURN ripristina il contenuto del PC salvato dalla CALL Le istruzioni, tranne RETURN, occupano due celle di memoria ciascuna, una per il codice e una per l’operando (x o a). Il salto indica che la prossima istruzione da eseguire none quella successiva, ma e l’indirizzo indicato nell’istruzione. Il dato x puo essere
Il modo immediato non si puo usare con l’istruzione STORE. I modi elencati sono detti modi di indirizzamento e a ognuno di essie associato un codice intero (0,1 o 2) che viene aggiunto al codice dell’istruzione. Ad esempio una ADD con indirizzamento immediato vale 20, una con indirizzamento diretto vale 21 e una con indirizzamento indiretto vale 22. Possiamo ora far vedere alcuni esempi di programmazione in assembler senza far vedere il codice macchina corrispondente, tranne che nel primo caso, ma indicando a fianco di ogni istruzione l’indirizzo in cui viene memorizzata.
Esempio 3 Somma di due numeri memorizzati nelle celle 100 e 101, il risultato deve essere memorizzato nella cella 102.
0 LOAD 100 2 ADD 101 4 STORE 102
Il codice macchina `e 1,100,21,101, 11,
Esempio 4 Trovare il massimo tra due numeri nelle celle 100 e 101, il risultato deve essere memorizzato nella cella 102.
Esempio 5 Calcolare XN^ , ove X sta in 100 e N sta in 101, il risultato deve essere memorizzato nella cella
0 LOAD # 2 STORE 102 4 LOAD 102 6 MULTIPLY 100 8 STORE 102 10 LOAD 101 12 SUBTRACT # 14 STORE 101 16 JUMPP 4
Esempio 6 Calcolare la somma di 50 numeri memorizzati a partire dalla locazione 200
0 LOAD # 2 STORE 100 4 LOAD # 6 STORE 101 8 LOAD # 10 STORE 102 12 LOAD 100 14 ADD (101) 16 STORE 100 18 LOAD 101 20 ADD # 22 STORE 101 24 LOAD 102 26 SUBTRACT # 28 STORE 102 30 JUMPP 12
Poichee estremamente complicato programmare in linguaggio macchina e inoltre e impossibile far girare un programma scritto per una macchina X in una macchina diversa Y sono stati inventati i linguaggi ad alto livello introdotti nel paragrafo precedente. Nonostante l’introduzione di questi linguaggi le CPU tradizionali sono in grado solo di eseguire programmi scritti in linguaggio macchina e quindie necessario tradurre i programmi dal linguaggio ad alto livello al linguaggio macchina. Esistono due tecniche distinte di traduzione di un programma: l’interpretazione e la compilazione. Si dice che un programma P e interpretato se la fase di traduzionee eseguita durante l’esecuzione. In pratica ogni singola istruzione di P viene singolarmente prima tradotta in linguaggio macchina e poi eseguita. Se un’istruzione deve essere eseguita piu volte verra di conseguenza tradotta pivolte. Si dice che un programma Pe compilato se la fase di traduzione e eseguita interamente prima dell’ese- cuzione. In pratica tutto il programma P viene tradotto nel programma P’ equivalente scritto in linguaggio macchina. I programmi interpretati sono eseguiti in modo piu lento perche ogni istruzione deve essere tradotta prima di essere eseguita, mentre nella compilazione il programma tradotto P’e eseguito direttamente.