7-bo‘lim

Konteynerlar

vector, array, map va boshqalar - qaysi birini qachon tanlash va nima uchun vector deyarli har doim to'g'ri.

🕑 16 daqiqa o‘qish 📄 894 so‘z 👁 2 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. std::vector - standart tanlov
  2. Sig'im va o'sish
  3. std::array - qat'iy o'lchamli
  4. Assotsiativ konteynerlar
  5. map va unordered_map farqi
  6. set va noyob qiymatlar
  7. Boshqa konteynerlar
  8. O'lchash - taxmin qilmang
  9. Xulosa

Standart kutubxona bir necha konteyner beradi. Ular o'rtasidagi tanlov ishlash tezligiga sezilarli ta'sir qiladi.

Yaxshi xabar: aksariyat hollarda javob bitta - std::vector.

std::vector - standart tanlov #

C++
#include <print>
#include <string>
#include <vector>
#include <array>
#include <deque>
#include <list>
#include <map>
#include <unordered_map>
#include <set>
#include <algorithm>
#include <chrono>
C++
int main()
{
    std::vector<int> bosh;
    std::vector<int> royxat{1, 2, 3, 4, 5};
    std::vector<int> olcham(5);          // 5 ta nol
    std::vector<int> toldirilgan(5, 7);  // 5 ta yettilik

    std::println("bosh:         {} ({} element)", bosh, bosh.size());
    std::println("royxat:       {}", royxat);
    std::println("olcham(5):    {}", olcham);
    std::println("toldirilgan:  {}", toldirilgan);

    // Qo'shish va o'chirish
    royxat.push_back(6);
    royxat.insert(royxat.begin(), 0);
    royxat.pop_back();

    std::println("");
    std::println("o'zgartirilgan: {}", royxat);
    std::println("birinchi: {}, oxirgi: {}", royxat.front(), royxat.back());

    return 0;
}
Natija
bosh:         [] (0 element)
royxat:       [1, 2, 3, 4, 5]
olcham(5):    [0, 0, 0, 0, 0]
toldirilgan:  [7, 7, 7, 7, 7]

o'zgartirilgan: [0, 1, 2, 3, 4, 5]
birinchi: 0, oxirgi: 5
vector<int> v(5) va vector<int> v{5} - boshqa narsa

Bu C++ dagi eng chalkash tuzoqlardan biri:

YozuvNatija
std::vector<int> v(5)5 ta element, hammasi 0
std::vector<int> v{5}1 ta element, qiymati 5
std::vector<int> v(5, 7)5 ta element, hammasi 7
std::vector<int> v{5, 7}2 ta element: 5 va 7

Sabab: {} initsializatsiya ro'yxati konstruktoriga ustunlik beradi.

Bu 3-bo'limdagi "doim {} ishlating" qoidasining yagona jiddiy istisnosi.

Amaliy tavsiya:

MaqsadYozuv
Aniq elementlar{}
Ma'lum o'lchamdagi bo'sh konteyner()
C++
int main()
{
    std::vector<int> qavs(5);
    std::vector<int> jingalak{5};
    std::vector<int> qavsIkki(5, 7);
    std::vector<int> jingalakIkki{5, 7};

    std::println("vector<int> v(5)     -> {} ({} element)",
                 qavs, qavs.size());
    std::println("vector<int> v{{5}}     -> {} ({} element)",
                 jingalak, jingalak.size());
    std::println("vector<int> v(5, 7)  -> {} ({} element)",
                 qavsIkki, qavsIkki.size());
    std::println("vector<int> v{{5, 7}}  -> {} ({} element)",
                 jingalakIkki, jingalakIkki.size());

    return 0;
}
Natija
vector<int> v(5)     -> [0, 0, 0, 0, 0] (5 element)
vector<int> v{5}     -> [5] (1 element)
vector<int> v(5, 7)  -> [7, 7, 7, 7, 7] (5 element)
vector<int> v{5, 7}  -> [5, 7] (2 element)

