সরাসরি পর্বে যান
পর্ব ৩০ · ৫৫-এর মধ্যে · মডিউল ৫

sequence container — vector, deque, list কখন কোনটা

Sequence Containers — vector, deque, list, forward_list & the Adaptors

Taught in Bangla, this lesson explains the STL sequence containers: why std::vector is the default, when deque, list and forward_list make sense, the stack, queue and priority_queue adaptors, emplace_back and std::erase_if, the complexity table and how CPU caches change the real answer.

এই পর্বে যা শিখবেন

  • STL-এর তিন স্তম্ভ — container, iterator, algorithm — আর কেন এই ভাগটা এত শক্তিশালী
  • vector, deque, list আর forward_list মেমরিতে কেমন দেখায়, আর সেই ছবি থেকে কোন কাজ কত দ্রুত
  • stack, queue আর priority_queue দিয়ে undo, টিকিট কাউন্টার আর জরুরি বিভাগের অগ্রাধিকার সামলানো
  • emplace_back বনাম push_back, আর erase-remove থেকে C++20-এর std::erase_if
  • Big-O টেবিল পড়া, আর কেন আসল হার্ডওয়্যারে মেপে দেখা ছাড়া সিদ্ধান্ত নেওয়া যায় না

১ · Container মানে সংরক্ষণের একটি কৌশল

পর্ব ২৮ আর ২৯-এ আপনি template লিখতে শিখেছেন — একবার কোড লিখে যেকোনো টাইপে চালানো। এবার দেখবেন C++-এর standard library সেই ক্ষমতা দিয়ে কী বানিয়েছে। মডিউল ৫-এর বাকি পর্বগুলোর কেন্দ্রে আছে STL: তৈরি করা data structure আর algorithm-এর একটি বিশাল বাক্স, যা প্রায় প্রতিটি C++ প্রোগ্রামে ব্যবহার হয়।

একটি প্রশ্ন দিয়ে শুরু করি। একটি বাস কাউন্টারের যাত্রীদের তালিকা, একটি গানের playlist, একটি টেক্সট এডিটরের undo-ইতিহাস আর হাসপাতালের জরুরি বিভাগের রোগীর তালিকা — চারটিই “অনেকগুলো জিনিস একটার পর একটা”। তাহলে চারটিতেই কি একই ধরনের তালিকা চলবে? না। কারণ চারটিতে সবচেয়ে বেশি যে কাজটি হয়, তা আলাদা:

পরিস্থিতিসবচেয়ে বেশি যে কাজযে container মানায়
বাস কাউন্টারের লাইনপেছনে যোগ, সামনে থেকে সরানোdeque / queue
এডিটরের undoশেষে যোগ, শেষ থেকে সরানোstack
জরুরি বিভাগসবচেয়ে গুরুতর রোগীকে আগে বের করাpriority_queue
Playlist সম্পাদনাগানের বড় অংশ এক জায়গা থেকে আরেক জায়গায় সরানোlist (splice)
মাসের বিক্রির হিসাবসারি ধরে পড়া, সূচকে যাওয়াvector
মূল ধারণা

একটি container হলো উপাদান রাখার একটি কৌশল। প্রতিটি কৌশল কিছু কাজ সস্তা করে, আর তার দাম দেয় অন্য কিছু কাজ দামি করে। কোনো container “সেরা” নয়। প্রশ্নটা হলো: আপনার প্রোগ্রাম কোন কাজটি সবচেয়ে বেশিবার করে? এই পর্বের sequence container-গুলো উপাদান রাখে আপনার দেওয়া ক্রমে — প্রথম, দ্বিতীয়, তৃতীয়। পরের পর্বের container-গুলো ক্রম নিজেরা ঠিক করে, key অনুযায়ী।

STL-এর তিন স্তম্ভ

STL-এর নকশার মূল চাল হলো তিনটি জিনিসকে আলাদা রাখা:

📦 Container

উপাদান রাখে আর মেমরির মালিক হয়। vector, deque, list, map, set…

এই পর্ব ও পর্ব ৩১

👉 Iterator

container-এর ভেতর দিয়ে হাঁটার একটি সাধারণ ভাষা: “এখন কোথায় আছি”, “পরেরটায় যাও”, “এখানে কী আছে”।

পর্ব ৩২

⚙️ Algorithm

sort, find, count, remove — iterator দিয়ে কাজ করে, তাই container কোনটা তা জানার দরকার নেই।

পর্ব ৩৩

এই ভাগের লাভটা হিসাব করে দেখুন। ধরুন ৫টি container আর ৫০টি algorithm আছে। প্রতিটি জোড়ার জন্য আলাদা কোড লিখলে লাগত ৫ × ৫০ = ২৫০টি সংস্করণ। কিন্তু মাঝখানে iterator নামের একটি সাধারণ “সংযোগের ভাষা” থাকায় লাগে ৫ + ৫০টি। নতুন একটি container বানালে, iterator দিলেই সব algorithm তার ওপর চলে। এই চিন্তার নাম generic programming, আর এর হাতিয়ার আপনার চেনা template.

STL-এর নকশা করেন Alexander Stepanov, Hewlett-Packard-এর গবেষণাগারে Meng Lee-এর সাথে। ১৯৯৩ সালের শেষে তিনি C++ standard কমিটির সামনে ধারণাটি তুলে ধরেন, আর ১৯৯৪ সালে কমিটি এটিকে খসড়া standard-এ নিয়ে নেয় — একটি নতুন ভাষার নিয়ম চূড়ান্ত হওয়ার আগেই এত বড় একটি লাইব্রেরি গ্রহণ করা ছিল সাহসী সিদ্ধান্ত। প্রথম C++ standard, C++98, প্রকাশিত হয় এই STL-কে সাথে নিয়ে। ভাবনার প্রশ্ন ০১-এ এর পেছনের গল্পটি আরও বিস্তারিত দেখবেন।

২ · std::vector — কেন সে ডিফল্ট

পর্ব ১৩-এ আপনি std::vector বিস্তারিত দেখেছেন, তাই এখানে শুধু এক অনুচ্ছেদের স্মরণ। vector উপাদান রাখে heap-এর একটি ফাঁকহীন ব্লকে — contiguous memory. ভেতরে তিনটি তথ্য: ব্লকের শুরু, কয়টি উপাদান আছে (size), আর কয়টির জায়গা আছে (capacity). শেষে যোগ করা গড়ে ধ্রুবক সময়ের কাজ — amortized O(1); জায়গা ফুরোলে বড় ব্লকে reallocation হয়, আর তখন পুরোনো সব iterator, pointer ও reference অচল হয়ে যায়। সূচক দিয়ে যেকোনো উপাদানে এক লাফে যাওয়া যায় — random access. কিন্তু মাঝখানে ঢোকালে বা মুছলে পেছনের সবাইকে সরতে হয়। আর আকার compile-এর সময়ই নির্দিষ্ট হলে std::array — একই ফাঁকহীন সারি, শুধু heap ছাড়া আর আকার বদলানোর ক্ষমতা ছাড়া।

এবার আসল প্রশ্ন: বইয়ে লেখা থাকে, linked list-এ মাঝখানে ঢোকানো O(1) আর vector-এ O(n). তাহলে বিশেষজ্ঞরা কেন বলেন “সন্দেহ হলে vector”? উত্তর লুকিয়ে আছে CPU কীভাবে মেমরি পড়ে, তার মধ্যে। CPU মেমরি থেকে একবারে একটি বাইট আনে না, আনে একটি ছোট টুকরো — cache line, সাধারণ কম্পিউটারে ৬৪ বাইট। নিচের stepper-এ একই আটটি সংখ্যা দুই রকম container-এ রেখে হেঁটে দেখুন।

std::vector<int>একটি ফাঁকহীন ব্লক
ঠিকানা: 1000 1004 1008 1012 ...
       [10 ][20 ][30 ][40 ]
       [50 ][60 ][70 ][80 ]
<--- একটি 64-বাইট cache line --->
৮টি int = ৩২ বাইট, বাড়তি কিছু নেই
std::list<int>আটটি আলাদা node
0x7a10 [prev|next|10] --> 0x3c40
0x3c40 [prev|next|20] --> 0x9f08
0x9f08 [prev|next|30] --> 0x1b70
0x1b70 [prev|next|40] --> ...
প্রতিটি node আলাদা জায়গায়,
প্রতিটিতে দুটি pointer বাড়তি
হিসাব৮টি int যোগ করতে
                vector    list
