Learn C++ STL

lesson ২ / ৯ · vector: যে container-টা সবার আগে হাতে আসে

Module ২ · vector: যে container-টা সবার আগে হাতে আসে

vector-এর প্রতিটা operation, একটা একটা করে, খরচসহ

Freeপড়া

এই lesson-এ যা শিখবে

  • পেশাদার programmer vector-এর উপর যত operation call করে, push_back থেকে data পর্যন্ত, সবগুলো ঠিক syntax-এ লিখতে পারবে।
  • প্রতিটার খরচ বলতে পারবে, O(1), amortised O(1) নাকি O(n), আর খরচটা কোথা থেকে আসে, বুঝিয়ে বলতে পারবে।
  • খালি vector-এ যে তিনটা call ভেঙে পড়ে, সেগুলো চিনতে পারবে, আর কোনো element বাদ না দিয়ে loop-এর ভিতরে erase করতে পারবে।

Kenji 100,000টা সংখ্যা পড়ে, আর ওর সেগুলো লাগবে উল্টো ক্রমে। তাই ও প্রতিটা নতুন সংখ্যা vector-এর সামনে insert করে, আর ওর program নেয় প্রায় এক সেকেন্ডের তিন ভাগের এক ভাগ। Amara লেখে push_back, তারপর vector-টা পেছন থেকে হাঁটে। ওরটা নেয় আধা millisecond, মানে প্রায় ছয়শো গুণ দ্রুত। দুটো program-ই ঠিক। তফাতটা এই lesson-এর একটা লাইনে: প্রতিটা call-এর খরচ।

খরচের লাইন কীভাবে পড়বে

নিচের প্রতিটা operation শেষ হয় এক সারির একটা table দিয়ে: call, তার খরচ, আর কেন। খরচ লেখা হয় big-O notation-এ। এটা বলে vector বড় হলে কাজ কীভাবে বাড়ে, কত nanosecond লাগে সেটা না।

খরচমানেউদাহরণ
O(1)size যা-ই হোক, একই অল্প কাজv[i], element 10টা হোক বা 1 কোটি
amortised O(1)অনেকগুলো call মিলিয়ে গড়ে constant; মাঝে মাঝে একটা call দামি পড়েpush_back, যেটা মাঝে মাঝে reallocate করে
O(n)যতগুলো element ছোঁয়, কাজ ততই বাড়েসামনে insert করলে প্রতিটা element সরে

এখানে প্রতিটা খরচ C++ standard নিজেই বেঁধে দেয়, আর Module 0-এ যেমন দেখেছ, cppreference-এর page-এ প্রতিটা মিলিয়ে নিতে পারো। তাই খরচের লাইন হলো কাজ কীভাবে বাড়বে তার একটা প্রতিশ্রুতি, যেটা প্রতিটা compiler-এ সত্যি।

push_back আর pop_back

push_back(x) শেষ element-এর পরে x যোগ করে। pop_back() শেষ element-টা সরিয়ে দেয়, আর কিছুই return করে না। শেষ মানটা কাজে লাগাতে চাইলে আগে back() দিয়ে পড়ে নাও।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{10, 20};
    v.push_back(30);
    cout << "after push_back: size " << v.size() << ", last " << v.back() << '\n';
    v.pop_back();
    cout << "after pop_back: size " << v.size() << ", last " << v.back() << '\n';
    return 0;
}
after push_back: size 3, last 30
after pop_back: size 2, last 20
Callখরচকারণ
v.push_back(x)amortised O(1)সাধারণত একটা ফাঁকা বাক্স ভরে; block ভরা থাকলে n-টা element একবার copy করে, আর দ্বিগুণ করার নিয়মে সেটা কমই ঘটে
v.pop_back()O(1)size এক কমে; কিছু সরে না, capacity-ও যেমন ছিল তেমন থাকে

সাবধান: খালি vector-এ pop_back() undefined behaviour। Playground-এ এটা শেষ হয়েছে সফল badge নিয়ে, আর পরে size() print করেছে 18446744073709551615। Vector-এর শেষটা ওর শুরুরও এক ধাপ আগে সরে গেছে, আর কেউ থামায়নি। যে pop_back-এর input তোমার হাতে নেই, তার আগে প্রতিবার !v.empty() check করো।

emplace_back(args) হলো push_back-এর ভাই। এটা element-টা ওর অংশগুলো থেকে সরাসরি জায়গাতেই বানায়, তাই v.emplace_back(3, 4) একটা vector<pair<int, int>>-এ pair (3, 4) যোগ করে। খরচ একই। তাই দুটো call-ই শেষে যোগ করে, আর সস্তা শুধু শেষ মাথাটাই।

size, empty আর capacity

