Lisää STL:n säiliöitä

Sarjojen (säiliö joka säilyttää alkioiden järjestyksen) lisäksi STL:ssä on nk. assosiatiivisia säiliöitä (associative container), jotka tallettavat alkiot sisäiseen toteutukseensa sellaisessa järjestyksessä, että tiedon haku olisi mahdollisimman nopeaa. Assosiatiivisissa säiliöissä alkioiden ei voi ajatella olevan peräkkäisjärjestyksessä, joten niillä ei myöskään ole järjestysnumeroa. Tieto tallennetaan assosiatiivisiin säiliöihin (haku)avaimen perusteella, jonka perusteella sitä myös etsitään. Jotta etsintä onnistuisi, hakuavaimen tyypillä pitää olla järjestys.

Tutustutaan aluksi kahteen C++:n tarjoamaan assosiatiiviseen säiliötyyppiin:

  • set on täysin analoginen Pythonin set-tyypin kanssa, joka puolestaan vastaa matemaattista joukkoa, jossa kukin arvo voi olla korkeintaan yhden kerran.
  • map vastaa Pythonin dict-tyyppiä, eli se sisältää hakuavain-hyötytieto -pareja, joista tietty hyötytieto on nopea löytää, jos tiedetään siihen liittyvä hakuavain.

Kuten vector-säiliö, myös set ja map vaativat oman include-rivinsä ohjelmankoodin alkuun:

#include <set>  // set-rakenteen käyttöä varten
#include <map>  // map-rakenteen käyttöä varten

Näiden kahden säiliötyypin jälkeen esitellään vielä parit, minkä jälkeen tutustutaan pikaisesti järjestämättömiin säiliöihin.

Esimerkkejä STL:n set-rakenteesta

Määritellään joukko, johon voi tallentaa merkkijonoja:

set<string> laiva_on_lastattu;

Määrittelyn jälkeen joukko on automaattisesti tyhjä.

Joukolla ei ole erillistä hakuavainta, vaan sen alkiot toimivat hakuavaimina. Tästä syystä joukon alkioiden tyypillä pitää olla järjestys.

Joukko voidaan alustaa toisella samantyyppisellä joukolla tai siihen voidaan sijoittaa toinen samantyyppinen joukko:

set<int> lottonumerot_1;
...
set<int> lottonumerot_2(lottonumerot_1);
...
lottonumerot_1 = lottonumerot_2;

Joukon alustaminen ja siihen sijoitaminen voi tapahtua myös aaltosulkulistasta:

set<int> alkulukuja = {2, 3, 5, 7, 11, 13, 17};
...

set<string> kaverit;
...
kaverit = { "Matti", "Maija", "Teppo" };

Joukkoon saadaan lisättyä uusi alkio insert-metodilla:

laiva_on_lastattu.insert("koirilla");

Lisäys toimii silloinkin, kun alkio kuuluu joukkoon jo entuudestaan, se vaan ei tee mitään.

Joukon alkioiden lukumäärä saadaan selville size-metodilla:

// Kavereiden lukumäärä
cout << kaverit.size() << endl;

Alkion kuulumista joukkoon voidaan tutkia find-metodilla:

// Sana ei ole vielä joukossa, lisätään se.
if ( laiva_on_lastattu.find(sana) == laiva_on_lastattu.end() ) {
    laiva_on_lastettu.insert(sana);
    cout << "Ok!" << endl;

// Sana oli joukossa jo ennestään.
} else {
    cout << "Hävisit, " << sana << " oli jo lastattu!" << endl;
}

Yksittäinen alkio voidaan poistaa joukosta erase-metodilla:

// Teppo ei ole enää kaveri.
kaverit.erase("Teppo");

Metodi clear tyhjentää joukon (poistaa kaikki alkiot):

laiva_on_lastattu.clear();

Vertailuoperaattorit toimivat joukoilla, joiden alkiotyyppi on sama:

if ( lottonumerot_1 == lottonumerot_2 ) {
    // Molemmissa joukoissa tismalleen sama alkiot.
    ...
} else {
    // Joukkojen sisällössä eroavaisuuksia.
    ...
}

Metodi empty palauttaa arvon true, vain jos joukko on tyhjä:

if ( not kaverit.empty() ) {
    // On ainakin yksi kaveri.
}

Esimerkkejä STL:n map-rakenteesta