মেমরি (মোটামুটি)  32 B   >= 192 B
cache line আনা    1       up to 8
পরের ঠিকানা      জানা     আগে node পড়তে হয়
prefetch         হয়       প্রায় হয় না
  1. vector-এর ছবি। আটটি int পরপর বসে আছে: 1000, 1004, 1008… প্রতিটি ৪ বাইট, মোট ৩২ বাইট। কোনো বাড়তি pointer নেই, ফাঁক নেই। পুরো সারিটি একটি ৬৪ বাইটের cache line-এই এঁটে যায়।
  2. vector-এ হাঁটা। প্রথম উপাদান পড়তে গিয়ে CPU পুরো cache line আনল — বাকি সাতটি বিনা খরচে হাতে চলে এল। সারি আরও লম্বা হলে CPU-র hardware prefetcher খেয়াল করে আপনি সোজা এগোচ্ছেন, আর পরের টুকরো আগেভাগেই এনে রাখে। এই সুবিধাই cache locality.
  3. list-এর ছবি। std::list-এ প্রতিটি উপাদানের জন্য আলাদা একটি node heap থেকে নেওয়া হয় — মান, আগের node-এর ঠিকানা আর পরের node-এর ঠিকানা। ৬৪-বিট কম্পিউটারে দুটি pointer-ই ১৬ বাইট, তার ওপর প্রতিটি আলাদা allocation-এর নিজস্ব হিসাবের খরচ। node-গুলো মেমরিতে কোথায় পড়বে, তা allocator-এর ইচ্ছা।
  4. list-এ হাঁটা। ২০-এ যেতে হলে আগে ১০-এর node পড়ে next জানতে হবে; তারপর একটি সম্পূর্ণ নতুন ঠিকানা। প্রতিটি লাফ সম্ভবত একটি নতুন cache line, আর প্রায়ই একটি cache miss — RAM থেকে আনতে cache-এর চেয়ে প্রায় একশো গুণ সময় লাগে। পরের ঠিকানা আগে জানা নেই বলে prefetcher-ও সাহায্য করতে পারে না।
  5. যোগফল। একই আটটি সংখ্যা: vector-এ ৩২ বাইট আর একটি cache line; list-এ অন্তত ১৯২ বাইট (৮ × ২৪) আর আটটি পর্যন্ত আলাদা cache line. লাখ লাখ উপাদানে এই পার্থক্য কয়েক গুণ থেকে দশ গুণেরও বেশি গতির ফারাক তৈরি করতে পারে। Big-O-র টেবিলে এর কোনো চিহ্ন নেই — সেখানে দুটোতেই হাঁটা O(n).

একটি লাইব্রেরির কথা ভাবুন। vector হলো একটি তাকে পরপর সাজানো বইয়ের সারি — এক হাতে পাঁচটা বই একসাথে টেনে নেওয়া যায়। list হলো এমন এক ধাঁধা, যেখানে প্রতিটি বইয়ের ভেতরে লেখা “পরের বই তিনতলার ১৭ নম্বর তাকে”। বই একটি-একটি করে খুঁজতেই সারাদিন যায় — প্রতিটি বই খুলে না দেখা পর্যন্ত জানাই যায় না পরেরটা কোথায়।

৩ · std::deque — দুই মাথাতেই দ্রুত

বাস টার্মিনালের টিকিট কাউন্টারে লাইন। নতুন যাত্রী আসে পেছনে, টিকিট পেয়ে চলে যান সামনের জন। মাঝে মাঝে একজন বয়স্ক যাত্রী বা প্রতিবন্ধী যাত্রীকে সরাসরি সামনে দাঁড় করানো হয়। অর্থাৎ দুই মাথাতেই যোগ আর মোছা। vector-এ সামনে যোগ বা সামনে থেকে মোছা মানে বাকি সবাইকে এক ঘর সরানো — O(n). লাইনে হাজার জন থাকলে প্রতিবার হাজারটি সরানো।

std::deque (উচ্চারণ “ডেক”, পুরো নাম double-ended queue) এই সমস্যার জন্যই। সে উপাদান রাখে একটি বড় ব্লকে নয়, কয়েকটি সমান মাপের টুকরোয় (chunk). আর একটি ছোট সূচিপত্র — যাকে লাইব্রেরিগুলো “map” বলে — মনে রাখে কোন টুকরো কোথায়। সামনে জায়গা লাগলে সামনে নতুন টুকরো, পেছনে লাগলে পেছনে। পুরোনো উপাদান কখনো সরে না। নিচের stepper-এ টুকরোর মাপ বোঝার সুবিধার জন্য ৪টি int ধরা হয়েছে; আসল লাইব্রেরিতে টুকরো বড় (GCC-র লাইব্রেরিতে ৫১২ বাইট, অর্থাৎ ১২৮টি int).

1std::deque<int> d;2d.push_back(1); d.push_back(2);3d.push_back(3); d.push_back(4);4d.push_front(0); d.push_front(-1);5d.push_front(-2);6// aro 8ti push_front
deque-এর ভেতরে (টুকরো = ৪ ঘর)
map
টুকরো
size
উপাদান সরানো
  1. খালি deque. একটি ছোট map (টুকরোর ঠিকানার তালিকা) আর মাঝখানে একটি টুকরো A. লাইব্রেরি ইচ্ছা করে টুকরোটি map-এর মাঝখানে রাখে, যাতে দুই দিকেই বাড়ার জায়গা থাকে।
  2. পেছনে দুটি যোগ. 1 আর 2 বসল A-এর মাঝখান থেকে। সামনের দুটি ঘর ফাঁকা — push_front-এর জন্য তৈরি।
  3. A-র পেছন ভরল, নতুন টুকরো B. 3 যোগ করতে গিয়ে দেখা গেল A-র শেষ ঘর ভরা। তাই নতুন টুকরো B নেওয়া হলো আর map-এ তার ঠিকানা লেখা হলো। খেয়াল করুন: 1 আর 2 একচুলও সরেনি — vector হলে এখানে সবাইকে নতুন ব্লকে সরাতে হতো।
  4. সামনে দুটি যোগ. A-র সামনের দুটি ফাঁকা ঘরে 0 আর -1 বসল। কোনো সরানো নেই, O(1).
  5. সামনে নতুন টুকরো Z. A-র সামনে আর জায়গা নেই, তাই map-এর বাঁ দিকের খালি ঘরে নতুন টুকরো Z, আর -2 বসল তার শেষ ঘরে। d[3] খুঁজতে deque একটু হিসাব করে — কোন টুকরো, টুকরোর কোন ঘর — তারপর সরাসরি যায়। তাই random access আছে, কিন্তু vector-এর চেয়ে একটু বেশি কাজ।
  6. map ভরে গেলে। আরও আটটি push_front-এ Z-এর বাকি তিন ঘর, নতুন টুকরো Y-এর চার ঘর আর X-এর একটি ঘর লাগল। কিন্তু পুরোনো map-এ X-এর জায়গা নেই — তাই বড় একটি নতুন map বানিয়ে শুধু টুকরোর ঠিকানাগুলো কপি করা হলো। উপাদানগুলো একটিও সরেনি, তাই তাদের reference ঠিক আছে। কিন্তু পুরোনো iterator-গুলো পুরোনো map-এর দিকে তাকিয়ে ছিল — সেগুলো আর ব্যবহারযোগ্য নয়।

শেষ ধাপটি deque-এর একটি সূক্ষ্ম নিয়ম, যা পরীক্ষায় আর ইন্টারভিউতে প্রায়ই আসে: deque-এর দুই মাথায় যোগ করলে সব iterator অচল হতে পারে, কিন্তু উপাদানের reference আর pointer ঠিক থাকে। আর মাঝখানে ঢোকালে বা মুছলে দুটোই অচল হয়। vector-এ reallocation-এ দুটোই যায়, list-এ কোনোটাই যায় না। কেন এমন — ভাবনার প্রশ্ন ০৩-এ বিস্তারিত।

deque সব জায়গায় vector-এর চেয়ে ভালো নয়। উপাদান একটি ব্লকে নেই বলে প্রতিটি d[i]-তে বাড়তি একটি হিসাব আর একটি বাড়তি মেমরি-পড়া লাগে; সারি ধরে হাঁটাও একটু ধীর, কারণ টুকরোর সীমায় এসে লাফ দিতে হয়। data() নেই — উপাদানগুলো একটি C-ধাঁচের ফাংশনে int* হিসেবে পাঠানো যায় না। আর Microsoft-এর লাইব্রেরিতে টুকরো খুব ছোট, ফলে সেখানে deque-এর খরচ আরও বেশি। নিয়ম: শুধু সামনে থেকে যোগ-মোছা সত্যিই ঘন ঘন হলে deque; নইলে vector.

৪ · std::list আর std::forward_list — শিকলের মতো জোড়া

std::list হলো একটি doubly linked list: প্রতিটি node নিজের মান রাখে, আর রাখে আগের ও পরের node-এর ঠিকানা। একে বলে node-based container. এই নকশা থেকেই তার তিনটি শক্তি আর তিনটি দুর্বলতা আসে।

✅ যা list ভালো পারে

জানা জায়গায় O(1) যোগ-মোছা — শুধু দুই প্রতিবেশীর pointer বদল। ঠিকানা স্থির — অন্য উপাদান যোগ বা মুছলেও বাকিদের iterator, pointer, reference ঠিক থাকে (reference stability). splice — উপাদান কপি না করে অন্য list-এ সরানো।

⚠️ যার দাম দিতে হয়

random access নেই — l[5] লেখাই যায় না, পাঁচবার হাঁটতে হয়। হাঁটা ধীর — প্রতিটি লাফে সম্ভাব্য cache miss. মেমরি বেশি — প্রতি উপাদানে দুটি pointer আর একটি আলাদা allocation.

Playlist সম্পাদনা — splice-এর আসল ব্যবহার

একটি গানের অ্যাপে আপনার playlist-এ ২০০টি গান। আপনি “বৃষ্টির গান” নামের ৩০টি গানের একটি অংশ টেনে playlist-এর শুরুতে নিয়ে যেতে চান। vector হলে: ৩০টি গান কপি বা move, তার আগের সব গান পেছনে সরানো। list-এ splice শুধু কয়েকটি pointer বদলায় — গানগুলো মেমরিতে যেখানে ছিল সেখানেই থাকে। আর যে স্ক্রিন একটি গানের দিকে iterator ধরে রেখেছিল (“এখন বাজছে”), সেটিও ঠিক থাকে।