যেকোনো vector-কে এই তিনটা প্রশ্ন করা যায়, আর প্রতিটার উত্তর আসে handle থেকেই, element-গুলো না ছুঁয়ে।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;
    cout << v.size() << ' ' << v.empty() << ' ' << v.capacity() << '\n';
    v.push_back(7);
    v.push_back(8);
    v.push_back(9);
    cout << v.size() << ' ' << v.empty() << ' ' << v.capacity() << '\n';
    return 0;
}
0 1 0
3 0 4
Callখরচকারণ
v.size(), v.empty(), v.capacity()O(1)handle জানে block কোথায় শুরু, element-গুলো কোথায় শেষ, আর block কোথায় শেষ; প্রতিটা উত্তর একটা বিয়োগ বা একটা তুলনা

সাবধান: empty() print হয় 1 বা 0 হিসেবে, কারণ এটা একটা bool। এটা কিছুই খালি করে না; সেটা করে clear()। তাই if (v.empty()) একটা প্রশ্ন, কোনো কাজ না।

operator[] আর at

দুটোই index i-এর element পড়ে বা লেখে। v[i] তোমার কথায় বিশ্বাস করে। v.at(i) আগে দেখে নেয় i size()-এর চেয়ে ছোট কি না, আর ছোট না হলে একটা exception দিয়ে program থামিয়ে দেয়।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{10, 20, 30, 40, 50};
    v[1] = 25;
    v.at(2) = 35;
    cout << v[1] << ' ' << v.at(2) << '\n';
    cout << v.at(5) << '\n';
    return 0;
}

Playground-এ run-টা শেষ হয়েছে Runtime error badge নিয়ে। Output pane ছিল খালি, আর error stream-এ এসেছে এটা:

terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 5) >= this->size() (which is 5)

দ্বিতীয় লাইনটা পড়ো: তুমি কোন index চেয়েছিলে, আর কোন size-এ গিয়ে আটকেছে। এমনকি 25 35-ও কখনো screen-এ আসেনি। Program থামার সময় cout ওই লাইনটা তখনো buffer-এ ধরে রেখেছিল, আর crash হলে flush বাদ পড়ে যায়, Module 1-এর fast input আর output-এর lesson যেমন সাবধান করেছিল। Lesson 01-এ Bob-এর v[v.size()]-এর সাথে মিলিয়ে দেখো: ওটা চুপচাপ একটা 0 print করে সফল হয়েছিল। দেখতে ঠিক অথচ ভুল উত্তরের চেয়ে জোরে থেমে যাওয়া অনেক ভালো।

Callখরচকারণ
v[i], v.at(i)O(1)element-এর address হলো block-এর শুরু থেকে i বাক্স পরে; at শুধু একটা তুলনা যোগ করে

সাবধান: code লেখা আর test করার সময় at() ব্যবহার করো, বিশেষ করে যখন index input থেকে হিসাব করে বের করা। Contest-এর code-এ চলে [], কারণ check করা index প্রতিবার access-এ একটা তুলনা খরচ করে। তাই [] দ্রুত, কিন্তু তোমার কথায় বিশ্বাস করে চলে; আর at() নিজে যাচাই করে নেয়।

front আর back

v.front() হলো প্রথম element, v[0]। v.back() হলো শেষটা, v[v.size() - 1], কিন্তু যে বিয়োগে ভুল হতে পারে সেটা ছাড়াই।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> temps{18, 21, 25, 19};
    cout << "first " << temps.front() << ", last " << temps.back() << '\n';
    temps.back() = 20;
    cout << "last now " << temps.back() << '\n';
    return 0;
}
first 18, last 19
last now 20
Callখরচকারণ
v.front(), v.back()O(1)জানা একটা address-এ একটা read

সাবধান: Zara-র প্রথম test সবসময় একটা খালি vector। খালি temps-এ temps.front() কোনো বার্তা ছাড়াই compile হয়েছে। Playground-এ শেষ হয়েছে Runtime error badge নিয়ে, আর কিছুই print করেনি, এমনকি আগের লেখাটাও না। খালি vector-এ back()-ও ঠিক একই কাণ্ড করেছে। তাই খালি vector-এ দুটোই undefined, আর ওদের বাঁচাতে পারে শুধু একটা empty() check।

একটা position-এ insert

v.insert(pos, x) x বসায় pos position-এর ঠিক আগে। Position হলো একটা iterator, মানে vector-এর একটা জায়গার চিহ্ন। আপাতত এটা লেখো v.begin() + i হিসেবে, মানে "index i-এর জায়গা"। Iterator ঠিকমতো বুঝিয়ে বলবে Module 11।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{10, 20, 40};
    v.insert(v.begin() + 2, 30);
    v.insert(v.begin(), 5);
    v.insert(v.end(), 50);
    cout << "v:";
    for (int x : v) {
        cout << ' ' << x;
    }
    cout << '\n';
    return 0;
}
v: 5 10 20 30 40 50

Index i-এ জায়গা বানাতে i থেকে শেষ পর্যন্ত প্রতিটা element এক বাক্স ডানে সরে। v.begin()-এ insert করলে সবগুলোই সরে। v.end()-এ insert করলে একটাও সরে না, আর সেটা push_back-এর মতোই।

Callখরচকারণ
v.insert(v.begin() + i, x)O(n - i), তাই সামনে O(n)position-এর পরের element-গুলো এক ঘর ডানে সরে; block ভরা থাকলে reallocation-ও হয়