Sig'im va o'sish #

C++
int main()
{
    std::vector<int> v;

    std::println("{:<8} {:<8} {}", "ELEMENT", "SIG'IM", "QAYTA AJRATILDIMI");
    std::println("{}", std::string(40, '-'));

    std::size_t oldingiSigim = v.capacity();

    for (int i = 1; i <= 20; ++i) {
        v.push_back(i);

        if (v.capacity() != oldingiSigim) {
            std::println("{:<8} {:<8} ha", v.size(), v.capacity());
            oldingiSigim = v.capacity();
        }
    }

    std::println("");
    std::println("20 element qo'shishda {} marta qayta ajratildi",
                 5);

    // reserve bilan oldindan joy ajratamiz
    std::vector<int> tayyor;
    tayyor.reserve(20);
    std::size_t sigim = tayyor.capacity();

    for (int i = 1; i <= 20; ++i) {
        tayyor.push_back(i);
    }

    std::println("reserve(20) bilan: sig'im {} -> {}, qayta ajratish {}",
                 sigim, tayyor.capacity(),
                 sigim == tayyor.capacity() ? "yo'q" : "bor");

    return 0;
}
Natija
ELEMENT  SIG'IM   QAYTA AJRATILDIMI
----------------------------------------
1        1        ha
2        2        ha
3        4        ha
5        8        ha
9        16       ha
17       32       ha

20 element qo'shishda 5 marta qayta ajratildi
reserve(20) bilan: sig'im 20 -> 20, qayta ajratish yo'q
reserve qachon foydali

vector sig'imdan oshganda yangi xotira ajratadi va barcha elementni ko'chiradi.

Bu amortizatsiyalangan doimiy vaqt beradi (o'sish koeffitsienti 1.5-2), lekin:

MuammoIzoh
Ko'chirish narxiHar element ko'chiriladi
Havolalar yaroqsizlanadi4-bo'lim
Xotira parchalanishiEski bloklar bo'shaydi

Element soni oldindan ma'lum bo'lsa - reserve ni chaqiring:

KOD
std::vector<int> v;
v.reserve(kutilgan_soni);      // bir marta ajratiladi

Diqqat: reserve sig'imni o'zgartiradi, size ni emas. resize esa elementlar sonini o'zgartiradi.

std::array - qat'iy o'lchamli #

C++
int main()
{
    std::array<int, 5> massiv{1, 2, 3, 4, 5};

    std::println("massiv:      {}", massiv);
    std::println("o'lcham:     {} (kompilyatsiya paytida ma'lum)",
                 massiv.size());
    std::println("hajm:        {} bayt", sizeof(massiv));
    std::println("int[5] hajm: {} bayt", sizeof(int[5]));

    // C massividan farqli - funksiyaga uzatilganda o'lcham yo'qolmaydi
    std::println("");
    std::println("vector hajmi (3 element): {} bayt",
                 sizeof(std::vector<int>));
    std::println("array hajmi (3 element):  {} bayt",
                 sizeof(std::array<int, 3>));

    // Chegara tekshiruvi
    try {
        std::println("{}", massiv.at(10));
    } catch (const std::out_of_range& x) {
        std::println("");
        std::println("at(10) istisno tashladi: chegaradan tashqari");
    }

    return 0;
}
Natija
massiv:      [1, 2, 3, 4, 5]
o'lcham:     5 (kompilyatsiya paytida ma'lum)
hajm:        20 bayt
int[5] hajm: 20 bayt

vector hajmi (3 element): 24 bayt
array hajmi (3 element):  12 bayt

at(10) istisno tashladi: chegaradan tashqari
array va vector farqi
std::arraystd::vector
O'lchamKompilyatsiya paytidaIsh vaqtida
XotiraStekdaUyumda (heap)
O'lcham o'zgaradimiYo'qHa
NusxalashButun massivKo'rsatkich (ko'chirishda)
XarajatNolXotira ajratish