std::list<std::string> playlist{"A", "B", "R1", "R2", "C"};
auto first = std::next(playlist.begin(), 2);   // "R1"
auto last  = std::next(first, 2);              // "C" (baad)
// [first, last) ongshoti shuru-te: kono gaan copy hoy na
playlist.splice(playlist.begin(), playlist, first, last);
// ekhon: R1 R2 A B C

একই list-এর ভেতরে splice সবসময় ধ্রুবক সময়ের কাজ। অন্য একটি list থেকে একটি অংশ আনলে লাইব্রেরিকে অংশটির দৈর্ঘ্য গুনতে হয় (কারণ C++11 থেকে size() O(1) হতে হবে), তাই সেটি অংশের দৈর্ঘ্যের সমানুপাতিক। কিন্তু খেয়াল করুন: first খুঁজতে আমরা শুরু থেকে হেঁটেছি — list-এ “জায়গাটি” খুঁজে পাওয়াই O(n). O(1) যোগ-মোছার সুবিধা তখনই সত্যি, যখন iterator আগে থেকেই হাতে আছে।

আরেকটি খুঁটিনাটি: std::sort random access চায়, তাই list-এ চলে না। list-এর নিজের l.sort() member আছে, যা node-গুলোর pointer বদলে সাজায় — উপাদান সরায় না। remove, unique, merge, reverse-ও list-এর নিজস্ব member হিসেবে আছে, একই কারণে।

std::forward_list — আরও হালকা শিকল

C++11-এ যোগ হয় std::forward_list: প্রতিটি node শুধু পরেরটির ঠিকানা রাখে। এক pointer কম, তাই মেমরি কম — কিন্তু পেছনে হাঁটা যায় না। নিজের আগের node জানে না বলে তার যোগ-মোছার ফাংশনের নাম insert_after আর erase_after: “এই node-এর পরে ঢোকাও”। আর তার size() member-ই নেই — standard ইচ্ছা করে রাখেনি, যাতে একটি গণনার সংখ্যা রাখার বাড়তি খরচও না লাগে। এর লক্ষ্য একটাই: হাতে লেখা C-ধাঁচের singly linked list-এর সমান খরচে নিরাপদ একটি container.

C জানলে

C-তে linked list নিজে লিখতেন: struct node { int value; struct node *next; };, প্রতিটি node-এর জন্য malloc, শেষে লুপ চালিয়ে free. মেমরিতে ছবিটা std::list বা std::forward_list-এ হুবহু একই। পার্থক্য হলো, C++-এ template যেকোনো টাইপের list দেয়, destructor নিজেই সব node ফেরত দেয় (RAII), আর iterator দিয়ে STL-এর algorithm চলে। stack আর queue-ও C-তে array বা linked list দিয়ে হাতে বানাতে হতো; C++-এ তারা তৈরি adaptor. ভেতরের pointer-এর কাজ গভীরভাবে দেখতে C কোর্সের লিংকড লিস্ট আর স্ট্যাক ও কিউ পর্ব দেখুন।

৫ · Container adaptor — stack, queue, priority_queue

এবার তিনটি পরিচিত নাম, যারা আসলে container নয়। এরা container adaptor: অন্য একটি container-এর ওপর বসানো একটি আবরণ, যা শুধু কয়েকটি কাজ করতে দেয় আর বাকিগুলো লুকিয়ে রাখে। উদ্দেশ্য — কোডের অর্থ স্পষ্ট করা আর ভুল ব্যবহার আটকানো।

adaptorনিয়মমূল কাজডিফল্ট ভেতরের container
std::stackLIFO — শেষে এলে আগে যায়push, top, popstd::deque
std::queueFIFO — আগে এলে আগে যায়push, front, back, popstd::deque
std::priority_queueসবচেয়ে বড় (ডিফল্টে) আগে যায়push, top, popstd::vector

তিনটিরই কোনো iterator নেই — ইচ্ছা করে। stack-এর মাঝখানে উঁকি দেওয়া মানেই সেটি আর stack নয়। আর খেয়াল করুন, pop() কিছু ফেরত দেয় না (void). ওপরের মান চাইলে আগে top(), তারপর pop(). দুটি আলাদা কেন — পরীক্ষার বাক্সে আর ভাবনার প্রশ্ন ০৫-এ উত্তর আছে। খালি stack বা queue-তে top(), front() বা pop() ডাকা undefined behavior — আগে empty() দেখুন।

টেক্সট এডিটরের undo ঠিক একটি stack: আপনি টাইপ করলেন, bold করলেন, একটি লাইন মুছলেন — প্রতিটি কাজ স্তূপের ওপরে রাখা হলো। Ctrl+Z চাপলে সবচেয়ে শেষের কাজটি আগে ফেরত যায়। বিয়েবাড়িতে ধোয়া থালার স্তূপও তাই — যে থালা সবার শেষে রাখা হয়েছে, সেটিই প্রথমে তোলা হয়। আর টিকিট কাউন্টারের লাইন হলো queue — যিনি আগে এসেছেন, তিনি আগে টিকিট পাবেন।

নিচের প্রোগ্রামে বাস কাউন্টারের লাইন (deque, কারণ বয়স্ক যাত্রীকে সামনে আনতে হবে) আর একটি এডিটরের undo (stack) একসাথে দেখুন:

#include <deque>
#include <print>
#include <stack>
#include <string>

int main() {
    std::deque<std::string> line{"Rahim", "Karim"};
    line.push_back("Salma");            // shobar pichone
    line.push_front("Elder-Ali");       // boyoshko jatri shamne
    std::println("next = {}, waiting = {}", line.front(), line.size());
    line.pop_front();                   // ticket dewa holo
    std::println("next = {}, last = {}", line.front(), line.back());

    std::stack<std::string> undo;       // editor-er undo itihash
    for (auto op : {"type A", "type B", "bold"}) undo.push(op);
    std::println("undo -> {}", undo.top());
    undo.pop();
    std::println("undo -> {}, left = {}", undo.top(), undo.size());
}

প্রথম লাইনে আসবে next = Elder-Ali, waiting = 4 — push_front তাঁকে Rahim-এরও আগে বসিয়েছে, আর Salma গেছেন সবার পেছনে। তাঁকে টিকিট দেওয়ার পরে (pop_front) দ্বিতীয় লাইন next = Rahim, last = Salma. এরপর undo stack-এ তিনটি কাজ রাখা হলো; top() দেখায় সবচেয়ে শেষেরটি: undo -> bold. একটি pop()-এর পরে ওপরে আসে আগেরটি: undo -> type B, left = 2. লক্ষ করুন, stack-এর ভেতরে কী আছে তা আমরা কোথাও লুপ চালিয়ে দেখিনি — stack সেটা দেয়ই না।

priority_queue — জরুরি বিভাগের হিসাব

ঢাকা মেডিকেলের জরুরি বিভাগে রোগী আসেন যে ক্রমে, চিকিৎসা হয় সেই ক্রমে নয় — হয় অবস্থা কত গুরুতর, সেই অনুযায়ী। একে বলে triage. সফটওয়্যারে একই কাজ: অপারেটিং সিস্টেমের task scheduler, নেটওয়ার্কের জরুরি প্যাকেট, Dijkstra-র shortest path algorithm. প্রতিবার দরকার “এখন সবচেয়ে গুরুত্বপূর্ণটি কোনটি?” — আর নতুন রোগী আসছেন অবিরাম।

প্রতিবার পুরো তালিকা sort করলে খরচ অনেক। priority_queue ভেতরে রাখে একটি binary heap: vector-এর ভেতরে এমনভাবে সাজানো একটি গাছ, যেখানে প্রতিটি parent তার সন্তানদের চেয়ে বড়। সূচক i-এর সন্তান 2i+1 আর 2i+2, parent (i-1)/2 — pointer লাগে না, সূচকের হিসাবই যথেষ্ট। (এই heap আর মেমরির heap বা free store সম্পূর্ণ আলাদা জিনিস, নামটা কাকতালীয়।) গাছের উচ্চতা log n, তাই যোগ আর সরানো O(log n), আর সবচেয়ে বড়টি দেখা O(1). নিচে চার রোগীর গুরুত্বের মাত্রা (বড় সংখ্যা = বেশি গুরুতর) heap-এ ঢোকানো আর বের করা দেখুন:

1std::priority_queue<int> er;2er.push(2);3er.push(5);4er.push(1);5er.push(4);6er.pop();7er.top();   // ?
ভেতরের vector আর গাছ
vector
গাছ
এই ধাপে
top()
  1. প্রথম রোগী, মাত্রা 2. খালি heap-এ প্রথম মানটিই root. top() = 2.
  2. মাত্রা 5 এলেন। নতুন মান সবসময় vector-এর শেষে বসে (সূচক 1). তারপর “ওপরে ওঠা” (sift up): parent (সূচক 0) হলো 2, আর 5 > 2 — জায়গা বদল। 5 এখন root.
  3. মাত্রা 1 এলেন। শেষে বসলেন (সূচক 2), parent 5. 1 < 5, তাই এখানেই থামে। একটি তুলনাতেই কাজ শেষ।
  4. মাত্রা 4 এলেন। শেষে বসলেন সূচক 3-এ; parent সূচক 1-এর 2. 4 > 2, বদল — এখন 4 সূচক 1-এ। নতুন parent 5; 4 < 5, তাই থামে। খেয়াল করুন, vector পুরো sort হয়নি — [5, 4, 1, 2]. heap শুধু নিশ্চিত করে, প্রতিটি parent তার সন্তানের চেয়ে বড়।
  5. pop() — সবচেয়ে গুরুতর রোগী চিকিৎসায় গেলেন। root (5) সরাতে হবে। শেষের মান (2) root-এ আনা হলো, vector ছোট হলো: [2, 4, 1]. এবার “নিচে নামা” (sift down): দুই সন্তানের বড়টি 4, আর 4 > 2 — বদল। [4, 2, 1]. প্রতিটি স্তরে একটি ধাপ, তাই O(log n).
  6. top() এখন 4. পরের সবচেয়ে গুরুতর রোগী। দশ লাখ রোগী থাকলেও push বা pop-এ মাত্র ২০টির মতো তুলনা লাগে (log₂ 10,00,000 ≈ ২০)।

