Download as pdf or txt
Download as pdf or txt
You are on page 1of 28

Complexitat

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

1. Introducció a la complexitat algorísmica .................................. 7


1.1. Complexitat temporal i complexitat espacial ................................ 7
1.2. Complexitat algorísmica de les estructures de control bàsiques .... 9
1.3. Complexitat de la combinació d’algorismes .................................. 10
1.4. Millor cas, pitjor cas i cas mitjà ...................................................... 15

2. Notació asimptòtica de la complexitat algorísmica ................ 17


2.1. Tipus de notació asimptòtica ......................................................... 17
2.2. Comparació de complexitats .......................................................... 18
2.3. Propietats de la notació O .............................................................. 19

3. Complexitat algorísmica dels tipus abstractes de dades ........ 21


3.1. Complexitat del TAD dels nombres naturals ................................. 21
3.2. Complexitat del TAD Conjunt ........................................................ 22

Resum ........................................................................................................ 23

Exercicis d’autoavaluació .................................................................... 25

Solucionari ............................................................................................... 26

Glossari ..................................................................................................... 28

Bibliografia .............................................................................................. 28
© FUOC • PID_00161968 • Mòdul 2 5 Complexitat algorísmica

Introducció

En aquest mòdul es defineixen els conceptes bàsics relacionats amb la comple-


xitat algorísmica. Aquesta eina s’usa per a comparar l’eficiència d’algorismes.
Com és d’esperar, un algorisme que s’utilitza per a resoldre un problema ha de
ser eficaç i eficient. Eficaç en tant que sigui capaç de resoldre el problema pro-
posat, i eficient en tant que ho resolgui al més ràpidament possible. Puix que
és possible disposar de diferents algorismes que resolguin un mateix problema
(tots ells són igualment eficaços), es tracta de comparar-los entre ells de mane-
ra que es triï el més eficient, que computacionalment parlant vol dir que usa
menys recursos per a trobar la solució desitjada.

La complexitat algorísmica, doncs, està relacionada amb la quantitat de recursos


que usa un algorisme per a resoldre un problema, una quantitat que està relaci-
onada amb els paràmetres que determinen la mida del problema. Així, per
exemple, el paràmetre que determina la mida del problema en un algorisme
d’ordenació seria el nombre d’elements que s’ordenen, mentre que els recursos
necessaris poden ser de dos tipus: quantitat de memòria o emmagatzemament
requerit, o bé el nombre de passos (o temps d’execució en un entorn controlat)
que necessita l’algorisme per a assolir la solució. Normalment, conèixer el temps
d’execució d’un algorisme és més útil (per la seva importància) que no la quan-
titat de memòria requerida.

En general, l’eficiència d’un algorisme esdevé rellevant per a un problema de


mida gran, és a dir, quan el nombre d’elements que manipula l’algorisme és ele-
vat. Per a mides petites, hi ha moltes variables que poden determinar el temps
d’execució (per exemple, la velocitat del processador, càrrega del sistema operatiu,
etc.) i tenir-hi una incidència significativa, mentre que per a mides grans, el temps
d’execució està bàsicament determinat per la pròpia estructura interna de l’algo-
risme (la seva complexitat algorísmica). Evidentment, un ordinador més potent
resoldrà el mateix problema més ràpidament que no pas un ordinador més sen-
zill; però si, per exemple, la mida del problema es multiplica per dos, l’increment
de temps que necessitarà un ordinador senzill serà equivalent a l’increment de
temps que necessitarà un ordinador més potent. La notació asimptòtica permet
donar una aproximació de la complexitat algorísmica i fer comparacions per a mi-
des de problema grans, independentment de la configuració de l’ordinador sobre
el qual s’executa l’algorisme.
© FUOC • PID_00161968 • Mòdul 2 6 Complexitat algorísmica

Objectius

Els objectius que s’espera que l’estudiant satisfaci amb els continguts d’aquest
mòdul són els següents:

1. Tenir una visió general dels conceptes de complexitat algorísmica i eficiència.

2. Conèixer les diferents notacions asimptòtiques per a denotar la complexitat


algorísmica.

3. Calcular la complexitat algorísmica de les estructures de control bàsiques.

4. Calcular la complexitat algorísmica dels tipus abstractes de dades.


© FUOC • PID_00161968 • Mòdul 2 7 Complexitat algorísmica

1. Introducció a la complexitat algorísmica

La complexitat algorísmica, informalment, és una mesura que permet als


programadors conèixer la quantitat de recursos que necessita un algorisme
per a resoldre un problema en funció de la seva mida. L’objectiu és compa-
rar l’eficiència d’algorismes a l’hora de resoldre un problema conegut.

De la mateixa manera, en el cas de tipus abstractes de dades (TAD), es tracta


