Heap Sort. L’algoritmo heap sort è il più lento di quelli di ordinamento
O(n * log n) ma, a differenza degli altri (fusione e quick sort) non
richiede una ricorsività massiccia o vettori multipli.
Ciò ne fa l’alternativa più attrattiva per grandi insiemi di milioni di dati.
Come il nome suggerisce, all’inizio l’heap sort forma un mucchio
dall’insieme dei dati, quindi rimuove da esso l’elemento maggiore e
lo colloca alla fine del vettore ordinato.
Quindi ricostruisce il mucchio, rimuove l’elemento più grande e lo
colloca nella successiva posizione libera a partire dalla fine del
vettore ordinato.
Ripete questo procedimento fino a che non vi siano più elementi nel
mucchio e il vettore ordinato sia pieno.
Le implementazioni elementari richiedono due vettori: uno per
contenere il mucchio e l’altro gli elementi ordinati.
Per eseguire un ordinamento sul posto e risparmiare lo spazio che
sarebbe necessario per il secondo vettore, l’algoritmo seguente
“bara” usando lo stesso vettore per memorizzare sia il mucchio sia
il vettore ordinato.
Ogni qualvolta un elemento sia rimosso dal mucchio, esso libera uno
spazio alla fine del vettore, in cui l’elemento rimosso può essere
collocato.
I vantaggi dell’heap sort sono quindi l’ordinamento sul posto e la non
ricorsività, che lo rendono una buona scelta per insiemi di dati
estremamente grandi.
Per contro è più lento degli ordinamenti per fusione e quick sort.
Segue il codice della funzione spostaGiù, che costruisce e
ricostruisce il mucchio
Efficienza dell’algoritmo heap sort
Ordinamento per fusione (merge). Gli algoritmi di ordinamento
visti finora presentano lo svantaggio che i tempi di esecuzione
crescono molto in fretta al crescere del numero n di elementi da
ordinare.
Un metodo completamente diverso di ordinamento e assi più veloce
per n grande è l’ordinamento per fusione (merge sort), che viene
descritto in maniera semplice facendo uso della ricorsività.
Esso applica la tecnica del divide et impera, consistente nel
suddividere il problema di partenza in due problemi analoghi, ma di
minore ampiezza. In questo caso:
1. si divide il vettore iniziale a in due sotto-vettori di uguali
dimensioni, uno contenente le sue componenti di posto dispari
(a1, a3, a5, ...), l’altro quelle di posto pari (a2, a4, a6,
...);
2. si ordina separatamente ciascuno dei due sotto-vettori (in maniera
ricorsiva);
3. si fondono i due sotto-vettori ordinati.
1. Suddivisione. La suddivisione è un problema banale, e si ottiene
con il semplicissimo ciclo for seguente:
for (k = 1 ; k <= n/2 ; k ++)
b[k] = a[k*2];
Per risparmiare spazio, esso lascia le componenti di posto dispari
nel vettore iniziale a, limitandosi a copiare quelle di posto pari nel
vettore b.
A questo punto è opportuno rinumerare le componenti del sotto-vettore
a, in modo che gli indici coincidano con i primi termini della
successione dei numeri interi, per esempio con il seguente ciclo for:
for (k = 1 ; k <= n/2 ; k++)
a[k] = a[k*2 – 1];
2. Ordinamento. L’ordinamento può essere eseguito, per ciascuno
dei due sotto-vettori a[k], b[k], con uno qualsiasi dei metodi visti
in precedenza, tenendo conto delle relative osservazioni.
3. Fusione. Anche la fusione risulta particolarmente semplice, in
quanto si dispone ora di due vettori
a = a[1], ... , a[l]
e
b = b[1], ... , b[m]
aventi per componenti dei numeri interi ordinati.
Supponiamo che l’ordinamento sia non decrescente, cioè che risulti
a[i] <= a[i+1] e b[i] <= b[i+1]
per qualsiasi valore di i.
Si tratta di costruire un vettore v con l + m = n componenti in cui
compaiono tutte le componenti di a e di b ordinate nella stessa
maniera.
Un algoritmo che risolve questo problema può essere il seguente:
1. si confronta a[1] con b[1] e si pone il minore (o uno qualsiasi,
se sono uguali) in v[1].
2. se si è scelto a[1], si confronta a[2] con b[1],
altrimenti
si confronta a[1] con b[2],
3.si pone il minore in v[2].
Il procedimento continua fino a che si sono confrontate tutte le
componenti, dopo di che il vettore a è stato ordinato nel vettore v.
Osserviamo che l’ordinamento per fusione richiede un numero di
operazioni proporzionale a l + m = n.
Questo algoritmo presenta il vantaggio di essere leggermente più
veloce dell’ordinamento heap per grandi liste.
Per contro richiede almeno il doppio di memoria di altri metodi,
essendo di tipo ricorsivo.
Le implementazioni elementari fanno uso di tre vettori, uno per
ciascuna metà del vettore di partenza, e uno per il vettore ordinato.
L’algoritmo seguente fonde i vettori sul posto, cosicché sono
necessari solo due vettori.
Esistono versioni non ricorsive dell’ordinamento per fusione, ma sulla
maggior parte delle macchine non producono alcun significativo
miglioramento di prestazioni.
L’ordinamento per fusione si può eseguire in modo semplice se si
rappresenta l’elenco dei dati da ordinare nella forma non già di un
vettore, ma di una lista concatenata.
Questa è una struttura dati che si può pensare costituita da tante
celle o record, ciascuna delle quali è suddivisa in due sotto-celle o
campi.
Le celle sono rappresentate da rettangoli, la cui parte sinistra
contiene un dato della lista, mentre la parte destra contiene un
puntatore, rappresentato da una freccia.
Il puntatore è un dato che viene interpretato come l’indirizzo del
record che segue quello attuale nell’ordinamento della lista.
Un punto al posto di un puntatore indica che quel record non punta
ad altri record.
In tal modo risulta particolarmente semplice non solo cambiare
l’ordine dei dati, ma anche eseguire su di essi le fondamentali
operazioni di
• inserimento
• cancellazione
• ricerca.
Efficienza dell’algoritmo di ordinamento per fusione
Quicksort. Sebbene l’algoritmo shell sort sia decisamente migliore
di quello per inserimento, non lo è in senso assoluto.
Infatti il migliore metodo di ordinamento per vettori fino a oggi
conosciuto è senz’altro l’algoritmo pubblicato nel 1961 da C.A.R.
Hoare.
Le sue prestazioni sono così spettacolari che il suo inventore lo
chiamò Quicksort (ordinamento veloce).
Quicksort partiziona il vettore da ordinare in due sezioni, la prima di
elementi “piccoli”, la seconda di elementi “grandi”. Quindi ordina
separatamente gli elementi piccoli e i grandi, in modo ricorsivo.
Idealmente il partizionamento dovrebbe avvenire rispetto a un
elemento, detto pivot, che sia la mediana dei valori dati.
Poiché però la ricerca della mediana richiede la scansione dell’intero
vettore, e ciò rallenterebbe l’algoritmo, si può scegliere per
semplicità come pivot l’elemento che occupa il posto mediano del
vettore.
Nell’esempio seguente viene selezionato come pivot l’elemento del
vettore contenente il valore 42.
Quindi si fa scorrere un indice, min, a partire dall’estremità sinistra
del vettore fino a che incontra il primo elemento maggiore del pivot
(in questo caso il 44), e un indice, max, a partire dall’estremità
destra fino a che incontra il primo elemento minore del pivot (in
questo caso il 18).
Questi due elementi vengono scambiati fra loro, dando luogo alla
situazione seguente:
Il processo viene quindi ripetuto facendo scorrere gli indici verso il
pivot, e individuando gli elementi 55 e 06
Anch’essi vengono scambiati, dando luogo alla situazione seguente:
Nel passo successivo i due indici min e max vengono a coincidere, e
ciò determina la fine del processo, con tutti gli elementi a sinistra del
pivot <= di esso, e tutti gli elementi alla sua destra >= di esso.
Quindi, come si suole dire, il vettore a è stato partizionato.
Il programma a fianco
chiede di scrivere 8
numeri da tastiera,
assume come pivot il
quarto ed esegue su
essi il partizionamento
indicato in
precedenza:
#include <stdio.h>
#define NUMEL 8
void main(void)
{
int min, max, pivot, temp, a[NUMEL];
for (min = 0; min < NUMEL; min++)
{
printf("Scrivi un numero: ");
scanf("%d", &a[min]);
}
min = 0;
max = NUMEL-1;
pivot = a[3];
while (min < max)
{
while (a[min] < pivot)
min ++;
while (a[max] > pivot)
max --;
temp = a[min];
a[min] = a[max];
a[max] = temp;
min ++;
max --;
}
for (min = 0; min < NUMEL; min++)
printf("\n%d ", a[min]);
}
Naturalmente a questo punto il vettore non è stato ancora ordinato,
ma solo partizionato.
Per ottenere l’ordinamento sarà però sufficiente applicare questo
procedimento a entrambe le partizioni ottenute, poi alle partizioni
delle partizioni e così via, fino a quando ogni partizione sarà
costituita da un solo elemento.
Per meglio comprendere la struttura del programma definitivo,
conviene scrivere un programma intermedio, che esegue il
partizionamento chiamando dall’interno della funzione principale
una funzione quickSort.
Questa si ottiene riscrivendo il programma precedente senza le
istruzioni di ingresso e uscita, che vanno invece riportate nel
programma principale.
Avremo quindi il seguente programma:
#include <stdio.h>
#define NUMEL 8
void main(void)
{
int i, a[NUMEL];
int quickSort(int []);
/* dichiara quickSort */
for (i=0; i < NUMEL; i++)
{
printf("Scrivi un numero: ");
scanf("%d", &a[i]);
}
quickSort(a);
/* chiama quickSort */
for (i = 0; i < NUMEL; i++)
printf("\n%d ", a[i]);
}
/* definisce la funzione quickSort */
int quickSort(int a[])
{
int min, max, pivot, temp;
min = 0;
max = NUMEL-1;
pivot = a[3];
while (min < max)
{
while (a[min] < pivot)
min ++;
while (a[max] > pivot)
max --;
temp = a[min];
a[min] = a[max];
a[max] = temp;
min ++;
max --;
}
}
Osserviamo che all’interno della funzione main() la funzione
quickSort è dichiarata (con l’istruzione int quickSort (int
[]);) in modo da ricevere come parametro un vettore di interi e
fornire un risultato intero (in questo caso un vettore).
A questo punto, il programma precedente va riscritto in modo che
effettui una chiamata ricorsiva alla funzione di ordinamento.
Vedremo più avanti il listato completo.
Per assegnare un valore al pivot, la funzione q_sort sceglie
semplicemente il primo elemento (a[min]). I restanti elementi del
vettore sono confrontati con il pivot, e messi alla sua sinistra o alla
sua destra a seconda del loro valore.
Efficienza. Quicksort è un algoritmo di ordinamento non-stabile e
non sul posto, in quanto richiede spazio nello stack.
Esso risulta poco efficiente nel caso in cui il vettore sia già ordinato.
In tale caso la funzione q_sort selezionerebbe l’elemento minore
come valore da assegnare al pivot e dividerebbe il vettore con un
elemento nella partizione di sinistra, e max-min elementi nell’altra.
Ogni chiamata ricorsiva a q_sort ridurrebbe solo di un’unità la
dimensione del vettore da ordinare. Sarebbero quindi necessarie n
chiamate ricorsive per effettuare l’ordinamento, portando a un
tempo di esecuzione di O(n2).
Se però si scegliesse a caso un elemento come pivot, sarebbe
estremamente improbabile il verificarsi del caso peggiore.
Se poi il pivot fosse la mediana di tutti i valori, che divide il vettore in
due parti uguali, il vettore verrebbe diviso in due a ogni passo, e il
tempo di esecuzione sarebbe O(n * lg n).
Quindi Quicksort ha complessità O(n * lg n) nel caso medio, e
O(n2) nel caso peggiore.
Efficienza dell’algoritmo quick sort
Miglioramenti. All’algoritmo Quicksort di base si possono apportare
diversi miglioramenti:
1. Nella funzione q_ sort si sceglie come pivot l’elemento centrale.
Se la lista è parzialmente ordinata, questa risulta una buona
scelta. Il caso peggiore si verifica quando l’elemento centrale è
l’elemento più piccolo o più grande a ogni chiamata a q_sort.
2. Per vettori piccoli viene chiamata la funzione insertSort.
A causa della ricorsività e di altri costi addizionali, Quicksort non
è un algoritmo efficiente su vettori di piccole dimensioni. Di
conseguenza, ogni vettore con meno di 12 elementi viene
ordinato usando l’ordinamento per inserimento. Il valore ottimale
di questa soglia non è critico, e varia con la qualità del codice
generato.
3. Si ha ricorsività di coda quando l’ultima istruzione di una funzione è
una chiamata alla funzione stessa. La ricorsività di coda può essere
sostituita dall’iterazione, ottenendo un utilizzo migliore dello stack.
4. Dopo che il vettore è stato partizionato, la partizione più piccola
viene ordinata per prima. Ciò determina un migliore utilizzo dello
stack, considerato che le partizioni più piccole sono ordinate - e
quindi eliminate - più velocemente.
La tabella seguente mostra le statistiche di tempo e l’uso dello stack
prima e dopo l’introduzione dei miglioramenti.
elementi
tempo (µs)
prima
dimensione stack
dopo
prima
dopo
16
103
51
540
28
256
1.630
911
912
112
4.096
34.183
20.016
1.908
168
65.536
658.00
3
460.73
7
2.436
252
Confronto. Come conclusione confrontiamo i principali algoritmi di
ordinamento trattati: per inserimento, shell sort, e quicksort. Ci sono
molti fattori che influenzano la scelta di un algoritmo di ordinamento:
 Ordinamento stabile. Ricordate che un algoritmo di ordinamento
