dimecres, 15 de novembre del 2017

Aprendre a programar aux - Bones i males pràctiques a PRO1

Aquest article se'm va acudir aquest dilluns, després de veure algunes correccions del primer parcial de PRO1. Veient diverses coses que ha entregat la gent, se m'ha acudit que potser calia parlar de bones i males pràctiques, de manera que no es tornin a veure coses així de rares.

Indentació

Aquí no hi ha gaire a dir. El codi, ben indentat. Què vol dir això? Que has de ser coherent. Primer cal determinar com ho faràs, si amb espais o tabulacions. Jo sóc molt més partidari de tabulacions, perquè així cadascú es configura l'editor per veure-ho com li agradi. Després cal definir l'amplada, jo tinc configurat perquè els tabulats ocupin l'equivalent a 4 espais. I un cop tens decidides aquestes dues coses, fes-ho sempre igual. 

Quan tabules?

Quan entris a un tros de codi que està dins d'alguna cosa (if, else, while, for, switch, una funció...), un nivell més. Així, quedaria
if (a) {
    codi
    if (b) {
        més codi
    }
}

Amplada de les línies

En resum, no es poden fer línies de més de 80 caràcters. Si tens una línia que ocupa més, mala idea, perquè costa molt de llegir (i més encara si utilitzes editors des de la consola). Si a nivell de PRO1 arribes a amplades així, probablement estàs fent alguna cosa malament. Però per arreglar-ho, sempre pots seguir a la línia següent. Poses simplement una \, i saltes a la següent línia 
int n=1+2+3+4+5+6+7+8+9+10+11+12+13+14+15+\
        16+17+18+19+20+21+22+23+24+25+26+27;

Les inclusions es fan bé

Sí, tal qual. Comencem forts. No pot ser veure codis de gent que utilitza una plantilla plena d'inclusions. O gent que no en posa algunes. Posa totes les que necessitis, i només les que necessitis. 

Algú s'estarà preguntant com pot ser que no incloguis coses que calen. Molt senzill. La llibreria iostream inclou la llibreria string, de manera que si utilitzes iostream, pots utilitzar strings sense incloure la seva llibreria. Però no té cap sentit fer-ho. Així que inclou tot el que calgui

Per altra banda, no em serveix que em diguis "no, si jo només incloc una llibreria, bits/stdc++.h. De debò, no inclogueu aquesta porqueria. Primer, perquè no és una llibreria estàndard, i segon, pel que he dit d'incloure només el que calgui. Ni explicaré què fa, no voldria ser responsable de que algú la utilitzi

El main no retorna res

Sí, en C era una pràctica comuna acabar el main amb un "return 0". En C++ no cal, i posar-lo és redundant

Respecta l'àmbit de les variables

Una variable té un àmbit. És la part del codi on existeix. Per exemple, al codi següent:
int s=0;
for (int i=0;i<n;++i) {
    int aux;
    cin>>aux;
    s+=aux;
}
cout<<s<<endl;

Aquí tenim quatre variables. Són s, i, n i aux. n no ens preocuparem del seu àmbit, assumeixo que ja ve declarada d'abans. Veiem que s es declara abans del for, mentre que al for declarem dues variables (i i aux). Doncs bé, l'àmbit d'aquestes dues és el for. Concretament, aux es declara a cada iteració del for, mentre que i dura mentre duri el for. Això serveix per utilitzar les variables només on calen. Si no ens cal la i fora del for, no la volem tenir declarada. Per això es diu que les variables s'han de declarar sempre a l'àmbit més intern possible

El for es fa bé

Un for té una estructura molt clara. I és la següent:
for (inicialització;comparació;modificació) {
    cos
}
Respecteu-la. No deixeu buida cap de les parts. Si ho feu, en el millor dels casos tindreu un codi fastigós, en el pitjor un bucle infinit. 
Quan s'inicialitza, es declara una (o més, però en general una) variable de control. Declara-la allà mateix. Al cap i a la fi, és una variable que en el 99,99% dels casos, només necessites al for. Normalment, quedarà una cosa molt similar a la següent
for (int i=0;i<n;++i)
Hi ha variacions, òbviament, potser arriba fins a n inclosa, a vegades es fa al revés (començant per n, i arribant a 0). I algun cop potser enlloc d'augmentar d'un en un, augmenta de dos en dos, o similar. Però la idea és que sigui similar a això

Important: Si has deixat buida una de les tres parts, probablement estigui malament
Important 2: No serveix posar a=a perquè no quedi buit. Sembla una tonteria, però és que ha passat
Per cert, també està prohibit modificar la variable de control. Jo no ho prohibiria, malgrat que em sembla una mala pràctica, però el tema és que no es pot. 

No trenquis el codi

Hi ha vegades on volem fer un bucle, però sabem que en algun moment potser ens hem d'aturar. I, buscant codis, a vegades et trobes una cosa com la següent

for (int i=0;i<n;++i) {
    ...
    if(...) break;
}

Això trenca molt el codi. No és gens visual, i està prohibit a PRO1. No ho feu, a més és cutre i lleig. En tot cas, una alternativa vàlida per PRO1 seria la següent

bool fi=false,
for (int i=0;i<n and not fi;++i) {
    ...
    fi=...;
}
Fent funcions hi ha una altra possibilitat, però ja l'explico més endavant 

Tot això no només s'aplica aquí, també s'aplica amb continue i goto

Booleans

Quan treballes amb booleans, saps que un booleà té dues possibilitats (true i false). Una condició (per exemple a==b) a C++ retorna també un booleà. Per tant, una cosa que no mola gens és la següent
bool b;
//codi, en algun moment b agafarà valors
if (b==true) ...
else ...

Això és completament absurd. b serà true o false. La comparació retornarà true o false. Per tant, per què cal comparar? Podem fer directament
if (b) ...
else ...

De la mateixa manera, si volguéssim comprovar que b és fals, podríem fer
if (not b) ...
else ...

Això també aplica per donar valors a un booleà. No pot ser que facis el següent
bool b;
if (condicio) b=true;
else b=false;

És molt millor fer
bool b=condicio;

Lazy evaluation

Això no ho he traduït, perquè no s'acostuma a veure traduït. Seria com avaluació mandrosa, o una cosa així. En essència es basa en les següents tautologies:

true or x=true
false and x=false

Aleshores, per què dedicar temps a mirar si x és cert o fals, si al cap i a la fi, podem saber-ho ja. Doncs el programa no ho mirarà. Això ens pot aportar avantatges. Per exemple
double a, b;
cin >>a>>b;
if (b!=0 and a/b>1) ...
else ...

No facis coses prohibides

Sembla una obvietat, no? Doncs no ho és. I no només pot implicar un 0 de manual, sinó que et poden invalidar l'automàtic també. Poso a continuació una sèrie de coses prohibides a PRO1 (ante la duda, pregunteu):
-Modificar la variable de control d'un for dins del cos d'aquest
-Utilitzar punters
-New i delete
-goto
-try, catch i throw
No necessitareu excepcions per res. Si el vostre codi us llença una excepció, probablement esteu fent alguna cosa molt malament, i per tant, arregleu el codi. No serveix capturar l'excepció i tractar-la
-template
-class
-crides a sistema
És molt típic veure gent que posa al final del codi un system("pause"). No ho feu. Si executeu des de consola, no cal. Si igualment necessiteu que s'esperi per veure-ho, poseu cin.get(). Però millor encara si no ho poseu
-Directives del preprocessador. És a dir, res que tingui un # a davant, excepte #include
-Llegir i escriure estil C. És a dir, scanf, printf, fprintf, write, read i similars
-Operacions sobre strings que no siguin declaració, assignació, comparar, llegir i escriure
Les següents es permeten:
string s;
string s="Hola";
if (s1<s2)...
cin >>s;
cout<<s<<endl;
Fins i tot, si jo fos el profe, us permetria la següent:
cout<<string(n,'*')<<endl;//mostra n asteriscs
Al cap i a la fi, no és res més que una de les opcions que creen un string. No obstant, pregunteu al profe

No es val utilitzar coses com la concatenació. A partir de que es facin vectors sí que es permeten més coses, en essència treballar amb ells com si fossin vectors, però inicialment no
-vectors abans de ser explicats a classe. Arrays mai es poden utilitzar
-operacions sobre vectors no explicades. 
És a dir, declarar-los sí, size també, accedir amb [] també. Push_back si inicialment no sabem la mida i ens fa un codi més senzill. La resta, no
-Inclusions que no siguin iostream, cmath, string, vector, algorithm
-Variables globals (excepte constants)
-Tipus no explicats. 
És a dir, utilitza bool, int, double i ja (string també, però no és un tipus). Cap altre. Ni short, ni long, ni unsigned, ni float, ni coses que se t'acudeixin
(enums es permeten. Estan desrecomanades, però es permeten)

Funcions i procediments. 

En el cas de les funcions/procediments (un procediment no retorna res, però jo en general parlaré de funcions), hi ha unes quantes coses a tenir en compte

Declaració

Les funcions es declaren donant l'especificació directament, ja que no farem recursivitat creuada ni coses rares. Per tant, posarem el codi en el moment de declarar-la. Com que fem això, hem de tenir en compte que si una funció B utilitza una funció A, la funció A ha d'estar abans de la B

Pas de paràmetres

Les funcions en general reben paràmetres. Quan són paràmetres normals (enters, doubles, strings normalets), es passen normal (és a dir, per còpia). Si volem que la funció modifiqui el paràmetre, o bé passar-li una cosa que ocupi molt (strings llargs, vectors o structs molt grans), li passarem per referència. Si volem que no es pugui modificar, serà referència constant. Per exemple
void foo (const string& biblia_en_vers) //No modifica
void foo2 (string& biblia_en_vers) //Modifica

Retorn

Per retornar es fa amb la paraula clau "return". Hem de posar què volem retornar (excepte als procediments), i ha de ser del tipus que retorna la nostra funció, o bé poder-s'hi convertir. És a dir, si ha de retornar un enter, podem retornar un enter directament, o bé un double (es convertirà a enter, perdent els decimals que pugui tenir)

Retorn i condicions

A vegades el retorn d'una funció depèn d'una o més condicions. En aquest cas, una cosa intuitiva seria:
int ret;
if (...) ret=..
else ret=...
return ret;

Això ho fan a algun problema de PRO2, però no els feu cas. És una mala decisió. Una altra cosa que se'ns pot acudir és
if (...) return ...;
else return ...;
Aquesta ja està prou bé. No obstant, com que quan retornem sortim de la funció, sabem que si seguim a la funció entrarem sí o sí al else. Per tant, pot quedar simplement
if (...) return ...;
return ...;

Retorn i bucles