ডিফল্ট ক্রম নিয়ে সবচেয়ে বড় ফাঁদ: std::priority_queue<int> তুলনার জন্য std::less নেয়, অথচ top-এ রাখে সবচেয়ে বড় মান — একটি max-heap. নাম “less” দেখে অনেকে (এবং অনেক AI) ধরে নেন ছোটটি আগে আসবে। সবচেয়ে ছোটটি আগে চাইলে (যেমন “সবচেয়ে কম অপেক্ষার সময়”, Dijkstra-র দূরত্ব) তৃতীয় template argument-এ comparator std::greater দিতে হয়, আর তখন দ্বিতীয় argument-ও লিখতে হয়:

std::priority_queue<int> maxq;      // top = shobcheye boro
std::priority_queue<int, std::vector<int>,
                    std::greater<int>> minq;   // top = shobcheye choto

৬ · যোগ আর মোছা — emplace_back, erase-remove আর std::erase_if

push_back বনাম emplace_back

push_back(x) একটি তৈরি বস্তু নেয় আর সেটি container-এ কপি বা move করে। emplace_back(args...) constructor-এর argument নেয় আর বস্তুটি সরাসরি container-এর ঘরেই তৈরি করে। deque আর list-এ emplace_front, আর সব sequence container-এ emplace(pos, args...)-ও আছে।

struct Ticket { std::string name; int seat; };
std::vector<Ticket> t;
t.push_back(Ticket{"Rahim", 12});   // agey ekta Ticket, tarpor move
t.emplace_back("Karim", 14);        // ghorer moddhei toiri (C++20 theke
                                    // aggregate-eo chole)

std::vector<std::vector<int>> g;
g.push_back(1000000);     // compile hoy na: bhul dhora porlo
g.emplace_back(1000000);  // chupchap 10 lakh 0-er vector!

দুটি কথা মনে রাখুন। প্রথমত, হাতে আগে থেকেই একটি বস্তু থাকলে দুটোর মধ্যে কোনো পার্থক্য নেই — v.push_back(s) আর v.emplace_back(s) একই কাজ করে। দ্বিতীয়ত, emplace_back explicit constructor-ও চুপচাপ ডেকে ফেলে। ওপরের শেষ লাইনে push_back(1000000) compile হয় না (int থেকে vector-এ গোপন রূপান্তর নিষেধ), কিন্তু emplace_back(1000000) দশ লাখ শূন্যের একটি vector বানিয়ে ফেলে — সম্ভবত যা আপনি চাননি। তাই পেশাদার নিয়ম: নতুন বস্তু argument থেকে বানাতে emplace_back, তৈরি বস্তু রাখতে push_back; “emplace সবসময় দ্রুত” বিশ্বাস করবেন না।

শর্ত মেনে অনেকগুলো মোছা — লুপে erase কেন ভুল

ধরুন কাউন্টারের লাইন থেকে যাঁদের অপেক্ষা ৪০ মিনিটের বেশি, তাঁদের অন্য কাউন্টারে পাঠাতে হবে — অর্থাৎ vector থেকে মুছতে হবে। সহজ চিন্তা: লুপ চালান, শর্ত মিললে v.erase(it). কিন্তু vector-এ প্রতিটি erase তার পেছনের সবাইকে এক ঘর সামনে সরায় — O(n). n উপাদানের অর্ধেক মুছলে মোট কাজ প্রায় n²/4 সরানো। এক লাখ উপাদানে সেটা প্রায় আড়াইশো কোটি সরানো! তার ওপর erase-এর পরে iterator অচল, তাই সাবধানে না লিখলে বাগ।

STL-এর সমাধান এক পাসের একটি কৌশল — erase-remove idiom. std::remove_if আসলে কিছু মোছে না: সে যাদের রাখতে হবে, তাদের ক্রম ঠিক রেখে সামনে সরিয়ে আনে, আর “নতুন শেষ” কোথায় তা ফেরত দেয়। তারপর container-এর erase পেছনের অংশটি এক কোপে কেটে ফেলে। ধাপে ধাপে দেখুন:

1auto it = std::remove_if(2    v.begin(), v.end(), tooLong);3v.erase(it, v.end());4// C++20: ek line-e5auto n = std::erase_if(v, tooLong);
v আর দুটি আঙুল (tooLong: মিনিট > 40)
v
পড়ছে
লিখবে
size
  1. শুরু। পাঁচজনের অপেক্ষার সময়: 12, 45, 7, 60, 30 মিনিট। দুটি আঙুল কল্পনা করুন: “পড়ছে” এগোয় প্রতিটি উপাদানে, “লিখবে” দেখায় পরের রাখার উপাদান কোথায় বসবে। 12 রাখার মতো, সে নিজের জায়গাতেই আছে — লিখবে-আঙুল এক ঘর এগোল।
  2. 45 — বাদ। শর্ত মিলেছে (৪০-এর বেশি), তাই কিছুই লেখা হলো না। লিখবে-আঙুল সূচক 1-এ দাঁড়িয়ে রইল।
  3. 7 — থাকবে। 7 লেখা হলো সূচক 1-এ (45-এর জায়গায়)। পুরোনো 7 সূচক 2-এ পড়ে আছে, তাতে কিছু আসে যায় না। লিখবে-আঙুল এখন 2.
  4. 60 বাদ, 30 থাকবে। 30 লেখা হলো সূচক 2-এ। পড়া শেষ। remove_if ফেরত দিল it = সূচক 3. খেয়াল করুন: size এখনো 5! পেছনের দুটি ঘরে কী আছে, standard নিশ্চিত করে না (“valid but unspecified”) — তাই এগুলো ?. নামে remove হলেও algorithm container-এর আকার বদলাতে পারে না, কারণ সে শুধু iterator পায়, container নয়।
  5. erase — পেছন কেটে ফেলা। v.erase(it, v.end()) শেষের দুটি সরিয়ে size 3 করল। পুরো কাজে প্রতিটি উপাদান মাত্র একবার পড়া আর বড়জোর একবার লেখা — O(n). লুপে erase-এর O(n²)-এর তুলনায় বিশাল পার্থক্য।
  6. C++20 — এক লাইনে। std::erase_if(v, tooLong) ভেতরে ঠিক এই দুই ধাপই করে, আর কয়টি মুছল (এখানে 2) ফেরত দেয়। নির্দিষ্ট একটি মান মুছতে std::erase(v, 45). list আর deque-এর জন্যও এগুলো আছে; list-এ ভেতরে pointer বদলেই কাজ হয়।

নিচের প্রোগ্রামে জরুরি বিভাগের ক্রম দুইভাবে (ডিফল্ট আর std::greater) আর অপেক্ষার তালিকা থেকে std::erase_if দিয়ে মোছা একসাথে দেখুন। std::println C++23 থেকে পুরো vector সরাসরি ছাপতে পারে:

#include <functional>
#include <print>
#include <queue>
#include <vector>

int main() {
    std::vector<int> severity{2, 5, 1, 4};      // ER-e asha rogi
    std::priority_queue<int> maxq(severity.begin(), severity.end());
    std::priority_queue<int, std::vector<int>, std::greater<int>>
        minq(severity.begin(), severity.end());
    for (; !maxq.empty(); maxq.pop()) std::print("{} ", maxq.top());
    std::println("<- default (max first)");
    for (; !minq.empty(); minq.pop()) std::print("{} ", minq.top());
    std::println("<- std::greater (min first)");

    std::vector<int> wait{12, 45, 7, 60, 30};   // minute
    auto removed = std::erase_if(wait, [](int m) { return m > 40; });
    std::println("removed {}, left {}", removed, wait);
}

প্রথম লাইন 5 4 2 1 <- default (max first) — ডিফল্ট priority_queue সবচেয়ে বড় মান আগে দেয়, যা জরুরি বিভাগের জন্য ঠিক। দ্বিতীয় লাইন 1 2 4 5 <- std::greater (min first) — একই মান, উল্টো ক্রম। দুটোই iterator-এর জোড়া থেকে একবারে তৈরি হয়েছে; ভেতরে লাইব্রেরি O(n) সময়ে পুরো heap সাজিয়ে নেয়। তৃতীয় লাইন removed 2, left [12, 7, 30] — ৪০ মিনিটের বেশি অপেক্ষার দুজন (45 আর 60) বাদ, বাকিদের ক্রম অপরিবর্তিত। এখানে শর্তটি একটি lambda — পর্ব ৩৪-এ বিস্তারিত দেখবেন।

৭ · জটিলতার টেবিল — আর তার সীমা

এবার সব একসাথে। নিচের টেবিল Big-O-তে লেখা: n উপাদান থাকলে কাজটি কীভাবে বাড়ে। “—” মানে container সেই কাজের member দেয়ই না।