de mesurar la idoneïtat d’una estructura o una altra per a la representació d’un
conjunt d’informació en funció de l’eficiència de les operacions que s’hi hau-
ran de realitzar. És evident que un programa resoldrà un problema (com ara
trobar la posició d’un element en una llista) en un cert temps, depenent d’un
seguit de factors: l’ordinador i el sistema operatiu sobre els quals s’executa el
programa, el llenguatge de programació emprat i el compilador, la plataforma
de desenvolupament, i la destresa i habilitat del programador; però, sobretot,
dependrà de la dificultat intrínseca i la mida del problema.

É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.

1.1. Complexitat temporal i complexitat espacial

La complexitat algorísmica pot mesurar dos conceptes diferents, que són


complementaris entre si:

1) Complexitat temporal: mesura el nombre d’unitats de temps que neces-


sita un algorisme (o una simple sentència) per a resoldre un problema amb
una entrada de mida N, i es denota per T(N).
© FUOC • PID_00161968 • Mòdul 2 8 Complexitat algorísmica

2) Complexitat espacial: mesura el nombre d’unitats de memòria que neces-


sita un algorisme (o una simple sentència) per a resoldre un problema amb
una entrada de mida N, i es denota per S(N).

Quan es parla d’unitats de temps o de memòria, no s’està fent sempre referèn-


cia a segons o bytes, respectivament, sinó que és una quantitat constant cone-
guda que permet fer comparacions relatives, usant l’1 com la unitat mínima
de cost. Per exemple, en el cas d’unitats de temps, en lloc d’utilitzar un segon
(o un nanosegon), s’utilitza com a unitat mínima de cost temporal el cost que
es correspon a fer l’operació més senzilla, per exemple una assignació.
D’aquesta manera, es minimitza la influència de l’arquitectura de l’ordinador,
el sistema operatiu, el compilador, etc., en el càlcul de la complexitat. En el cas
d’unitats de memòria, sí que s’acostuma a usar el byte com a unitat mínima,
tot i que també es poden utilitzar altres longituds d’acord amb l’arquitectura
de les dades que s’usen (per exemple, paraules de 64 bits). Habitualment, però,
no es treballa amb el cost en unitats mínimes de cada operació, sinó que es fa
una simplificació i se suposa que totes les operacions senzilles (implementades
directament per la CPU o la UAL, és a dir, les comparacions, assignacions, ope-
racions aritmètiques, etc.) tenen el mateix cost.

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.

De fet, les dues complexitats no són independents, ja que alguns algorismes


poden millorar la seva complexitat temporal a costa d’empitjorar l’espacial i a
l’inrevés. Per exemple, un algorisme que necessiti realitzar molts càlculs inter-
medis repetitius pot reduir el temps de càlcul si els emmagatzema per reapro-
fitar-los. És a dir, si s’emmagatzema informació redundant augmentant la
complexitat espacial, es pot reduir la complexitat temporal, i a l’inrevés.

Reaprofitament de càlculs El concepte de recursivitat es


desenvolupa en el mòdul “Arbres”.

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

1.2. Complexitat algorísmica de les estructures


de control bàsiques

Tots els llenguatges procedimentals d’alt nivell, com ara Pascal, C o el Java ma-
teix, comparteixen les mateixes estructures de control:

• Seqüència: les instruccions s’executen una després de l’altra.

• Alternativa: depenent d’una condició, s’executarà un bloc d’instruccions


o un altre (o bé no se n’executarà cap).

• Repetitiva: un bloc d’instruccions es repeteix un cert nombre de vegades


(que pot ser zero o més) depenent d’una condició.

Per qüestions de simplicitat, no es diferencien els costos individuals entre cada


tipus d’operació, ja que es tracta d’obtenir una estimació en funció de la mida
del problema, i no tant de l’entorn (ordinador, sistema operatiu, compilador,
etc.) en què s’executa l’algorisme que el resol. Per tant, cada sentència o ins-
trucció individual, avaluació d’una condició o d’una expressió aritmètica sen-
zilla, etc., es considera que tenen una complexitat temporal T(N) = 1. És a dir
que, de fet, no depenen de N i són constants. Consegüentment, el cost estimat
depèn més del nombre d’operacions realitzades que no pas realment de les
operacions en concret, i només dependrà de N quan el nombre d’operacions
també en depengui, com ara per exemple repetir un bloc d’instruccions dintre
d’un bucle que depèn de N.

Considerem el fragment de codi en Java següent:

int suma = 0;

for (int k = 1; k < = N; k++) {


if ((k%2)==1) {
suma+= k;
}
}

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:

T(N) = 1 + 1 + 1 · (N + 1) + (1 + (1 + 1)) · N + 1 · (N/2) + 1 = 4N + N/2 + 4.

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.

1.3. Complexitat de la combinació d’algorismes

Habitualment, els algorismes es poden descompondre en diverses etapes, cadas- Observació