Una mica com abans, hi ha situacions on podem saber ja el valor de retorn dins d'un bucle. En aquest cas, podem simplement retornar-ho dins del for. Per exemple
bool es_primer(int n) {
    for (int i=2;i*i<=n;++i) {
        if (n%i==0) return false;
    }
    return true;
}
Es pot fer diferent, podríem fer la que hi ha a continuació, però és més lleig i pitjor en general en tots els aspectes
bool es_primer(int n) {
  bool res=true;
    for (int i=2;i*i<=n and res;++i) {
        if (n%i==0) res=false;
    }
    return res;
}

Funcions i procediments recursius

En general, una funció recursiva es defineix amb un cas base, i una manera de reduir-ho tot fins al cas base. Per tant, hem de fer-ho de manera que en el cas base no torni a cridar a la recursiva. Per exemple, si tenim la funció per calcular el factorial, hem de fer que quan n==1 o n==0 ens retorni 1, i si no, cridi a la funció recursiva. Hi ha dues maneres, i partidaris de les dues
bool factorial (int n) {
    if (n>1) return n*factorial(n-1);
    return 1;
}
bool factorial (int n) {
    if (n<=1) return 1;
    return n*factorial(n-1);
}
Aquí pot semblar que les dues són iguals. No obstant, en casos més llargs són bastant diferents. Per exemple, existeix l'algorisme d'exponenciació ràpida, que calcula a^n en temps logarítmic (traducció: Molt ràpid)
int exp_rap(int a, int n) {
    if (n>0) {
        int aux=exp_rap(a,n/2);
        if (n%2==0) return aux*aux;
        return aux*aux*a;
    }
    return 1;
}
int exp_rap(int a, int n){
    if (n==0) return 1;
    int aux=exp_rap(a,n/2);
    if (n%2==0) return aux*aux;
    return aux*aux*a;
}
Jo personalment sóc més partidari de la segona versió. En aquesta, el cas base en poses a davant de tot, i així no has de posar tota la resta dins d'un if.
En funcions gairebé sempre es fa així. Però en procediments... En procediments molts cops es fa diferent. Per exemple
//Mou n torres de la torre a fins la torre c
//utilitzant b com a auxiliar
void hanoi (int a, int b, int c, int n) {
    if (n>0) {
        hanoi(a,c,b,n-1);
        cout <<a<<"->"<<c<<endl;
        hanoi(b,a,c,n-1);
    }
}
void hanoi (int a, int b, int c, int n) {
    if (n==0) return;
    hanoi(a,c,b,n-1);
    cout <<a<<"->"<<c<<endl;
    hanoi(b,a,c,n-1);
}

Jo personalment sóc més partidari de la segona en els dos casos. Normalment, a PRO1, utilitzen la segona per funcions, i la primera per procediments. Això ja a criteri de cadascú, però intenteu ser coherents, per no liar-vos més encara

Nomenclatura

En aquest àmbit hi ha molta discussió, així que perfectament us podeu trobar que jo us dic una cosa, i algú us diu una altra diferent. No obstant, jo us diré que em feu cas a mi (òbviament, si ho faig així, és perquè crec que és millor)

Variables

Sempre que defineixis una variable, aquesta ha d'estar definida en minúscules. Per altra banda, si vols separar paraules, sempre han d'estar separades per _. Hi ha gent que utilitza la notació camelcase, que vindria a ser posar en majúscula la inicial de les paraules, si n'hi ha més d'una
int salari_minim;//La que jo recomano
int salariMinim;//lower camelcase
int SalariMinim;//upper camelcase

Sobretot a PRO1 no utilitzeu la tercera, ja que interferiria amb el que direm més tard. En general en variables no es discuteix tant, la majoria utilitza la primera

A més, és important posar noms explicatius, però no ens passem. Els noms han de ser prou curts (tampoc posem lletres per tot)

Funcions

En el cas de funcions, a C era molt típic utilitzar camelcase, mentre que a C++ s'opta més per la _ (almenys la STL). Jo particularment opto per això
Per altra banda, en C, quan volíem fer diverses funcions que facin el mateix però rebent diferents paràmetres (per exemple, el màxim de 2 números, o de 4), calia diferenciar els noms. Per això, en C existien coses com abs, labs, i similars. En C++ això ja no passa, pots tenir funcions que es diguin igual sempre i quan rebin paràmetres diferents (que el compilador pugui diferenciar clarament). Així, podem tenir
void torres_hanoi(int A, int B, int C, int n) {
    //el codi, no ve al cas
}
void torres_hanoi(int n) {
    torres_hanoi(1,2,3,n);
}
int main() {
    int n;
    while (cin >>n) torres_hanoi(n);
}

En el main llegeix la n, i crida a la funció torres_hanoi(int n). Aquesta crida a l'altra. Si estiguéssim programant una llibreria, l'usuari només necessitaria utilitzar la funció que rep la n. Això permetria canviar l'algorisme fàcilment sense que a ell li afecti. Per exemple, podríem fer que la funció rebi caràcters enlloc d'enters, i no passaria res.
double max(const double& a, const double& b) {
    //comprovacions pels NaN, inf i -0.0
    return a<b?b:a;
}
int max (int a, int b) {
    return b<a?a:b;
}
En aquest cas, ens convindria declarar diverses funcions per diferents tipus. Perquè en els nombres en coma flotant, hi ha casos estranys (el NaN, els infinits, el -0.0...), i ens pot anar bé treballar així. De fet, en aquest aspecte fallen bastant std::max i std::min, així que si creiem que ens pot afectar, és millor utilitzar fmax i fmin

Structs

Una struct en general és com si crees un nou tipus. El conveni per mi és senzill, i seria posar en majúscula només la primera lletra. Quedaria una cosa així:
struct Punt {
    double x, y;
};

No reinventis la polvora

Hi ha coses que són tan òbvies que segurament algú les ha fet ja. Per exemple, imaginem que et donen un programa que en algun punt necessita tenir el vector ordenat. No et posaràs a programar un algorisme d'ordenació, no?

La STL

Existeix la STL, que vol dir Standard Template Library. He fet un article que explica unes quantes coses de les que té, però la majoria no es poden utilitzar a PRO1, així que aquí posaré les que penso que sí. Davant del dubte, sempre es pot preguntar

pair<T1,T2>

Imaginem que necessitem retornar dos valors a l'hora. Una idea que se'ns podria acudir seria
struct Parella {
    int x, y;
};

Com que això és una cosa que pot caldre per moltes ocasions, ja se'ls va acudir crear una classe per fer això. És la classe pair, que conté dos valors (que poden ser de diferent tipus). Es declara fent
pair<T1,T2> p;
Pots donar-li valor, o bé en la declaració
pair<int,int> p(1,3);
O element per element
pair<int,int> p;
...
p.first=3;
p.second=4;
Fins i tot podem fer-ho amb la funció make_pair
p=make_pair(5,7);

Les pairs tenen l'operador < definit, de manera que es poden ordenar vectors de parelles sense fer una nova funció. Aquest compara el primer element, i si són iguals, compara el segon

sort

Òbviament, no ens posarem a programar algorismes d'ordenació a cada problema que necessitem ordenar. No ens posarem a fer un bubblesort (perquè és pura merda); o selection sort, o insertion sort, o mergesort, per cada problema on necessitem ordenar (encara que està bé saber com són aquests). Enlloc d'això, podem utilitzar el sort, definit a la llibreria algorithm, i no ens preocuparem de com ordena
El sort bàsicament és una funció que rep dos (o tres) paràmetres. La versió senzilla rep dos paràmetres, que indiquen on comença el vector i on acaba. Concretament, rep iteradors apuntant allà. No cal saber què són els iteradors, simplement que s'obté un que apunta a la primera posició fent v.begin(), i un que apunta al final fent v.end(). Per tant, per ordenar un vector faríem
sort (v.begin(), v.end());
La versió que rep tres paràmetres, el que fa és rebre una funció que indica com es compara. Això serveix per si volem ordenar decreixentment, o per si el que volem ordenar no té definit l'operador <. 

Functors per comparar

Això és una cosa que no tinc ni idea de si està permès o no. Per tant, pregunteu. La idea és que quan es va programar la STL, ja s'imaginaven que podria caldre comparar amb operadors diferents de <. Així que van definir functors (no cal saber què són) per comparar diferent. La típica és greater<T>, que compara dos elements de tipus T comprovant si el primer és més gran que el segon. Així, podríem fer
vector<int> v(n);
...
sort(v.begin(),v.end(),greater<int>);

Això ens permet ordenar el vector decreixentment sense preocupar-nos de programar cap funció

Vectors i funcions

A l'hora d'utilitzar funcions i vectors, la cosa és una mica complexa. Suposo que a PRO1 us hauran dit que mai passeu vectors per còpia ni els retorneu. Que sempre els heu de passar per còpia a les funcions, i modificar-los allà mateix. Això és una manera senzilla de no liar-se. Però què coi, som informàtics o no? Per tant, podem veure com podrien estar implementats els vectors

template<typename T>
class Vector {
    T* dades;
    size_t size;
    //totes les funcions
};

Això, òbviament, és un resum. Hi ha més coses. Però amb això ja ens serveix per veure com es fa tot. dades és un punter que apunta a les size dades que tenim. 

Què passa quan passem per còpia un vector?

Quan passem per còpia un vector, hem de copiar tot això. Això implica reservar espai per les dades, copiar-les totes allà... Bastant cost. Concretament, és cost lineal. Per tant, el que farem nosaltres és no copiar-ho. Si només volem fer consultes, referència constant. I si volem modificar-lo (compte, les modificacions són definitives) doncs referència a seques

Què passa quan retornem un vector?

Aquí hi ha dos casos. El primer, seria quan retornem un vector creat a la funció. Per exemple:
vector<int> llegir_vector() {
    int n;
    cin >>n;
    vector<int> v(n);
    for (int i=0;i<n;++i) cin >>v[i];
    return v;
}
En aquest cas, crea un vector, reserva espai per n elements i els llegeix. En el moment que va a retornar, el compilador s'adona que v es destruirà després del retorn. Així que el que fa és fer un swap amb el contingut del vector que rep això. És a dir, si fem
vector<int> vec=llegir_vector();
Tenim vec, que està buit. I tenim el v de la funció, que té el que hem llegit. Com que v es destruirà, i el contingut de vec no el volem per res, intercanvia size i dades dels dos. No ha de copiar les n dades, sinó que simplement fa que el punter de vec apunti al lloc on apuntava el punter de v, i viceversa. És a dir, el retorn té cost constant. 

L'altre cas seria quan retornem un vector que ja existeix. Per exemple
vector<int> fila_iessima(vector<vector<int>>& matriu, int i) {
    return matriu[i];
}

En aquest cas, com que la fila no es destruirà, el compilador determina que s'han de copiar tots els elements, cosa que pot ser necessària en algun cas, però en general no

