STL andmestruktuurid I
Pärast selle praktikumi läbimist üliõpilane
- teab erinevaid andmestruktuure ja iteraatoreid
- tutvub põhjalikumalt järjestikuste andmestruktuuridega
Sisukord
1. Standardmallide teek STL | 6. Massiiv <array> |
2. Andmestruktuurid | 7. Kahe otsaga järjekord |
3. Järjestikused andmestruktuurid | 8. Listid |
4. Vektor | Enesetestid |
5. Iteraatorid |
Standardmallide teek STL (Standard Template Library)
C++ standardmallide teek STL sisaldab andmekogumite haldamiseks erinevaid võimalusi. Teek STL sisaldab neli peamist komponenti:
- andmestruktuurid (STL-is containers)
- iteraatorid
- algoritmid
- funktsiooniobjektid e funktorid (functors)
Andmestruktuurid
STL andmestruktuure võib liigitada järgmiselt:
- järjestikused
- assotsiatiivsed
- adapterid
Järjestikuste ja assotsiatiivsete andmestruktuuride erinevus:
- järjestikuses andmestruktuuris elemendid salvestatakse ja saadakse kätte nende asukoha järgi
- assotsiatiivses andmestruktuuris korral elemendid salvestatakse ja saadakse kätte võtme abil
Joonisel on toodud enamkasutatavad andmestruktuurid.
Järjestikused andmestruktuurid
Järjestikusteks dünaamilisteks andmestruktuurideks on vector
, list
, deque
. Nende kasutamiseks tuleb sisse lugeda vastav päisefail.
Andmestruktuur | Päisefail |
---|---|
Vektor (vector) | <vector> |
List (list) | <list> |
Kahe lõpuga järjekord (double-ended queue) | <deque> |
NB! Järjestikune andmestruktuur on ka fikseeritud pikkusega array
päisefailist <array>
.
Vektor (vector)
Vektoreid käsitlesime põgusalt ka varem. Vektori loomiseks on palju erinevaid võimalusi, vaatame neist olulisemaid.
vector<double> v1; // tühi 'double' tüüpi vektor vector<int> v2(5); // 5-elemendiga 'int' tüüpi vektor, elemendid nullid vector<int> v3(5, 10); // 5-elemendiga 'int' tüüpi vektor, kõik elemendid on 10 vector<int> v4{5, 10}; // 2-elemendiga 'int' tüüpi vektor, elemendid 5 ja 10 vector<int> v5(v3); // vektori v5 loomine v3 baasil
Defineerime funktsiooni erinevat tüüpi vektorite ekraanile kuvamiseks. Elementide arvu saame funktsiooni size()
abil.
template<typename T> void print_vektor(const vector<T> &v) { // parameetriks 'T' tüüpi vektor viitena for (size_t i{}; i < v.size(); ++i) { cout << v[i] << " "; } cout << '\n'; }
NB! Kui ei ole vaja andmestruktuuri muuta, siis lisame parameetrile const
, mis garanteerib, et andmestruktuuri elemente ei muudeta. Katse siiski elemente muuta annab kompilaatori vea.
Loome main
funktsioonis mõned vektorid ja kuvame ekraanile
int main() { vector<double> v1; // tühi 'double' tüüpi vektor cout << "---\n"; print_vektor(v1); vector<int> v2(5); // 5-elemendiga 'int' tüüpi vektor, elemendid nullid cout << "---\n"; print_vektor(v2); vector<int> v3(5, 10); // 5-elemendiga 'int' tüüpi vektor, kõik elemendid on 10 cout << "---\n"; print_vektor(v3); vector<int> v4{5, 10}; // 2-elemendiga vektor, elemendid 5 ja 10 cout << "---\n"; print_vektor(v4); vector<int> v5(v3); // vektori v5 loomine teise v3 baasil cout << "---\n"; print_vektor(v5); | --- --- 0 0 0 0 0 --- 10 10 10 10 10 --- 5 10 --- 10 10 10 10 10 |
Funktsioon print_vektor
kasutab vektori läbimiseks indekseid. On võimalik ka lihtsam foreach
tsükkel, mida saab kasutada ka teiste andmestruktuuride korral. Vaatame siin kahte varianti. Esimeses variandis tehakse igast elemendist koopia muutujasse el
template<typename T> void print_vektor_foreach(const vector<T> &v) { // parameetriks vektor viitena for (auto el : v) { // el on koopia järjekordsest elemendist cout << el << " "; } cout << '\n'; }
Teises kasutame viidet järjekordsele elemendile auto& el
, mis ei tee elemendist koopiat ja nõuab seetõttu vähem ressursse.
template<typename T> void print_vektor_foreach_koopiata(const vector<T> &v) { // parameetriks vektor viitena for (auto& el : v) { // el on viide järjekordsele elemendile cout << el << " "; } cout << '\n'; }
Vektori elementide muutmiseks eemaldame päisest parameetri omaduse const
:
void print_vektor(vector<T> &v)
Elemente saab muuta ka foreach
tsükliga, kui parameetriks on mittekonstantne vektor viitena
template<typename T> void muuda_vektor_foreach(vector<T> &v) { // parameetriks vektor viitena for (auto& el : v) { // el on viide järjekordsele elemendile el += 2; // suurendame elementi kahe võrra } }
Katsetame:
vector<int> v = {5, 3, 6, 8}; muuda_vektor_foreach(v); print_vektor_foreach_koopiata(v); | 7 5 8 10 |
Iteraatorid
Andmestruktuurides (STL konteinerites) on defineeritud iteraatorid. Iteraatorid kasutavad päist <iterator>
. Iteraatorit võib käsitleda viida (pointer) üldistusena. Iteraator on objekt, mis võimaldab juurdepääsu (viidata) andmestruktuuri elementidele sarnaselt viitadele. Enamiku iteraatorite korral saab kasutada operaatoreid *
ja ->
objektide kirjutamiseks või lugemiseks, operaatorit ++
iteraatori nihutamiseks ühe koha võrra edasi ning iteraatorite võrdlemist ==
või !=
abil. Iteraatoreid on palju erinevaid tüüpe. Olulisemad neist on kaht tüüpi iteraatorid:
- kahesuunalised (bidirectional) iteraatorid, mida saab nihutada operaatoritega
++
ja--
. - otsejuurdepääsuga (random access) iteraatorid, mis on kahesuunalised ja võimaldavad lisaks teostada etteantud positsioneerimist. On lubatud iteraatoriga täisarvu liitmine ja lahutamine, samuti iteraatorite lahutamine.
Igal andmestruktuuril (konteineril) on oma iteraatoritüüp, sarnaselt nagu igal andmetüübil on oma viidatüüp. Andmestruktuuridel vektor<T>
ja array<T>
on pidev (contiguous) iteraator ja andmestruktuuril list<T>
on kahesuunaline (bidirectional) iteraator.
Vaatame iteraatorite kasutamist vektori näitel. Näiteks,
v.begin()
tagastab vektori v
iteraatori, mis viitab vektori v
esimesele elemendile ja
v.end()
tagastab vektori v
iteraatori, mis viitab kohale peale vektori v
viimast elementi. Järgmisele elemendile liikumiseks on vaja lihtsalt iteraatorit ühe võrra suurendada. Kirjutame ümber vektori elementide ekraanile kuvamise funktsiooni iteraatori abil.
template<typename T> void print_vektor_iteraatoriga(const vector<T>& v) { // parameetriks vektor viitena auto algus = v.begin(); // algus on iteraator, mis viitab esimesele elemendile auto lõpp = v.end(); // lõpp on iteraator, mis viitab kohale peale viimast elementi while (algus != lõpp) { cout << *algus << " "; // viidatav element algus++; // nihutamine ühe elemendi võrra } cout << '\n'; }
Kirjutame sama funktsiooni kasutades for
-tsüklit. Defineerime siin iteraatori kasutades tüüpi <T>
eraldi. Funktsiooni parameetriks olev vektor ei saa olla enam konstantne.
template<typename T> void print_vektor_iteraatoriga_for(vector<T>& v) { typename vector<T>::iterator it = v.begin(); // defineerime vektorile <T> tüüpi iteraatori for (; it != v.end(); it++) { cout << *it << " "; } cout << '\n'; }
Järgmises tabelis on toodud tähtsamad tegevused vektoritega (kõik tegevused ei ole seotud iteraatoritega).
Funktsioon | Kirjeldus |
---|---|
front() | Tagastab viite vektori esimesele elemendile |
back() | Tagastab viite vektori viimasele elemendile |
at(n) | Tagastab viite vektori n -le elemendile |
empty() | Tagastab true , kui vektor on tühi, vastasel juhul false |
pop_back() | Eemaldab vektorist viimase elemendi (ei tagasta) |
push_back(const T& x) | Lisab elemendi x vektori lõppu |
begin() | Tagastab iteraatori, mis viitab vektori esimesele elemendile |
insert(iterator it, const T& x) | Lisab elemendi x iteraatoriga viidatud kohale |
erase(iterator it) | Kustutab iteraatoriga viidatud elemendi |
end() | Tagastab iteraatori, mis viitab vektori lõppu (viimase elemendi järele) |
size() | Tagastab elementide arvu vektoris |
capacity() | Tagastab elementide arvu, mida antud vektor on võimeline mahutama ilma ruumi juurde võtmata |
clear() | Eemaldab vektorist kõik elemendid |
Järgnev programmilõik illustreerib põhiliste funktsioonide kasutamist vektorite korral
vector<int> v = {5, 2, 6}; cout << "Esialgne vektor: \n"; print_vektor_foreach_koopiata(v); int& esimene = v.front(); // viide esimesele elemendile int& viimane = v.back(); // viide viimasele elemendile cout << "Esimene ja viimane element: " << esimene << " " << viimane << '\n'; int& teine = v.at(1); // element indeksiga 1 cout << "Element indeksiga 1: " << teine << '\n'; cout << boolalpha << "v.empty(): " << v.empty() << '\n'; v.pop_back(); // kustutab viimase elemendi cout << "Kustutati viimane element\n"; print_vektor_foreach_koopiata(v); v.push_back(8); // lisab lõppu 8 cout << "Lisati lõppu 8\n"; print_vektor_foreach_koopiata(v); auto it = v.begin(); // viit (iteraator) esimesele elemendile it += 2; // iteraatori nihutamine kahe võrra paremale cout << "it += 2: iteraator viitab nüüd elemendile " << *it << '\n'; it = v.insert(it, 3); // lisab iteraatoriga viidatud elemendiks 3 cout << "Lisati iteraatori kohale element 3\n"; print_vektor_foreach_koopiata(v); cout << "Iteraator viitab nüüd elemendile " << *it << '\n'; it--; // viida nihutamine ühe võrra vasakule cout << "it--: iteraator viitab nüüd elemendile " << *it << '\n'; it = v.erase(it); // kustutatakse iteraatoriga viidatud element cout << "Kustutati iteraatoriga viidatud element\n"; print_vektor_foreach_koopiata(v); cout << "Iteraator viitab nüüd elemendile " << *it << '\n'; auto suurus = v.size(); // elementide arv cout << "Vektori elementide arv: " << suurus << '\n'; v.clear(); // eemaldab kõik elemendid cout << "Tühi vektor: " << '\n'; print_vektor_foreach_koopiata(v); | Esialgne vektor: 5 2 6 Esimene ja viimane element: 5 6 Element indeksiga 1: 2 v.empty(): false Kustutati viimane element 5 2 Lisati lõppu 8 5 2 8 it += 2: iteraator viitab nüüd elemendile 8 Lisati iteraatori kohale element 3 5 2 3 8 Iteraator viitab nüüd elemendile 3 it--: iteraator viitab nüüd elemendile 2 Kustutati iteraatoriga viidatud element 5 3 8 Iteraator viitab nüüd elemendile 3 Vektori elementide arv: 3 Tühi vektor: |
Täpsemalt saab vektorite kohta uurida aadressil https://en.cppreference.com/w/cpp/header/vector
Iteraatorite kohta saab uurida lähemalt aadressil https://en.cppreference.com/w/cpp/iterator/iterator
Massiiv array
Fikseeritud suurusega massiiv array
hoiab oma elemente mälus lineaarses järjestuses. Massiivi ei saa suurendada, elementidele on juurdepääs indeksi abil (operaator []
) või funktsioonide at, front, back
abil. Näiteid andmestruktuuriga array
opereerimisest:
#include <array> #include <iostream> #include <string> using namespace std; int main() { array<int, 5> m1{{3, 4, 5, 1, 2}}; // 3, 4, 5, 1, 2 for (auto &el: m1) cout << el << ' '; cout << '\n'; array<int, 5> m2 = {1, 2, 3, 4, 5}; //1, 2, 3, 4, 5 for (auto &el: m2) cout << el << ' '; cout << '\n'; array<string, 2> m3 = {"kaks", "kolm"}; for (auto &el: m3) cout << el << ' '; cout << '\n'; // Täidame m2 10-tega m2.fill(10); for (auto &el: m2) cout << el << ' '; cout << '\n'; return 0; } | 3 4 5 1 2 1 2 3 4 5 kaks kolm 10 10 10 10 10 |
Põhilised erinevused C++ andmestruktuuride vector
ja array
vahel:
vector | array |
---|---|
Dünaamilise suurusega | Staatilise suurusega |
Suurus võib muutuda programmi töö käigus | Elementide arv on teada kompileerimise ajal |
Elemente võib lisada, elemente võib kustutada | Elemente ei saa lisada ega kustutada |
Võtab rohkem mälu kui array | On mäluefektiivsem |
Vectorit (vector
) on parem kasutada elementide sagedase lisamise ja kustutamise korral, samas massiiv array
sobib olukorras, kus on vajalik elementidele sagedane juurdepääs.
Täpsemalt saab array
kohta uurida aadressil https://en.cppreference.com/w/cpp/header/array
Kahe otsaga järjekord deque
Kahe otsaga järjekord deque
toetab sarnaselt vektoriga efektiivset elemendi lisamist ja kustutamist nii alguses kui ka lõpus. deque
toetab ka otsejuurdepääsu elementidele.
Kahe otsaga järjekorra deque
toimingud on peaaegu samad, mis vektori puhul, mõne väikese
erinevusega. Andmestruktuuril deque
ei ole erinevalt vektorist mahutavuse (capacity
) mõistet, sest STL standardi järgi ei pea elemente salvestama kõrvuti. Andmestruktuur deque
pakub lisaks funktsioonile push_back
ka funktsioone push_front
, pop_front
() ja pop_back
.
Näide deque
elementide lisamisest, asendamisest ja kustutamisest:
#include <iostream> #include <deque> using namespace std; template <typename T> void tryki(deque<T>* d){ for (T el: *d) { cout << el << " "; } cout << '\n'; } int main() { deque<int> d1 = {1, 2, 3, 4}; d1.insert(d1.begin() + 2, 25); // 1, 2, 25, 3, 4 d1.pop_front(); // 2, 25, 3, 4 d1.erase(d1.begin() + 1); // 2, 3, 4 d1.push_front(11); // 11, 2, 3, 4 d1[1] = 5; // 11, 5, 3, 4 d1.at(0) = 12; // 12, 5, 3, 4 d1.front() = 10; // 10, 5, 3, 4 d1.back() = 20; // 10, 5, 3, 20 tryki(&d1); return 0; } | 10 5 3 20 |
Täpsemalt saab deque
kohta uurida aadressil
https://en.cppreference.com/w/cpp/container/deque
Listid list
ja forward_list
List list
paigutab oma elemendid topeltahelana (doubly linked list), aga forward_list
paigutab elemendid tavalise ahelana (linked list). Kumbki ei paiguta elemente mällu järjest. Seetõttu ei ole listide elemente võimalik kätte saada indeksi abil, st operaatorit operator[]
ei toetata. Elemendi kättesaamiseks käiakse list elementhaaval läbi alates algusest (forward_list
), st saab kasutada ainult ühesuunalisi iteraatoreid; seevastu list
toetab ka kahesuunalisi iteraatoreid, st juurdepääs on andmestruktuuri mõlemast otsast. Kui järg on sobiva elemendi peal, siis elementide lisamine ja kustutamine on lihtne, sest vaja on tõsta vaid paar viita ümber.
Listi keskel olevat elementi saab kätte ainult iteraatori abil. Vaatame listi kasutamist näite abil. Kui listimuutuja on viit, siis tuleb list initsialiseerida käsu new
abil ja hiljem mälu vabastada.
#include <iostream> #include <list> #include <iterator> using namespace std; void tryki(list<int>* l){ list<int>::iterator it; for (it = l->begin(); it != l->end(); ++it) cout << ' ' << *it; cout << '\n'; } void täida(list<int>* l){ for (int i = 0; i < 5; ++i) { l->push_back(i); } cout << '\n'; } int main() { list<int> l1; list<int> l2; list<int>* l3 = new list<int>; for (int i = 0; i < 5; ++i) { l1.push_back(i * 2); // 0 2 4 6 8 l2.push_front(i * 3); // 12 9 6 3 0 } täida(l3); // 0 1 2 3 4 cout << "List 1 (l1) on : "; tryki(&l1); // 0 2 4 6 8 cout << "List 2 (l2) on : "; // 12 9 6 3 0 tryki(&l2); cout << "List 3 (l3) on : "; // 0 1 2 3 4 for (list<int>::iterator it = l3->begin(); it != l3->end(); ++it) { cout << ' ' << *it; } cout << '\n'; cout << "l1.front() : " << l1.front() << '\n'; // 0 cout << "l1.back() : " << l1.back() << '\n'; // 8 cout << "l1.pop_front() \n"; l1.pop_front(); // eemaldab algusest 0 cout << "l1.pop_back() \n"; l1.pop_back(); // eemaldab lõpust 8 cout << "List 1 (l1) on : "; tryki(&l1); // 2 4 6 delete l3; return 0; } | List 1 (l1) on : 0 2 4 6 8 List 2 (l2) on : 12 9 6 3 0 List 3 (l3) on : 0 1 2 3 4 l1.front() : 0 l1.back() : 8 l1.pop_front() l1.pop_back() List 1 (l1) on : 2 4 6 |
Järgmises tabelis on kokku võetud põhilised operatsioonid järjestikustel andmestruktuuridel
Käsk | vector | deque | array | list | forward_list | Kirjeldus |
---|---|---|---|---|---|---|
operator[] | + | + | + | - | - | Tagastab elemendile viida indeksi järgi. Indeksi kontrolli ei toimu. |
at() | + | + | + | - | - | Tagastab elemendile viida indeksi järgi. Kui indeks on väljaspool piire, tekib viga |
front() | + | + | + | + | + | Tagastab viida esimesele elemendile. Kui tühi, siis ettearvamatu. |
back() | + | + | + | + | - | Tagastab viida viimasele elemendile. Kui tühi, siis ettearvamatu. |
clear() | + | + | - | + | + | Kustutab kõik elemendid. size() on nüüd 0 |
erase() | + | + | - | + | - | Kustutab elemendi. |
insert() | + | + | - | + | - | Lisab ühe või mitu elementi enne iteraatori positsiooni. |
fill() | - | - | + | - | - | Täidab andmekogumi antud elemendiga. |
push_back() | + | + | - | + | - | Lisab elemendi lõppu. |
pop_back() | + | + | - | + | - | Eemaldab viimase elemendi. |
push_front() | - | + | - | + | + | Lisab elemendi algusse. |
pop_front() | - | + | - | + | + | Eemaldab elemendi algusest. |
Enesetestid
NB! Enesetestides eeldame, et on kasutatud standardnimeruumi (using namespace std;
)