cuna de les quals fa una tasca diferent per a la resolució d’un problema. Cada
És important no confondre n
etapa és, de fet, un subalgorisme que executa una tasca concreta amb la seva amb N, ja que són valors dife-
rents. N fa referència a la mida
complexitat pròpia. Per tant, si un algorisme A es pot descompondre en n subal- del problema, mentre que n fa
referència al nombre de subal-
gorismes A1, A2, ..., An, de complexitats conegudes T1, T2, ..., Tn, aleshores es pot gorismes.
aplicar un conjunt de regles per a calcular la complexitat de l’algorisme A.

• Si dos o més algorismes s’executen seqüencialment

A1
A2
...
An

la complexitat total serà la suma de les complexitats parcials, és a dir,

T(N) = T1(N) + T2(N) + ... + Tn(N)


© FUOC • PID_00161968 • Mòdul 2 11 Complexitat algorísmica

• Si en una estructura alternativa com la següent:

si (condició) aleshores // condició es denota per C


A1
sino
A2
fsi

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:

T(N) = p · T1(N) + (1 – p) · T2(N) + TC(N). Observació

Fixeu-vos que l’avaluació de la


Si no es coneixen les probabilitats de cada valor resultat d’avaluar l’expressió, condició pot dependre de la
mida del problema N, tot i que
no podrem calcular el cost mitjà d’una execució de l’algorisme per a dades de habitualment serà una compa-
mida N. En aquest cas, una bona mesura és considerar que la complexitat total ració i, per tant, en general
TC(N) = 1.
és el màxim de les complexitats parcials, és a dir:

T(N) = màx(T1(N),T2(N)) + TC(N).

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à.

De la mateixa manera, si l’estructura alternativa només té una branca, es pot con-


siderar que, en el pitjor cas (quan la condició és certa), la complexitat total és:

T(N) = T1(N) + TC(N).

• Si en una estructura repetitiva com la següent:

mentre (condició) { // condició es denota per C


A
}

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:

T(N) = N · TA(N) + (N + 1) · TC(N).


© FUOC • PID_00161968 • Mòdul 2 12 Complexitat algorísmica

Això és cert només si l’algorisme A manté una complexitat constant al llarg


de totes les iteracions del bucle. Si no fos així, i la complexitat depengués
de la iteració, caldria fer una descomposició de les diferents execucions de
l’algorisme A i fer un sumatori de la complexitat en cada execució, és a dir:

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).

De la mateixa manera, si el bucle s’executés un nombre log(N) de vegades,


la complexitat total es calcularia com:

T(N) = log(N) · TA(N) + (log(N) + 1) · TC(N)

És a dir, un bucle que depèn de la mida del problema en funció de G(N),


incrementa la complexitat del problema original en G(N) vegades, més el
cost d’avaluar la condició G(N) + 1 vegades. Aquesta regla s’aplica directa-
ment en cas de dos o més bucles imbricats de la forma següent:

mentre (condició1) { // aquest bucle s’executa G1(N) cops


mentre (condició2) { // i aquest G2(N) cops
A
}
}

i dóna una complexitat total (en aquest cas), de:

T ( N ) = G1( N ) ⋅ (G2 ( N ) ⋅ TA ( N ) + (G2 ( N ) + 1) ⋅ TC2 ( N )) + (G1( N ) + 1) ⋅ TC1 ( N )

Si els costos d’avaluar les expressions són constants, és a dir,


TC1 ( N ) = TC2 ( N ) = 1 , i es poden negligir respecte al cost de l’algorisme
TA(N), aleshores l’expressió anterior es pot simplificar a:

T(N) = G1(N) · G2(N) · TA(N)

És a dir, la complexitat creix de forma multiplicativa per a cada nivell de


bucle.

• Si en una sentència s (una instrucció o bé l’avaluació d’una expressió) es fa


una crida a una funció o procediment que té complexitat Ts(N), es consi-
dera que la sentència és un subalgorisme amb complexitat Ts(N) i s’aplica
la regla de descomposició d’un algorisme en subalgorismes.
© FUOC • PID_00161968 • Mòdul 2 13 Complexitat algorísmica

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.”

Donald Knuth, The art of computer programming (volum 2, secció 3.2.1)

A partir d’això, podem deduir (si analitzem l’algorisme de congruència lineal)


que les crides a aquest mètode tenen cost constant. Sempre que es fa una crida
a un mètode del JDK cal tenir en compte, però, que pot tenir un cost no cons-
tant en funció de la mida de l’entrada.

Aleshores, partim del codi següent:

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;

public Bingo(int nombreDeJugadorsMaxim,int nombreDeBoles,int midaCartro) {


jugadors = new Jugador[nombreDeJugadorsMaxim];
nombreDeJugadors = 0;
this.nombreDeBoles = nombreDeBoles;
this.midaCartro = midaCartro;
generadorNumerosCartro = new Random();
}

public void prepararPartida() {


System.out.println("Preparant partida...");
© FUOC • PID_00161968 • Mòdul 2 14 Complexitat algorísmica

for (int i = 0;i<nombreDeJugadors;i++) {


Conjunt<Integer> cartro = generarCartro();
jugadors[i].setCartro(cartro);
}
bolesCantades=new ConjuntVectorImpl<Integer>(nombreDeBoles);
}