সাবধান: v.end()-এর পরের কোনো position undefined, আর কেউ এটা check করে না। তিনটা element-এর vector-এ v.insert(v.begin() + 5, 99) Playground-এ সফল হয়েছে আর size বলেছে 4, যদিও index 5 বলে কিছু ছিলই না। তাই position বানাও এমন index থেকে, যেটা তুমি check করে নিয়েছ: 0 থেকে size() পর্যন্ত, দুই মাথাসহ।

একটা position-এ erase, আর loop-এর ভিতরে erase

v.erase(pos) pos-এর element-টা সরিয়ে দেয়। v.erase(first, last) একটা range সরায়: first থেকে শুরু, last-এর ঠিক আগে শেষ। দুটোই একটা iterator return করে, যেটা দেখায় সরানো element-এর জায়গায় এখন যে element বসে আছে।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{5, 10, 20, 30, 40, 50};
    v.erase(v.begin());
    v.erase(v.begin() + 1, v.begin() + 3);
    cout << "v:";
    for (int x : v) {
        cout << ' ' << x;
    }
    cout << '\n';
    return 0;
}
v: 10 40 50

প্রথম call 5-কে সরিয়েছে। দ্বিতীয়টা, যা বাকি ছিল তার index 1 আর 2, মানে 20 আর 30 সরিয়েছে। সরানো element-এর পরের সবকিছু বাঁয়ে সরে এসে ফাঁকটা ভরে দেয়।

Callখরচকারণ
v.erase(v.begin() + i)O(n - i), তাই সামনে O(n)ফাঁকের পরের element-গুলো বাঁয়ে সরে; capacity বদলায় না

এবার {2, 4, 5, 6} থেকে সব জোড় সংখ্যা সরাও। Bob সেই loop-টাই লেখে, যেটা ও একটা array-এর জন্য লিখত।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{2, 4, 5, 6};
    for (size_t i = 0; i < v.size(); i++) {
        if (v[i] % 2 == 0) {
            v.erase(v.begin() + i);
        }
    }
    cout << "v:";
    for (int x : v) {
        cout << ' ' << x;
    }
    cout << '\n';
    return 0;
}
v: 4 5

4 বেঁচে গেছে। Index 0-এ 2 erase হওয়ার পরে 4 সরে এসে বসেছে index 0-এ, আর loop ততক্ষণে চলে গেছে index 1-এ। তাই প্রতিটা erase করা element-এর ঠিক পরেরটা কখনো test-ই হয় না।

নিরাপদ loop erase-এর return করা মানটা কাজে লাগায়। Erase করলে যেখানে আছ সেখানেই থাকো; নইলে এক ধাপ সামনে যাও।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{2, 4, 5, 6};
    for (auto it = v.begin(); it != v.end();) {
        if (*it % 2 == 0) {
            it = v.erase(it);
        } else {
            ++it;
        }
    }
    cout << "v:";
    for (int x : v) {
        cout << ' ' << x;
    }
    cout << '\n';
    return 0;
}
v: 5

*it পড়ে iterator যে element দেখায় সেটা, ঠিক যেমন *p একটা pointer দিয়ে পড়ে। এই loop ঠিক আছে, কিন্তু প্রতিটা erase তবুও বাকিগুলোকে সরায়, তাই অনেকগুলো erase হলে প্রতিটার খরচ O(n)। Amara সবকিছু এক pass-এ সরায় erase-remove idiom দিয়ে। <algorithm>-এর remove_if যে element-গুলো রাখতে হবে সেগুলো সামনে এনে জড়ো করে, আর বলে দেয় ওরা কোথায় শেষ। তারপর একটা erase লেজটা কেটে ফেলে।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

bool is_even(int x) {
    return x % 2 == 0;
}

int main() {
    vector<int> v{2, 4, 5, 6, 7, 8};
    v.erase(remove_if(v.begin(), v.end(), is_even), v.end());
    cout << "v:";
    for (int x : v) {
        cout << ' ' << x;
    }
    cout << '\n';
    return 0;
}
v: 5 7
n-টার মধ্যে k-টা element সরানোর উপায়খরচ
loop-এর ভিতরে it = v.erase(it)O(k x n) পর্যন্ত: প্রতিটা erase বাকিগুলো সরায়
v.erase(remove_if(...), v.end())O(n): প্রতিটা element বড়জোর একবার সরে

In C++20

std::erase(v, value) আর std::erase_if(v, pred) পুরো idiom-টা এক call-এই সেরে ফেলে, আর return করে কয়টা element গেল। এরা থাকে <vector>-এর ভিতরেই। Playground-এর C++17-এ একই লাইন থেমে যায় error: 'erase_if' was not declared in this scope দিয়ে; নিচের Run button Playground খোলে C++20-এ।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{3, -1, 4, -1, -5, 9, 2};
    auto removed = erase_if(v, [](int x) { return x < 0; });
    cout << "removed " << removed << '\n';
    erase(v, 4);
    cout << "v:";
    for (int x : v) {
        cout << ' ' << x;
    }
    cout << '\n';
    return 0;
}
removed 3
v: 3 9 2