dimarts, 19 de setembre del 2017

Aprendre a programar 18 - Consells per la pràctica

Arribats a aquest moment del curs, toca posar-se amb la pràctica de PRO2. Li dedicaràs gran part del teu temps, sobretot si fas com la majoria i t'hi poses a l'últim moment. Jo he pensat que podia aprofitar aquest article per donar uns quants consells, que intentaré que no siguin els que es donen sempre. No seran aquells de "no deixis les coses per l'últim moment", sinó que intenten ser útils de debò.

La STL és la teva amiga

La STL (Standard Template Library) són una sèrie de llibreries, que contenen classes, funcions i functors molt útils. A PRO1 la veiem molt superficialment, i a PRO2 la veiem una mica més, però tampoc molt. No obstant, sense tornar-nos uns experts tampoc, podem aprofitar-la una mica

Llistes, les justes

Quan hem de triar una estructura de dades, molts cops acabem triant la llista. És molt típic que, si volem tenir una estructura de dades que la seva mida vagi canviant, utilitzem la llista. Però molts cops això és una mala elecció. 
Si volem insertar només al final, o ens és indiferent la posició: En aquest cas, jo utilitzaria un vector. El push_back en un vector té temps constant amortitzat. Però en general, és més ràpid fer push_back en un vector que en una llista, cosa que es pot entendre quan estudiem la implementació interna de les llistes. A més, així conservem l'accés aleatori en temps constant. 
Si volem insertar al principi, o principi i final: En aquest cas, jo optaria per la deque. Té un funcionament similar al vector, però podem insertar en temps constant tant al principi com al final (constant de debò, no amortitzat). El push_back a més és molt més ràpid que en un vector

I, molt important, no les utilitzem si és obvi que hem d'utilitzar una altra. Hi ha vegades que és obvi que una clsa s'ha pensat per ser resolta d'una manera determinada. Per exemple, a la pràctica del Q2 2016-2017, s'havia de generar un arbre genealògic. Aquí, el millor era utilitzar un arbre, ja sigui el que ens donen els professors, com un propi (millor el que ens donen, per temps bàsicament). Hi ha gent que va utilitzar una llista per fer-ho. Ens en podem sortir, sí, però de la mateixa manera, pot ser que no ens en sortim, i la caguem. 

Si l'ordre no importa, per què mantenir-lo? 

Fins ara hem estudiat els sets i els maps. Però, i si us digués que tenir-ho ordenat no sempre és positiu? Sí, ja sé que el que ens permet insertar i eliminar en temps logarítmic és justament que estan ordenats. Però existeixen un parell d'estructures a la STL, que es diuen unordered_set i unordered_map. Tenen les mateixes funcions que els sets i els maps, però internament estan implementats amb taules de hash. Què vol dir això? Doncs, que a nosaltres ens afecti, que insertar, eliminar i buscar triguen temps constant en el cas mitjà, de manera que, si l'ordre no ens importa, tot anirà molt més ràpid
Per utilitzar aquestes dues, hem d'utilitzar C++11. És el que s'utilitza a PRO2, però si vols assegurar-te de que l'utilitzes, fixa't si quan compiles tens el flag -std=c++11 o -std=c++0x. Si hi ha un dels dos, ja està. Si hi ha algun superior a C++11, igual, tot i que m'estranyaria

No programis el que ja està programat

Sembla una tonteria, no? Quants cops hem fet una cosa així?
sort(v.begin(),v.end(),cmp);
Milers i milers. Molts cops, per ordenar decreixentment. Doncs bé, la pròpia STL té un functor que és greater<T>, de manera que podem fer
sort(v.begin(),v.end(),greater<T>());
Només cal que T tingui definit l'operador >
A la llibreria functional en trobem molts més d'aquest estil. Les típiques coses que podem necessitar, és probable que hi siguin

Cout, els justos

És típic. Volem veure on comença a fallar el programa, i posem molts cout per tot el programa, que ens van avisant d'on estem. Així podem identificar on està l'error. Ara, quan hem arreglat el problema, ens hem de posar a eliminar tots els cout, i és molt fàcil que ens en deixem uns quants. 
Però la pròpia llibreria iostream ens dóna una eina per fer això. El flux de sortida cerr. Normalment tenim una sèrie de canals d'entrada i sortida. En C podíem utilitzar les funcions read i write per operar a través d'ells. Teníem el canal 0, que era el de lectura (en C++ hi podem accedir amb cin), el canal 1, que era el d'escriptura normal (en C++ hi podem accedir amb cout), i el canal 2, que era el d'errors (en C++ hi podem accedir amb cerr).

La gràcia d'aquest flux de sortida, és que com que utilitza un canal diferent, pel jutge és "invisible". Sí que veu el seu contingut, però no l'avalua. Per tant, si se'ns cola un, no serà un vermell, afectarà (molt poc) al rendiment i ja. De la mateixa manera, no ens cal eliminar-los mentre estiguem fent proves, redirigim la sortida normal a un fitxer, i els errors a un altre, i cap problema
Per redirigir la sortida d'errors, es fa amb 2>. Com ja sabeu, l'entrada és amb <, i la sortida estàndard amb >. Aleshores, si volem que l'entrada surti del fitxer entrada.txt, la sortida normal vagi a sortida.txt, i la d'errors a errors.txt, faríem
$./programa <entrada.txt >sortida.txt 2>errors.txt

Compte!

S'ha d'anar amb compte amb una cosa. Que el Jutge no miri la sortida d'errors, no vol dir que els professors no la vegin. I, si poseu missatges com "bitch", "puta", "polla", "hey madafaka", doncs tindreu una imatge que potser us interessa poc. Així que poseu missatges aptes per ser llegits per un professor, perquè ha passat

Compte 2!

Que el Jutge no els vegi, no vol dir que no els faci. Què vol dir això? Que faran anar el programa una mica més lent. Per exemple, al joc d'EDA, hi va haver uns quants estudiants eliminats només per això, tenien tants missatges d'error que els petava el jugador. Així que, per l'enviament definitiu, assegura't de treure'ls tots. Per les teves proves és indiferent

No es retornen iteradors

Aquesta és molt important. A la meva pràctica, per exemple, vam fer una classe Biblioteca, que en un moment havia de retornar un Text. Un Text tenia un set<string> i un vector<Frase>, de manera que vam pensar que, ja que retornar-lo sencer era una burrada de cost, podíem retornar un iterador que hi apunti, i ja està. MALAMENT! En el meu cas, era un map<string,Text>::iterator, de manera que el primer element era el títol, i el segon el text. Què passa si de cop decideixo que vull canviar la implementació interna, i tenir el text en un unordered_map? O en una estructura més diferent encara? No només hauríem de modificar aquesta classe, sinó que tocaria modificar el main, que utilitzava aquesta funció. I la idea de la programació modular, és que els mòduls siguin independents entre ells. 

La solució?

La solució és clara. No cal retornar un iterador, quan podem retornar una referència. És a dir, igual que en la funció següent no copiem la matriu, sinó que indiquem on és, per què no podem fer el mateix, però retornant?
void escriure (const Matriu& mat);

Sí, quan sortim d'una funció, tot el que haguem declarat dins d'aquesta desapareix. Per això, normalment, no s'explica res d'això a PRO1. Podríem cometre l'error de fer
vector<int>& llegir(int n) {
    vector<int> v(n);
    for(int i=0;i<n;++i) cin >>v[i];
    return v;
}
I, un cop haguem sortit, v desapareix, i falla. No obstant, la gràcia aquí és que volem retornar coses que segueixen existint, ja que fins que no destruïm la classe, tot seguirà allà.
Retornar per referència és una cosa així:
vector<int>& fila_mes_llarga(Matriu& mat){
    int pos_max=0;
    for (int i=1;i<int(mat.size());++i) {
         if (v[i].size()>v[pos_max].size()) pos_max=i;
    }
    return mat[pos_max];
}
Aquí no copiem la fila aquesta, sinó que indiquem on està. 

Compte a l'hora d'agafar-ho!

Quan ho agafem, hem de vigilar. Estem retornant una referència, però si volem desar-ho en una variable normal, acabarà copiant-se igualment. 
vector<int> v=fila_mes_llarga(mat); //Aquí ho copiem
Per tant, ha de ser desat en una referència. Una cosa així:
vector<int>& v=fila_mes_llarga(mat);
També hem d'anar amb compte. Si ara modifiquem v, es modificarà la fila corresponent a mat. Si no volem modificar-ho, el millor que podem fer és que sigui constant

Sobrecàrrega d'operadors

Quan tu crees una classe, la classe no té els operadors que tenen els tipus bàsics. No tenim els operadors de comparació, no podem sumar, restar... Però això té una solució fàcil. I és programar-los. 

Operadors de comparació

Són els primers que vaig necessitar sobrecarregar. Normalment el que es fa és programar l'operador <, i potser també el ==, i la resta es fan en funció d'aquests. Al cap i a la fi, tenim les següents propietats
a>b <-> b<a
a<=b <-> not a>b
a>=b <-> not a<b
a==b <-> not (a<b or b<a)
a!=b <-> not a==b

Com que la comparació == implica fer dos comparacions de <, molts cops preferim fer-la també, ja que així va més ràpid.

class Frase {
private:
    vector<string> paraules;
    ...
public:
    ...
    bool operator<(const Frase& f);
    bool operator<=(const Frase& f);
    bool operator>(const Frase& f);
    bool operator>=(const Frase& f);
    
    bool operator==(const Frase& f);
    bool operator!=(const Frase& f);
};

bool Frase::operator<(const Frase& f) {
    int n=min(paraules.size(),f.paraules.size());
    for (int i=0;i<n;++i) {
        if (paraules[i]!=f.paraules[i]) return paraules[i]<f.paraules[i];
    }
    return paraules.size()<f.paraules.size();
}

bool Frase::operator>(const Frase& f) {
    return f<*this;
}

bool Frase::operator<=(const Frase& f) {
    return not *this>f;
}

bool Frase::operator>=(const Frase& f) {
    return not *this<f;
}

bool Frase::operator==(const Frase& f) {
    if (paraules.size()!=f.paraules.size()) return false;
    int n=paraules.size();
    for (int i=0;i<n;++i) {
        if (paraules[i]!=f.paraules[i]) return false;
    }
    return true;
}

bool Frase::operator!=(const Frase& f) {
    return not *this==f;
}

És a dir, la funció es diu operator< (o el que sigui), i rep un altre objecte del mateix tipus. Aleshores, els comparem, i retornem cert si el paràmetre implicit és menor (o el que sigui) que l'altre

Per comparar amb una altra classe diferent, ho faríem com una funció friend. No ho poso aquí, però si algú en algun moment ho necessita, pot posar-me un comentari, i li explico