protected Conjunt<Integer> generarCartro() {


Conjunt<Integer> cartro=
new ConjuntVectorImpl<Integer>(midaCartro);
for (int n =0;n<midaCartro;n++)
cartro.afegir(generadorNumerosCartro.nextInt(nombreDeBoles) + 1);
return cartro;
}
...
}

uoc.ei.exemples.modul2.Jugador

...
public class Jugador {
private String nom;
private Conjunt<Integer> cartro;

public Jugador(String nom) {


this.nom = nom;
}

public void setCartro(Conjunt<Integer> cartro) {


this.cartro = cartro;
System.out.println(this.toString());
}
...
}

La crida a generarCartro() es pot descompondre en una altra crida al construc-


tor ConjuntVectorImpl(), que podem suposar que té cost T(N) = 1, un bucle
que s’executa MC cops, i la sentència return, també amb complexitat cons-
tant T(N) = 1. Dintre del bucle es fa una crida al mètode afegir(); però cal tenir
en compte que com a paràmetre s’executa generadorNumerosCartro.nextInt()
que –com hem dit abans– només fa una crida al mètode del JDK per a generar
números aleatoris, que podem suposar que té cost T(N) = 1 i una suma addici-
onal. Així, el cost de la sentència que es repeteix MC cops només depèn real-
ment del cost del mètode afegir() implementat dintre de ConjuntVectorImpl. A
més, la instrucció for amaga una assignació de la variable n que s’executa un
© FUOC • PID_00161968 • Mòdul 2 15 Complexitat algorísmica

cop, una instrucció d’increment de n que s’executa MC vegades i una condició


(més petit que) que s’ha d’avaluar (MC + 1) cops.

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.

El mètode cercarPosicioElement() –que realment fa la tasca de buscar un element– fa


un total de dues assignacions; un bucle que es repeteix com a molt MC vegades,
dintre del qual s’executen una assignació; una crida al mètode equals() que equival
a fer una comparació entre enters (el tipus base del constructor genèric); i una sen-
tència if que, en cas de ser certa, fa executar una operació d’increment (que fa un
total de quatre operacions, com a molt, dintre del bucle). A més, comprovar la
condició del bucle requereix tres operacions: dues comparacions i una AND,
que s’executen MC + 1 vegades. Finalment, el mètode cercarPosicioElement acaba
amb l’avaluació de l’operador ternari que suposarem que són dues operacions bà-
siques (avaluació de la condició i l’assignació), i la sentència return. Per tant, el cost
d’aquest mètode és, en el pitjor cas, T(MC) = 4 · MC + 3 · (MC + 1) + 5 = 7 · MC + 8.

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:

T(MC) = MC · (7 · MC + 12 + 3) + (MC + 1) + 3 = 7 · MC2 + 16 · MC + 4

De manera intuïtiva, si ens quedem només amb el terme de major creixement,


7 · MC2 –que és el que determina realment els valors de T(MC) per a valors
grans de MC– podem veure que en analitzar l’algorisme trobarem dos bucles
imbricats (tot i que no directament, sinó mitjançant crides a mètodes), i ob-
servarem que tots dos s’executen MC cops cadascun. Com veurem, aquest és
el procés per a obtenir mesures senzilles per conèixer la complexitat d’un al-
gorisme.

És interessant observar que la complexitat temporal d’aquest mètode no de-


pèn del nombre de jugadors, sinó només de la mida del cartró, un fet d’altra
banda coherent amb la seva funcionalitat. Aquest exemple permet veure que
l’estudi de la complexitat temporal pot arribar a ser molt complicat, fins i tot
amb les simplificacions pertinents relatives als bucles i a les crides a altres mè-
todes. A més, cal fer suposicions sobre el comportament dels algorismes en
funció de l’entrada, com és el cas de les sentències if o while, on la condició
pot ser certa o falsa un nombre desconegut de vegades, tal com es discuteix a
continuació.
© FUOC • PID_00161968 • Mòdul 2 16 Complexitat algorísmica

1.4. Millor cas, pitjor cas i cas mitjà

En general, es pot considerar que tots els algorismes tenen un comportament


determinista, de manera que, per a la mateixa entrada, generen la mateixa sor-
tida efectuant exactament les mateixes operacions. Depenent de l’entrada, els
resultats esperats s’obtindran més o menys ràpidament. Per exemple, un algo-
risme de cerca d’un element en un vector de N elements acabarà en un sol pas
si l’element buscat es troba en la primera posició explorada, o bé necessitarà
fins a N passos si l’element buscat es troba en l’última posició explorada. I en
mitjana, si totes les entrades possibles són equiprobables, l’algorisme necessi-
tarà (N + 1)/2 passos per a trobar l’element en qüestió.

Per tant, el mateix algorisme presenta un comportament diferent en funció de