কাজvectordequelistforward_list
শেষে যোগO(1) গড়েO(1)O(1)—
শুরুতে যোগO(n) (insert দিয়ে)O(1)O(1)O(1)
মাঝখানে যোগ/মোছা, iterator হাতে আছেO(n)O(n)O(1)O(1) (_after)
সূচক দিয়ে [i]O(1)O(1)— (হাঁটলে O(n))— (হাঁটলে O(n))
মান দিয়ে খোঁজাO(n)O(n)O(n)O(n)
সাজানো থাকলে binary searchO(log n)O(log n)কার্যত O(n)কার্যত O(n)
প্রতি উপাদানে বাড়তি মেমরিপ্রায় শূন্য (+ফাঁকা capacity)সামান্য (map + আধা-ভরা টুকরো)দুটি pointer + allocationএকটি pointer + allocation
সারি ধরে হাঁটার আসল গতিসবচেয়ে দ্রুতদ্রুতধীরধীর

এই টেবিল পড়ার সময় তিনটি সতর্কতা মাথায় রাখুন।

  • “iterator হাতে আছে” শর্তটাই সব। list-এর O(1) যোগ চমৎকার — কিন্তু বাস্তবে সাধারণত জায়গাটি আগে খুঁজতে হয়, আর list-এ খোঁজা O(n), তাও প্রতিটি ধাপে cache miss সহ। vector-এও খোঁজা O(n), কিন্তু ফাঁকহীন মেমরিতে সেই হাঁটা অনেক গুণ দ্রুত, আর সরানোর কাজটাও (memmove-এর মতো) একটানা মেমরিতে হয়।
  • Big-O ধ্রুবক গুণক লুকায়। O(n)-এর একটি অ্যালগরিদম যদি প্রতি ধাপে ১ ন্যানোসেকেন্ড নেয় আর O(1)-এর আরেকটি প্রতিবার ১০০ ন্যানোসেকেন্ডের একটি cache miss খায়, তাহলে ছোট-মাঝারি n-এ প্রথমটিই জিতবে। Big-O বলে n অসীমের দিকে গেলে কী হয়; আপনার প্রোগ্রামের n অসীম নয়।
  • মেপে দেখুন। Bjarne Stroustrup ২০১২ সালের GoingNative সম্মেলনের বক্তৃতায় একটি পরীক্ষা দেখান: এলোমেলো সংখ্যা সাজানো ক্রমে ঢোকানো, তারপর এলোমেলো জায়গা থেকে মোছা — অর্থাৎ ঠিক সেই কাজ, যেখানে বইয়ের হিসাবে list-এর জেতার কথা। তাঁর মাপা সব আকারে vector অনেক এগিয়ে ছিল। ব্যাখ্যা উপরের দুটি পয়েন্টেই। তবে এটিও একটি মাপ, চিরন্তন নিয়ম নয় — বড় বস্তু, ভারী কপি বা বিশেষ ব্যবহারের ধরনে ফল বদলাতে পারে। তাই আসল নিয়ম: নিজের ডেটায়, optimization চালু করে, মেপে দেখুন (পর্ব ৪৯-এ মাপার পদ্ধতি দেখবেন)।
Big-O বলে কীভাবে বাড়ে।
Cache বলে আজ কত দ্রুত চলে।

৮ · কোনটি বেছে নেবেন — সিদ্ধান্তের পথ, আর std::span

সব মিলিয়ে একটি সহজ সিদ্ধান্তের পথ। ওপর থেকে নিচে পড়ুন, প্রথম যে প্রশ্নের উত্তর “হ্যাঁ”, সেখানেই থামুন:

#প্রশ্ন“হ্যাঁ” হলে
১key দিয়ে খুঁজবেন (নাম দিয়ে নম্বর, roll দিয়ে ছাত্র)?পর্ব ৩১-এর associative container — map, set, unordered_map
২শুধু শেষেরটি, প্রথমটি বা সবচেয়ে বড়টি দরকার?stack / queue / priority_queue
৩আকার compile-এর সময়ই জানা আর ছোট?std::array
৪সামনে থেকেও ঘন ঘন যোগ-মোছা হয়?std::deque
৫উপাদানের ঠিকানা কখনোই বদলানো চলবে না, বা বড় অংশ ঘন ঘন splice, আর মেপে দেখেছেন?std::list (মেমরি খুব কম হলে forward_list)
৬বাকি সব ক্ষেত্রেstd::vector

খেয়াল করুন, ৫ নম্বর প্রশ্নে কতগুলো শর্ত। ঠিকানা স্থির রাখার দরকার হলেও অনেক সময় vector-এ সূচক রাখা, বা std::vector<std::unique_ptr<T>> (বস্তু heap-এ স্থির, vector-এ শুধু pointer) list-এর চেয়ে ভালো কাজ করে। আর মাঝখানে অল্প কয়েকবার ঢোকানো-মোছা vector-এর জন্য কোনো সমস্যাই নয়।

std::span — যেকোনো সারির দিকে একটি জানালা

একটি ফাংশন লিখছেন, যা কিছু সংখ্যার গড় বের করবে। ডাকতে পারে কেউ vector দিয়ে, কেউ std::array দিয়ে, কেউ C-array দিয়ে। প্রতিটির জন্য আলাদা overload? পর্ব ১৪-এ std::string_view একই সমস্যা লেখার জন্য সমাধান করেছিল — মালিক না হয়ে শুধু “শুরুর ঠিকানা আর দৈর্ঘ্য” দেখা। C++20-এর std::span একই কাজ করে যেকোনো ফাঁকহীন সারির জন্য — একটি non-owning view:

double average(std::span<const int> xs);   // vector, array, C-array - shobai chole

std::vector<int> a{1, 2, 3};
std::array<int, 2> b{4, 5};
average(a);  average(b);

লক্ষ করুন “ফাঁকহীন” শব্দটি: span-এর ভেতরে শুধু একটি pointer আর একটি সংখ্যা, তাই সে deque বা list দেখাতে পারে না — তাদের উপাদান এক সারিতে নেই। এটাও vector-এর পক্ষে আরেকটি যুক্তি: ফাঁকহীন সারি C API, span, SIMD আর GPU — সবার সাথে সহজে মেলে। span-এর খুঁটিনাটি আর তার dangling-এর বিপদ পর্ব ১৩-এ দেখেছেন; iterator আর range দিয়ে আরও সাধারণ “যেকোনো container নেওয়া” ফাংশন লিখবেন পর্ব ৩২ ও ৩৫-এ।

পরীক্ষা ও ইন্টারভিউতে
  • vector আর list-এর মূল পার্থক্য? — vector ফাঁকহীন মেমরি, random access O(1), মাঝখানে যোগ O(n); list আলাদা node, random access নেই, জানা জায়গায় যোগ O(1), iterator স্থির। বাস্তবে cache-এর কারণে vector প্রায়ই দ্রুত।
  • stack আর queue-কে container adaptor বলে কেন? — এরা নিজে উপাদান রাখার কৌশল নয়; ভেতরের একটি container-এর (ডিফল্ট deque) ওপর সীমিত interface বসায়। std::stack<int, std::vector<int>> লিখে ভেতরের container বদলানো যায়।
  • priority_queue ডিফল্টে কোনটি আগে দেয়? min-heap কীভাবে? — সবচেয়ে বড়টি (max-heap). min-heap-এর জন্য তৃতীয় argument std::greater<T>, সাথে দ্বিতীয় argument std::vector<T>.
  • stack::pop() মান ফেরত দেয় না কেন? — মান কপি করে ফেরত দিতে গিয়ে কপি exception ছুড়লে উপাদানটি ততক্ষণে মুছে যেত, হারিয়ে যেত। তাই দেখা (top) আর সরানো (pop) আলাদা।
  • erase-remove idiom কী? — v.erase(std::remove_if(v.begin(), v.end(), pred), v.end()): remove_if রাখার উপাদান সামনে আনে, erase পেছন কাটে; O(n). C++20-এ std::erase_if(v, pred).

৯ · AI-এর যুগে — না মেপে list, লুপে erase আর উল্টো heap

AI টুলগুলো container বাছাইয়ে প্রায়ই পাঠ্যবইয়ের Big-O মুখস্থ করা ছাত্রের মতো আচরণ করে: “মাঝখানে ঢোকাতে হবে? তাহলে linked list.” তাদের কোড compile হয়, ছোট ইনপুটে ঠিক উত্তরও দেয় — সমস্যা ধরা পড়ে এক লাখ উপাদানে, যখন প্রোগ্রাম হঠাৎ কয়েক সেকেন্ড থেমে থাকে। সবচেয়ে সাধারণ ভুলগুলো:

AI যা করেসমস্যাযা হওয়া উচিত
“দ্রুত insert-এর জন্য” std::listজায়গা খুঁজতেই O(n) আর cache miss; প্রায়ই vector-এর চেয়ে ধীরডিফল্ট vector; list শুধু মাপার পরে, স্পষ্ট কারণে
লুপের ভেতরে v.erase(it)মোট O(n²); ভুলভাবে লিখলে উপাদান বাদ পড়ে বা অচল iteratorstd::erase_if বা erase-remove
সংখ্যা জেনেও reserve নেইবারবার reallocation আর কপিলুপের আগে v.reserve(n)
“সবচেয়ে ছোট আগে” বোঝাতে priority_queue<int>ডিফল্ট max-heap — উল্টো ক্রম, নিঃশব্দ ভুল ফলstd::greater<int> দিয়ে স্পষ্ট min-heap
queue বানাতে vector + erase(begin())প্রতিটি সরানো O(n)std::queue বা std::deque
deque-এ push_back-এর পরে পুরোনো iterator ব্যবহারiterator অচল (reference ঠিক থাকলেও)সূচক বা reference রাখুন, iterator নতুন করে নিন
খালি stack/queue-তে top()/pop()undefined behaviorআগে empty() যাচাই