[](int x) { return x < 0; } হলো জায়গাতেই লেখা ছোট একটা function, একে বলে lambda। Lambda শেখাবে Module 12; এখানে এটাকে পড়ো "x কি negative?" হিসেবে।

Run in Compiler

তাই erase পরের position return করে, loop-কে সেটা ব্যবহার করতেই হবে, আর একসাথে অনেকগুলো সরাতে একটাই erase-remove।

clear

v.clear() সব element সরিয়ে দেয়। Size হয়ে যায় 0। Block থেকে যায়, তাই capacity বদলায় না।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v(1000, 1);
    v.clear();
    cout << "size " << v.size() << ", capacity " << v.capacity() << '\n';
    return 0;
}
size 0, capacity 1000
Callখরচকারণ
v.clear()O(n)প্রতিটা element destroy হয়; int-এর বেলায় এতে কোনো কাজ নেই, একটা string-এর বেলায় প্রতিটার memory free হয়

সাবধান: clear() memory রেখে দেয়, আর loop-এ বারবার vector ভরার সময় তুমি ঠিক এটাই চাও। তাই clear করা vector খালি, কিন্তু ছোট না।

resize আর assign

v.resize(n) size-কে ঠিক n বানায়। বাড়লে যোগ হয় 0, অথবা তুমি যে মান দাও তার copy। কমলে শেষ থেকে কাটা পড়ে। v.assign(n, x) পুরোনো element-গুলো ফেলে দিয়ে x-এর n-টা copy বসায়।

#include <iostream>
#include <vector>
using namespace std;

void show(const vector<int>& v) {
    for (int x : v) {
        cout << x << ' ';
    }
    cout << "(size " << v.size() << ")\n";
}

int main() {
    vector<int> v{1, 2, 3};
    v.resize(5);
    show(v);
    v.resize(7, 9);
    show(v);
    v.resize(2);
    show(v);
    v.assign(4, 8);
    show(v);
    return 0;
}
1 2 3 0 0 (size 5)
1 2 3 0 0 9 9 (size 7)
1 2 (size 2)
8 8 8 8 (size 4)
Callখরচকারণ
v.resize(n), v.assign(n, x)O(n)প্রতিটা নতুন element লেখা হয়, প্রতিটা সরানো element destroy হয়

সাবধান: resize(7, 9) শুধু নতুন বাক্সগুলো 9 দিয়ে ভরে; পুরোনো element-গুলোর মান যেমন ছিল তেমনই থাকে। তাই resize লম্বাই বদলায়, আর assign ভিতরের সব মান বদলে দেয়।

reserve আর shrink_to_fitIntermediate

v.reserve(n) capacity-কে অন্তত n বানায়, তাই এরপর n পর্যন্ত push করলে কখনো reallocate হয় না। Size এতে বদলায় না। v.shrink_to_fit() vector-কে বলে বাড়তি বাক্সগুলো ফেরত দিতে।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;
    v.reserve(100);
    cout << "after reserve: size " << v.size() << ", capacity " << v.capacity() << '\n';
    for (int i = 0; i < 10; i++) {
        v.push_back(i);
    }
    cout << "after 10 pushes: size " << v.size() << ", capacity " << v.capacity() << '\n';
    v.shrink_to_fit();
    cout << "after shrink_to_fit: size " << v.size() << ", capacity " << v.capacity() << '\n';
    return 0;
}
after reserve: size 0, capacity 100
after 10 pushes: size 10, capacity 100
after shrink_to_fit: size 10, capacity 10
Callখরচকারণ
v.reserve(n), v.shrink_to_fit()reallocate করলে O(n), না করলে O(1)নতুন block মানে প্রতিটা element সেখানে copy করা

সাবধান: reserve জায়গা বানায়, element না। Kenji লিখেছিল v.reserve(5); v[0] = 42;। Playground-এ এটা সফল হয়েছে, size print করেছে 0, আর v-এর উপর range-for কিছুই print করেনি। 42 গিয়ে পড়েছে এমন একটা ফাঁকা বাক্সে, যেটা element না। Element চাইলে resize বা v(n) constructor ব্যবহার করো। shrink_to_fit শুধু একটা অনুরোধ; GCC-র library সেটা রাখে, কিন্তু standard অনুযায়ী কোনো library চাইলে এটা উপেক্ষাও করতে পারে।

swap

a.swap(b), অথবা swap(a, b), একই type-এর দুইটা vector-এর ভিতরের জিনিস অদলবদল করে।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> today{1, 2, 3};
    vector<int> tomorrow(100000, 7);
    today.swap(tomorrow);
    cout << today.size() << ' ' << tomorrow.size() << '\n';
    return 0;
}
100000 3
Callখরচকারণ
a.swap(b)O(1)দুইটা handle নিজেদের তিনটা pointer অদলবদল করে; element যতই থাকুক, একটাও সরে না