l’entrada concreta, i no únicament de la seva mida, la qual cosa en complica
molt l’anàlisi a causa de la gran quantitat d’entrades possibles. Es parla, ales-
hores, del comportament de l’algorisme en el millor cas, en el pitjor cas, i en
el cas mitjà.

El comportament en el millor cas es produeix quan l’algorisme que ha de re-


soldre un problema es troba una entrada per la qual s’arriba al resultat esperat,
i això sense cap operació o potser amb només una comprovació. Per exemple,
ordenar un vector de N elements que ja ha estat ordenat prèviament. Aquest
cas es considera trivial i normalment no s’usa per a comparar l’eficiència dels
algorismes, ja que no aporta informació valuosa. A més, la majoria d’algoris-
mes tenen la mateixa complexitat en el millor cas, normalment T(N) = 1. Per
aquest motiu, aquesta informació no serveix per a decidir quin algorisme és el
més eficient.

El comportament en el pitjor cas es produeix quan l’algorisme es troba una


entrada que obliga a recórrer totes les dades d’entrada. Per exemple, buscar un
element en un vector en el qual no es troba; això no es descobreix fins que no
s’han inspeccionat tots els elements. Tot i que també pot semblar un cas trivi-
al, en aquest cas sí que és important, perquè hi pot haver algorismes semblants
que tinguin una complexitat diferent en el pitjor cas i, per tant, pot ser un mo-
tiu per a decantar-se per un algorisme o per un altre.

Finalment, el cas mitjà estudia el comportament de l’algorisme per a qualse-


vol entrada possible, i assumeix que totes les entrades són equiprobables, o bé
que segueixen una distribució coneguda. És important destacar que el càlcul
en el cas mitjà pot ser molt complicat, o fins i tot impossible si la distribució
de les entrades possibles és desconeguda. Això fa que a vegades s’usin simula-
cions per ordinador, com ara el mètode de Montecarlo, per a estimar la com- Web recomanat

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

2. Notació asimptòtica de la complexitat algorísmica

L’ús del nombre d’operacions bàsiques executades per un algorisme, denotat


per T(N), pot ser complicat per a fer comparacions entre algorismes. Per a va-
lors petits de N, les constants que acompanyen els termes de T(N) poden in-
fluir molt significativament en el cost total que s’està avaluant, i poden portar
cap a conclusions errònies respecte a l’eficiència de l’algorisme. En canvi, el
que interessa és conèixer el comportament de l’algorisme per a valors de N
grans, una situació en què l’elecció correcta d’un algorisme pot permetre
reduir la complexitat algorísmica necessària per a resoldre un problema de for-
ma significativa. Un exemple clar són els algorismes d’ordenació, en què per a
valors petits de N, els algorismes simples –com ara el d’inserció, el de selecció o el
de la bombolla– poden ser més eficients que d’altres de més complexes –com ara
el quicksort, el shellsort o el heapsort. En canvi, per a valors grans de N, és ben sabut L’algorisme heapsort s’explica
amb detall en el mòdul “Cues amb
prioritat” d’aquesta assignatura.
que aquests darrers algorismes són molt més eficients malgrat la seva complexitat
intrínseca.

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.

2.1. Tipus de notació asimptòtica

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

Podeu trobar més informació


Es diu que un algorisme F té una complexitat O(G(N)) si existeixen dues de les altres notacions en
l'obra següent:
constants C i N0 per a les que es compleixi |F(N)| < C · G(N) per a tot N > N0.
Herbert S. Wilf. Algorithms
and complexity. Disponible en
línia a:
http://
És a dir, informalment, un algorisme F té complexitat O(G) si el nombre d’opera- www.math.upenn.edu/
~wilf/AlgoComp.pdf
cions necessàries queda fitat pel comportament de G per a valors grans de N. Nor-
malment, les funcions G (ordres de complexitat) són senzilles, sense constants. En
realitat, O(G) correspon a un conjunt de funcions, i amb la notació O s’aconse-
gueix posar en un mateix conjunt totes les funcions amb costos “comparables”,
és a dir, equivalents amb independència de factors externs (maquinari, sistema
operatiu, llenguatge de programació, etc.). Respecte a la constant C, la seva pre-
© FUOC • PID_00161968 • Mòdul 2 18 Complexitat algorísmica

sència en la definició de la notació O fa que, en una funció G amb complexitat


O(G), totes les funcions de la forma C · G (en què C és una constant) pertanyin
també a O(G). Per tant, es parla de complexitat O(N), per exemple, però no de
complexitat O(2 · N), ja que asimptòticament són equivalents segons la definició.
Així, per exemple, N pertany a O(N) i 2 · N també pertany a O(N).

La taula 1 mostra les complexitats algorísmiques més importants.

Taula 1
Notació Nom Exemple d’algorisme

O(1) Constant Accés a un element d’un vector

O(log N) Logarítmica Cerca binària

O(N) Lineal Cerca seqüencial