Operador d'assignació

Aquest és l'operador =. Tindria una forma així:
Frase& operator=(const Frase& f);//Capçalera, al fitxer .hh

Frase& Frase::operator=(const Frase& f) {
    paraules=f.paraules;
    return *this;
}

La idea és, bàsicament, copiar el que hi hagi, i retornar una referència a l'element actual. Això és per poder fer a=b=c=d. Quan fem c=d, retornem el valor per referència, de manera que li podem posar a b, i igual amb b

Lectura 

Una cosa que hem treballat molt poc és la lectura i escriptura de variables. El que se'ns ha explicat sempre és que si fem cin>>v, llegirem la variable v. Però, per què això funciona?

Flux d'entrada

En realitat, cin no és cap funció que llegeixi. cin és un flux de dades d'entrada, que les llegeix del canal estàndard (el número 0). Nosaltres el que fem és llegir d'allà utilitzant l'operador >>, que en els fluxos està sobrecarregat per fer que la seva funció sigui llegir. Està fet de manera que els espais, tabulats i salts de línia siguin els que indiquen que s'ha acabat el valor actual. Per això, si fem
string s;
cin>>s;
I l'entrada és "Hola que tal", el que hi haurà a s és "Hola". En canvi, si inicialment haguéssim fet cin.get(), i després cin>>s, el valor retornat per cin.get() seria 'H', i a s hi hauria "ola"

Getline, la clau

Imaginem que ens diuen que a cada línia hi haurà una ordre, i una sèrie de números, que hauràs d'operar segons això. Per exemple, posem que les ordres són "SUMA", "MITJANA" i "PRODUCTE". Una cosa que podem fer és anar llegint com a string, i si veiem que no és cap instrucció, suposar que és un número, convertir-lo i operar. Però per què complicar-nos?
Podem fer una cosa així:
string s;
getline(cin,s);
I a s ens quedarà tota la línia. És a dir, enlloc de llegir fins al primer separador, llegirà fins al primer salt de línia

Crear els nostres fluxos

Un cop tenim llegida tota la línia, queda decidir com treballem amb ella. I, com que estem acostumats a treballar amb fluxos, per què no seguir així?
Tenim una llibreria, que és sstream (amb doble s), que serveix per crear fluxos amb un string. Ens interessa, concretament, tenir un flux d'entrada (istringstream). Així que el que faríem és
string s;
while (getline(cin,s)) {
    istringstream flux(s);
    flux>>s;
    if (s=="SUMA") {
        int res=0;
        int act;
        while (flux>>act) res+=act;
        cout<<res<<endl;
    }
    else if (s=="MITJANA"){
        int i=0, act;
        double res=0;
        while(flux>>act) {
            res+=act;
            ++i;
        }
        res/=i;
        cout<<res<<endl;
    }
    else if (s=="PRODUCTE") {
        int res=1;
        int act;
        while (flux>>act) res*=act;
        cout<<res<<endl;
    }
    else cout<<"ERROR"<<endl;
}

Funcions útils

Hi ha una sèrie de coses que són tan, però tan típiques, que programar-les de nou és absurd. Estan ja programades, ja sigui en llibreries, o en webs i llocs així. He pensat que podia fer-ne un petit resum, i anar-lo ampliant

Convertir un string a enter

stoi

Aquesta és bastant simple. Està a la llibreria string, rep un string, i retorna l'enter al que representa. No obstant, per experiència pròpia, a vegades falla i no sé per què. En aquest cas, hi ha una altra opció

atoi

Aquesta és molt típica. En C, i a C++ ho heretem, podem passar arguments al main. És a dir, quan fem
$./programa hola adeu
Podem fer que el programa rebi "hola" i "adeu". Els rebíem com un vector de strings (strings de C), de manera que, si hi havia un número, l'havíem de convertir a enter (o al tipus que sigui). Ho fèiem amb la funció atoi (Array TO Integer). Aquesta rep un array de chars, i retorna l'enter al que representa. Per fer-ho, haurem d'utilitzar la funció c_str, de la classe string, que retorna l'array que representa al nostre string. És a dir, una cosa així
int n=atoi(s.c_str());
La funcio atoi està a la llibreria cstdlib

Implementacions pròpies

Si no ens funciona stoi, podem implementar-lo nosaltres. Per fer-ho, se m'acudeixen dues maneres

int stoi(const string& s) {
    return atoi(s.c_str());
}

int stoi(const string& s) {
    istringstream f(s);
    int res;
    f>>res;
    return res;
}

La primera, bàsicament, utilitza atoi. Molt simple, i no ens compliquem la vida. La segona, per altra banda, utilitza un flux d'entrada, creat amb la llibreria sstream. 

Altres usos

Aquí he mostrat com convertir un string a enter. Podem convertir a altres, per exemple, tenim stol (per convertir a long), stoll (per convertir a long long), stof i stod (per convertir a float i a double, respectivament). Amb atoi funciona igual, tenim atol, atoll...

Convertir un número a string

Per convertir a string, és prou fàcil. Tenim la funció to_string, de la llibreria string, que converteix un número a string (el número pot ser de qualsevol tipus). 
Si no ens funciona la funció, sempre podem fer això:
string to_string(int n) {
    ostringstream f;
    f<<n;
    return f.str();
}

Nombres aleatoris

Imaginem que volem un número aleatori. Això, per la pràctica de PRO2, no hauria de fer falta, però mai se sap. De qualsevol manera, mai està de més saber-ho fer

Estil C

En C, la manera de generar nombres aleatoris utilitzant funcions estàndard era utilitzant la funció rand(). Aquesta retornava un enter aleatori entre 0 i RAND_MAX (està definit a la mateixa llibreria). El que fèiem és acotar aquest interval fent el mòdul, i sumant. Una cosa així
int random(int min, int max) {
    return rand()%(max-min+1)+min;
}
És senzill. Volem tenir max-min números. Fem el mòdul, i tenim un número a l'interval [0,max-min]. Per tant, li sumem el mínim, i tindrem un número a l'interval [min,max]. 
Per fer que sigui gairebé aleatori, hem de fer que a cada execució del programa sigui diferent. Això es faria posant la crida següent a l'inici del main
srand(time(NULL));
La funció srand serveix per inicialitzar la seed dels nombres aleatoris. La funció time retorna un valor que serà diferent cada vegada que la cridem, tampoc cal saber gaire més. 

Aleshores, per fer això, hauríem d'incloure la llibreria cstdlib (per les funcions rand i srand), i la llibreria ctime (per la funció time)

Estil C++

Com hem vist, a C la cosa estava molt limitada. Podíem generar nombres aleatoris, sí, però només enters, i ens havíem de complicar massa la vida. Per això a C++ apareix la llibreria random, que ens permet generar nombres pseudoaleatoris molt millor
Primer de tot, hem de fer un generador. Tenim una classe per això, que és default_random_engine. Un cop tenim aquest generador, podem utilitzar les diverses funcions que té per crear nombres aleatoris. Les més interessants per nosaltres són uniform_int_distribution i uniform_real_distribution, que generen nombres aleatoris utilitzant una distribució uniforme (la qual té la característica que tots els seus elements tenen la mateixa probabilitat). En tenim altres, com bernoulli, binomial, exponencial, poisson, normal... Les podem veure aquí. Un exemple d'us seria

int main() {
    default_random_engine generador;
    uniform_int_distribution<int> aleatori(0,9);
    for (int i=0;i<10;++i) {
        cout<<aleatori(generador)<<endl;
    }
}

És a dir, declarem el generador i el functor que ens farà nombres aleatoris. Cada cop que volem cridar nombres aleatoris, cridem al functor, passant-li com a paràmetre el generador.

Temps

A vegades dubtem entre dues implementacions, i volem comprovar si afecten significativament a l'eficiència. Estant a PRO2, no sabem gaire de calcular eficiència, així que ens pot ser útil mirar-ho empíricament. I, com que no ens posarem a cronometrar-ho amb el rellotge, és útil aprendre a fer-ho amb l'ordinador

Des de la consola

Aquesta opció només l'he provat a consoles de Linux. Suposo que en altres hi ha manera de fer-ho, però no la conec. Bàsicament utilitzem l'instrucció time. Així:
$time ./programa.exe <entrada.txt >sortida.txt
És important que l'entrada vingui d'un fitxer, ja que si la posem manualment, aquest temps també el compta. I ens pot tergiversar els càlculs. La sortida prefereixo redirigir-la perquè així només ens surti a la consola el temps. 

Editant el codi

Aquesta opció també és útil, tot i que prefereixo la primera, ja que no ens fa modificar el codi. Utilitzarem la llibreria ctime, que té una funció (clock) que ens retorna els ticks de rellotge consumits fins al moment. Aquests depenen de la unitat, però tenim una constant, que és CLOCKS_PER_SEC, que serveix per convertir els ticks de rellotge a segons. 
Sabent això, la idea és agafar els ticks a l'inici i al final, calcular la diferència, i convertir a segons. És a dir:
int main() {
    int c=clock();
    ...
    c=clock()-c;
    cout<<double(c)/CLOCKS_PER_SEC<<endl;
}

Per altra banda, si no volem saber el temps en segons, sinó que volem comparar entre dues opcions, el més senzill és mostrar c directament, sense passar-ho a segons

Limits numèrics

Mai us ha passat que voleu tenir el nombre més gran possible? O el més petit? O qualsevol dada així? És una putada, perquè clar, la mida dels números pot variar en funció del sistema

Mètode cutre

Per aquest mètode necessitem conèixer la representació interna dels números. Sabem que els enters amb signe es representen utilitzant Complement a 2, de manera que el bit de més pes és negatiu, i la resta positius. Així, si tenim nombres de 32 bits, el nombre més petit possible serà 0x80000000, i el més gran 0x7FFFFFFFF. 
Per altra banda, com a dada important, el -1 és 0xFFFFFFFF. Sabent això, ja podem aconseguir els mínims i màxims
int max=(-1)>>1;
int min=1<<(sizeof(int)*8-1);
És a dir, la primera és agafar el -1 (0xFFFFFFFF), i desplaçar-lo una posició a la dreta (quedant 0x7FFFFFFF). La segona, per la seva banda, és desplaçar 1 cap a la dreta tantes posicions com bits representin el número menys 1. Així tindrem un 1 al bit de més pes

Per fer els respectius mínims i màxims d'enters sense signe, és encara més fàcil. El mínim sense signe és 0 (òbviament). El màxim serà -1 (per una raó molt simple, -1 és 0xFF..., que si no hi ha signe, és el màxim). 

Mètode guai