Muista STL:n rakenteista poiketen map-rakenteen määrittely sisältää sekä hakuavaimen että hyötytiedon tyypit:

map<string, double> hinnasto;

Määrittelyn jälkeen alustamaton map-säiliö on tyhjä.

Kuten muutkin säiliöt, myös map voidaan alustaa toisesta saman tyyppisestä map-säiliöstä ja siihen voidaan sijoittaa saman tyyppinen map:

map<string, string> sanakirja_1;
...
map<string, string> sanakirja_2(sanakirja_1);
...
sanakirja_1 = sanakirja_2;

Myös aaltosulkulistalla alustaminen tai sijoittaminen toimii:

map<string, double> hinnasto_2 = {
    { "maito",  1.05 },
    { "juusto", 4.95 },
    { "liima",  3.65 },
};
...
hinnasto_2 = {
    { "pippuri", 2.10 },
    { "kerma",   1.65 },
    { "suklaa",  1.95 },
};

Hakuavaimeen liittyvän tiedon käsittely tapahtuu at-metodilla:

cout << hinnasto_2.at("kerma") << endl;  // 1.65
hinnasto_2.at("kerma") = 1.79;

Käytettäessä at-metodia pitää kuitenkin muistaa vaaratilanne: Jos hakuavainta ei löydy map:istä, tuloksena on poikkeus ja ohjelman suorituksen päättyminen. Huomaa siis: at-metodilla map:iin ei saa lisättyä uusia alkioita.

Vaaratilanne vältetään tarkistamalla, onko hakuavain map:issä ennen at-metodin kutsua, kuten on tehty seuraavassa koodissa.

if ( sanakirja_3.find(sana) != sanakirja_3.end() ) {
    // Sana löytyi mapista.
    cout << sanakirja_3.at(sana) << endl;
} else {
    // Sana ei ole mapissa.
    cout << "Virhe, tuntematon hakusana!" << endl;
}

Siinä hyödynnetään vanhaa tuttua seikkaa, että find palauttaa end()-iteraattorin, jos etsittyä tietoa ei löydy.

Avain–hyötytieto -parin saa poistettua map:ista antamalla erase-metodille parametrina poistettavan hakuavaimen:

if ( hinnasto_2.erase("suklaa") ) {
    // Poisto onnistui, "suklaa" ei
    // ole enää hinnastossa
    ...
} else {
    // Poisto epäonnistui, "suklaa" ei ollut
    // hinnastossa alunperinkään.
    ...
}

Vaikka olemassa olemattoman hakuavain–hyötytieto -parin poisto ei olekaan virhe, ennen poistoa tilanteen voisi halutessaan varmistaa find-metodilla.

Uusi tietopari saadaan lisättyä insert-metodilla:

sanakirja_1.insert( { sana, sana_englanniksi } );

Jos edellisessä sana on sanakirja_1:ssa hakuavaimena jo valmiiksi, insert ei tee mitään.

Metodi size-palauttaa map:iin talletettujen tietoparien lukumäärän:

cout << "Sanakirjassa on " << sanakirja_2.size()
     << " sanaparia." << endl;

Myös map-rakenteen alkioiden läpikäynti on toteutettu iteraattorien avulla. Tämä ei kuitenkaan ole aivan yhtä suoraviivaista kuin muiten STL-säiliöiden tapauksessa, koska jokainen alkio sisältääkin kaksi osatietoa: hakuavaimen ja hyötytiedon. Tämä on toteutettu siten, että map-rakenteen alkiot ovat itse asiassa tietueita (vrt. edellisen kierroksen materiaali tieto-ohjatusta ohjelmoinnista). Jos siis on esimerkiksi määritelty map-rakenne

map<int, string> opiskelijat = {
    // opnum, nimi
    { 123456, "Teekkari, Teemu" },
    ...
};

niin rakenteeseen talletetut alkiot olisivat tyypiltään seuraavan kaltaisia tietueita:

struct {
    int    first;
    string second;
};

jossa tietueen kenttien nimet tosiaankin ovat first hakuavaimelle ja second hyötytiedolle.

Täten map-rakenteen alkiot voidaan käydä iteraattorin avulla läpi seuraavasti:

#include <iostream>
#include <string>
#include <map>

using namespace std;