O(N log N) Lineal-logarítmica Algorisme d’ordenació quicksort


2
O(N ) Quadràtica Algorismes d’ordenació simples

O(N3) Cúbica Multiplicació de matrius


N
O(2 ) Exponencial Partició de conjunts

É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.

2.2. Comparació de complexitats

É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

100 6,64 100 664 104 106 1030

1.000 9,97 1.000 9.970 106 109 10301

10.000 13,29 10.000 132.900 108 1012 103.010

100.000 16,61 100.000 1.661.000 1010 1015 1030.103

1.000.000 19,93 1.000.000 19.930.000 1012 1018 10301.030


© FUOC • PID_00161968 • Mòdul 2 19 Complexitat algorísmica

Multiplicar la mida del problema per un ordre de magnitud té un increment molt


El BlueGene/L
diferent depenent de la complexitat de l’algorisme. Amb la potència dels superor-
El superordinador BlueGene/L
dinadors actuals –que poden executar gairebé 1014 operacions en un segon–, fins d’IBM pot efectuar un màxim
de 91,75 teraflops (el prefix te-
i tot amb una mida de només N = 100, un algorisme amb complexitat exponen-
ra– equival a un bilió de vega-
cial no es pot resoldre en un temps raonable, ja que es necessitarien 1016 segons des, és a dir, 1012 operacions
en coma flotant per segon).
o, el que és el mateix, 317 milions d’anys.

En general, els algorismes amb complexitat exponencial són computacionalment


intractables segons els estàndards actuals, i no es poden atacar directament a par-
tir de mecanismes basats en la força bruta que proven totes les combinacions pos-
sibles per a arribar a la solució desitjada, sinó que cal utilitzar algorismes que, a
partir d’una solució aproximada, obtenen una solució més bona (o fins i tot òpti-
ma) a partir de tècniques aproximades o heurístiques.

Figura 1

2.3. Propietats de la notació O

La primera propietat, ja esmentada abans, és que l’ordre de qualsevol funció


de la forma O(k · g) en què k és una constant és O(g); és a dir, sigui quina sigui
la funció de creixement segons el paràmetre d’entrada, la seva complexitat
creix proporcionalment a g.

Usant la definició de la notació O, veiem també que O(g1 + g2) és O(màx(g1,g2)).


És a dir, si un algorisme G té dos o més subalgorismes que s’executen seqüen-
cialment sobre el mateix conjunt d’entrada, la complexitat (temporal o espa-
© FUOC • PID_00161968 • Mòdul 2 20 Complexitat algorísmica

cial) de G serà la mateixa que la del subalgorisme que presenti la complexitat


(o funció g) més elevada. En altres paraules, si un algorisme fa dues tasques, la
tasca que tingui un comportament asimptòtic més costós és la que determina-
rà la complexitat total de l’algorisme. El mateix criteri es pot aplicar al cas de
l’estructura de control alternativa, tal com s’ha comentat.

Això vol dir que, en general, si un algorisme es pot descompondre en diverses


parts, i una d’aquestes té una complexitat algorísmica superior a la resta, la com-
plexitat de l’algorisme quedarà determinada per aquesta part. Per exemple, si un
algorisme de cerca intenta ordenar les dades d’entrada amb un algorisme O(N2) o
O(N log N) per a aplicar-hi després una cerca dicotòmica, –que és més eficient que
la cerca seqüencial (O(log N) de la cerca dicotòmica enfront de O(N) de la cerca
seqüencial)–, el resultat final és pitjor que si es fes la cerca seqüencial directament,
ja que la complexitat de l’algorisme d’ordenació és superior.

De la mateixa manera –i parlant de la complexitat algorísmica dels tipus abs-


TAD és la sigla de
tractes de dades–, la complexitat d’un TAD dependrà de les operacions dispo- tipus abstracte de dades.

nibles, la seva implementació i la complexitat resultant; per la qual cosa, un


TAD que sigui eficient per a unes operacions pot no ser-ho per a les altres. Per
tant, depenent del tipus d’operacions que calgui executar més sovint, caldrà
triar els algorismes i les implementacions de cadascuna de les operacions en
funció de les necessitats. Així, en l’exemple anterior, si sobre un conjunt gran
de dades es fan moltes cerques, sí que pot ser interessant ordenar-lo un cop,
encara que sigui costós, i reduir el cost global de les operacions de cerca.
© FUOC • PID_00161968 • Mòdul 2 21 Complexitat algorísmica

3. Complexitat algorísmica dels tipus


abstractes de dades

En el cas concret dels tipus abstractes de dades, quan es parla de complexitat


algorísmica és interessant relacionar els conceptes d’implementació, eficiència i
complexitat. L’objectiu és ser capaços de triar el tipus de contenidor més apro-
piat per a una certa col·lecció d’objectes en funció de certes restriccions tem-
porals i espacials.

La idea bàsica és que cada TAD té una complexitat coneguda en funció de la