Aquest mètode és el que us recomanaria. No ens compliquem la vida en càlculs estranys. Què passaria si intentem calcular el mínim d'un enter amb signe en un sistema on els bytes no són de 8 bits? Perquè un byte no necessàriament és de 8 bits, malgrat que és el més comú
Per fer-ho bé, utilitzarem la llibreria limits. Aquesta conté una classe, que és numeric_limits, amb funcions que ens permeten obtenir informació sobre cada tipus numèric (no només de tipus enter, també de coma flotant). Les funcions que em semblen més útils són:
min: En els enters, retorna el número més petit representable. En coma flotant, el més petit positiu i diferent de 0 (un possible valor de retorn seria 1.17549e-038).
max: Retorna el màxim valor finit
lowest: En enters, igual que min. En coma flotant, normalment retorna -max()
En general, amb aquestes 3 ja faríem. En tenim moltes més, les podeu consultar aquí
int minima_diferencia(const vector<int>& v) {
    int min_dif=numeric_limits<int>::max();
    int n=v.size();
    for (int i=0;i<n-1;++i) {
        int a=v[i];
        for (int j=i+1;j<n;++j) {
            int aux=abs(a-v[j]);
            if (min_dif>aux) min_dif=aux;
        }
    }
    return min_dif;
}
Un exemple d'us seria aquest

C++11

C++11 és una actualització de C++ que apareix al 2011 (sí, sóc un geni). Inclou moltes coses molt i molt útils. Està més explicada aquí, però faré un resum de coses que poden ser útils

Inferència de tipus

No us heu plantejat que hi ha situacions on el tipus d'una variable té el nom molt llarg, i realment no cal que ho sigui. Si tenim un map<int,pair<string,int>>, i volem fer-ne un iterador, quedaria una cosa així
map<int,pair<string,int>>::iterator
Molt llarg. I, si volem fer un for, queda encara més llarg
for(map<int,pair<string,int>>::iterator it=m.begin();it!=m.end();++it)...
I, realment, la funció begin() sempre retorna un iterador. Així que, per què no deixar al compilador que dedueixi ell sol?
Doncs C++11 permet fer això. Utilitzant la paraula clau auto (que abans tenia un altre significat), li diem que determini ell mateix el tipus
for (auto it=m.begin();it!=m.end();++it)...
En general, la idea d'això és fer-ho en casos on sigui molt obvi, i al moment. No serveix fer
auto x;
...
x=m.begin();
Hem de tenir sempre un inicialitzador, que utilitzem per saber quin és el tipus

For en un rang

Sóc molt fan d'aquesta. Permet recórrer tota una estructura de dades, de manera més senzilla. Bàsicament fem:
for (T x:estructura) {
...
}

Sempre i quant l'estructura emmagatzemi elements de tipus T. Per exemple, per recórrer una llista d'enters faríem
list<int> l;
...
for (int x;l) {
...
}
Això equivaldria a fer
for (list<int>::iterator it=l.begin();it!=l.end();++it) {
    int x=*it;
...
}
Però què passaria si volem modificar l'estructura? Per exemple per llegir-la. En aquest cas, faríem
for (int& x:l) ...
Això serveix perquè x faci referència als elements, permetent-nos modificar l'estructura. 

Com deveu intuir, això només serveix amb estructures que tinguin iteradors. Per altra banda, utilitzant la paraula clau auto, podem fer coses molt guais

Llistes d'inicialització

Això és molt útil encara. Imaginem que volem fer un programa que faci coses relacionades amb els escacs. I penses "ei, anem a fer un vector amb els moviments possibles del cavall". Aquest no canviarà, de manera que el podem fer constant. Però com l'inicialitzem?
Abans de C++11, amb un vector no es podia fer, i havíem d'utilitzar un array. Amb C++11, podem fer això amb les estructures de dades de la STD:
const vector<int> v={1,2,3};
I v tindrà els elements 1, 2 i 3. 

Recursivitat: La clau

Una llista d'inicialització es defineix recursivament. Podem dir que està composada per una de les dues opcions següents:
-Una sèrie d'elements, separats per comes
-Una sèrie de llistes d'inicialització, separades per comes
És bastant senzill, aleshores. Podem fer
vector<pair<int,int>> SALTS={{1,2},{1,-2},{-1,2},{-1,-2},{2,1},{2,-1},{-2,1},{-2,-1}};
És a dir, s'analitzen primer les llistes més internes. Com que sabem que ha de ser un vector<pair<int,int>>, sabem que les llistes internes seran parelles. 

/*
 * Pre: v no buit
 * Retorna el mínim i el màxim del vector
 * Post: el primer element de la parella retornada és el mínim,
 *  El segon és el màxim
 */
pair<int,int> min_max(const vector<int>& v) {
    int minim, maxim;
    minim=maxim=0;
    for (int x:v) {
        if (x<minim) minim=x;
        if (x>maxim) maxim=x;
    }
    return {minim,maxim};
}

Com podem veure, podem utilitzar les llistes d'inicialització, no només per inicialitzar variables, sinó també per crear el valor de retorn d'una funció

Aprendre a programar 17 - Implementació de classes i Makefile

Anem ja per la sessió 8 de laboratori, i cada cop queda menys. Aquesta sessió la dedicarem a implementar classes. Al lab programareu rentadores i cubetes, però us donen ja les capçaleres implementades, i a mi això no m'agrada. Per tant, jo explicaré des de 0, com fer capçaleres, i després com fer el seu codi.

Capçaleres

Aquest és el fitxer .hh, amb les declaracions de la classe. Només conté capçaleres, perquè el que fa la directiva include és substituir-se pel que hi ha al fitxer indicat. Això es fa al moment de compilar, de manera que si tinguéssim el codi al .hh, ens carregaríem la modularitat

Una mica de preprocessador

Primer de tot, cal explicar com funciona la directiva include. En C i C++, abans de compilar es fa una passada del preprocessador. Aquest té unes quantes directives. Nosaltres fins ara només hem utilitzat aquesta, include. El que fa és substituir-se pel contingut del fitxer que indiquem. Així, quan incloem vector, el que fem és buscar aquest fitxer, agafar les seves declaracions, i posar-les allà. 
Un problema que ens pot passar és que incloguem dos cops el mateix fitxer. Per exemple, si tenim un programa que utilitza vector i Cjt_estudiants, Cjt_estudiants inclou també vector, estaríem incloent dos cops les mateixes declaracions. I això dóna error de compilació. Per evitar aquest problema, fem el següent
#ifndef CLASSE_HH
#define CLASSE_HH
totes les capçaleres
#endif

Podríem parlar molt del preprocessador, però aquí només ens cal parlar de dues directives. La primera és ifndef. Aquesta és, bàsicament, un condicional. Si no està definida la macro CLASSE_HH, s'avalua el que hi ha dins, fins arribar al seu corresponent endif. Dins d'això, com que el que ens interessa és que tota la part d'aquest codi es compili només un cop, hem de definir CLASSE_HH. Així, a la propera inclusió, estarà ja definit, i no entrarem aquí.
A partir d'aquí, toca escriure tot el contingut d'aquesta classe

Inclusions

Ara toca incloure tot el que necessitem per aquesta classe. Per exemple, si volem utilitzar set, list i iostream, faríem

#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

#endif

Comencem amb la classe

Ara toca posar la declaració de la classe. Aquesta es declara igual que una struct, de manera que ens quedaria

#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {

};

#endif


Atributs privats

Una classe tindrà sempre uns atributs, que jo els prefereixo com a privats. És on tindrem tot desat. Una cosa així:
#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {
private: //Optatiu, no cal posar-ho
    int a, b;
    set<int> c;
    list<double> d;
};

#endif

Allà on posa private, no és obligatori. Per defecte, en una classe, tot és privat fins que s'indica el contrari. No obstant, està bé posar-ho per claredat. 
Si volem posar funcions que ens ajudin a fer alguna cosa, les podem posar aquí. Aquestes no seran accessibles directament des de fora de la classe, sinó que només s'hi pot accedir des d'altres mètodes de la classe

Funcions públiques

Aquestes són les funcions que interactuen amb l'objecte. Les podem dividir en les categories següents:

Constructores:

Són les que creen una nova instància de la classe. Per això no retornen res, però tampoc són void. Simplement tenen el nom de la classe i, opcionalment, paràmetres

#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {
private: //Optatiu, no cal posar-ho
    int a, b;
    set<int> c;
    list<double> d;
public:
    //Constructores
    Hola();
    Hola(int a, int b);
    Hola(const Hola& h);
};

#endif

En el meu exemple, he fet que hi ha una que crea una instància de la classe buida, una altra que té els dos enters, i una altra que fa una còpia del paràmetre

Destructores:

Normalment només n'hi ha una, que destrueix l'objecte quan sortim del seu àmbit. En les classes que fem de moment, no ens cal fer res, ja que tot el que utilitzem ja es destrueix sol (tant els tipus bàsics, com les classes de la STL, ja tenen programades les funcions destructores). No obstant, quan fem classes amb punters, allà sí que haurem de programar la destructora. Aquí la posem buida, quedant així:
#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {
private: //Optatiu, no cal posar-ho
    int a, b;
    set<int> c;
    list<double> d;
public:
    //Constructores
    Hola();
    Hola(int a, int b);
    Hola(const Hola& h);
    //Destructora
    ~Hola();
};

#endif

Modificadores

Aquestes serveixen per modificar l'objecte. Normalment són funcions void, ja que només les volem per modificar coses, però per res més. Una cosa així:

#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {
private: //Optatiu, no cal posar-ho
    int a, b;
    set<int> c;
    list<double> d;
public:
    //Constructores
    Hola();
    Hola(int a, int b);
    Hola(const Hola& h);
    //Destructora
    ~Hola();
    //Modificadores
    /*
     * Pre: Cert
     * Post: La classe conté un nou element, que és n
     */
    void afegir(int n);
    /*
     * Pre: Existeix un element igual a n
     * Post: L'element ha sigut eliminat
     */
    void eliminar(int n);
};

#endif

En algun cas sí que retornen alguna cosa. Per exemple, poden retornar un booleà, que confirmi que s'ha eliminat allò

Normalment posem en un comentari què fa la funció, amb una precondició (que s'ha de complir per executar la funció), una postcondició (que es complirà quan haguem executat la funció), i, opcionalment, un resum del que fa

Consultores

Aquestes consulten coses a l'objecte. Per tant, retornen alguna cosa, sigui un booleà, un enter, o qualsevol cosa que ens pugui interessar. Jo no afegeixo les pre i post, perquè m'estic inventant la classe i no fa res útil, però realment s'haurien de posar

#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {
private: //Optatiu, no cal posar-ho
    int a, b;
    set<int> c;
    list<double> d;
public:
    //Constructores
    Hola();
    Hola(int a, int b);
    Hola(const Hola& h);
    //Destructora
    ~Hola();
    //Modificadores
    /*
     * Pre: Cert
     * Post: La classe conté un nou element, que és n
     */
    void afegir(int n);
    /*
     * Pre: Existeix un element igual a n
     * Post: L'element ha sigut eliminat
     */
    void eliminar(int n);
    //Consultores
    bool hi_es(int n);
    int maxim();
    int minim();
};