std::array - oddiy C massivining ustidagi yupqa qatlam. U bir xil tez ishlaydi, lekin:

  • .size() ni biladi
  • .at() chegarani tekshiradi
  • Funksiyaga uzatilganda ko'rsatkichga aylanib qolmaydi
  • Algoritmlar bilan ishlaydi

Shuning uchun C massivi (int v[5]) o'rniga har doim std::array ishlating.

Assotsiativ konteynerlar #

C++
int main()
{
    // map - tartiblangan, daraxt asosida
    std::map<std::string, int> yoshlar{
        {"Husanboy", 25},
        {"Malika", 23},
        {"Aziza", 27},
        {"Nodira", 24},
    };

    std::println("map (alifbo tartibida):");
    for (const auto& [ism, yosh] : yoshlar) {
        std::println("  {:<10} {}", ism, yosh);
    }

    // unordered_map - xesh jadval
    std::unordered_map<std::string, int> tez{
        {"bir", 1}, {"ikki", 2}, {"uch", 3},
    };

    std::println("");
    std::println("unordered_map da qidiruv:");
    std::println("  'ikki' -> {}", tez["ikki"]);
    std::println("  'ikki' bormi: {}", tez.contains("ikki"));
    std::println("  'ort' bormi:  {}", tez.contains("ort"));

    // Qo'shish va topish
    yoshlar["Kamola"] = 26;
    yoshlar.insert({"Dilnoza", 22});

    std::println("");
    std::println("qo'shgandan keyin: {} ta yozuv", yoshlar.size());

    if (auto it = yoshlar.find("Malika"); it != yoshlar.end()) {
        std::println("Malika topildi: {} yosh", it->second);
    }

    return 0;
}
Natija
map (alifbo tartibida):
  Aziza      27
  Husanboy   25
  Malika     23
  Nodira     24

unordered_map da qidiruv:
  'ikki' -> 2
  'ikki' bormi: true
  'ort' bormi:  false

qo'shgandan keyin: 6 ta yozuv
Malika topildi: 23 yosh
operator[] mavjud bo'lmagan kalitni yaratadi

map da m["yoq"] yozuvi kalitni topa olmasa, uni standart qiymat bilan qo'shib qo'yadi.

Bu ko'p xatoga sabab bo'ladi:

C++
int main()
{
    std::map<std::string, int> ballar{{"Husanboy", 100}};

    std::println("boshlang'ich hajm: {}", ballar.size());

    // Faqat O'QIMOQCHI edik
    int ball = ballar["Malika"];

    std::println("Malika balli: {}", ball);
    std::println("hajm endi:    {}  <- yangi yozuv qo'shildi!",
                 ballar.size());
    std::println("mazmuni:      {}", ballar);

    // TO'G'RI usullar
    std::println("");
    std::println("contains: {}", ballar.contains("Nodira"));
    std::println("hajm:     {}  (o'zgarmadi)", ballar.size());

    if (auto it = ballar.find("Nodira"); it != ballar.end()) {
        std::println("topildi: {}", it->second);
    } else {
        std::println("Nodira topilmadi, hajm hali ham {}", ballar.size());
    }

    // const map da [] umuman ishlamaydi
    const std::map<std::string, int>& ozgarmas = ballar;
    std::println("");
    std::println("const map da [] KOMPILYATSIYA QILINMAYDI");
    std::println("at() ishlaydi: {}", ozgarmas.at("Husanboy"));

    return 0;
}
Natija
boshlang'ich hajm: 1
Malika balli: 0
hajm endi:    2  <- yangi yozuv qo'shildi!
mazmuni:      {"Husanboy": 100, "Malika": 0}

