Professional Documents
Culture Documents
Mòdul 2. Complexitat Algorísmica
Mòdul 2. Complexitat Algorísmica
algorísmica
Eficiència d’algorismes
i tipus abstractes de dades
Julià Minguillón Alfonso
PID_00161968
Mòdul 2
© FUOC • PID_00161968 • Mòdul 2 Complexitat algorísmica
Índex
Introducció .............................................................................................. 5
Objectius ................................................................................................... 6
Resum ........................................................................................................ 23
Solucionari ............................................................................................... 26
Glossari ..................................................................................................... 28
Bibliografia .............................................................................................. 28
© FUOC • PID_00161968 • Mòdul 2 5 Complexitat algorísmica
Introducció
Objectius
Els objectius que s’espera que l’estudiant satisfaci amb els continguts d’aquest
mòdul són els següents:
És important destacar que es mesura un cost en unitats (de temps, però també
de memòria) que un algorisme necessita per a resoldre un problema amb una
entrada en funció de la seva mida. Tot i que els algorismes s’acostumen a des-
criure amb llenguatges d’alt nivell, com ara Java, l’execució es fa en un entorn
controlat, com és una CPU que executa instruccions molt bàsiques –moure in-
formació, realitzar càlculs aritmètics, saltar entre diferents punts del programa,
etc. Idealment, els recursos necessaris haurien d’estimar-se en aquest nivell,
però aleshores es perd l’abstracció que proporciona el llenguatge d’alt nivell i
això afegeix una complicació addicional per a comparar algorismes. En conse-
qüència, és necessari estudiar la complexitat algorísmica en un nivell més ele-
vat, usant les estructures de control (seqüencial, alternativa i iterativa) com a
eines bàsiques per a la construcció d’algorismes i, per tant, també bàsiques per
a mesurar la seva eficiència.
D’altra banda, i atès que quan es parla d’eficiència d’algorismes el més important
acostuma a ser la rapidesa amb què s’executa, quan es parla de complexitat algo-
rísmica normalment ens referim a la complexitat temporal. Això és així perquè
és més fàcil incrementar la capacitat de memòria d’un ordinador que no pas
augmentar-ne la velocitat de procés i, per tant, és millor dissenyar algorismes
que s’executin ràpidament que no pas algorismes que utilitzin menys quantitat
de memòria. Això pot no ser així en altres contextos, en què la capacitat de me-
mòria és limitada (per exemple, en un satèl·lit que envia dades a una estació re-
ceptora) i l’única opció és usar algorismes més lents, però amb unes necessitats
de memòria limitades.
Això és evident en el cas d’algorismes recursius, com ara el càlcul de la seqüència de Fi-
bonacci per a un conjunt d’enters determinat, en dos nivells: el primer, pel càlcul de cada
Seqüència de Fibonacci
terme F(n), ja que es fa a partir dels dos anteriors, F(n – 1) i F(n – 2). De la mateixa manera,
per a calcular F(n – 1) s’usen F(n – 2) i F(n – 3), i així successivament. Per tant, si s’emma- El terme F(n) de la seqüència
gatzema F(n – 2) en algun lloc, es pot reaprofitar i estalviar-se haver-lo de calcular de nou, de Fibonacci es calcula com
i passa el mateix amb tots els altres termes de la seqüència. En el segon nivell, per cada F(n) = F(n − 1) + F(n − 2), en
element del conjunt del qual es vol calcular el nombre que ocupa la posició en la seqüèn- què F(1) = 1 i F(2) = 1 són
els dos primers termes
cia de Fibonacci, si els elements s’ordenen de més petit a més gran, serà possible reapro-
de la seqüència.
fitar tots els càlculs intermedis.
© FUOC • PID_00161968 • Mòdul 2 9 Complexitat algorísmica
Tots els llenguatges procedimentals d’alt nivell, com ara Pascal, C o el Java ma-
teix, comparteixen les mateixes estructures de control:
int suma = 0;
System.out.println(suma);
Observació
Les operacions realitzades són dues assignacions que es fan un únic cop (les
Fixeu-vos que la condició del
variables suma i k), una comparació (la condició del bucle k ≤ N) que es fa N + 1 bucle s’avalua N + 1 vegades,
ja que l’última iteració que re-
cops, un autoincrement i una avaluació d’una condició (que inclou una opera- torna ‘fals’ i provoca la sortida
del bucle, també s’avalua.
ció mòdul i una comparació) que es fan N cops, una suma que es fa N/2
© FUOC • PID_00161968 • Mòdul 2 10 Complexitat algorísmica
cops, i finalment una crida a una funció del sistema que es fa un cop. Per
tant:
Cal observar, però, que totes les operacions (assignació, suma, etc.) s’estan
comptant amb el mateix cost (la unitat mínima de cost), mentre que segura-
ment l’operació aritmètica de suma tindrà un cost real d’execució major que
la comparació. Aquesta simplificació és necessària per a no complicar excessi-
vament l’anàlisi de la complexitat d’un algorisme. De fet, per a fer una anàlisi
exhaustiva de qualsevol algorisme, caldria anar al codi màquina generat pel
compilador per a una plataforma concreta, i conèixer el cost de cada operació,
la qual cosa faria que l’anàlisi deixés de ser independent de la plataforma. Per
a comparar algorismes escrits en llenguatges de programació d’alt nivell, cal
fer simplificacions com aquesta.
El càlcul de la complexitat pot ser tan senzill com s’ha vist en aquest exemple,
però en la majoria de casos els programes a analitzar seran molt més comple-
xos, i intentar comptar les operacions realitzades directament pot ser molt
complicat. Aleshores, la complexitat es calcula agrupant les instruccions en
blocs, i utilitzant les regles de càlcul per a combinacions d’algorismes que es
descriuen en el subapartat següent.
A1
A2
...
An
Entre dos subalgorismes s’executen l’un o l’altre segons que s’acompleixi o no una
expressió (una condició); la complexitat de l’algorisme dependrà de quina branca
s’executi. La branca que s’executi dependrà de les dades d’entrada, però no de la
seva mida. Per a diferents dades s’executarà una branca o l’altra. Per tant, en ge-
neral no podrem calcular el cost temporal exacte de l’algorisme per a dades de
mida N. Aleshores, el més apropiat com a mesura de complexitat seria proporcio-
nar una mesura del cost mitjà per a una execució de l’algorisme amb dades de
mida N, en funció de quants cops s’executa cada branca de l’algorisme en terme
mitjà. Així doncs, la complexitat total serà la suma ponderada dels valors possi-
bles de la funció d’avaluació per la distribució de probabilitats {p, 1 – p} més el cost
d’avaluar la condició TC(N), és a dir:
Això correspon al que es coneix com a anàlisi del pitjor cas, que es desenvolu-
parà més endavant, mentre que el cas anterior es correspon al que es coneix
com a cas mitjà.
un algorisme A amb complexitat TA(N) s’executa dintre d’un bucle que depèn
exactament de la mida del problema N, la complexitat resultant es calcula com:
N
T ( N ) = ∑ TA (N ,j ) + (N + 1) ⋅ TC (N )
j =1
Quan TA(N,j) = TA(N) per a tota iteració j (és a dir, la complexitat es manté
constant), el sumatori es redueix a N · TA(N).
Exemple d’aplicació
Suposem un bingo amb NJ jugadors, en què no es canta línia, sinó només bingo
(quan un jugador exhaureix tots els números del seu cartró). Abans de comen-
çar una partida, es genera un cartró aleatori per a cada jugador, que represen-
tem mitjançant un conjunt de nombres enters, amb MC que indica la mida
del cartró, que és la mateixa per a tots els jugadors. D’això s’encarrega el mè-
tode generarCartro(), del qual volem calcular la complexitat algorísmica usant
les propietats que acabem de descriure, sense fer-ne una anàlisi exhaustiva. Es
pot suposar que les crides als mètodes per a generar la sortida per pantalla te-
nen una complexitat temporal constant T(N) = 1, tot i que caldrà tenir en
compte els casos en què es faci ús de toString() per a imprimir un conjunt d’ele-
ments, per exemple. La generació de números aleatoris es fa mitjançant un
mètode del JDK, la documentació del qual diu:
“An instance of this class is used to generate a stream of pseudorandom numbers. The
class uses a 48-bit seed, which is modified using a linear congruential formula.”
uoc.ei.exemples.modul2.Bingo
...
public class Bingo {
private Jugador[] jugadors;
private int nombreDeJugadors;
private int nombreDeBoles;
private int midaCartro;
private Conjunt<Integer> bolesCantades;
private Random generadorNumerosCartro;
uoc.ei.exemples.modul2.Jugador
...
public class Jugador {
private String nom;
private Conjunt<Integer> cartro;
Alhora, el mètode afegir() fa una crida al mètode hiEs(), i en cas de que es complei-
xi la condició, s’executen dues sentències. Si s’aplica la propietat de l’estructura
alternativa, el cost del mètode afegir() és, doncs, el cost del mètode hiEs() més dos.
Pel que fa al mètode hiEs(), fa una crida a cercarPosicioElement(), una comparació i
la sentència return.
Recuperem ara els resultats intermedis que havien quedat pendents de calcu-
lar en cada crida a un nou mètode. El mètode hiEs() té una complexitat de
T(MC) = 7 · MC + 10 i, aleshores, el mètode afegir() té T(MC) = 7 · MC + 12.
Finalment, el mètode generarCartro() té una complexitat temporal de:
plexitat en el cas mitjà. I això també fa que la complexitat en el pitjor cas Podeu trobar informació del
mètode de Montecarlo a la
prengui una rellevància especial, ja que si no és possible comparar pel cas Wikipèdia:
mitjà, el pitjor cas pot ser una fita superior vàlida per a fer comparacions en- http://en.wikipedia.org/wiki/
Monte_Carlo_method
tre algorismes.
© FUOC • PID_00161968 • Mòdul 2 17 Complexitat algorísmica
Per tant, és necessari disposar d’una notació que permeti comparar algorismes
directament, sense haver de preocupar-se pels casos particulars que apareixen
quan es tenen en compte les constants de les diferents funcions T(N). Per això,
s’usa el que es coneix com a notació asimptòtica, que permet fer-se una idea de
la complexitat d’un algorisme quan la mida de l’entrada d’un problema es fa
molt gran.
Depenent del tipus d’anàlisi que es realitzi, ens podem trobar fins a cinc nota- Les cinc notacions
són o, O, ~, Ω i Θ.
cions asimptòtiques diferents. Totes elles estan relacionades, i algunes són més
útils que d’altres segons l’ús que se’ls vulgui donar. La notació més usada és
l’anomenada o gran, i es denota per O. Bibliografia
recomanada
Taula 1
Notació Nom Exemple d’algorisme
És important destacar que la notació O ens permet establir una fita superior
del comportament d’un algorisme F. És evident que es poden trobar infinites
funcions G de manera que es compleixi que F és d’ordre O(G). Per exemple, si
un algorisme té una complexitat temporal T(N) = 2N2 + N, es pot dir que T(N)
és d’ordre O(N2), però també O(N3). Això no obstant, el fet de dir que T(N) és
d’ordre O(N2) aporta molta més informació que no pas O(N3). Les complexitats
T(N) que són combinacions de potències de N, com ara la quadràtica o la cú-
bica, s’anomenen polinòmiques, i són el llindar del que avui es considera com-
putacionalment tractable.
És possible fer-se una idea del cost real que representa tenir o no disponible un
algorisme amb una complexitat limitada a mesura que la mida del problema
va creixent. La taula 2 mostra el temps proporcional (per exemple, en segons)
que necessita un algorisme segons la mida de l’entrada i la seva complexitat.
Taula 2
N vs O(N) log2 N N N log2 N N2 N3 2N
Figura 1
Per a demostrar aquest fet, podem utilitzar l’exemple del TAD que implementa
els nombres naturals, on es poden trobar dues implementacions diferents: una
primera que fa servir el tipus int de Java –en què el nombre natural s’emma-
gatzema directament pel seu valor–, i una segona implementació que usa un
vector d’elements booleans –en què cada nombre natural N es representa per
un conjunt d’elements en què els N primers elements són ‘cert’ i la resta són
‘fals’. Tot i que aquest exemple és lluny de ser realista, permet una primera
comparació de les complexitats de les dues implementacions.
que s’executen en un temps constant, independentment de la mida (la mag- El llenguatge Java proporciona
una implementació dels nom-
nitud, en aquest cas) del nombre natural representat. Podríem dir que l’efici- bres enters molt eficient en
ència d’aquesta implementació és perfecta. temps constant, i la implemen-
tació del TAD se n’aprofita.
complexitat temporal, els mètodes pred() i succ() tenen una complexitat O(N),
–ja que cal recórrer tots els elements del vector fins a trobar la posició del que
es representa. El mateix passa en la resta de mètodes: la causa d’aquesta
complexitat temporal és la necessitat d’executar un bucle dintre del mèto-
de cercarDarreraPosicio() i duplicarVector().
Taula 3
Operació Complexitat Raonament
hiEs() O(N)
esborrar() O(N)
© FUOC • PID_00161968 • Mòdul 2 23 Complexitat algorísmica
Resum
Exercicis d’autoavaluació
package uoc.ei.exemples.modul2.Matriu
...
public class Matriu {
private double[][] elements;
private int m,n;
Solucionari
1. Començarem calculant la complexitat temporal T(N) de cada mètode. En aquest cas, però,
parlarem de T(M,N) o de T(M,N,P), ja que els paràmetres d’entrada (la mida d’una matriu
que determina el cost de l’algorisme) són dos (i en el cas de multiplicar dues matrius, tres,
en haver de coincidir el nombre de columnes de la primera matriu amb el nombre de files
de la segona), és a dir, multipliquem una matriu de M × N elements per una de N × P.
Es fan dues assignacions i una crida a l’operador new per a generar la matriu bidimensio- Vegeu la definició i les propietats
de la complexitat algorísmica en el
nal. Si suposem que la reserva de memòria necessària per a la matriu és independent de la subapartat 2.3 d’aquest mòdul.
seva mida, tenim que T(M,N) = 3 i, per tant, si apliquem la definició de complexitat algo-
rísmica i les propietats que se’n deriven, O(M,N) = 1, és a dir, s’executa en temps constant
independentment de la mida de la matriu. De fet, no cal aplicar la definició: es busca el
terme que acompanya qualsevol funció de M o de N i s’agafa el que té el creixement asimp-
tòtic més gran. En aquest exemple, en què no apareix cap dels dos paràmetres, la comple-
xitat és constant.
Tots dos mètodes fan un sol accés a un element de la matriu, per la qual cosa T(M,N) = 1
i O(M,N) = 1; ja que en Java s’usa accés directe per a accedir a un element d’un array. És
important conèixer, però, com s’emmagatzemen els arrays en Java, ja que si hi hagués una
estructura especial (per exemple, per a tractar eficientment matrius molt grans amb molts
elements buits, usant un TAD dissenyat per a aquest objectiu), el fet que T(M,N) sigui cons-
tant podria no ser cert.
Mètode multiplicarPer
En aquest cas, hi ha una crida al constructor de Matriu, dos bucles imbricats que s’executen M
(el nombre de files de la primera matriu) i P (el nombre de columnes de la segona matriu) ve-
gades respectivament. Dintre d’aquests bucles hi ha una assignació, una crida al mètode set i
un tercer bucle que s’executa N vegades (el nombre de columnes de la primera matriu, idèntic
al nombre de files de la segona). Dintre d’aquest bucle més interior es fan dues crides al mètode
get, una multiplicació, i una operació combinada de sumar i assignar, que podem suposar que
n’és una de sola. Cal recordar que cada bucle inclou una assignació, una comparació (que
s’executa un cop més) i un increment. Per tant, es va del bucle més intern cap a fora:
s += get(i,k)*B.get(k,j);
double s = 0;
for (int k = 0; k < n; k++) {// aquí n és N
s += get(i,k)*B.get(k,j);
}
X.set(i,j,s);
T(M,N,P) = 1 + (P + 1) + P · (6 · N + 4) + P = 6 · P · N + 6 · P + 2
T(M,N,P) = 1 + (M + 1) + M · (6 · P · N + 6 · P + 2) + M =
=6·M·N·P+6·M·P+4·M+2
T(M,N,P) = 6 · M · N · P + 6 · M · P + 4 · M + 6
Pel que fa a la seva complexitat algorísmica, eliminant les constants irrellevants queda
O(M · N · P), que és el terme que presenta un major creixement. En general, calcular la
complexitat de forma sistemàtica és una tasca complicada i tediosa, mentre que aplicant
simplificacions és possible arribar al mateix resultat de forma molt més immediata. En
aquest exemple, el fet de tenir tres bucles imbricats que envolten una sentència amb com-
plexitat O(1), fa que aplicant la propietat multiplicativa dels bucles, es pugui arribar direc-
tament al mateix resultat, tal com mostra la figura 2.
Figura 2
Aleshores, suposant que les matrius són de mides similars (no són vectors, per exemple) –
és a dir, de N × N elements–, el cas T(N,N,N) es pot simplificar a T(N) = 6 · N3 + 6 · N2 + 4 · N
+ 6. Aplicant la definició de complexitat algorísmica, podem dir que aquest algorisme per
a multiplicar dues matrius bidimensionals té una complexitat O(M · N · P) o, en general,
O(N3), és a dir, cúbica. Sempre que un algorisme depengui d’una entrada determinada per
dos o més dimensions, és interessant fer l’exercici de plantejar-se què succeeix quan les
dimensions són comparables o, al contrari, quan es poden considerar constants. En aquest
exemple, si M = 1 i P = 1 –les matrius són vectors unidimensionals–, el resultat és T(1,N,1)
= 6 · N + 16, o el que és el mateix, O(N); és a dir, multiplicar vectors té una complexitat
lineal respecte a la longitud dels vectors.
El mètode setCartro() fa una assignació i una crida al mètode println(), però cal tenir en
compte que també fa una crida a toString() i, en aquest cas, el fet que es tracti d’un conjunt
d’enters fa que no se suposi que té un cost constant. En la implementació de ConjuntVec-
torImpl, es pot veure que el mètode toString() fa bàsicament un recorregut per tots els ele-
ments; així doncs, simplificant, podem suposar que té un cost T(MC) = MC. Per tant, el
mètode setCartro() té una complexitat T(MC) = MC + 2.
Aleshores, el cost de prepararPartida() és:
Si prescindim de les constants, i agafem només els termes de més creixement, podem dir
que aquest algorisme té una complexitat O(NJ · MC2). En aquest cas, com NJ i MC són mag-
nituds completament diferents, podem estudiar què passa quan una de les dues és molt
més gran que l’altra. En el cas habitual que els cartrons tinguin una mida afitada, el parà-
metre que pot créixer molt és NJ, el nombre de jugadors. Per tant, si considerem MC2 cons-
tant, l’algorisme té una complexitat O(NJ), és a dir, de creixement lineal amb el nombre
de jugadors. Notem, també, que prepararPartida() no depèn del paràmetre NB, ja que de fet
aquest paràmetre no intervé fins el moment de jugar la partida.
Glossari
algorisme heurístic m Algorisme que soluciona un problema sense assegurar que la solu-
ció trobada sigui l’òptima, però que generalment obté una bona solució parcial, propera a
l’òptima. Els problemes computacionalment intractables s’acostumen a resoldre usant algo-
rismes heurístics.
notació asimptòtica f Notació que permet simplificar totes les constants que acompanyen
les funcions TA(N) o SA(N) en l’anàlisi de la complexitat algorísmica, i fer comparacions per a
valors grans de N. La notació més usada és la O, però n’hi ha fins a cinc de diferents, depenent
del seu ús.
Bibliografia
Aho, A.; Hopcroft, J.; Ullman, J. (1998). Estructuras de datos y algoritmos. Wilmington:
Addison-Wesley Iberoamericana.