#endif

El codi

Ara ens toca implementar totes les funcions que hem fet. La cosa no és gaire difícil. Primer de tot, hem d'incloure les capçaleres que ja hem creat. Podem incloure més coses aquí, però jo personalment, penso que totes les inclusions estan millor al .hh. Igual que l'ordre "using namespace std", penso que també està millor al .hh. Per tant, inicialment tindríem en el codi, una cosa així:

#include "Hola.hh"

Ara toca començar a implementar tot. Començarem per ordre, tot i que això ja és indiferent, aquí l'ordre pot ser el que ens vingui de gust. Per indicar on està la funció, ho fem amb ::. És a dir, posem la classe, els ::, i el nom de la funció. Una cosa així:

#include "Hola.hh"

Hola::Hola() {
    a=b=0,
    c=set<int>();
    d=list<double>();
}

Hola::Hola(int a, int b) {
    this->a=a;
    this->b=b;
    //Aquí inicialitzaríem c i d
}

Hola::Hola(const Hola& h) {
    a=h.a;
    b=h.b;
    c=h.c;
    d=h.d;
}

Aquí estem veient les 3 inicialitzadores. 
La primera, com que està buida, li poso 0 als números, i tant el conjunt com la llista els poso buits. 
La segona, rebem un valor de a i de b, així que els posem. Per distingir entre la a paràmetre, i la a atribut, el que fem és utilitzar el punter this, que és un punter que apunta a la nostra classe. Si fem this->a, la a serà l'atribut, mentre que si posem a a seques, serà el paràmetre (ja que emmascara l'atribut). 
La tercera, el que fa és copiar un objecte. Per tant, copiem tots els membres i ja. Veurem gent que utilitza this per deixar clar quin és l'atribut destí i quin l'atribut de l'objecte copiat. 

Quedaria afegir les altres funcions. Seria fent així amb totes:

void Hola::afegir(int n) {
    //Codi de la implementació
}

Makefile

Ara ens queda el makefile. Aquest és un arxiu que serveix per compilar un projecte, de manera que no haguem d'escriure totes les ordres cada vegada que vulguem compilar. A més, ell mateix s'ocupa de tornar a compilar només els fitxers que calguin, de manera que si tenim 3 classes, i només hem modificat una, la única que tornem a compilar és aquella. 

Per explicar-ho, imaginarem que tenim un projecte format per
-C1
-C2, que inclou C1
-C3, que també inclou C1
-main.cc, que inclou C2 i C3

El fitxer és el següent:
OPCIONS = -D_JUDGE_ -D_GLIBCXX_DEBUG -O2 -Wall -Wextra -Wno-uninitialized -Wno-sign-compare -std=c++0x

all: main.exe
main.exe: main.o C1.o C2.o C3.o
    g++ $OPCIONS -o main.o C1.o C2.o C3.o -o main.exe
main.o: main.cc C1.hh C2.hh C3.hh
    g++ $OPCIONS -c main.cc
C2.o: C2.cc C2.hh C1.o C1.hh
    g++ $OPCIONS -c C2.cc
C3.o: C3.cc C3.hh C1.o C1.hh
    g++ $OPCIONS -c C3.cc
C1.o: C1.cc C1.hh
    g++ $OPCIONS -c C1.cc
clean:
    rm *.o
    rm main.exe

La primera línia fa que, quan posem $OPCIONS, sigui igual a tot el següent. Són els flags que s'utilitzaven a PRO2 quan la vaig fer i, en general, crec que encara són els mateixos més o menys. 
Aleshores, tenim les opcions. All, que es crida per defecte si no diem res, i tota la resta (main.exe, main.o, C2.o, C3.o, C1.o i clean). Cada una té, després dels dos punts, les de les que depèn i, a sota i amb un tabulat (compte, ha d'estar TABULAT, no serveixen espais), la o les instruccions que serveixen per crear-lo. Per exemple, per crear C1.o depenem de que existeixin C1.cc i C1.hh, i ho fem amb g++ $OPCIONS -c C1.cc. Això serveix perquè, quan cridem al make (per defecte anirà a all), mirarem si tenim main.exe actualitzat. Si no el tenim, mirarem què cal per tenir-lo. Necessitem tenir main.o, C1.o, C2.o i C3.o. Els farem en aquest ordre. Podem veure que tant main.exe, com C2.o, com C3.o, necessiten a C1.o. La gràcia del makefile és que només compila si hem modificat algun dels fitxers font, de manera que compilarà només un cop C1.o, i la resta ja sabrà que el tenim creat i actualitzat. 

Tenint aquest fitxer, podem compilar fàcilment qualsevol dels fitxers, compilar-ho tot a l'hora, netejar quan haguem acabat. 
$make
Aquesta compila tot el projecte
$make C1.o
Aquesta ens crea C1.o
$make main.o
Aquesta ens crea main.o, i tots els fitxers necessaris per crear-lo
$make clean
Aquesta elimina els fitxers .o, i main.exe

divendres, 8 de setembre del 2017

Aprendre a programar 16 - Sets i maps

Un cop arribats a aquest punt, ja tenim una estructura de dades que permet l'accés aleatori en temps constant a costa de tenir insercions i eliminacions en cost lineal, i una altra que permet insercions i eliminacions en cost constant, a costa de tenir l'accés aleatori en temps lineal. Ara imaginem, però, que volem llegir uns quants nombres, i tenir emmagatzemat quins hem llegit ja. Com ho podríem fer?

Vector de booleans?

És una opció. Un vector de booleans, inicialment a false, que quan apareix un element posi la seva posició a true. El problema és que tindrem un vector enorme, que probablement no aprofitarem. Imaginem, si no, que volem emmagatzemar tots els enters positius existents. 

Vector ordenat amb els números?

Una altra opció. Inicialment és buit. Quan apareix un número, l'insertem a la posició on hauria d'anar (cost lineal). Per localitzar el número, fem una cerca dicotòmica, temps logarítmic. 

Llista ordenada amb els números?

Una altra opció. Insertar té cost constant un cop tenim la posició, però trobar-la té cost lineal (ja que no hi podem fer una cerca dicotòmica). Per tant, té cost lineal tant la cerca com la inserció

Llista no ordenada amb els números?

Insertar té cost constant (insertem on vulguem i au). La cerca té cost lineal, ja que hem de recórrer tota la llista. El problema és que si volem evitar repeticions, hem de fer que abans d'insertar busqui si ja hi és o no, per tant, tindríem cost lineal
Nota: insertar al final d'un vector en general és més ràpid que fer-ho en una llista, tot i que ara no ve al cas la raó


Sembla que de moment no hem estudiat cap estructura de dades que ens permeti representar un conjunt matemàtic de manera eficient. Cap? No! En tenim una de molt bona (que te la pots saltar si només t'interessa aprovar l'assignatura, anant directament al set)

Arbres binaris de cerca


Un arbre binari de cerca és un arbre definit de manera que els elements del fill esquerre són menors o iguals que l'arrel, i els elements del fill dret són més grans o iguals. Per tant, per trobar un element, seria una cosa així:

bool cerca (Arbre<int>& a, int x) {
    if (a.es_buit()) return false;
    if (a.arrel()==x) return true;
    int ar=a.arrel();
    Arbre<int> fe,fd;
    a.fills(fe,fd);
    if (ar<x) {
        if (cerca(fd,x)) {
            a.plantar(ar,fe,fd);
            return true;
        }
    }
    else {
        if (cerca(fe,x)) {
            a.plantar(ar,fe,fd);
            return true;
        }
    }
    a.plantar(ar,fe,fd);
    return false;
}

Mirem el node actual. Si hi és, retornem cert. Si no, mirem si hauria d'estar a la dreta o a l'esquerra, i mirem allà. Això ens permet mirar només un node per nivell. La gràcia d'això és que, al anar duplicant la quantitat de nodes a cada nivell, la quantitat de nivells és logarítmicament proporcional a la quantitat d'elements (un arbre complet de 255 elements té 8 nivells, un de 511 en té 9...)
Al final, ens trobem amb que cercar un element té cost logarítmic, ja que, com a molt, haurem de visitar un cop cada nivell. Per tant, la cerca té cost logarítmic

Si volem insertar, primer hem de decidir si acceptem repeticions. En el que volem ara, que és emmagatzemar si un número ja ha aparegut o encara no, jo no n'acceptaré. 

//Pre: cert
//Post: x està a l'arbre. Si ja hi era, retorna false
// Si l'hem insertat, retorna true
bool insertar (Arbre<int>& a, int x) {
    if (a.es_buit()) {
        Arbre<int> fe,fd;
        a.plantar(x,fe,fd);
        return true;
    }
    if (a.arrel()==x) return false;
    Arbre<int> fe,fd;
    int ar=a.arrel();
    a.fills(fe,fd);
    bool res;
    if (x<ar) res=insertar(fe,x);
    else res=insertar(fd,x);
    a.plantar(ar,fe,fd);
    return res;
}

És a dir, fem la nostra cerca, i si el trobem, vol dir que ja hem acabat. Si ens acabem l'arbre i no l'hem trobat, l'insertem. Insertar té temps constant, ja que només crea el nou node i l'enganxa. Per tant, fer la inserció té temps logarítmic, ja que hem de buscar on insertem, i fer-ho. 
Amb això implementarem la següent estructura de dades. 

Set

Un set és, en essència, un conjunt matemàtic. Emmagatzema les dades ordenades, sense repeticions, permetent-nos afegir, eliminar i consultar eficientment. Concretament, té les següents funcions:

begin i end:

Retornen els respectius iteradors

empty i size:

El que podríem esperar

insert:

Inserta l'element. La seva capçalera és:
pair<iterator,bool> insert (const value_type& val);
La posició on s'insertarà la busca automàticament. El retorn són una parella, on el primer element apunta a la posició on tenim el valor, i el segon ens indica si l'hem insertat o no.
La funció té temps logarítmic
També tenim una versió que rep un iterador que fa de pista. Aquest apunta a un element proper, i ens serveix per insertar l'element més ràpid. 

erase:

Elimina l'element. Hi ha tres versions, que són:
iterator  erase (const_iterator position);
size_type erase (const value_type& val);
iterator  erase (const_iterator first, const_iterator last);
La primera versió rep un iterador que apunta a l'element que volem eliminar. Té, per tant, temps constant amortitzat (ja tenim la posició a eliminar, només hem d'eliminar-lo). Retorna un iterador que apunta al següent element
La segona versió rep el valor que volem eliminar. Té, per tant, temps logarítmic, ja que l'hem de buscar. Retorna la quantitat d'elements eliminats (és a dir, o 0 o 1). 
La tercera versió rep dos iteradors, i elimina els elements que hi ha al rang. Té cost lineal respecte a la quantitat d'elements