stabile lascia gli elementi uguali nella stessa posizione relativa al
termine dell’ordinamento. L’ordinamento per inserimento è l’unico
algoritmo stabile fra quelli trattati.
 Spazio. Un algoritmo sul posto non richiede memoria aggiuntiva per
portare a termine l’ordinamento. Sia l’ordinamento per inserimento
sia shell sort sono algoritmi sul posto. Quicksort richiede spazio
nello stack per la ricorsività, quindi non è un algoritmo sul posto.
L’invenzione di artifici ha ridotto notevolmente il tempo richiesto
dall’algoritmo.
• Tempo. Non è difficile che il tempo richiesto per ordinare un insieme
di dati raggiunga ordini di grandezza astronomici, come abbiamo
visto nella tabella delle funzioni di n. La tabella seguente mostra le
complessità relative di ogni metodo.
istruzioni
complessità
media
complessità
del caso peggiore
inserimento
9
O(n2)
O(n2)
shell sort
17
O(n1.25)
O(n1.5)
quicksort
21
O(n lg n)
O(n2)
metodo
Nella tabella seguente sono riportati i tempi di ordinamento, partendo
da un insieme di dati ordinati in modo casuale.
elementi
da ordinare
inserimento
16
39 µs
256
4.969 µs
4.096
1,315 sec
65.536 416,437 sec
shell
quicksort
45 µs
51 µs
1.230 µs
911 µs
0,033 sec 0,020 sec
1,254 sec 0,461 sec
Semplicità. La tabella dellavideata precedente mostra il numero di
istruzioni necessarie per ogni algoritmo. Algoritmi più semplici portano
a meno errori in fase di codifica.
Ordinamento esterno
GIi algoritmi di ordinamento visti finora prevedevano tutti i seguenti
passi:
• caricare i dati in memoria
• ordinare i dati (in memoria), quindi
• scrivere i risultati.
Essi non possono venire applicati quando i dati da ordinare non
trovano posto nella memoria principale del computer e sono,
invece, memorizzati in un dispositivo periferico di memoria
secondaria, per esempio un’unità a nastro.
In questo caso i dati sono rappresentati con un file sequenziale, la cui
caratteristica è di rendere accessibile una, e una sola, componente
in qualsiasi momento.
Si tratta di una severa restrizione, a confronto con le possibilità che
offre la struttura vettore, che richiede una tecnica di ordinamento
esterno.
La tecnica più importante è l’ordinamento per fusione, che combina
due (o più) sequenze ordinate in una sola mediante ripetute
selezioni tra le componenti che sono correntemente accessibili.
L’algoritmo di ordinamento detto fusione diretta consiste nei
seguenti quattro passi:
1. si suddivide la sequenza a in due metà, chiamate b e c;
2. si fondono b e c combinando elementi singoli in coppie ordinate;
3. detta a la sequenza ottenuta, si ripetono i passi 1 e 2, questa
volta fondendo coppie ordinate in quadruple ordinate;
4. si ripetono i passi precedenti, fondendo quadruple in ottuple e
così si continua, raddoppiando ogni volta la lunghezza delle
sottosequenze che vengono combinate, fino al completo
odinamento della sequenza iniziale.
Consideriamo, per esempio, la seguente lista
Nel passo 1. essa viene suddivisa nelle nelle due sequenze
La fusione delle singole componenti (che sono
sequenze ordinate di lunghezza 1) in coppie
ordinate dà luogo a
Suddividendo un’altra volta
nel mezzo e combinando le
coppie ordinate otteniamo:
Una terza suddivisione, seguita dalla fusione,
produce finalmente il risultato desiderato:
Ogni operazione che tratta l’intero insieme dei dati viene chiamata
fase. Il più piccolo sottoprocesso che, per ripetizione, costituisce il
processo di ordinamento, viene chiamato passata o stadio.
Nell’esempio precedente sono state necessarie tre passate per
ordinare, ognuna delle quali era costituita da una fase di
suddivisione e da una di fusione.
Per potere eseguire l’ordinamento sono necessari tre nastri; perciò
il processo viene chiamato fusione con tre nastri.
Fusione bilanciata (a una fase). In verità, le fasi di suddivisione non
contribuiscono all’ordinamento, poiché esse non permutano in alcun
modo gli elementi. In un certo senso, sono azioni improduttive, anche
se costituiscono la metà di tutte le azioni di copia.
Ebbene, le fasi di suddivisione possono essere eliminate, combinandole
con quelle di fusione.
Invece di creare una sola sequenza, il risultato del processo di fusione
viene immediatamente redistribuito su due nastri, che saranno le
sorgenti per la passata successiva.
Questo metodo viene chiamato ordinamento per fusione bilanciata o
fusione a una fase, in antitesi al precedente ordinamento a due fasi.
È evidente che il nuovo metodo è superiore al vecchio, perché necessita
solo della metà di operazioni di copia.
Il prezzo di questo miglioramento è un quarto nastro.
Bibliografia
Aho, Alfred V. and Jeffrey D. Ullman [1983].
Data Structures and Algorithms.
Addison-Wesley, Reading, Massachusetts.
Cormen, Thomas H. [2001].
Introduction to Algorithms.
McGraw-Hill, New York.
Knuth, Donald E. [1998].
The Art of Computer Programming, Volume 3, Sorting
and Searching.
Addison-Wesley, Reading, Massachusetts.
Pearson, Peter K. [1990].
Fast Hashing of Variable-Length Text Strings.
Communications of the ACM, 33(6):677-680, June 1990.
Pugh, William [1990].
Skip Lists: A Probabilistic Alternative to Balanced Trees.
Communications of the ACM, 33(6):668-676, June 1990.
Algoritmo di cifratura RSA
L’algoritmo RSA è stato pubblicato nel 1978 da R. L. Rivest, A.
Shamir e L. Adelman (da cui il nome), sebbene la sua prima
descrizione si debba a James H. Ellis (1970) e le sue prime
implementazioni pratiche a C. C. Cocks (1973) e Malcom J.
Williamson (1974).
Ellis, Cocks e Williamson lavoravano all’epoca nel Communications
Electronics Security Group delle forze armate britanniche.
RSA è il più moderno algoritmo di cifratura, ed è applicato per
garantire un elevato livello di sicurezza ai cosiddetti documenti
elettronici, in particolare nel caso di trasmissione attraverso reti
telematiche.
La cifratura avviene di solito sostituendo i caratteri di cui è composto
un messaggio m, detto messaggio in chiaro, con altri in base a un
determinato criterio, detto chiave; si produce così un messaggio
cifrato c.
L’operazione inversa di decifrazione avviene sostituendo i caratteri del
messaggio cifrato c in modo da riottenere quello originario in chiaro
m.
Questa seconda operazione può essere effettuata usando la stessa
chiave impiegata per la cifratura – e allora si parla di chiavi
simmetriche – oppure una chiave diversa - e allora si parla di chiavi
asimmetriche.
L’algoritmo RSA è di tipo asimmetrico, in quanto usa due chiavi
diverse, una per cifrare i messaggi, l’altra per decifrarli; ciò fa sì che
il mittente e il destinatario non si debbano scambiare la chiave di
cifratura/decifrazione prima di potere comunicare, con un evidente
vantaggio in termini di sicurezza.
Infatti la chiave di cifratura può tranquillamente essere resa pubblica,
ad es. mettendola a disposizione su Internet o stampandola su un
bollettino, in quanto la sua conoscenza non permette affatto (o
permette con molta difficoltà) di risalire alla chiave di decifrazione,
che deve invece rimanere privata (cioè segreta).
In altri termini, la conoscenza della chiave pubblica di una persona A
permette a chiunque di mandare un messaggio cifrato ad A,
messaggio che solo A potrà decifrare con la propria chiave privata,
ma non fornisce alcuna informazione sulla chiave privata di A.
La cifratura di un testo richiede dapprima che esso sia convertito in
una serie di numeri (digitalizzazione); questi potrebbero essere per
esempio i codici ASCII o EBCDIC dei suoi caratteri.
A questo punto, come indica la figura,
il mittente A usa la chiave pubblica del destinatario B per cifrare il
messaggio m da inviargli; ottiene così il messaggio cifrato c, che può
essere inviato senza alcuna precauzione a B.
Questi impiega la propria chiave privata per decifrare il messaggio
cifrato c, ricostruendo quello originario m.
Costruzione della coppia di chiavi asimmetriche. La costruzione
delle due chiavi di cifratura e decifrazione si basa sul fatto che,
mentre è abbastanza semplice moltiplicare tra loro due numeri
primi anche molto grandi, l’operazione inversa di trovare i fattori
primi di un numero abbastanza grande è assai più difficile.
La costruzione avviene attraverso i seguenti passi:
1. si moltiplicano tra loro due numeri primi p, q sufficientemente
grandi, ottenendosi come prodotto il numero n
[per fare un esempio semplice, supponiamo p = 3, q = 5,
quindi p*q = n = 15]
2. si sceglie un intero D che non sia divisibile né per p-1 né per
q-1
[nel nostro esempio scegliamo D = 11, che non è divisibile né
per 2 né per 4].
3. si calcola un intero E tale che il resto della divisione di E*D per
(p-1)*(q-1) sia uguale a 1[1].
Ciò equivale a risolvere il seguente problema:
Trovare un valore di k tale che l’espressione
k*(p-1)*(q-1)+1
E = --------------D
abbia un valore intero
Osserviamo che tale equazione è detta diofantea, e non può essere
risolta con una formula, ma solo per tentativi, cioè sostituendo a k i
successivi valori 1, 2, 3,... finché si trova per E un valore intero.
[1]
Ricordiamo che la funzione modulo, indicata con mod, indica il resto della
divisione di due numeri interi. Perciò la precedente condizione si esprime
dicendo che deve risultare E*D = 1 mod(p-1)*(q-1)
L’operazione si può eseguire automaticamente con un programma di
foglio elettronico, trovandosi le soluzioni dell’equazione indicate in
tabella.
k
E
4
15
26
37
...
3
11
19
27
...
Scegliamo, ad es., k = 4 a cui corrisponde E = 3.
La chiave pubblica di cifratura, da comunicare, è la coppia
(E,n).
La chiave privata di decifrazione, da tenere segreta, è la coppia
(D,n).
Cifratura e decifrazione. Il messaggio cifrato, c, è il resto della
divisione di mE per n [1].
Il messaggio originario, m, è il resto della divisione di cD per n [2].
Se scegliamo come chiave pubblica la coppia
(E = 3, n = 15)
come chiave privata la coppia
(D = 11, n = 15)
e come messaggio m i primi 10 numeri interi, otteniamo i valori
indicati in Tabella.
[1]
In termini matematici ciò si esprime dicendo che c = mE mod n
[2] In termini matematici ciò si esprime dicendo che m = cD mod n
Mettiamoci adesso dalla parte di un cracker il quale intercetti il
messaggio cifrato, c, e voglia da esso ricavare il messaggio
originario, m.
Dato che egli sa che E=3 e che n=15, tutto ciò di cui ha bisogno
è di conoscere i due numeri primi, p, q, che moltiplicati tra loro
danno 15.
Infatti dalla conoscenza di p, q, troverebbe p-1 (=2) e q-1 (=4),
e da essi risalirebbe facilmente al numero D, non divisibile né
per 2 né per 4.
Come abbiamo visto, una volta trovato D il messaggio originario,
m, si ottiene da quello cifrato, c, dalla formula
m = mod(cD, n)
già usata nella Tabella precedente.
Ebbene, la inviolabilità dei messaggi cifrati con l’algoritmo RSA
risiede proprio nella difficoltà di fattorizzare numeri che siano il
prodotto di numeri primi molto grandi (ricordiamo che di recente
è sato scoperto il numero primo più grande, lungo oltre 9 milioni
di caratteri).
Scarica

Fonda8