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 

dijous, 7 de setembre del 2017

Aprendre a programar 14 - Piles i cues

Arribats a aquest punt, toca aprendre a utilitzar altres estructures de dades. Fins ara hem utilitzat vectors, però no sempre ens va bé utilitzar vectors. A vegades necessitem emmagatzemar les dades de diferent manera, o necessitem estructures de dades més complexes, o més simples... Per això tenim més estructures de dades (JA! Us pensàveu que amb els vectors en teníem prou? No sabeu res hahaha). A PRO2 en veurem unes quantes, de les moltes que hi ha a la STL.

Pila

La primera estructura que veurem és la pila. És una estructura del tipus LIFO (Last In - First Out), que vol dir que traiem els elements de més recents a menys recents. Ens la podem imaginar com si traguéssim els plats del rentaplats, i els poséssim en una pila per passar-los a l'armari. El primer plat que podrem agafar serà l'últim que haguem posat a la pila
Per utilitzar-la, s'utilitza la classe stack, que trobem a la llibreria stack (esperable, no?). Tenim bàsicament les següents funcions:

push: Afegeix un element a la pila
pop: Elimina l'element de dalt de tot (és a dir, l'últim que hem afegit)
top: Retorna l'element de dalt
size: Ens retorna la mida
empty: Retorna true si la mida és 0, false si és diferent

Aleshores, treballar amb una pila és bastant senzill. Per afegir utilitzem push, per treure pop, per consultar top

Podem provar de fer alguna cosa. Per exemple, mirar si una expressió està ben parentitzada. És a dir, una expressió ben parentitzada es podria definir:
buit: Ben parentitzada
(expressió): Ben parentitzada
[expressió]: Ben parentitzada
{expressió}: Ben parentitzada 
És a dir, un string buit seria una expressió ben parentitzada. () també. (()) també. ([]) també. ([({})]) també. 
Podem veure com cada parèntesi obert té un que el tanca. Sabent això, podem utilitzar una pila per fer-ho:

bool ben_parentitzat(const string& s) {
    stack<char> st;
    for(int i=0;i<int(s.length());++i) {
        char c=s[i];
        if (c==')') {
            if (s.top()!='(') return false;
            s.pop();
        }
        else if (c==']') {
            if (s.top()!='[') return false;
            s.pop();
        }
        else if (c=='}') {
            if (s.top()!='[') return false;
            s.pop();
        }
        else s.push(c);
    }
    return s.empty();
}

int main() {
    string s;
    while(cin>>s) {
        if (ben_parentitzat(s)) cout <<"SI"<<endl;
        else cout<<"NO"<<endl;
    }
}

És a dir, si obrim el parèntesi, l'empilem. Si el tanquem, comprovem que coincideixi amb l'últim obert i, si coincideix, vol dir que ja hem tancat l'últim, de manera que el podem treure. Si no coincideix, acabem directe. Quan acabem, si la pila està buida, vol dir que estava bé, mentre que si té contingut, vol dir que faltaven parèntesis per tancar

Sembla una estructura molt senzilla, no? Doncs té moltes utilitats. Per exemple, per programar l'algorisme DFS iteratiu fa falta una pila. També s'utilitza una pila per emmagatzemar les variables locals de les funcions, així com pel pas de paràmetres d'una funció a una altra. Per això mateix, quan fem una funció recursiva infinita, ens acaba sortint l'error "stack overflow". 

Cua:

Un cop hem après què és una pila, podem mirar-nos la cua. La cua és una estructura FIFO (First In - First Out). Això vol dir que el primer element que entra és el primer que surt. Ho podem veure com la cua del super, el primer que s'hi posa és el primer en ser atès, i l'últim que s'hi posa és l'últim en ser atès (deixant de banda la gent que es cola). Les seves funcions són:

push: Afegeix un element a la cua
pop: Elimina l'element de davant (és a dir, el primer que hem afegit)
front: Retorna l'element de davant
size: Ens retorna la mida
empty: Retorna true si la mida és 0, false si és diferent

Aleshores la idea d'utilitzar una cua és que el primer element insertat és el primer que surt. Formats FIFO s'utilitzen per moltes coses. Per exemple, per programar l'algorisme DFS, que es fa a EDA, s'utilitzen cues.. O, per exemple, els fluxs de dades (cin, cout, cerr) funcionen igual, el primer que entra al flux és el primer que en surt. El mateix amb les pipes de Linux. I podríem seguir. 

La classe queue la trobem a la llibreria queue (sorprenent). 

Aprendre a programar 13 - L'examen de lab de PRO2