int main() {
    map<int, string> opiskelijat = {
        { 200001, "Teekkari, Tiina" },
        { 123456, "Teekkari, Teemu" },
        // ···
    };

    map<int, string>::iterator iter;
    iter = opiskelijat.begin();
    while ( iter != opiskelijat.end() ) {
        cout << iter->first << " "
             << iter->second << endl;
        ++iter;
    }
}

Koodissa on syytä panna merkille, kuinka iteraattorin osoittaman tietueen kenttiä (osatietoja) käsitellään operaattorilla -> tavanomaisen . -operaattorin sijaan.

Jos halutaan välttää iteraattoreiden suora käyttö, map:in alkiot voidaan käydä läpi myös for-rakenteella:

#include <iostream>
#include <string>
#include <map>

using namespace std;

int main() {
    map<int, string> opiskelijat = {
        { 200001, "Teekkari, Tiina" },
        { 123456, "Teekkari, Teemu" },
        // ···
    };

    for ( auto tietopari : opiskelijat ) {
        cout << tietopari.first << " "
             << tietopari.second << endl;
    }
}

Koska tietopari ei ole edellisessä silmukassa iteraattori, vaan varsinainen map:istä löytyvä tietue, käytetään first- ja second-kenttien käsittelyyn normaalia . -operaattoria.

STL:n pair

Edellä tarkasteltiin map-rakennetta. Sen alkioiden mainittiin olevan tietueita, joilla on kaksi kenttää. Tällainen 2-alkioinen tietue voidaan kuvata parina.

Pareja voidaan käyttää pelkästäänkin, ja ne löytyvät utility-kirjastosta, joten niitä varten pitää koodin alkuun kirjoittaa:

#include <utility>

Nimensä mukaan pari koostuu kahdesta alkiosta, joista ensimmäiseen viitataan nimellä first ja toiseen nimellä second, kuten olikin jo puhetta map-rakenteen yhteydessä.

Seuraavassa esitellään erilaisia tapoja esitellä ja alustaa pareja:

pair <int, char> pair1;
pair1.first = 1;
pair1.second = 'a';

pair <int, char> pair2 (1, 'a');

pair <int, char> pair3;
pair3 = make_pair(1, 'a');

auto pair4 = make_pair(1, 'a');

Järjestämättömiä säiliöitä

Edellä esitetyillä map- ja set-säiliöillä on sellainen ominaisuus, että niiden alkiot pidetään avainkentän mukaisessa järjestyksessä. Uusi alkio lisätään siis aina sellaiseen kohtaan, että järjestys säilyy.

Edellä nähtiin esimerkki, jossa map-rakenne käydään läpi iteraattorin avulla tai for-silmukassa. Esimerkkien tulostuslauseet tulostavat alkiot nousevassa järjestyksessä. Joissakin tilanteissa tämä on hyödyllinen ominaisuus. Tämän kurssin (STL-säiliöihin liittyvissä) tehtävissä tulostuksilta vaaditaan usein tietty järjestys, sillä tämä helpottaa automaattitestien toteuttamista.

Yleisesti ottaen järjestyksen ylläpitäminen kuitenkin tekee joistakin operaatioista tehottomia. Tästä syystä esimerkiksi map- ja set-säiliöille on STL:ssa vastineet: unordered_map ja unordered_set. Kuten nimestäkin voi päätellä, jälkimmäisissä säiliöissä alkiot eivät ole järjestyksessä.

Koska järjestystä ei tarvitse ylläpitää, saadaan alkioiden etsintä, lisääminen ja poisto toimimaan keskimäärin tehokkaammin kuin järjestetyissä säiliöissä. Järjestämättömiä säiliöitä kannattaa käyttää, jos alkioiden järjestyksellä ei ole merkitystä, eikä säiliötä käydä kokonaan läpi alkio alkiolta, vaan sieltä vain etsitään yksittäisiä alkioita.

Myöhemmin tulevalla kurssilla “Tietorakenteet ja algoritmit” pohditaan tarkemmin eri säiliöiden tehokkuuksia, ja juuri tehokkuussyistä siellä käytetään useimmiten unordered_map- ja unordered_set-säiliöitä säiliöiden map ja set sijasta.

Muita esimerkkejä

Muutaman laajemman esimerkin STL-säiliöiden käyttämisestä löydät kurssin materiaaleista: examples/05/cashier ja examples/05/calendar.