AI-কে কাজ দেওয়ার সময় কোন কাজ কতবার হবে তা বলে দিন, আর container বাছাইয়ের কারণ লিখতে বলুন। তখন সে মুখস্থ উত্তর না দিয়ে আপনার পরিস্থিতি নিয়ে ভাবতে বাধ্য হয়:

আমি C++23 শিখছি (g++, -O2 -Wall -Wextra). একটি বাস কোম্পানির টিকিট কাউন্টারের ছোট সিমুলেশন লেখো: - যাত্রী আসে লাইনের পেছনে; বয়স্ক যাত্রী আসেন সামনে। - প্রতি মিনিটে সামনের যাত্রী টিকিট পান। - ৪০ মিনিটের বেশি অপেক্ষা করা সবাইকে একবারে অন্য কাউন্টারে পাঠাতে হবে। - জরুরি টিকিট (বেশি নম্বর = বেশি জরুরি) আলাদা অগ্রাধিকার তালিকায় যাবে। নিয়ম: ১. প্রতিটি তালিকার জন্য কোন container নিলে, আর কোন কাজটি সবচেয়ে বেশিবার হবে বলে নিলে — দুই লাইনে কারণ লেখো। ২. std::list নিলে মাপার একটি ছোট পরীক্ষাও দাও। ৩. শর্ত মেনে মোছায় std::erase_if, লুপে erase নয়। ৪. priority_queue-এর ক্রম কোনটি আগে দেয়, কোডের মন্তব্যে স্পষ্ট লেখো। ৫. সংখ্যা জানা থাকলে reserve করো; খালি container-এ top/front/pop-এর আগে empty() যাচাই করো। শেষে বলো, কোন লাইনে কোন iterator অচল হতে পারে।

AI-এর লেখা container-কোড যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১STL কীভাবে C++-এ এল? একটি লাইব্রেরির নকশা কেন পুরো ভাষার চিন্তাধারা বদলে দিল?

Alexander Stepanov-এর মাথায় ধারণাটি ঘুরছিল ১৯৭০-এর দশক থেকেই: algorithm আসলে কোনো নির্দিষ্ট data structure-এর সম্পত্তি নয়। “একটি সারিতে সবচেয়ে বড়টি খোঁজা” array-তেও যা, linked list-এও তা — শুধু “পরেরটায় যাও” আর “এখানে কী আছে” জানা দরকার। ১৯৮০-এর দশকে David Musser-এর সাথে তিনি Ada ভাষায় একটি generic লাইব্রেরি বানান, কিন্তু সেখানকার ভাষা তাঁর চিন্তা পুরোপুরি প্রকাশ করতে পারছিল না।

C++-এর template এসে সেই সুযোগ দেয়: একবার লেখা কোড প্রতিটি টাইপের জন্য আলাদা করে compile হয়, তাই generic হলেও হাতে লেখা কোডের মতোই দ্রুত। Hewlett-Packard-এর গবেষণাগারে Stepanov আর Meng Lee মিলে container, iterator আর algorithm-এর একটি পূর্ণ নকশা দাঁড় করান। ১৯৯৩ সালের শেষে কমিটির সভায় এটি উপস্থাপন করা হয়। তখন standard প্রায় চূড়ান্ত হওয়ার পথে, আর এত বড় একটি লাইব্রেরি শেষ মুহূর্তে যোগ করা ঝুঁকির ছিল। Andrew Koenig-এর মতো কমিটির সদস্যরা উৎসাহ দেন, আর ১৯৯৪ সালে কমিটি এটি খসড়ায় নিয়ে নেয়। HP তাদের implementation বিনামূল্যে সবার জন্য প্রকাশ করে, ফলে compiler-নির্মাতারা দ্রুত গ্রহণ করতে পারেন। ১৯৯৮ সালে প্রথম ISO C++ standard বের হয় STL-কে নিয়েই।

এর প্রভাব শুধু কয়েকটি তৈরি class নয়। STL শেখায়, interface নকশা করতে হয় জটিলতার নিশ্চয়তা দিয়ে — standard শুধু বলে না vector কী করে, বলে কত দ্রুত করতে হবে (যেমন push_back amortized O(1)). এটি এক অভিনব ধারণা ছিল: performance-ও চুক্তির অংশ। আর iterator-এর মাঝখানে থাকা M × N সমস্যাকে M + N বানিয়েছে — ঠিক যেমন compiler-এর জগতে একটি সাধারণ মধ্যবর্তী ভাষা করে। পরে Java-র Collections, C#-এর LINQ, Rust-এর iterator trait — অনেক ভাষার লাইব্রেরিতেই এই চিন্তার ছাপ দেখা যায়। C++20-এর ranges (পর্ব ৩৫) আর concepts (পর্ব ৪০) একই পথের পরের ধাপ: iterator-এর জোড়ার বদলে একটি range, আর template-এর শর্ত স্পষ্ট ভাষায়।

মূল উপলব্ধি: ভালো abstraction খরচ লুকায় না, খরচের চুক্তি দেয়। STL জনপ্রিয় হয়েছে কারণ সে একই সাথে সাধারণ আর দ্রুত — আর কোন কাজের দাম কত, তা লিখিতভাবে জানিয়ে দেয়।

প্র ০২পাঠ্যবইয়ে linked list এত গুরুত্ব পায়, অথচ পেশাদার C++-এ std::list খুব কমই সঠিক উত্তর হয়। কেন? আর কখন সত্যিই হয়?

পর্ব ১৩-এর ভাবনার প্রশ্নে আপনি cache-এর কারণে vector-এর জয় দেখেছেন। এখানে প্রশ্নটা আরেকটু গভীর: list-এর খরচ ঠিক কোথায় কোথায়?

  • প্রতিটি উপাদানে একটি allocation: দশ লাখ উপাদান মানে দশ লাখবার allocator-এর কাছে মেমরি চাওয়া আর পরে দশ লাখবার ফেরত দেওয়া। allocator নিজেও প্রতিটি খণ্ডের হিসাব রাখে — একটি ৪ বাইটের int-এর জন্য বাস্তবে ৩২ বাইট বা তারও বেশি খরচ হতে পারে।
  • pointer ধরে হাঁটা: পরের node-এর ঠিকানা আগের node না পড়া পর্যন্ত জানা যায় না। CPU-র আধুনিক কৌশলগুলো — prefetch, একসাথে অনেক মেমরি-পড়া চালু রাখা — এখানে কাজ করে না। প্রতিটি ধাপ আগেরটির জন্য অপেক্ষা করে।
  • খোঁজাই আসল খরচ: “O(1) insert” সত্যি শুধু iterator হাতে থাকলে। বেশিরভাগ বাস্তব কাজে সেই জায়গাটি আগে খুঁজতে হয়।

তাহলে list কখন? যখন তার অনন্য নিশ্চয়তাগুলো সত্যিই দরকার: (১) অন্য উপাদান যোগ-মোছা হলেও প্রতিটি উপাদানের ঠিকানা আর iterator স্থির থাকবে — যেমন অনেক জায়গা থেকে একই উপাদানের দিকে iterator ধরে রাখা আছে; (২) splice — বড় অংশ এক তালিকা থেকে আরেক তালিকায় কপি ছাড়া সরানো, যেমন একটি LRU cache-এ সাম্প্রতিক ব্যবহৃত উপাদান সামনে আনা (এর প্রচলিত নকশা list আর একটি hash map একসাথে); (৩) উপাদান move বা কপি করাই যায় না, বা খুব দামি। বাস্তবে আরও একটি জনপ্রিয় রূপ আছে — intrusive list, যেখানে next/prev pointer বস্তুর ভেতরেই থাকে, আলাদা node লাগে না। Linux kernel-এর list_head এর বিখ্যাত উদাহরণ। আর ঠিকানা স্থির রেখে ফাঁকহীন টুকরোয় রাখার জন্য C++26-এ std::hive নামে একটি নতুন container যোগ হচ্ছে, যা list-এর নিশ্চয়তার অনেকটা দেয় অনেক কম খরচে।

মূল উপলব্ধি: list একটি বিশেষ নিশ্চয়তার (স্থির ঠিকানা, splice) জন্য দাম দেওয়া container, গতির জন্য নয়। গতি চাইলে আগে vector, তারপর মাপা — list শুধু তখনই, যখন সেই নিশ্চয়তা না থাকলে নকশা ভেঙে পড়ে।

প্র ০৩deque-এর দুই মাথায় push করলে সব iterator অচল হতে পারে, অথচ reference ঠিক থাকে — একই উপাদানের দিকে তাকানো দুটি জিনিসের ভাগ্য আলাদা কেন?

উত্তরটা লুকিয়ে আছে দুটির ভেতরের গঠনে। একটি reference (বা pointer) হলো সরাসরি উপাদানের ঠিকানা — যেমন “টুকরো A-এর তৃতীয় ঘর”। deque-এ push_front বা push_back কখনো পুরোনো উপাদান সরায় না: সে হয় কোনো টুকরোর খালি ঘরে লেখে, নয়তো নতুন টুকরো নেয়। তাই উপাদানের ঠিকানা আজীবন একই থাকে, আর reference ঠিকই থাকে।