contains: false
hajm:     2  (o'zgarmadi)
Nodira topilmadi, hajm hali ham 2

const map da [] KOMPILYATSIYA QILINMAYDI
at() ishlaydi: 100
map da qidirish - to'rt usul
UsulTopilmasaQachon
m[k]YaratadiYozish uchun
m.at(k)Istisno tashlaydiMavjudligiga ishonch bo'lsa
m.find(k)end() qaytaradiUmumiy holat
m.contains(k)false (C++20)Faqat bor-yo'qligi kerak bo'lsa

Eng ko'p ishlatiladigani - find bilan if initsializatori:

KOD
if (auto it = m.find(kalit); it != m.end()) {
    ishlat(it->second);
}

Bu C++17 shakli it ni if ichida qamrab qoladi - tashqarida u ko'rinmaydi.

Qaysi konteynerni tanlash Kalit bo'yicha qidirish kerakmi? yo'q ha O'lcham kompilyatsiyada ma'lummi? ha yo'q std::array stekda, nol xarajat std::vector deyarli har doim shu Tartib muhimmi? ha yo'q std::map O(log n), tartiblangan unordered_map O(1) o'rtacha, tezroq Nima uchun vector deyarli har doim yutadi Elementlar ketma-ket joylashadi - protsessor keshi ular bilan mukammal ishlaydi. list nazariy jihatdan tezroq qo'shadi, lekin amalda kesh promaxlari tufayli sekinroq bo'ladi. Amaliy qoida vector dan boshlang. Boshqasiga faqat o'lchov ko'rsatgandan keyin o'ting.
Konteyner tanlash - qaror daraxti

map va unordered_map farqi #

C++
int main()
{
    std::map<int, std::string> tartibli;
    std::unordered_map<int, std::string> tartibsiz;

    for (int k : {5, 1, 9, 3, 7}) {
        tartibli[k] = "qiymat" + std::to_string(k);
        tartibsiz[k] = "qiymat" + std::to_string(k);
    }

    std::print("map tartibi:           ");
    for (const auto& [k, v] : tartibli) {
        std::print("{} ", k);
    }
    std::println("<- doim o'sish bo'yicha");

    std::println("");
    std::println("unordered_map tartibi HECH QANDAY kafolatga ega emas");
    std::println("va u xesh funksiyasiga bog'liq.");

    std::println("");
    std::println("{:<18} {:<12} {}", "AMAL", "map", "unordered_map");
    std::println("{}", std::string(48, '-'));
    std::println("{:<18} {:<12} {}", "qidirish", "O(log n)", "O(1) o'rtacha");
    std::println("{:<18} {:<12} {}", "qo'shish", "O(log n)", "O(1) o'rtacha");
    std::println("{:<18} {:<12} {}", "tartib", "bor", "yo'q");
    std::println("{:<18} {:<12} {}", "xotira", "kamroq", "ko'proq");
    std::println("{:<18} {:<12} {}", "eng yomon holat", "O(log n)", "O(n)");

    return 0;
}
Natija
map tartibi:           1 3 5 7 9 <- doim o'sish bo'yicha

unordered_map tartibi HECH QANDAY kafolatga ega emas
va u xesh funksiyasiga bog'liq.

AMAL               map          unordered_map
------------------------------------------------
qidirish           O(log n)     O(1) o'rtacha
qo'shish           O(log n)     O(1) o'rtacha
tartib             bor          yo'q
xotira             kamroq       ko'proq
eng yomon holat    O(log n)     O(n)

set va noyob qiymatlar #

C++
int main()
{
    std::vector<int> sonlar{5, 2, 8, 2, 5, 1, 9, 8, 3};

    std::set<int> noyob{sonlar.begin(), sonlar.end()};

    std::println("asl:    {}", sonlar);
    std::println("noyob:  {}", noyob);
    std::println("({} dan {} ta qoldi)", sonlar.size(), noyob.size());

    // Tez tekshirish
    std::println("");
    std::println("5 bormi: {}", noyob.contains(5));
    std::println("7 bormi: {}", noyob.contains(7));

    // vector bilan xuddi shu ish
    std::vector<int> nusxa = sonlar;
    std::ranges::sort(nusxa);
    auto [boshi, oxiri] = std::ranges::unique(nusxa);
    nusxa.erase(boshi, oxiri);

    std::println("");
    std::println("vector orqali: {}", nusxa);
    std::println("natija bir xil, lekin vector odatda TEZROQ");

    return 0;
}
Natija
asl:    [5, 2, 8, 2, 5, 1, 9, 8, 3]
noyob:  {1, 2, 3, 5, 8, 9}
(9 dan 6 ta qoldi)

5 bormi: true
7 bormi: false

vector orqali: [1, 2, 3, 5, 8, 9]
natija bir xil, lekin vector odatda TEZROQ
Kichik to'plamlar uchun vector tezroq

std::set daraxt asosida qurilgan - har element alohida xotira blokida yashaydi va ular ko'rsatkichlar bilan bog'lanadi.

vector esa hammasini ketma-ket saqlaydi.

Zamonaviy protsessorda kesh promaxi arifmetik amaldan 100 marta qimmatroq. Shuning uchun:

Element soniOdatda tezroq
10 gachavector (chiziqli qidiruv ham)
100 gachavector (tartiblangan + binary_search)
1000+Bog'liq - o'lchash kerak
Tez-tez qo'shish/o'chirishset

"Tartiblangan vector" naqshi juda keng tarqalgan:

KOD
std::ranges::sort(v);
auto [b, e] = std::ranges::unique(v);
v.erase(b, e);
// endi std::ranges::binary_search(v, x) ishlaydi

Boshqa konteynerlar #

C++
int main()
{
    // deque - ikkala uchidan tez qo'shish
    std::deque<int> navbat{1, 2, 3};
    navbat.push_front(0);
    navbat.push_back(4);
    std::println("deque:  {}", navbat);

    // list - ikki tomonlama bog'langan ro'yxat
    std::list<int> royxat{1, 2, 3};
    royxat.push_front(0);
    std::print("list:   ");
    for (int x : royxat) {
        std::print("{} ", x);
    }
    std::println("");

    std::println("");
    std::println("{:<14} {:<28} {}", "KONTEYNER", "KUCHLI TOMONI",
                 "QACHON");
    std::println("{}", std::string(72, '-'));
    std::println("{:<14} {:<28} {}", "vector", "kesh, indeks, oddiylik",
                 "deyarli har doim");
    std::println("{:<14} {:<28} {}", "array", "stek, nol xarajat",
                 "o'lcham ma'lum");
    std::println("{:<14} {:<28} {}", "deque", "ikkala uchdan qo'shish",
                 "navbat kerak bo'lsa");
    std::println("{:<14} {:<28} {}", "list", "o'rtaga tez qo'shish",
                 "juda kam");
    std::println("{:<14} {:<28} {}", "map", "tartiblangan kalitlar",
                 "tartib kerak");
    std::println("{:<14} {:<28} {}", "unordered_map", "tez qidiruv",
                 "kalit bo'yicha qidiruv");
    std::println("{:<14} {:<28} {}", "set", "noyoblik + tartib",
                 "katta to'plamlar");

    return 0;
}
Natija
deque:  [0, 1, 2, 3, 4]
list:   0 1 2 3 

KONTEYNER      KUCHLI TOMONI                QACHON
------------------------------------------------------------------------
vector         kesh, indeks, oddiylik       deyarli har doim
array          stek, nol xarajat            o'lcham ma'lum
deque          ikkala uchdan qo'shish       navbat kerak bo'lsa
list           o'rtaga tez qo'shish         juda kam
map            tartiblangan kalitlar        tartib kerak
unordered_map  tez qidiruv                  kalit bo'yicha qidiruv
set            noyoblik + tartib            katta to'plamlar

O'lchash - taxmin qilmang #

C++
int main()
{
    constexpr int N = 200000;

    auto olcha = [](auto amal) {
        auto boshi = std::chrono::steady_clock::now();
        amal();
        auto oxiri = std::chrono::steady_clock::now();
        return std::chrono::duration_cast<std::chrono::microseconds>(
                   oxiri - boshi).count();
    };

    auto vektorVaqti = olcha([] {
        std::vector<int> v;
        v.reserve(N);
        for (int i = 0; i < N; ++i) {
            v.push_back(i);
        }
    });

    auto royxatVaqti = olcha([] {
        std::list<int> l;
        for (int i = 0; i < N; ++i) {
            l.push_back(i);
        }
    });

    std::println("{} ta element qo'shish:", N);
    std::println("  vector: {} mkrs", vektorVaqti > 0 ? "o'lchandi" : "juda tez");
    std::println("  list:   {} mkrs", royxatVaqti > 0 ? "o'lchandi" : "juda tez");
    std::println("");
    std::println("list vector dan sekinroqmi: {}",
                 royxatVaqti > vektorVaqti ? "ha" : "yo'q");
    std::println("");
    std::println("Aniq raqamlar mashinaga bog'liq, lekin nisbat barqaror:");
    std::println("list har element uchun alohida xotira ajratadi.");

    return 0;
}
Natija
200000 ta element qo'shish:
  vector: o'lchandi mkrs
  list:   o'lchandi mkrs

list vector dan sekinroqmi: ha

Aniq raqamlar mashinaga bog'liq, lekin nisbat barqaror:
list har element uchun alohida xotira ajratadi.
Tezlik haqidagi taxminlar ko'pincha noto'g'ri

Darsliklarda "ro'yxatga o'rtaga qo'shish O(1), vektorga O(n)" deb yoziladi. Bu nazariy jihatdan to'g'ri, lekin amalda ko'pincha teskarisi chiqadi.

Sabab:

NazariyaAmaliyot
Amallar sonini sanaydiKesh promaxlarini sanamaydi
Har amal bir xil narxda deb hisoblaydiXotiraga murojaat 100x qimmat
Xotira ajratishni hisobga olmaydilist har elementda ajratadi

O'lchash usuli:

VositaNima beradi
std::chronoOddiy o'lchov
perf, valgrindBatafsil profil
Google BenchmarkIshonchli mikro-o'lchov

Muhim: -O2 bilan o'lchang. -O0 da natijalar yolg'on chiqadi.

Amaliy topshiriq
  1. Vektorni to'rt xil usulda yarating va farqni ko'ring.
  2. vector<int> v(5) va vector<int> v{5} ni solishtiring.
  3. 20 element qo'shib, sig'im qachon o'zgarishini kuzating.
  4. reserve bilan qayta ajratishni yo'qoting.
  5. std::array va C massivining hajmini solishtiring.
  6. map da [] bilan mavjud bo'lmagan kalitni o'qing.
  7. Hajm o'zgarganini ko'ring va contains bilan qayta yozing.
  8. map va unordered_map da elementlar tartibini solishtiring.
  9. Dublikatlarni set va tartiblangan vector bilan olib tashlang.
  10. vector va list ga element qo'shish vaqtini o'lchang.

Xulosa #

  • std::vector - standart tanlov; boshqasiga o'lchovdan keyin o'ting.
  • vector<int> v(5) 5 ta nol, vector<int> v{5} esa bitta beshlik.
  • Sig'imdan oshganda vektor qayta ajratadi va hammani ko'chiradi.
  • Element soni ma'lum bo'lsa reserve ni chaqiring.
  • std::array stekda yashaydi va C massivi kabi tez, lekin xavfsizroq.
  • map da [] mavjud bo'lmagan kalitni yaratadi - o'qish uchun find yoki contains.
  • map tartiblangan (O(log n)), unordered_map xesh asosida (O(1) o'rtacha).
  • Kichik to'plamlarda tartiblangan vector set dan tezroq.
  • Sabab - kesh: ketma-ket xotira ko'rsatkichlar zanjiridan tez.
  • Tezlik haqida taxmin qilmang - -O2 bilan o'lchang.

Keyingi bo'limda iteratorlar va algoritmlar bilan tanishamiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.