seva implementació. De fet, s’indica la complexitat de cadascuna de les opera-
cions que ofereix el TAD. Així, per exemple, en la implementació dels nom-
bres naturals usant el tipus int de Java que ja coneixem, les operacions de Vegeu el tipus int de Java presentat
en el mòdul “Tipus abstractes de
dades” d’aquesta assignatura.
predecessor i successor d’un nombre natural són totes dues d’ordre O(1); és a
dir, que es fan en temps constant independentment del nombre natural que
es manipula. Això pot semblar molt eficient (i, de fet, ho és), però cal tenir en
compte que la representació dels nombres naturals utilitzant el tipus enter de
Java limita el rang possible de nombres representats, la qual cosa pot no ser
vàlida per a resoldre algun problema en què calgui manipular nombres natu-
rals molt grans. Per tant, és possible que la representació interna més eficient
no sigui sempre vàlida o estigui limitada a una certa mida de problema.

3.1. Complexitat del TAD dels nombres naturals

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.

En principi, la implementació utilitzant un int resulta ideal: la complexitat es-


El TAD dels nombres
pacial és O(1), i la complexitat temporal de tots els mètodes és també O(1); ja naturals en Java

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.

En canvi, la representació basada en un vector de booleans presenta una efici-


ència menor: la complexitat espacial és O(N) –ja que l’espai necessari creix li-
nealment amb la magnitud del nombre natural representat– i, pel que fa a la
© FUOC • PID_00161968 • Mòdul 2 22 Complexitat algorísmica

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().

3.2. Complexitat del TAD Conjunt

En aquest cas, un conjunt de N elements es representa mitjançant un vector


d’almenys N posicions de tipus Object; per tant la seva complexitat espacial
serà O(N). Pel que fa a la complexitat temporal, la taula 3 mostra el resultat a
partir de la implementació de cadascuna de les operacions disponibles.

Taula 3
Operació Complexitat Raonament

La reserva de memòria per part del sistema operatiu


constructor O(1) i la màquina virtual és una operació que no depèn
de la quantitat de memòria demanada.

El mètode cercarPosicióElement(), al qual criden tots tres


afegir() O(N) mètodes, ha de recórrer seqüencialment tot el vector
de N elements.

hiEs() O(N)

esborrar() O(N)
© FUOC • PID_00161968 • Mòdul 2 23 Complexitat algorísmica

Resum

La complexitat algorísmica permet mesurar l’eficiència d’un algorisme per a


resoldre un problema de mida coneguda. Aquesta mesura permet fer compa-
racions entre algorismes diferents amb l’objectiu de triar el més eficient per a
resoldre el problema. De la mateixa manera, és possible triar el tipus abstracte
de dades més adient per a cada problema, depenent de les restriccions tempo-
rals o espacials imposades. L’eficiència es pot mesurar respecte a la quantitat
d’operacions necessàries per a resoldre el problema –en aquest cas es parla de
complexitat temporal– o bé respecte a la quantitat de memòria necessària –en
aquest cas, parlaríem de complexitat espacial. Com que és més fàcil afegir recur-
sos de memòria que no pas incrementar la velocitat d’un ordinador, habitual-
ment, el concepte d’eficiència es refereix a la complexitat temporal, és a dir, al
nombre d’operacions necessàries per a resoldre un problema.

D’altra banda, tot i que és possible mesurar la complexitat algorísmica en fun-


ció de les operacions que realitza un algorisme, és millor utilitzar alguna no-
tació que permeti efectuar comparacions senzilles per a valors de N grans, de
la mida del problema per resoldre. Una d’aquestes notacions es coneix com
a notació asimptòtica O, que permet fer comparacions senzilles entre l’eficièn-
cia dels algorismes. La idea de la notació O és tenir una fita superior de l’ordre
de creixement de la complexitat en funció de la mida del problema. Així, un
algorisme que resolgui un problema amb complexitat quadràtica O(N2) és mi-
llor que un algorisme que ho faci amb complexitat cúbica O(N3), per exemple.

En el cas particular dels tipus abstractes de dades, és important conèixer-ne la im-


plementació, que és la que realment determina la complexitat algorísmica de les
operacions que es realitzen internament. El tipus d’emmagatzemament utilitzat
per a gestionar les dades i les operacions disponibles lligades a aquest emmagat-
zemament determinen la complexitat de cada determinada operació. Així, per
exemple, el fet que uns elements s’emmagatzemin ordenats permet aconseguir
un tipus de cerca més eficient que redueix la complexitat d’O(N) a O(log N), men-
tre que l’operació d’inserció segurament tindrà una complexitat més elevada a
causa de la necessitat de mantenir aquests elements ordenats.

En la resta de mòduls d’aquest material, en què es descriuen els diferents tipus


abstractes de dades i les seves diferents implementacions, es detallarà el càlcul
de la seva complexitat espacial i temporal a partir de la representació interna
i de les operacions disponibles.
© FUOC • PID_00161968 • Mòdul 2 25 Complexitat algorísmica