সাবধান: 100,000টা element swap করতে যা খরচ, 3টা swap করতেও তাই। তাই swap হলো handle-এর অদলবদল, copy না।

begin, end আর dataIntermediate

v.begin() চিহ্ন দেয় প্রথম element-এ, আর v.end() শেষটার ঠিক পরের জায়গায়। Part Four-এর algorithm-গুলো এদের একটা জোড়া নেয়। v.data() প্রথম element-এর একটা সাধারণ pointer return করে, যে code একটা C array চায় তার জন্য।

#include <algorithm>
#include <cstdio>
#include <vector>
using namespace std;

int main() {
    vector<int> v{40, 10, 30, 20};
    sort(v.begin(), v.end());
    const int* p = v.data();
    printf("%d %d %d %d\n", p[0], p[1], p[2], p[3]);
    printf("end - begin = %d\n", (int)(v.end() - v.begin()));
    return 0;
}
10 20 30 40
end - begin = 4
Callখরচকারণ
v.begin(), v.end(), v.data()O(1)প্রতিটা এমন একটা pointer, যেটা handle আগে থেকেই ধরে রাখে

সাবধান: v.end() শেষ element-এর পরে, তাই *v.end() এমন একটা বাক্স পড়ে, যেটা নেই। দূরত্ব end - begin হলো size। তাই begin(), end() জোড়া দিয়েই প্রতিটা algorithm বলে "পুরো vector"।

সব খরচ এক table-এ, আর Kenji-র মাপ

Operationখরচ
push_back, emplace_backamortised O(1)
pop_back, back, front, [], atO(1) (খালি vector-এ undefined, শুধু at বাদে, যেটা throw করে)
size, empty, capacity, begin, end, data, swapO(1)
index i-এ insert, eraseO(n - i): সামনে O(n), শেষে O(1)
clear, resize, assignO(n)
reserve, shrink_to_fitreallocate করলে O(n)

এবার Kenji-র প্রশ্নটা মেপে দেখা যাক। নিচের program সামনে 100,000টা সংখ্যা insert করে, আর loop-টার সময় মাপে <chrono> দিয়ে, যেটা standard-এর ঘড়ি। আমরা এটা Compiler Explorer-এ তিনবার চালিয়েছি, Playground-এর -O2 -std=c++17-এ GCC 12, প্রতিবার একটা করে run। বাকি দুই run-এ বদলেছে শুধু loop-এর লাইনটা: একবার v.push_back(i);, আরেকবার v.insert(v.begin() + v.size() / 2, i);।

#include <chrono>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    const int n = 100000;
    auto start = chrono::steady_clock::now();
    vector<int> v;
    for (int i = 0; i < n; i++) {
        v.insert(v.begin(), i);
    }
    auto stop = chrono::steady_clock::now();
    chrono::duration<double, milli> took = stop - start;
    cout << v.size() << " elements in " << took.count() << " ms\n";
    return 0;
}
100000 elements in 308.871 ms
100,000টা call: push_back বনাম মাঝখানে আর সামনে insert 100,000টা call-এর সময়, GCC 12, -O2 -std=c++17, প্রতিটা একবার চালানো push_back 0.53 ms মাঝখানে insert 152 ms সামনে insert 309 ms মাঝখানে প্রতি call-এ প্রায় অর্ধেক element সরে, তাই সময়ও লাগে সামনের প্রায় অর্ধেক।

সামনে insert মোট প্রায় n2 / 2টা element সরিয়েছে, মানে প্রায় 500 কোটি। মাঝখানেরটা সরিয়েছে তার অর্ধেক, আর ওর সময়ও অর্ধেক। push_back প্রায় কিছুই সরায়নি, আর ছিল প্রায় ছয়শো গুণ দ্রুত। তাই খরচের লাইনগুলো শুধু তত্ত্ব না: মাপের চেহারাটাই ওরা।

Example 1: push_back, back আর pop_back দিয়ে একটা undo stack

Alice-এর drawing app প্রতিটা action একটা vector-এর শেষে লিখে রাখে। Undo শেষেরটা সরিয়ে দেয়। Vector-এর সস্তা মাথা শুধু পেছনেরটা, তাই এটা একদম মাপমতো একটা stack।

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main() {
    vector<string> history;
    history.push_back("draw circle");
    history.push_back("fill blue");
    history.push_back("draw square");

    for (int undo = 0; undo < 4; undo++) {
        if (history.empty()) {
            cout << "nothing to undo\n";
        } else {
            cout << "undo: " << history.back() << '\n';
            history.pop_back();
        }
    }
    cout << history.size() << " actions left\n";
    return 0;
}
undo: draw square
undo: fill blue
undo: draw circle
nothing to undo
0 actions left

চতুর্থ undo এসে vector খালি পেয়েছে, তাই pop_back call করেনি। ওই একটা if-ই হলো Zara-র test, program-এর ভিতরেই লিখে রাখা।

Run in Compiler
Example 2: Kenji-র reverse, এবার O(n)-এ