find:

Retorna un iterador que apunta a l'element (si hi és), o a l'end (si no hi és). Cost logarítmic

Un exercici

Podem fer un exercici, ja que, de fet, en el Jutge és el que hi ha. El problema X83904. 
void llegir_set(set<string>& S) {
    string s;
    while (cin >>s and s!=".") S.insert(s);
}

void actualitzar_totes(set<string>& totes, const set<string>& S) {
    set<string>::iterator it=totes.begin();
    while (it!=totes.end()) {
        if (S.find(*it)!=S.end()) ++it;
        else it=totes.erase(it);
    }
}

void actualitzar_cap(set<string> &cap, const set<string> &S) {
    set<string>::iterator it=cap.begin();
    while (it!=cap.end()) {
        if (S.find(*it)!=S.end()) {
            it=cap.erase(it);
        }
        else ++it;
    }
}

void mostrar_set(const set<string>& S) {
    for (set<string>::const_iterator it=S.begin();it!=S.end();++it) cout <<' '<<*it;
    cout <<endl;
}

int main(){
    set<string> cap_activitat;
    llegir_set(cap_activitat);
    int n;
    cin >>n;
    set<string> totes_activitats(cap_activitat);
    for (int i=0;i<n;++i) {
        set<string> actual;
        llegir_set(actual);
        actualitzar_totes(totes_activitats, actual);
        actualitzar_cap(cap_activitat,actual);
    }
    cout <<"Totes les activitats:";
    mostrar_set(totes_activitats);
    cout <<"Cap activitat:";
    mostrar_set(cap_activitat);
}

És a dir, llegim tots els elements, i tenim dues còpies, ja que inicialment, els que fan totes les activitats són els mateixos que no en fan cap. Aleshores, anem llegint els que fan cada activitat. Fem una funció que actualitzi totes, de manera que si un no apareix, l'eliminem, i una altra que actualitzi cap, de manera que si apareix un, l'eliminem. I ja està

Map:

Ara presento una estructura molt similar al set. Aquest és el map, o diccionari. És com un set, però amb un valor associat. És a dir, té una sèrie de claus, i a cada clau un valor. Està ordenat per les claus, i les que no poden repetir-se són les claus. 
Les funcions que té són en essència les mateixes que el set, però canviant l'element per una parella. És a dir, l'insert, enlloc de rebre un element, rep una parella. La desreferència de l'iterador igual, ens dóna una parella
A més d'això, tenim l'operador []. Aquest el que fa és, si no hi ha l'element, el crea, i si hi és, el modifica. I no, no és un embarbussament. La diferència fonamental és:
map<int,int> m, m2;
m.insert(pair<int,int>(3,3));
m2.insert(pair<int,int>(3,3));
...
m[3]=4;
m2.insert(pair<int,int>(3,4));

Si miréssim com queden, veuríem que hem modificat m, mentre que m2 queda igual. Aquesta és la diferència fonamental

Truc amb els iteradors

Ara els iteradors fan referència a una parella, enlloc de fer-la a un sol element. Per tant, si volem accedir al segon de la parella (per exemple, per incrementar), el que faríem intuitivament seria
++(*it).second;
No obstant, tenim una altra eina, que és l'operador ->. Aquest el que fa és que, quan estem apuntant a una estructura, donar-nos aquell element en concret. Per exemple
++it->second;
Ens dóna el segon element. Això és heretat dels punters, però és molt pràctic

Un exercici

Podem fer l'exercici X34352 del jutge. 

int main() {
    map<string,int> m;
    char c;
    string s;
    while(cin >>c>>s) {
        if (c=='a') {
            map<string,int>::iterator it=m.find(s);
            if (it!=m.end()) ++it->second;
            else m[s]=1;
        }
        else {
            map<string,int>::iterator it=m.find(s);
            if (it!=m.end()) cout<<it->second<<endl;
            else cout<<0<<endl;
        }
    }
}

Fem un map que tingui un string com a clau, i un enter com a valor. Anem llegint l'entrada, si hem d'afegir, mirem si hi és o no. Si ja hi és, incrementem el valor. Si no hi és, l'afegim i li posem un 1. Si volem mostrar, mirem si hi és. Si hi és, mostrem la freqüència, i si no hi és, un 0. 

Vectors de mida variable

Això ho poso aquí perquè, malgrat que no té a veure amb els sets ni amb els maps, es fa a la mateixa sessió. A més n'he parlat una mica al principi, així que ho poso 
Mai us heu preguntat com funciona internament un vector? Porteu des de PRO1 utilitzant-los. Normalment ens diuen que reserva espai a posicions de memòria seguides, i realment no és fals. Però no és gaire precís tampoc. 

Un vector, internament, conté una sèrie d'atributs, tals com la mida actual, o un punter que apunta al primer element. La memòria es reserva utilitzant l'operador new[], que ara no cal explicar què fa.
Quan declarem un vector de mida n, el que fa és reservar espai per exactament n elements. Ara, què passa si ens adonem de que necessitem més espai? O si no sabem la mida que necessitarem. Existeix una funció, a la que molts professors de PRO1 tenen mania, que és el push_back. Aquest, el que fa és incrementar en 1 la mida del vector, i posar allà l'element que volem posar
Espera un moment, no havies dit que reservàvem exactament la mida del vector? Això com es menja?
Fàcil.

Reservar més espai, i moure'ns allà

Si tenim espai per n elements, i volem tenir-ne n+1, el que hem de fer és reservar més espai, i desplaçar el vector cap allà. El problema que ens trobem, aleshores, és que desplaçar un vector té cost lineal respecte la quantitat d'elements que desplacem. Però aleshores, la solució és fàcil. Reservar més espai del necessari. Enlloc de reservar espai per n+1 elements, reservem només per potències de 2. Aleshores, si tenim un codi com el següent:
vector<int> v;
for(int i=0;i<n;++i) v.push_back(i);
Imaginem que n=63. Aleshores, faríem:
-Reservem espai per 1 element. Posem el primer
-Reservem espai per 2 elements. Posem el segon
-Reservem espai per 4 elements. Posem el tercer
-Posem el quart
-Reservem espai per 8 elements. Posem el cinquè
-Posem el sisè
-Posem el setè
-Posem el vuitè
-Reservem espai per 16 elements. Posem el novè
...
-Reservem espai per 32 elements. Posem el dissetè
...
-Posem el 63è

Podríem calcular el cost d'això. En molts casos és constant, i en alguns lineal. No obstant, l'important és saber quin cost en tindrà quan en fem molts. Si en fem molts, fent la mitjana, veiem que és cost constant amortitzat. No creix el cost en funció de la quantitat d'elements. Tinguem 8 elements o 800.000, el cost en mitjana serà el mateix

Oju (que vol dir ull)

Als professors no els agrada que utilitzem push_back sense una justificació. Per exemple, un dia vaig llegir un codi com el següent, en una pàgina que teòricament ensenyava a programar

En els vectors

int n;
vector<int> v;
cin >>n;
int a;
for (int i=0;i<n;++i) {
    cin >>a;
    v.push_back(a);
}

Utilitzar push_back aquí no té cap justificació lògica. Sabem quants elements tindrem, i sabem que reservar l'espai inicialment és més eficient que anar modificant la mida. Per tant, fer això restaria de nota manual. 

piles/cues

bool ben_parentitzat(const string& s) {
    vector<char> v;
    for (int i=0;i<int(s.length());++i) {
        if (s[i]=='(' or s[i]=='[' or s[i]=='{') {
            v.push_back(s[i]);
        }
        else {
    if (v.empty()) return false;
            if (s[i]==')' and v.back()!='(') return false;
            else if (s[i]==']' and v.back()!='[') return false; 
            else if (s[i]=='}' and v.back()!='{') return false;
            v.pop_back();
        }
    }
    return v.empty();
}

En aquest cas, estem utilitzant un vector enlloc d'una pila. No utilitzem res del que permet el vector. Ni l'accés aleatori, ni el recorrem... Només accedim a l'últim element, i això ja ens ho fa una pila (de manera més eficient)
Nota: una pila de la STL es pot implementar internament amb una deque o un vector (per defecte, una deque). Una deque s'implementa amb un vector de punters, de manera que realment, utilitzant una pila, estem utilitzant un vector. No obstant, es pot fer la prova per veure què va més ràpid, si fer push_back en un vector, o fer push en una pila. No l'he fet, però m'apostaria alguna cosa a que és més ràpid fer-ho en una pila

llistes

Si necessitem insertar només al final, o ens és indiferent la posició, jo realment utilitzaria un vector. Contràriament al que pot semblar, és més eficient fer push_back en un vector, que en una llista
Per altra banda, si vols tenir l'estructura ordenada, potser també surt més a compte utilitzar el vector. Insertar tindrà cost lineal, perquè haurem de desplaçar els elements del vector per fer espai, però amb la llista hauríem de buscar la posició també en temps lineal (a no ser que tinguem ja la posició). El que ens aporta aquí utilitzar el vector, és que hi podem fer una cerca dicotòmica

Aprendre a programar 15 - Llistes i arbres binaris

Ja tenim 3 estructures de dades. Us semblen moltes? HA! No heu vist res encara. N'hi ha moltes més encara. La llista completa seria: Array, deque, forward_list, list, map, multimap, multiset, priority_queue, queue, set, stack, unordered_map, unordered_multimap, unordered_set, unordered_multiset, vector i vector<bool>. Per sort per nosaltres, només utilitzarem list, map, queue, set, stack, vector i vector<bool>. I ara quina deu tocar?

Llistes

Ara veiem les llistes. Fins ara hem vist que en una pila podem insertar només a dalt, i en una cua només al final. En un vector també podem insertar al final en temps constant amortitzat. Ara, què passa si necessitem insertar molts cops i que no sigui al final? Amb el vector, hauríem de desplaçar els elements per fer espai pel nou, cosa que té temps lineal. Massa temps, si n'hem de fer moltes. Per això apareixen les llistes

Insercions i eliminacions en temps constant

Una llista té una implementació interna que permet insertar i eliminar en temps constant, a la posició que sigui. Principi, final, o qualsevol, totes en temps constant. Tingui un element o un milió, trigarem similar en insertar un element. Podem veure com funcionen

Iteradors