Exercicis d’autoavaluació

1. Calculeu la complexitat temporal a partir de la notació algorísmica del programa següent,


que calcula el producte de dues matrius de M × N elements.

package uoc.ei.exemples.modul2.Matriu

...
public class Matriu {
private double[][] elements;
private int m,n;

public Matriu (int m, int n) {


this.m = m;
this.n = n;
elements = new double[m][n];
}

public void set(int i, int j, double s) {


elements[i][j] = s;
}

public double get(int i, int j) {


return elements[i][j];
}

public Matriu multiplicarPer(Matriu B) {


Matriu X = new Matriu(m,B.n);
for (int i = 0; i < m; i++) {
for (int j = 0; j < B.n; j++) {
double s = 0;
for (int k = 0; k < n; k++) {
s += get(i,k)*B.get(k,j);
}
X.set(i,j,s);
}
}
return X;
}

2. Calculeu la complexitat temporal a partir de la notació algorísmica del mètode anomenat


prepararPartida() a l’exemple del joc del bingo desenvolupat en el subapartat 1.3 d’aquest
mòdul.
© FUOC • PID_00161968 • Mòdul 2 26 Complexitat algorísmica

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.

Mètode Matriu (constructor)

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.

Mètodes set i get

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:

a) El bucle següent té una complexitat T(M,N,P) = 4, o també O(1):

s += get(i,k)*B.get(k,j);

b) Aquest altre bucle té la complexitat següent:

T(M,N,P) = 1 + 1 + (N + 1) + N · 4 + N + 1 = 6 · N + 4, o també: O(N)

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);

c) El bucle següent té la complexitat indicada a continuació:

T(M,N,P) = 1 + (P + 1) + P · (6 · N + 4) + P = 6 · P · N + 6 · P + 2

for (int j = 0; j < B.n; j++) { // aquí B.n és P


...
}
© FUOC • PID_00161968 • Mòdul 2 27 Complexitat algorísmica

d) Finalment, el bucle següent té una complexitat:

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

for (int i = 0; i < m; i++) { // aquí m és M


...
}

Afegint el constructor i la sentència return final, obtenim:

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.

2. Volem analitzar el mètode prepararPartida() en funció de les mides que intervenen en el


problema: nombre de jugadors (ho denotarem per NJ), mida del cartró (denotat per MC) i
nombre de boles (denotat per NB); per tant parlarem de T(NJ,MC,NB). Si observem el codi,
veurem que aquests paràmetres determinen la mida de les operacions (estructures iterati-
ves, crides a mètodes, etc.). Aleshores podem procedir, primer, aplicant la primera propi-
etat (descomposició):
1) La sentència amb la crida a println() té un cost T(NJ,MC,NB) = 1.
2) Hi ha un bucle que s’executa NJ cops. Per tant, si apliquem la propietat multiplicativa
dels bucles i les simplificacions pertinents, la complexitat temporal serà 1 + NJ · 1 + (NJ + 1)
+ NJ · TA(NJ,MC,NB). En aquesta fòrmula, 1 correspon a l’assignació que inicialitza la vari-
able i, NJ · 1 representa els NJ increments de la variable i a cada iteració i NJ + 1 representa
les comprovacions de la comparació de final de bucle. Finalment, A és l’algorisme format
per les crides a generarCartro(), que té una complexitat T(MC) = 4 · MC2 + 11 · MC + 2 i
setCartro(), que desenvoluparem amb posterioritat.
3) Finalment, es fa una crida al constructor ConjuntVectorImpl(), el qual té un cost constant
T(N) = 1 en fer només una crida al constructor Object() genèric. Observeu que aquest construc-
tor només depèn d’un paràmetre genèric, i no de tres com el mètode prepararPartida().
© FUOC • PID_00161968 • Mòdul 2 28 Complexitat algorísmica

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:

T(NJ, MC, NB) = 1 + NJ · 1 + (NJ + 1) + NJ · (4 · MC2 + 11 · MC + 2 + MC + 2) + 2


= 4 · NJ · MC2 + 12 · NJ · MC + 6 · NJ + 4.

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.

complexitat espacial f Nombre d’unitats de memòria que necessita un algorisme A per a


resoldre un problema amb una entrada de mida N, i es denota per SA(N).

complexitat polinòmica f Complexitat que es pot expressar mitjançant un polinomi i


que, en general, vol dir que l’algorisme en qüestió és computacionalment tractable.

complexitat temporal f Nombre d’unitats de temps que necessita un algorisme A per a


resoldre un problema amb una entrada de mida N, i es denota per TA(N).

computacionalment intractable adj Dit d’aquell problema d’una complexitat algorís-


mica exponencial, la resolució del qual té un cost que creix exponencialment amb la mida
de l’entrada, com ara, per exemple, generar tots els subconjunts possibles d’un conjunt.

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.

Biggs, N. L. (1994). Matemática discreta. Barcelona: Vicens Vives.

You might also like