Kenji-র input উল্টো ক্রমে print করা দরকার। ও সস্তা push_back-ই রাখে, আর পেছন দিক থেকে হাঁটে। Index নিচের দিকে গোনে, তাই এটা একটা int, কখনো size_t না, যেটা 0-এর নিচে গেলে ঘুরে যেত।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    vector<int> v;
    int x;
    while (cin >> x) {
        v.push_back(x);
    }
    for (int i = (int)v.size() - 1; i >= 0; i--) {
        cout << v[i] << (i > 0 ? ' ' : '\n');
    }
    return 0;
}
9 7 5 3 1

ওই output-টা input 1 3 5 7 9-এর জন্য। (int)v.size() - 1 আগে convert করে, তারপর বিয়োগ করে, তাই খালি vector-এ পাওয়া যায় -1, আর loop চলেই না। দুইটা মানের মাঝে বসে space, আর শেষ মানটার পরে লাইন শেষ হয়।

Run in Compiler
Example 3: Amara একটা playlist গোছায়

Amara-র playlist-এ আছে গানের দৈর্ঘ্য, second-এ। ও erase-remove দিয়ে এক মিনিটের ছোট সব গান ফেলে দেয়, তারপর একটা insert দিয়ে সামনে বসায় 30 second-এর একটা intro। সামনে একবার insert করা কোনো সমস্যা না; সমস্যা ছিল Kenji-র loop ভর্তি insert।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

bool too_short(int seconds) {
    return seconds < 60;
}

int main() {
    vector<int> songs{215, 45, 190, 30, 260, 58, 175};
    songs.erase(remove_if(songs.begin(), songs.end(), too_short), songs.end());
    songs.insert(songs.begin(), 30);

    int total = 0;
    cout << "playlist:";
    for (int s : songs) {
        cout << ' ' << s;
        total += s;
    }
    cout << '\n' << songs.size() << " songs, " << total / 60 << " min " << total % 60 << " s\n";
    return 0;
}
playlist: 30 215 190 260 175
5 songs, 14 min 30 s

তিনটা গান 60 second-এর কম ছিল, আর এক pass-এই বাদ পড়ে গেছে। Intro-টা too_short দিয়ে check হয়নি, কারণ ওটা insert হয়েছে সরানোর পরে।

Run in Compiler

এটা কোথায় কাজে লাগে

  • Emacs. ওর text buffer একটা gap buffer: একটাই array, cursor-এর জায়গায় একটা ফাঁক। টাইপ করলে পুরো file না সরিয়ে শুধু ফাঁকটা ভরে। এটা আছেই এই কারণে যে সাধারণ array-এর মাঝখানে insert হলো O(n), উপরের সেই খরচের লাইন।
  • Visual Studio Code. ওর text buffer একটা piece tree, যেটা নতুন করে লেখার গল্প team 2018 সালের একটা post-এ লিখেছিল। লাইনগুলো একটা array-তে রাখা হয় না, একই কারণে: মাঝখানে insert করলে file-এর বাকিটা সরানো চলবে না।
  • C++ standard library-এর std::stack. নিচের container থেকে ওর লাগে ঠিক push_back, pop_back আর back, তাই stack<int, vector<int>> হলো vector-এর উপর বানানো একটা stack। Module 6 এটা খুলে দেখাবে।

যে ভুলগুলো সবাই করে

১. ধরে নেওয়া যে pop_back মানটা return করে।

int last = history.pop_back();

প্রতিটা command line-এ error, Playground-সহ: error: void value not ignored as it ought to be। pop_back কিছুই return করে না। আগে back() পড়ো, তারপর pop_back() call করো। তুমি একটা মান আশা করবে, কারণ "pop" শুনলে মনে হয় কিছু একটা তোমার হাতে তুলে দেবে।

২. Iterator দিয়ে erase করে, আবার সেই iterator-টাই ব্যবহার করা।

for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it % 2 == 0) {
        v.erase(it);
    }
}

কোনো command line-এই বার্তা নেই। Playground-এ {2, 4, 5, 6} দিয়ে এটা শেষ হয়েছে Runtime error badge নিয়ে। শেষ erase-এর পরে ++it end() পার হয়ে গেছে, আর loop আর কখনো end()-এর দেখা পায়নি। লেখো it = v.erase(it);, আর এক ধাপ এগোও শুধু তখন, যখন কিছু erase হয়নি। তুমি সাধারণ loop-টাই লিখবে, কারণ বাকি সব জায়গায় তো এটাই লেখো।

৩. reserve-কে এমনভাবে ব্যবহার করা, যেন ও element বানায়।

vector<int> v;
v.reserve(5);
v[0] = 42;

কোনো বার্তা নেই, Playground-এ সফল-ও, কিন্তু v.size() তখনো 0, আর range-for কিছুই print করে না। Size-এর বাইরে v[0] লেখা undefined behaviour, capacity-র ভিতরে হলেও। লেখো vector<int> v(5); বা v.resize(5);। তুমি দুটো গুলিয়ে ফেলবে, কারণ দুটোই একটা সংখ্যা নেয়, আর দুটোই জায়গা বানায়।