কিন্তু deque-এর iterator-কে আরও বেশি কিছু জানতে হয়। ++it করলে টুকরোর শেষে পৌঁছে পরের টুকরোয় লাফ দিতে হবে — আর পরের টুকরো কোথায়, তা লেখা আছে map-এ। তাই একটি সাধারণ implementation-এ (যেমন GCC-র) iterator চারটি জিনিস রাখে: বর্তমান উপাদানের ঠিকানা, বর্তমান টুকরোর শুরু ও শেষ, আর map-এর কোন ঘরে এই টুকরোর ঠিকানা লেখা। দুই মাথায় যোগ করতে গিয়ে map ভরে গেলে লাইব্রেরি নতুন একটি বড় map বানায় আর পুরোনোটি ফেরত দেয়। iterator তখনো পুরোনো map-এর ঘরের দিকে তাকিয়ে আছে — সেখানে ++ করলে মৃত মেমরি পড়বে। standard তাই সহজ ও নিরাপদ নিয়ম দিয়েছে: দুই মাথায় যোগ করলে সব iterator অচল ধরে নিন, যদিও reference টিকে থাকে।

মাঝখানে ঢোকালে বা মুছলে কিন্তু উপাদানগুলো সত্যিই সরে (যে দিকে কম উপাদান, সেই দিকের সবাই), তাই তখন reference-ও যায়। আর দুই মাথা থেকে মুছলে শুধু মুছে যাওয়া উপাদানের reference-iterator অচল হয় (পেছন থেকে মুছলে শেষের iterator-ও)। তুলনা করুন: vector-এ reallocation হলে iterator, pointer, reference — সব একসাথে যায়, কারণ পুরো ব্লক সরে। list-এ কিছুই যায় না, কারণ node কখনো সরে না, আর iterator শুধু node-এর দিকে তাকায়।

এটি সফটওয়্যার নকশার একটি সাধারণ শিক্ষা: একটি “হাতল” (handle) যত বেশি ভেতরের কাঠামো মনে রাখে, কাঠামো বদলালে সে তত সহজে অচল হয়। ডেটাবেসের index, ফাইল সিস্টেমের inode, গেমের entity id — সব জায়গায় প্রকৌশলীরা ভাবেন, হাতলটি কী মনে রাখবে আর কী প্রতিবার নতুন করে খুঁজবে।

মূল উপলব্ধি: invalidation-এর নিয়ম মুখস্থ না করে ভাবুন — এই হাতল ঠিক কী মনে রাখে, আর এই কাজে সেটির কোনটা নড়ল? উত্তর থেকেই নিয়ম বেরিয়ে আসে।

প্র ০৪Big-O যদি এত গুরুত্বপূর্ণ হয়, তাহলে বাস্তব হার্ডওয়্যারে O(n)-এর কোড কীভাবে O(log n) বা O(1)-এর কোডকে হারায়? Big-O কি তাহলে ভুল?

Big-O ভুল নয় — সে একটি নির্দিষ্ট প্রশ্নের উত্তর দেয়: n অনেক বড় হলে খরচ কোন হারে বাড়ে। কিন্তু তার মডেলে ধরে নেওয়া হয়, প্রতিটি মেমরি-পড়ার খরচ সমান। আধুনিক কম্পিউটারে সেটা সত্যি নয়। মোটামুটি হিসাবে (Jeff Dean-এর জনপ্রিয় “Latency numbers every programmer should know” তালিকার মতো সূত্রে), CPU-র সবচেয়ে কাছের L1 cache থেকে পড়তে লাগে এক ন্যানোসেকেন্ডের মতো, আর RAM থেকে প্রায় একশো ন্যানোসেকেন্ড। অর্থাৎ কোথা থেকে পড়ছেন, তার ওপর একই “একটি ধাপ” একশো গুণ সস্তা বা দামি হতে পারে।

কয়েকটি বাস্তব উদাহরণ:

  • ছোট n-এ সরল খোঁজা: ৩০টি উপাদানে একটি সাজানো vector-এ সোজা লুপে খোঁজা প্রায়ই binary search-এর চেয়েও দ্রুত — সব একটি-দুটি cache line-এ, আর লুপের শাখা CPU আগেই আন্দাজ করতে পারে।
  • সাজানো vector বনাম std::set: দুটোতেই খোঁজা O(log n), কিন্তু set-এর প্রতিটি ধাপ একটি নতুন node — সম্ভাব্য cache miss. যেখানে ডেটা একবার তৈরি করে বারবার খোঁজা হয়, সেখানে সাজানো vector-এ binary search প্রায়ই কয়েক গুণ দ্রুত। পর্ব ৩১-এ এই তুলনা আবার আসবে।
  • Stroustrup-এর পরীক্ষা: সাজানো ক্রমে ঢোকানো আর এলোমেলো জায়গা থেকে মোছায় vector list-কে হারিয়েছিল, কারণ দুটিতেই জায়গা খোঁজা O(n), আর সেই হাঁটায় list প্রতিটি ধাপে cache miss খায়।

তবে উল্টো ভুলও করবেন না। n সত্যিই বড় হলে — কোটি উপাদান, বারবার মাঝখানে কাজ — একসময় Big-O-ই জেতে; O(n²) কোনো cache দিয়ে বাঁচানো যায় না। লুপে erase-এর উদাহরণটি মনে করুন। সঠিক অবস্থান হলো: Big-O দিয়ে আগে বাজে পথগুলো বাদ দিন, তারপর একই শ্রেণির বিকল্পগুলোর মধ্যে মেমরির বিন্যাস আর মাপ দিয়ে সিদ্ধান্ত নিন। গেম ইঞ্জিন আর ডেটাবেসে এই চিন্তার নাম data-oriented design — ডেটা কীভাবে মেমরিতে সাজানো, সেটাই নকশার কেন্দ্র।

মূল উপলব্ধি: Big-O হলো মানচিত্র, cache হলো রাস্তার আসল যানজট। ঢাকায় দূরত্ব দেখে নয়, যানজট দেখে রাস্তা বাছেন — প্রোগ্রামেও তাই: হিসাব দিয়ে শুরু, মাপ দিয়ে সিদ্ধান্ত।

প্র ০৫std::stack নিজে একটি container না হয়ে “adaptor” কেন? সরাসরি একটি stack class বানালে কী ক্ষতি হতো?

stack-এর আসল পরিচয় কোনো মেমরি-বিন্যাস নয়, বরং একটি নিয়ম: শুধু ওপর থেকে রাখা, দেখা আর সরানো। এই নিয়ম vector-এর ওপরেও চলে, deque-এর ওপরেও, list-এর ওপরেও — যার back(), push_back() আর pop_back() আছে। STL-এর নকশাকাররা তাই মেমরি রাখার কাজ (container) আর আচরণের নিয়ম (adaptor) আলাদা করেছেন। একই বছর, ১৯৯৪ সালে, “Gang of Four”-এর Design Patterns বইয়ে এই কৌশলের নাম দেওয়া হয় Adapter pattern: একটি জিনিসের interface বদলে অন্য একটি প্রত্যাশিত interface দেওয়া।

লাভ তিনটি। পছন্দের স্বাধীনতা: ডিফল্টে ভেতরে deque, কারণ বড় হতে গেলে উপাদান সরাতে হয় না; কিন্তু আপনি জানেন vector আপনার ক্ষেত্রে দ্রুত হবে, তাহলে std::stack<int, std::vector<int>> লিখুন — stack-এর কোড একটুও বদলায় না। ভুল আটকানো: stack-এ iterator নেই, [] নেই — কেউ ভুল করে undo-ইতিহাসের মাঝখানে হাত দিতে পারবে না। কোড পড়েই বোঝা যায় এখানে LIFO নিয়ম চলছে; এটি সংকেত দেওয়ার মতোই দরকারি। কম কোড: লাইব্রেরিকে মেমরি-ব্যবস্থাপনা আরেকবার লিখতে হয়নি; stack-এর পুরো implementation কয়েক লাইনের, সব কাজ ভেতরের container-কে পাঠানো।

একই নকশা থেকে আসে pop()-এর void ফেরত দেওয়া। ১৯৯৪ সালে Tom Cargill একটি প্রভাবশালী লেখায় দেখান, “সরাও আর মান ফেরত দাও” একসাথে করা একটি generic stack exception-safe হতে পারে না: উপাদান মুছে ফেলার পরে ফেরত-মান কপি করতে গিয়ে কপি exception ছুড়লে, উপাদানটি চিরতরে হারিয়ে যায়। তাই STL দেখা (top) আর সরানো (pop) আলাদা রেখেছে — অসুবিধাজনক লাগলেও নিরাপদ। (C++11-এর move semantics এই সমস্যা অনেকটা কমিয়েছে, কিন্তু interface আর বদলানো হয়নি।)