Ara ja hem fet les 3 primeres sessions, i se suposa que ja sabem més o menys modificar una classe. La sessió 4 és per practicar per l'examen de laboratori, que consistirà en modificar una classe que, en general, acostuma a ser Cjt_estudiants. Aquí, a diferència dels altres problemes, acostumen a donar-nos de manera que només haguem de fer dos o tres funcions. No té gaire diferència respecte les sessions anteriors, i, per tant, penso que no cal dedicar un article només a això. Així que he pensat que parlaré del meu examen de lab, que va ser completament diferent a la majoria. Si més tard veig necessari completar amb altres examens, els afegiré al final

Quan et canvien completament el format

Suposo que el lector, igual que jo, estudia els examens de la universitat com si fos l'autoescola. Es mira què entra, i es posa a fer examens per veure com ho porta. És molt típic, i conec molta gent que ho fa així. A algunes assignatures ja avisen que no és una bona manera, i que es pot patinar. I això és el que va passar el meu any (2015-2016 Q2). 
Sí, imagineu la sorpresa dels estudiants del torn 1, quan arriben i es troben que han d'implementar una cua. Sobretot pels que no tenen gaire clar què és una cua, ja que no entraven a aquest examen. 
Si algú vol l'examen, pot trobar-lo aquí, amb el PDF, els fitxers i una solució (que va treure un 10 a l'examen). Jo proposo que intenteu resoldre'l, després us podeu mirar les idees que dono per resoldre-ho, i finalment la meva solució

Primera idea:

Una primera idea seria la següent. 

Per fer el push, seria tan senzill com afegir l'element a v[t], i incrementar t i n.Evidentment, retoquem la suma i nest_amb_nota, si toca
Per fer el pop, simplement desplacem tots els elements al rang [1,t) una posició a l'esquerra. Decrementem t i n. De la mateixa manera, si cal, retoquem la suma i nest_amb_nota
Per escriure, simplement faríem un for i escriuríem cada estudiant

Aquesta solució és bastant intuïtiva. I, si no ens fixem gaire en l'invariant, doncs és el que se'ns passarà pel cap. 

El seu problema:

Aquesta solució té un problema, i és que cada pop que fem implica desplaçar tots els elements. A més, l'invariant ens diu que tenim un membre de la classe que es diu p, que indica l'inici de la cua, un que es diu t, que indica el final, i un que es diu n, que indica la mida. Amb la nostra solució, són inútils, ja que el primer sempre serà 0, i el final sempre serà n. 
Però la classe es torna més simple, podríeu dir. Sí, és cert, però es torna més lenta. Un pop implica desplaçar n-1 estudiants. Sí, en principi la mida màxima és 10, de manera que com a molt estarem desplaçant 9 estudiants. Però per què desplaçar 9, si ho podem fer bé i no desplaçar-ne cap? O, mirem d'una altra manera. Imaginem que necessitem una cua que pugui tenir fins a 1.000.000 d'estudiants. Seria divertit fer pop, no?

I tan malament està això?

Sí. Perquè us feu una idea, l'examen era 60% automàtica, i 40% manual. Hi va haver una quantitat desproporcionada de gent amb un 6, perquè tenien un 10 d'automàtica, i un 0 de manual. Semblava que només hi haguessin 3 notes (0, 6 i 10). Sobretot 6, després 0, i pocs 10. Però notes diferents, encara menys. 

Una bona solució?

Doncs una bona solució seria aprofitar tots els atributs de la classe. 

Fer un push és senzill, posem la variable a la posició t, incrementem n i recalculem t. Si l'estudiant té nota, ho retoquem una mica, i ja està
Fer el pop és similar. Si té nota, retoquem, i aleshores incrementem p i decrementem n. Si p ha arribat al màxim, ho posem a 0, i ale
Fer l'escriure és bastant trivial. Et recorres de p fins a t (no inclòs), i mostres els estudiants. Pots utilitzar la funció escriure, o fer-te el xulo com vaig fer jo, i fer-ho a mà (millor la primera)

dilluns, 4 de setembre del 2017

Aprendre a programar 12 - Modificar una classe

Un cop hem arribat aquí, ja sabem com utilitzar la classe Estudiant, i Cjt_estudiants. Pot haver costat més o menys aconseguir-ho, però al final ho hem aconseguit. Doncs bé, ara ja no haurem d'escriure programes que la utilitzin. Com us quedeu?

Funcionament intern d'una classe

A l'article anterior vèiem com funciona una classe des del punt de vista de l'usuari extern. Aquí veurem com funciona des de dins

Públic vs privat

Una classe té 3 tipus d'atributs i mètodes. Són públics, privats i protegits. Als protegits no els farem gaire cas, a PRO2 no els utilitzem. 

Públic:

Normalment tenim mètodes públics, més que atributs. Són els mètodes amb els que treballem des de fora, i serveixen per interactuar amb la classe sense preocupar-nos del funcionament intern. En algun cas, també hi ha atributs, com per exemple en el cas de la classe pair, que tenim dos atributs públics (first i second)

Privat:

Aquí tenim mètodes i atributs. Normalment els atributs són els que emmagatzemen la informació que necessitem, mentre que els mètodes (s'entén que privats) són auxiliars cridats per les funcions públiques. Per exemple, a Cjt_estudiants tenim un mètode públic que és consultar_estudiant, que rep un DNI i retorna l'estudiant corresponent. Doncs ho fa utilitzant una funció auxiliar que fa la cerca dicotòmica, i que està com a privada. A aquesta no hi podem accedir des de fora, ja que és privada

Com funciona Cjt_estudiants?

Sí, comencem directament per aquí. El funcionament d'Estudiant és bastant senzill, i a més, no ens la faran modificar. Si a algú li interessa, és trivial, però per nosaltres seguirà sent una caixa negra. 

Atributs privats:

Bàsicament tenim 3 atributs privats. 

MAX_NEST:

És un enter que indica la mida màxima del conjunt. Pot ser 10, 60, 30.000, o qualsevol altra cosa. La idea és que hi puguin cabre tots els estudiants que vulguem

vest:

Aquest és el vector que emmagatzema els estudiants. És un vector<Estudiant> de mida MAX_NEST

nest.

És un enter que emmagatzema la quantitat d'estudiants que tenim. La idea és que estaran emmagatzemats entre la posició 0 i la posició nest-1

Mètodes públics

Afegir estudiant:

Aquesta el que fa és desplaçar tots els elements amb dni>est.dni una posició cap a la dreta, i un cop ha arribat al punt on el dni ja no és més gran, posa allà l'estudiant. Això, evidentment, si hi cap. Per tant, té cost lineal respecte a la mida

Modificar estudiant:

Fa la cerca dicotòmica per buscar on està aquell dni, i modifica aquella posició perquè hi hagi l'estudiant que volem

Modificar ièssim

Modifica l'ièssim, sempre que la posició sigui vàlida

Mida i mida màxima:

Retornen l'enter corresponent

Existeix estudiant:

Fa la cerca dicotòmica buscant el dni, i retorna si hi és o no

Consultar estudiant

Fa la cerca dicotòmica, i retorna l'estudiant que està a la posició retornada

Consultar ièssim:

Retorna vest[i-1]

Llegir:

Llegeix nest. Després fa nest iteracions, fent vest[i].llegir(). Finalment ordena el conjunt

Escriure:

Fa nest iteracions, fent vest[i].escriure()

Com modifiquem?

Normalment ens fan fer exercicis on ens diuen "hem afegit tal atribut privat, i tal funció. Modifica-ho perquè funcioni". El primer que hem de fer és descarregar els arxius del Jutge, allà hi ha les capçaleres i tot. Un cop ho tenim, modifiquem allà directament

Un exemple. Fem l'exercici X68173, que, com podeu veure a l'imatge, no és gaire difícil

No, de debò, no és gaire difícil. El que passa és que, al no haver-hi casos de proves, el que feia era provar-los directament al jutge, comprovant que compilessin i ja (excepte a l'enviament 1 i 15, que se'm va passar). No obstant, jo us diria que us feu un joc de prova (o més), i així no tindreu tants errors

Dit això:

Conjunt d'estudiants amb imax - X68173

Descarreguem els arxius, i veiem que tenim PRO2Exepcio.hh, Estudiant.hh, Estudiant.cc i Cjt_estudiants.hh. Per tant, el que ens toca fer és Cjt_estudiants.cc. De fet, l'únic que ens cal és afegir-hi les dues operacions que ens demanen, i modificar les que calgui. Comencem triant quines funcions cal modificar.

Constructores?

Només en tenim una. No cal fer-hi res, no hi ha màxim perquè està buit. Podem posar-li un valor fora del rang, perquè quedi clar que no tenim màxim. Depenent de com decidim implementar-ho, però jo crec que millor modificar-la

Modificadores?

Afegir estudiant, per si afegim un estudiant amb nota major, o la posició de l'estudiant amb nota màxima canvia
Modificar estudiant, per si modifiquem l'estudiant que té la nota màxima
Modificar ièssim, pel mateix
Esborrar estudiant, que de fet no està implementada

És a dir, totes

Consultores?

Aquí no n'hem de modificar cap. Al cap i a la fi, no modifiquen, consulten. L'únic que hem de fer és implementar estudiant nota max

Lectura/escriptura?

A la de lectura hem de mirar quina és la posició amb nota més gran. A la d'escriptura, res

Ja sabem quines hem de modificar. Ara toca fer-ho. Pots, o currar-te de comprovar si s'ha modificat alguna cosa, si cal recórrer tot el conjunt... O pots simplement recalcular el màxim al final. Jo vaig aconseguir el verd així, per tant, es pot fer així i no et compliques la vida. O pots fer-ho bé, esforçant-te. Això ja depèn de tu