৪. খালি হতে পারে এমন vector-এ front() বা back()।

vector<int> temps;
cout << "first reading: " << temps.front() << '\n';

কোনো command line-এই বার্তা নেই; Playground-এ Runtime error badge, আর কোনো output-ই নেই। আগে if (!temps.empty()) check করো। তুমি check-টা বাদ দেবে, কারণ sample input-এ সবসময় data থাকে, আর Zara-র খালি test-এ থাকে না।

মাথা খাটাও

Bob একটা index loop দিয়ে {-1, -2, 3} থেকে negative সংখ্যাগুলো সরায়।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{-1, -2, 3};
    for (size_t i = 0; i < v.size(); i++) {
        if (v[i] < 0) {
            v.erase(v.begin() + i);
        }
    }
    cout << "v:";
    for (int x : v) {
        cout << ' ' << x;
    }
    cout << '\n';
    return 0;
}

এটা print করে v: -2 3। ঠিক কেন -2 বেঁচে গেল আর -1 গেল না, বুঝিয়ে বলো। তারপর index loop রেখেই, iterator বা remove_if ছাড়া, এটা ঠিক করার দুইটা উপায় বলো।

প্রতিটা pass-এর পরে i আর vector-টা trace করো। i যখন 1 হয়, -2 তখন কোথায়? তারপর ভাবো, erase-এর পরে i না সরালে কী বদলাত, বা loop উল্টো দিক থেকে হাঁটলে।

অনুশীলন ১সহজ

একটা খাতা দেরিতে এসেছে, আর Amara-কে ওর নম্বরটা তালিকার k নম্বর জায়গায় বসাতে হবে, 1 থেকে গুনে। জায়গা 1 মানে সবার আগে; জায়গা n + 1 মানে সবার শেষে।

Input. এক লাইনে n, তারপর n-টা পূর্ণসংখ্যা, তারপর এক লাইনে k আর x।

Output. n + 1টা মান এক লাইনে, মাঝে একটা করে space, আর x থাকবে k নম্বর জায়গায়।

Constraints. 1 <= n <= 200000, 1 <= k <= n + 1। প্রতিটা মান -1000000000 থেকে 1000000000-এর মধ্যে।

Sample. Input 5, 10 20 30 40 50 আর 3 25 দিলে 10 20 25 30 40 50।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<int> marks(n);
    for (int i = 0; i < n; i++) {
        cin >> marks[i];
    }
    int k, x;
    cin >> k >> x;

    // Insert x so that it becomes the k-th value, counting from 1,
    // then print all n + 1 values on one line.

    return 0;
}

insert-at-position নামে গ্রেড হয়। Hidden test-এ আছে k = 1 আর k = n + 1, যেগুলো এক ঘর সরে যাওয়া position ধরে ফেলে।

Run in Compiler
অনুশীলন ২মাঝারি

Sensor বিগড়ে গেলে Zara-র weather station একটা negative সংখ্যা লিখে রাখে। প্রতিটা negative reading সরাও, আর যা থাকে তা ক্রম ঠিক রেখে print করো।

Input. এক লাইনে n, তারপর n-টা পূর্ণসংখ্যা।

Output. যে মানগুলো 0 বা তার বেশি, ওদের ক্রমে, এক লাইনে, মাঝে একটা করে space। কিছুই না থাকলে print করো empty।

Constraints. 1 <= n <= 20000। প্রতিটা পূর্ণসংখ্যা -1000000000 থেকে 1000000000-এর মধ্যে।

Sample. Input 7 আর 3 -1 4 -1 -5 9 2 দিলে 3 4 9 2।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<int> readings(n);
    for (int i = 0; i < n; i++) {
        cin >> readings[i];
    }

    // Remove every negative reading with erase, without skipping any,
    // then print the rest on one line, or "empty".

    return 0;
}

drop-negatives নামে গ্রেড হয়। Hidden test-এ আছে পরপর দুইটা negative, সব negative, আর একটাও negative না।

Run in Compiler
অনুশীলন ৩মাঝারি

David তিনটা command দিয়ে একটা তালিকা test করে। push x শেষে x যোগ করে, আর print তালিকাটা print করে। pop শেষ মানটা সরায়, আর তালিকা খালি হলে কিছুই করে না।

Input. এক লাইনে n, তারপর n-টা command, প্রতি লাইনে একটা।

Output. প্রতিটা print-এর জন্য এক লাইন: মানগুলো, মাঝে একটা করে space, অথবা empty।

Constraints. 1 <= n <= 200000, 1 <= x <= 1000। সব print command মিলিয়ে বড়জোর 100000টা মান print হয়।

Sample. Input 7, তারপর push 5, push 8, print, pop, pop, pop, print দিলে 5 8 আর empty।

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<int> list;
    for (int i = 0; i < n; i++) {
        string command;
        cin >> command;
        // "push": read x and add it at the end.
        // "pop": remove the last value, only if there is one.
        // "print": print the list on one line, or "empty".
    }
    return 0;
}

push-pop-print নামে গ্রেড হয়। Sample-এর তৃতীয় pop একটা খালি তালিকায়, আর hidden test-এ এমন আরও অনেক আছে।

Run in Compiler
অনুশীলন ৪কঠিন

Bob-এর form একসাথে কয়েকটা item delete করে। ও পায় তালিকাটা, আর কোন কোন জায়গা delete করতে হবে, 1 থেকে গোনা আর ছোট থেকে বড় ক্রমে। প্রতিটা জায়গার জন্য একবার করে erase call করলে প্রতিবার খরচ O(n), তাই কাজটা এক pass-এ সারো।

Input. এক লাইনে n আর m, এক লাইনে n-টা পূর্ণসংখ্যা, তারপর এক লাইনে m-টা জায়গা p1 < p2 < ... < pm।

Output. যে মানগুলো থেকে যায়, ওদের ক্রমে, এক লাইনে, মাঝে একটা করে space, অথবা empty।

Constraints. 1 <= m <= n <= 200000, 1 <= pi <= n। প্রতিটা মান -1000000000 থেকে 1000000000-এর মধ্যে।

Sample. Input 6 3, 10 20 30 40 50 60 আর 1 3 4 দিলে 20 50 60।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;
    vector<int> items(n);
    for (int& x : items) {
        cin >> x;
    }
    vector<int> places(m);
    for (int& p : places) {
        cin >> p;
    }

    // Keep a second index that says where the next kept value goes,
    // copy each kept value there, then resize the vector once.

    return 0;
}

আলাদা করে গ্রেড হয় না। Lesson যে কথাটা শুধু ইঙ্গিতে বলেছে: erase-remove প্রতিটা রাখা element একবারই সরায়, আর একটা write index দিয়ে তুমিও হাতে হাতে ঠিক একই কাজ করতে পারো।

Run in Compiler

সচরাচর যে প্রশ্নগুলো আসে

  • at() যদি বেশি নিরাপদ হয়, তাহলে কেউ [] লেখে কেন?

    কারণ check-টা প্রতিবার access-এ একটা তুলনা খরচ করে, আর loop-এর ভিতরে সীমাটা loop নিজেই check করে রাখে। যেখানে index তোমার নিয়ন্ত্রণের বাইরে থেকে আসে, সেখানে at() লেখো। Contest-এর solution লেখে [], আর বদলে edge case-গুলো test করে নেয়।

  • clear() কি memory ফেরত দেয়?

    না। Size হয় 0 আর capacity থেকে যায়, উপরের program যেমন 1000 print করেছে। সত্যিই memory ফেরত চাইলে পরে shrink_to_fit() call করো, যদিও সেটা কমই লাগে।

  • push_back-এর বদলে কি সব জায়গায় emplace_back লেখা উচিত?

    একটা int-এর জন্য দুটো একই কাজ করে। emplace_back কাজে আসে যখন element কয়েকটা অংশ দিয়ে বানানো, যেমন একটা pair। যেটা পড়তে বেশি পরিষ্কার, সেটাই লেখো।

  • শেষে erase করলে খরচ O(1), কিন্তু সামনে O(n) কেন?

    কারণ element-গুলোকে কোনো ফাঁক ছাড়া পাশাপাশি থাকতে হয়। শেষেরটা erase করলে কোনো ফাঁক তৈরি হয় না; প্রথমটা erase করলে একটা ফাঁক হয়, আর সেটা ভরতে বাকি প্রতিটা element বাঁয়ে সরে।

মূল কথা

  • Vector-এর শেষ মাথাটা সস্তা: push_back amortised O(1), pop_back আর back O(1)।
  • Index i-এ insert বা erase-এর খরচ O(n - i), তাই সামনে insert-এর একটা loop O(n2), 100,000টায় মাপা হয়েছে 309 ms।
  • v[i] তোমার কথায় বিশ্বাস করে; at(i) check করে আর std::out_of_range throw করে; খালি vector-এ front, back আর pop_back undefined।
  • Loop-এর ভিতরে লেখো it = v.erase(it); অনেকগুলো সরাতে erase-remove, বা C++20-এ std::erase_if।
  • reserve জায়গা বানায় আর resize element বানায়; clear capacity রেখে দেয়, আর swap O(1)-এ handle অদলবদল করে।
  • আরও গভীরে যেতে চাইলে: Under the Hood, vector কীভাবে বড় হয় আর তাতে কী কী ভাঙে (Pro)।

এরপর lesson 03 এই operation-গুলো কাজে লাগাবে ছয়টা সম্পূর্ণ program-এ, পাঁচ লাইনের একটা program থেকে শুরু করে সত্যিকারের একটা marks report পর্যন্ত।

lesson ২ শেষ

শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।

পরেরটা: পুরো program: পাঁচ লাইন থেকে সত্যিকারের একটা tool পর্যন্ত

vector-এর প্রতিটা operation, একটা একটা করে, খরচসহ | Learn C++ STL | Progsity