Un iterador és una classe que utilitzem per recórrer una estructura de dades. No és una cosa nova del tot, ja que els vectors també en tenien, però en els vectors podíem obviar la seva existència i recorre'ls per l'índex. Aquí no podem, ja que els elements d'una llista no s'emmagatzemen en posicions contínues. Així que anem pas a pas

Què és un iterador?

Un iterador és una classe que fa referència a un element d'una estructura de dades i, en la majoria de casos, s'utilitza per recórrer aquesta estructura

Com es declara?

Si volem declarar un iterador d'una llista d'enters, seria fent list<int>::iterator o list<int>::const_iterator. Si el volem declarar d'un vector de char, seria fent vector<char>::iterator o vector<char>::const_iterator

Diferències entre iterator i const_iterator?

iterator permet modificar el contingut de l'objecte. const_iterator no, podem recórrer l'objecte, però es queda com estava

Iteradors importants?

Bàsicament hi ha dos. Són begin() i end(). Begin apunta al primer element de l'estructura, i end apunta a fora de l'estructura. És a dir, l'estructura està al rang d'iteradors [begin,end). 

Com ens movem per l'estructura?

Amb els operadors ++ i --. ++it fa que apunti al següent element, mentre que --it fa que apunti a l'anterior. Els vectors, per exemple, permeten sumar-hi números, però les llistes no. 

Com consultem l'element?

Amb l'operador de desreferència *. Si tenim que it apunta a un element, fent *it consultem aquest element

Funcions de la classe list:

Un cop sabem com recórrer una llista, necessitem saber com treballar-hi. I, en aquest cas, tenim bastantes més funcions que als anteriors. Posaré les que em semblin més necessàries, si en voleu més, estan aquí. Són les següents:

begin: 

Retorna un iterador que apunta al primer element de la llista

end:

Retorna un iterador que apunta a l'end, és a dir, passat l'últim element. 

size:

Retorna la mida de la llista

front i back:

Retornen el primer i l'últim element, respectivament

push_front i push_back:

Afegeixen un element al principi i al final, respectivament. 

pop_front i pop_back:

Eliminen el primer i l'últim element, respectivament

insert:

Inserta un element. La versió més utilitzada rep un iterador indicant la posició on volem insertar, i l'element que insertem, i retorna un iterador que apunta a l'element insertat. 

erase:

Elimina un element. Rep un iterador apuntant a l'element que volem eliminar, i retorna un iterador apuntant al següent del que hem eliminat. Compte! L'iterador que li hem passat ja no és vàlid. Una cosa típica és recórrer la llista eliminant sota certes condicions. En aquest cas, hauríem de fer
...
if(*it==n) it=erase(it);
...
Així l'iterador que estem utilitzant segueix sent vàlid (ja que la funció retorna un iterador a l'element de després de l'eliminat. 

splice:

Hi ha diverses versions, la més utilitzada és la que transfereix una llista a l'altra. És a dir, si féssim
l.splice(l.begin(),llista2);
El que estaríem fent és transferir els elements de llista2 al principi de l. La llista2 quedaría buida

Tenim més funcions, i les que he posat són molt més extenses. Com sempre, Google i aquesta pàgina tenen les respostes que volem. Nosaltres encara tenim estructures per conèixer


Arbre binari:

NOTA: la classe explicada aquí ha quedat obsoleta. Han creat una nova classe, de la que parlo aquí. Malgrat tot, tot el que he explicat aquí es pot aplicar amb la nova classe, simplement canviant les funcions que utilitzem

Espera espera, arbres binaris? Això no sortia a la llista d'estructures de dades que vas fer, no? Realment no. És una estructura de dades no estàndard, que ens donen els professors de PRO2.

La definició de l'arbre binari és recursiva. Un arbre binari pot ser:
-Buit
-Un node (pare), i dos arbres binaris com a fills. 
No ens preocuparem (per ara) de com s'implementa internament. Només hem de saber això, que un arbre pot ser buit, o un node i dos arbres com a fills. 
Un detall que podem observar és que al fitxer Arbre.hh tenim tota la implementació. No acostuma a ser bona idea implementar coses al fitxer hh, però quan utilitzem templates, ens hi veiem obligats (per raons que no venen al cas).

Glossari:

Això no és res més que una sèrie de termes sobre els arbres (de moment, binaris) bastant utilitzats.
Node: Un arbre no deixa de ser un graf (si feu o heu fet M1, perfecte, si no... Bé, busca a Google, tampoc cal saber-ne gaire). Els elements de l'arbre, per tant, són nodes
Arrel: El node pare d'un arbre
Subarbre: El fill de l'arbre. En els arbres binaris, en tenim dos
Fulla: Un node que no té fills (normalment fem que siguin fills buits)

Funcions:

a_buit: Converteix el paràmetre implícit en un arbre buit
swap: Intercanvia els dos arbres
plantar: Rep un valor i dos arbres. El paràmetre implícit es transforma en un arbre tal que el pare té el valor, i els fills són els arbres que li hem passat
fills: Rep dos arbres buits. El primer es converteix en el fill esquerre del paràmetre implícit, i el segon en el dret. El paràmetre implícit deixa de ser vàlid, per altra banda
arrel: Retorna l'arrel de l'arbre
es_buit: Retorna true si l'arbre és buit, false si no
I ja està. No hi ha més funcions, i tampoc calen. La recursivitat farà la màgia

Iterar és màgic, recursivar és diví

Anem a fer uns quants exemples de coses que podem fer amb arbres.

Llegir en preordre:

La idea és anar llegint com si ens recorreguéssim l'arbre amb una cerca en profunditat. La lectura en preordre es defineix recursivament (sí, sóc un pesat) de la següent manera:
-llegim el pare
-llegim el fill esquerre
-llegim el fill dret
Es pot fer de moltes maneres, però hem de trobar una manera d'indicar que ja ha acabat aquella branca. Per exemple, jo ho faré amb el 0, de manera que en el meu arbre no hi ha 0. Per llegir-ho, seria una cosa així:

void llegir(Arbre<int>& A) {
    int n;
    cin >>n;
    if (n!=0) {
        Arbre<int> fe, fd;
        llegir(fe);
        llegir(fd);
        A.plantar(n,fe,fd);
    }
}

És a dir, llegim l'element que serà el pare de l'arbre actual. Si és diferent de 0, vol dir que hem de llegir els fills esquerre i dreta, i plantar-ho. Si és 0, vol dir que aquest arbre ha de ser nul, per tant no fem res. 

Comprovar si un element hi és

bool hi_es (int x, Arbre<int>& A) {
    if (A.es_buit()) return false;
    Arbre<int> fe, fd;
    if(A.arrel()==x) return true;
    A.fills(fe,fd);
    bool r=hi_es(x,fe) or hi_es(x,fd);
    A.plantar(fe,fd);
    return r;
}

Anem a fer algun problema

Podem fer l'exercici 1 de l'examen del 14/11/2016. Aquest ens demana calcular la mitjana i la desviació dels elements d'un arbre binari. Ens demana implementar dues funcions, i en una d'elles hem de triar els paràmetres que passem. Aquesta és la funció d'immersió, que és la que entra dins de l'arbre. Podem dir que és la que fa i desfà. 

Com ho fem?

Tant per calcular la mitjana com per calcular la desviació necessitem conèixer la suma dels elements, i la quantitat d'elements. Per tant, farem que la capçalera d'immersió sigui
void i_estadist(Arbre<double>& a, double& suma, int& n)
Aleshores, volem calcular tant la suma com la quantitat d'elements, no? Podem fer que quedi una cosa així:

// Pre: a=A, suma=0 i n=0
// Post: suma representa la suma dels elements de l'arbre A
// n representa la quantitat d'elements de l'arbre A
void i_estadist(Arbre<double>& a, double& suma, int& n) {
    if (a.es_buit()) return;
    double valor=a.arrel();
    Arbre<double> fe, fd;
    a.fills(fe,fd);
    double suma_e=0, suma_d=0;
    int ne=0, nd=0;
    i_estadist(fe,suma_e,ne);
    i_estadist(fd,suma_d,nd);
    a.plantar(valor,fe,fd);
    suma=suma_e+suma_d+valor;
    n=ne+nd+1;
}
// Pre: a = A és un arbre no buit
// Post: mitjana representa la mitjana dels elements de l’arbre A,
// desviacio representa la desviaci´o dels elements de l’arbre A
void estadist(Arbre<double>& a, double& mitjana, double& desviacio) {
    double suma=0;
    int n=0;
    i_estadist(a,suma,n);
    mitjana=suma/n;
    desviacio=suma*suma/(n*n);
}

Si a és buit, no hem de sumar res, ni comptar cap element. Si no és buit, calculem la suma i la quantitat d'elements de cada fill. La suma de l'arbre serà la suma del fill esquerre més la del fill dret més el valor de l'arrel. La quantitat d'elements de l'arbre serà la quantitat d'elements del fill esquerre més la del dret més 1. 
Per últim, ens demanen demostrar que això funciona. I, per fer-ho, utilitzarem un mètode que als informàtics ens encanta, i és la inducció. La meva serà de pa sucat amb oli, però a un examen s'ha de fer bé. 

Cas base: En el cas buit, com que no modifiquem res, tenim que suma=0 i n=0. No hi ha elements, i per tant, no sumen res. Correcte
Inducció: Suposarem que la funció funciona. En aquest cas, la suma dels elements d'un arbre no buit serà la suma dels del fill esquerre més la dels del fill dret més l'arrel (Correcte). La quantitat d'elements serà la quantitat del fill esquerre més la del fill dret més 1. Correctes els dos.

Apunts importants sobre els arbres

Hi ha una sèrie de coses importants a tenir en compte a l'hora de programar utilitzant arbres. Actualment no sabem com funcionen els arbres per dins, però sí que penso que és important conèixer alguna cosa. Si més no, el cost de les funcions. En essència m'interessen unes funcions concretes
Plantar: Aquesta té cost constant en la majoria dels casos. L'únic cas on el cost no és constant és quan els dos arbres són el mateix (no que siguin iguals, compte, que siguin el mateix objecte exactament). En aquest cas, té cost lineal respecte als elements d'aquest arbre. 
Fills: Aquesta sempre té cost constant
Arrel: Aquesta té cost constant respecte la quantitat d'elements de l'arbre. Òbviament, si és un arbre de vectors, el temps dependrà de la quantitat d'elements del vector

Per què em sembla important remarcar aquests 3 costos? Fàcil. Quan no sabem com funciona la classe internament, podem pensar que les dues primeres tenen cost lineal respecte a la mida. Al cap i a la fi, sembla que copia els elements dels fills al pare, o del pare als fills. No obstant, funciona diferent. Si no ho sabem, podem trobar-nos que intentem utilitzar-los molt poc, però l'alternativa utilitzada tingui un cost més elevat. Sabent això, sabem que utilitzar-les no és un drama