মূল উপলব্ধি: “কীভাবে রাখা হয়” আর “কী করতে দেওয়া হয়” — দুটি আলাদা দায়িত্ব। আলাদা রাখলে দুটিকেই স্বাধীনভাবে বদলানো যায়, আর সীমিত interface নিজেই একটি নিরাপত্তা-বেষ্টনী।

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — হাতে-কলমে দেখা আর ভাবাই আসল কাজ।

  1. Container বাছাই: প্রতিটির জন্য একটি container বেছে নিন আর এক লাইনে কারণ বলুন — (ক) একটি মোবাইল ওয়ালেটের লেনদেনের ইতিহাস, যা শুধু শেষে যোগ হয় আর মাসের শেষে পুরোটা পড়া হয়; (খ) ব্রাউজারের Back বোতাম; (গ) মেট্রো স্টেশনের টিকিট মেশিনের লাইন; (ঘ) একটি সার্ভারে কাজের তালিকা, যেখানে সবচেয়ে কম সময়ে শেষ হওয়া কাজ আগে চলবে।

    (ক) std::vector — শুধু শেষে যোগ আর সারি ধরে পড়া; সংখ্যা আন্দাজ থাকলে reserve. (খ) std::stack — সবশেষে দেখা পাতায় আগে ফেরা, LIFO. (Forward বোতামও থাকলে দুটি stack.) (গ) std::queue — আগে এলে আগে, FIFO; ভেতরে ডিফল্ট deque. (ঘ) std::priority_queue সাথে std::greater — “সবচেয়ে কম সময়” আগে চাই, অর্থাৎ min-heap; ডিফল্ট দিলে উল্টো ক্রম পেতেন।

  2. Heap হাতে চালান: একটি খালি std::priority_queue<int>-এ ক্রমে 3, 8, 6, 9, 1 push করুন। প্রতিটি push-এর পরে ভেতরের vector কেমন দেখাবে লিখুন (stepper-এর নিয়মে)। তারপর দুবার pop করলে top কত?

    push 3 → [3]. push 8 → শেষে বসে, parent 3 ছোট, বদল → [8, 3]. push 6 → [8, 3, 6], parent 8 বড়, তাই থামে। push 9 → সূচক 3-এ, parent সূচক 1-এর 3, বদল → [8, 9, 6, 3]; parent 8, বদল → [9, 8, 6, 3]. push 1 → [9, 8, 6, 3, 1], parent 8 বড়, তাই থামে।

    প্রথম pop: শেষের 1 root-এ → [1, 8, 6, 3]; বড় সন্তান 8, বদল → [8, 1, 6, 3]; 1-এর সন্তান 3, বদল → [8, 3, 6, 1]. top = 8. দ্বিতীয় pop: 1 root-এ → [1, 3, 6]; বড় সন্তান 6, বদল → [6, 3, 1]. top = 6. (ভেতরের ঠিক বিন্যাস লাইব্রেরি ভেদে আলাদা হতে পারে, কিন্তু top সবসময় একই।)

  3. বাগ শিকার: নিচের কোডটি জোড় সংখ্যা মুছতে চায়। দুটি সমস্যা খুঁজুন আর এক লাইনে ঠিক করুন: for (auto it = v.begin(); it != v.end(); ++it) if (*it % 2 == 0) v.erase(it);

    সমস্যা ১ — অচল iterator: erase(it)-এর পরে it আর ব্যবহারযোগ্য নয়, অথচ লুপ তার ওপর ++it করছে — undefined behavior. ঠিকভাবে লিখতে হলে it = v.erase(it); আর তখন ++ করা চলবে না। (তা না হলে মোছার পরে সামনে আসা পরের উপাদানটি লাফিয়ে যাবে — পরপর দুটি জোড় সংখ্যা থাকলে দ্বিতীয়টি থেকে যাবে।)

    সমস্যা ২ — গতি: প্রতিটি erase পেছনের সবাইকে সরায়, মোট O(n²).

    সমাধান: std::erase_if(v, [](int x) { return x % 2 == 0; }); — এক পাসে O(n), কোনো iterator হাতে রাখার ঝামেলা নেই।

  4. নিজে মেপে দেখুন (কোড লেখার কাজ): একটি প্রোগ্রাম লিখুন, যা ১ লাখ এলোমেলো সংখ্যা একটি সাজানো std::vector আর একটি সাজানো std::list-এ ঠিক জায়গায় ঢোকায় (প্রতিবার শুরু থেকে হেঁটে জায়গা খুঁজে)। std::chrono দিয়ে দুটোর সময় মাপুন, -O2 দিয়ে compile করে। কী পেলেন, কেন?

    কাঠামো: একই seed দিয়ে std::mt19937 থেকে সংখ্যা নিন (যাতে দুটিতে একই ইনপুট যায়)। vector-এর জন্য std::find_if দিয়ে প্রথম বড় উপাদান খুঁজে insert; list-এর জন্যও একই find_if আর insert. সময় মাপুন std::chrono::steady_clock দিয়ে। (vector-এ চাইলে std::lower_bound-এ binary search করতে পারেন — তখন পার্থক্য আরও বাড়ে, কিন্তু ন্যায্য তুলনার জন্য প্রথমে দুটিতেই সোজা হাঁটুন।)

    সাধারণ কম্পিউটারে vector অনেক গুণ দ্রুত পাওয়ার কথা — সঠিক অনুপাত আপনার CPU, compiler আর মেমরির ওপর নির্ভর করে, তাই সংখ্যাটি নিজেই দেখুন। কারণ: দুটিতেই জায়গা খোঁজা O(n) আর মোট O(n²); vector-এর হাঁটা আর সরানো একটানা মেমরিতে, যেখানে prefetcher সাহায্য করে; list-এর প্রতিটি ধাপ একটি pointer-লাফ আর সম্ভাব্য cache miss. উপাদান বড় struct হলে (যেমন ২০০ বাইট) ফল বদলাতে পারে — সেটাও মেপে দেখুন। এটাই এই পর্বের আসল শিক্ষা।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমmax-heap-এ (std::priority_queue) একটি নতুন মান push করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. নতুন মানটি ভেতরের vector-এর একেবারে শেষে বসানো
  2. সূচক (i-1)/2 দিয়ে তার parent খুঁজে তুলনা করা
  3. নতুন মান parent-এর চেয়ে বড় হলে দুটির জায়গা বদল
  4. parent বড় বা সমান হলে, অথবা root-এ পৌঁছালে থামা

কোন standard container দুই মাথাতেই ধ্রুবক সময়ে যোগ করতে দেয়, আবার সূচক দিয়ে [i]-ও দেয়?

  • std::vector
  • std::deque
  • std::list
  • std::forward_list

std::priority_queue<int> pq; — এতে 3, 9, 4 push করার পরে pq.top() কত?

  • 3, কারণ সে প্রথমে ঢুকেছে
  • 3, কারণ সে সবচেয়ে ছোট
  • 9
  • 4, কারণ সে শেষে ঢুকেছে

std::stack<int> লিখলে ভেতরে ডিফল্টভাবে কোন container থাকে?

  • std::deque<int>
  • std::vector<int>
  • std::list<int>
  • কোনোটাই নয়, stack নিজের array রাখে

একটি std::list-এর মাঝখানে নতুন একটি উপাদান insert করা হলো। আগে থেকে ধরে রাখা অন্য উপাদানগুলোর iterator-এর কী হয়?

  • সবগুলো অচল হয়ে যায়
  • insert-এর পেছনের সবগুলো অচল হয়
  • iterator অচল, কিন্তু reference ঠিক থাকে
  • সবগুলো ঠিক থাকে

n উপাদানের একটি vector থেকে লুপের ভেতরে একটি-একটি করে erase দিয়ে প্রায় অর্ধেক উপাদান মোছা হলো। মোট সময় কোন শ্রেণির, আর std::erase_if-এ কোন শ্রেণির?

  • দুটোই O(n)
  • লুপে O(n²), erase_if-এ O(n)
  • লুপে O(n log n), erase_if-এ O(log n)
  • দুটোই O(n²)

Big-O হিসাবে linked list-এ জানা জায়গায় insert O(1) আর vector-এ O(n). তবু সাজানো ক্রমে ঢোকানোর পরীক্ষায় vector প্রায়ই দ্রুত হয়। মূল কারণ কোনটি?

  • compiler list-কে গোপনে vector-এ বদলে দেয়
  • Big-O হিসাবটাই ভুল
  • জায়গা খুঁজতে দুটিতেই O(n) লাগে, আর vector-এর ফাঁকহীন মেমরি cache-এ অনেক দ্রুত পড়া যায়
  • vector-এ insert আসলে O(1)

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Container মানে একটি কৌশল; বাছাই করুন যে কাজটি সবচেয়ে বেশিবার হয় তা দেখে। STL-এ container, iterator আর algorithm আলাদা — মাঝখানে iterator.
  2. ডিফল্ট std::vector — ফাঁকহীন মেমরি আর cache-এর জোরে বাস্তবে প্রায় সবসময় দ্রুত; আকার নির্দিষ্ট হলে std::array.
  3. deque দুই মাথায় O(1), দুই মাথায় যোগে iterator যায় কিন্তু reference থাকে; list ঠিকানা স্থির আর splice দেয়, কিন্তু হাঁটা ধীর আর random access নেই।
  4. stack / queue / priority_queue হলো adaptor; priority_queue ডিফল্টে সবচেয়ে বড় আগে দেয়, ছোট আগে চাইলে std::greater.
  5. লুপে erase নয় — std::erase_if বা erase-remove; Big-O দিয়ে বাজে পথ বাদ, তারপর মেপে সিদ্ধান্ত।

পরের পর্বে: এখন পর্যন্ত ক্রম ঠিক করেছেন আপনি। কিন্তু “এই roll নম্বরের ছাত্র কে?” — এমন প্রশ্নে সারি ধরে হাঁটা ধীর। পর্ব ৩১-এ দেখবেন map, set আর unordered_map — যেখানে key দিয়ে খোঁজাই আসল কাজ, আর ক্রম ঠিক করে container নিজে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান