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

STL algorithm — লুপ না লিখে তৈরি সমাধান

STL Algorithms — Ready-made, Tested Solutions Instead of Raw Loops

Taught in Bangla, this lesson replaces hand-written loops with named STL algorithms: find_if, count_if and transform, why remove does not erase, sort, stable_sort and partial_sort, comparators and strict weak ordering, binary search on sorted data, the accumulate int trap and complexity guarantees.

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

  • “no raw loops” ভাবনা — কেন নাম-ওয়ালা algorithm হাতে লেখা লুপের চেয়ে পড়তে সহজ আর নিরাপদ
  • <algorithm> আর <numeric>-এর প্রধান পরিবারগুলো: খোঁজা, বদলানো, সাজানো, binary search, min/max, হিসাব
  • predicate আর comparator কী, আর comparator-এর strict weak ordering নিয়ম কেন ভাঙা যাবে না
  • তিনটি চিরচেনা ফাঁদ: remove ছাড়া erase, accumulate-এ 0, আর না-সাজানো ডেটায় binary search
  • complexity-র নিশ্চয়তা, parallel execution policy আর ranges-এর ঝলক, আর AI-এর লেখা লুপ যাচাই করার পদ্ধতি

১ · লুপ নয়, উদ্দেশ্য লিখুন — “no raw loops” ভাবনা

ধরুন একটি কোচিং সেন্টারের মডেল টেস্টের ফল আপনার হাতে, একটি std::vector<int> marks-এ। এখন চারটি প্রশ্ন: কতজন পাস করেছে (৩৩ বা তার বেশি)? সর্বোচ্চ নম্বর কত? গড় কত? আর মেধাতালিকা কেমন? এ পর্যন্ত যা শিখেছেন, তা দিয়ে প্রথম প্রশ্নের উত্তর সম্ভবত এভাবে লিখবেন:

int pass = 0;
for (std::size_t i = 0; i < marks.size(); ++i) {
    if (marks[i] >= 33) ++pass;
}

কোডটি ঠিক আছে। কিন্তু একজন পাঠককে এটি বুঝতে মাথার ভেতরে লুপটি চালিয়ে দেখতে হয়: একটি counter শূন্য থেকে শুরু, একটি index, শর্ত < না <=, প্রতিবার কী বাড়ছে। চার লাইনের প্রতিটিতে ভুলের একটি করে সুযোগ — counter-এর শুরুর মান, সীমার শর্ত (off-by-one), ভুল ভ্যারিয়েবল বাড়ানো। এবার একই কাজ standard library-র একটি algorithm দিয়ে:

auto pass = std::count_if(marks.begin(), marks.end(),
                          [](int m) { return m >= 33; });

নামটিই বলে দিচ্ছে কী হচ্ছে: “শর্ত পূরণ করে এমন কয়টি আছে, গোনো।” [](int m) { return m >= 33; } অংশটি একটি lambda — নামহীন ছোট একটি ফাংশন, যা ঠিক সেই জায়গাতেই লেখা হয়, যেখানে দরকার। এখানে এটি শুধু বলে “এই নম্বরটি কি পাস?” lambda-র পুরো ব্যাকরণ পর্ব ৩৪-এ আসবে; এই পর্বে আমরা কেবল এমন ছোট, এক লাইনের lambda ব্যবহার করব।

রিকশায় উঠে আপনি দুইভাবে পথ বলতে পারেন। একভাবে: “সোজা যান, দ্বিতীয় মোড়ে বাঁয়ে, পেট্রোল পাম্পের পরে ডানে, তারপর আরও তিনটা গলি…” আরেকভাবে: “শাহবাগ যাবেন?” দ্বিতীয়টি ছোট, ভুল বোঝার সুযোগ কম, আর চালক পথটা আপনার চেয়ে ভালো জানেন — হয়তো জ্যাম এড়ানোর একটি শর্টকাটও জানেন। হাতে লেখা লুপ হলো মোড়ে মোড়ে নির্দেশ; algorithm হলো গন্তব্যের নাম বলা। আর standard library-র “চালক” হাজার হাজার প্রোগ্রামারের হাতে পরীক্ষিত।

এই ভাবনাকে বিখ্যাত করেন Adobe-এর প্রকৌশলী Sean Parent, ২০১৩ সালের GoingNative সম্মেলনের “C++ Seasoning” বক্তৃতায়। তাঁর প্রথম পরামর্শ ছিল তিন শব্দের: no raw loops. raw loop মানে এমন হাতে লেখা for/while, যার কাজ আসলে একটি পরিচিত algorithm, কিন্তু কোডে তার নাম কোথাও লেখা নেই। কথাটা “লুপ নিষিদ্ধ” নয়। কথাটা হলো: লুপ লিখতে বসলে আগে জিজ্ঞেস করুন, এই কাজের কোনো নাম আছে কি? থাকলে সেই নামটিই ডাকুন। না থাকলে লুপটিকে একটি ভালো নামের নিজের ফাংশনে ভরে দিন — তাহলে বাকি কোডে আবার নামটিই ডাকা হবে।

পড়তে সহজ

count_if, all_of, stable_sort — নাম পড়েই উদ্দেশ্য বোঝা যায়। পাঠককে লুপ “চালিয়ে” দেখতে হয় না।

কী হচ্ছে, কীভাবে নয়

ভুল কম

index, counter, সীমার শর্ত — এসব algorithm-এর ভেতরে একবার সঠিকভাবে লেখা। আপনার কোডে off-by-one লেখার জায়গাই থাকে না।

পরীক্ষিত, বহু বছরের

দ্রুত

algorithm-গুলো template, তাই compiler আপনার টাইপ আর আপনার শর্তের জন্য আলাদা করে optimize করে। std::sort সাধারণত হাতে লেখা সরল sort-এর চেয়ে অনেক দ্রুত।

নিশ্চিত complexity

বদলানো সহজ

কাল সমান্তরালে চালাতে চাইলে প্রায়ই শুধু একটি execution policy যোগ করলেই হয় (৮ নম্বর অংশ)। হাতে লেখা লুপে পুরোটা নতুন করে লিখতে হতো।

ভবিষ্যতের জন্য তৈরি

STL-এর জনক Alexander Stepanov. তিনি ১৯৭০-৮০-এর দশক থেকে “generic programming” নিয়ে কাজ করছিলেন — algorithm এমনভাবে লেখা, যাতে সেটি ডেটার টাইপ আর ডেটা কোথায় রাখা আছে, দুটো থেকেই স্বাধীন থাকে। David Musser-এর সাথে তিনি প্রথমে Scheme আর Ada-তে এমন লাইব্রেরি বানান। পরে Hewlett-Packard Labs-এ Meng Lee-র সাথে C++ template ব্যবহার করে তৈরি করেন Standard Template Library, যা ১৯৯৪ সালে C++ standard-এর খসড়ায় গৃহীত হয় আর C++98-এ standard-এর অংশ হয়। Stepanov-এর মূল অন্তর্দৃষ্টি: container আর algorithm-এর মাঝখানে iterator বসালে M টি container আর N টি algorithm-এর জন্য M × N টি সংস্করণ লিখতে হয় না, M + N টিই যথেষ্ট।

২ · algorithm-এর ভাষা — range, predicate আর ফেরত আসা iterator

প্রায় সব algorithm থাকে দুটি header-এ: <algorithm> (খোঁজা, বদলানো, সাজানো — শতাধিক ফাংশন) আর <numeric> (যোগফল, ক্রমযোগ, ক্রমিক সংখ্যা)। সবার কথা বলার ধরন একই, তাই একবার ধরনটা বুঝলে নতুন algorithm শেখা মানে শুধু নতুন একটি নাম শেখা।

১. কাজ করে একটি range-এ, container-এ নয়

পর্ব ৩২-এ দেখেছেন, একটি iterator হলো container-এর ভেতরে একটি জায়গা দেখানোর উপায়। algorithm নেয় দুটি iterator — first আর last — আর কাজ করে half-open range [first, last)-এ: first-এর উপাদান থেকে শুরু, last-এর ঠিক আগে শেষ। last নিজে range-এর বাইরে। এই ধরনের কারণে খালি range লেখা সহজ (first == last), আর উপাদানের সংখ্যা সরাসরি last - first.

যেহেতু algorithm শুধু iterator দেখে, একই std::find কাজ করে std::vector-এ, std::array-এ, std::deque-এ, std::list-এ, এমনকি সাধারণ C array-তেও। কিন্তু এর একটি বড় পরিণতি আছে: algorithm container-কে দেখতেই পায় না, তাই container-এর আকার বদলাতে পারে না। সে উপাদান পড়তে, লিখতে, জায়গা বদলাতে পারে; কিন্তু একটি উপাদানও মুছে vector ছোট করতে পারে না। ৪ নম্বর অংশে remove-এর রহস্য এই কথা থেকেই আসবে।

২. ফল আসে iterator হয়ে — না পেলে last

খোঁজার algorithm সাধারণত পাওয়া উপাদানের iterator ফেরত দেয়। কিছু না পেলে ফেরত দেয় last — অর্থাৎ “range-এর শেষে পৌঁছে গেছি, পাইনি।” তাই ফল ব্যবহারের আগে সবসময় যাচাই:

auto it = std::find(marks.begin(), marks.end(), 100);
if (it != marks.end()) {
    std::println("100 found at index {}", it - marks.begin());
}   // না মিলিয়ে *it পড়লে: end() dereference — undefined behavior

৩. শর্ত দেওয়া হয় predicate আর comparator দিয়ে

অনেক algorithm-এর নামের শেষে _if — find_if, count_if, remove_if. এরা একটি predicate নেয়: এমন একটি ফাংশন, যা একটি উপাদান নিয়ে true বা false বলে — “এটি কি পাস?”, “এটি কি খালি?” সাজানো আর খোঁজার algorithm নেয় একটি comparator: দুটি উপাদান নিয়ে উত্তর দেয় “a কি b-এর আগে বসবে?” দুটিই হতে পারে সাধারণ ফাংশন, lambda, বা function object (পর্ব ২৩-এর operator()-ওয়ালা object). দেওয়া না হলে ডিফল্ট হলো == (খোঁজায়) আর < (সাজানোয়)।

মূল ধারণা

algorithm তিনটি জিনিস আলাদা করে: কোথায় (iterator range), কী নিয়মে (predicate/comparator), আর কী কাজ (algorithm-এর নাম)। আপনি শুধু প্রথম দুটি দেন; তৃতীয়টি standard library-র পরীক্ষিত কোড। এই ভাগই Stepanov-এর M + N-এর জাদু।

একটি সতর্কতা: কিছু container-এর নিজের member function আছে, যা একই কাজ আরও ভালোভাবে করে। std::set বা std::map-এ খুঁজতে s.find(x) লিখুন — এটি O(log n), অথচ std::find এক এক করে সব দেখে, O(n). আর std::list সাজাতে lst.sort(), কারণ std::sort-এর random-access iterator লাগে, যা list দিতে পারে না (পর্ব ৩০ ও ৩১)। নিয়ম: container-এর নিজের সংস্করণ থাকলে সেটিই আগে।

৩ · খোঁজা, গোনা, যাচাই — যারা ডেটা বদলায় না

প্রথম পরিবারটি শুধু পড়ে, কিছু লেখে না। তাই এদের const ডেটাতেও চালানো যায়, আর এরা সবচেয়ে নিরাপদ। প্রতিটির একটি ছোট উদাহরণ দেখুন (সবখানে m হলো নম্বরের একটি vector):

algorithmপ্রশ্নটিফেরত দেয়
find৯৯ কোথায় আছে?প্রথম মিলের iterator, না পেলে last
find_ifপ্রথম ফেল-করা নম্বর কোথায়?প্রথম যার জন্য predicate সত্য
count / count_ifকয়জন ১০০ পেয়েছে? কয়জন পাস?সংখ্যা
all_of / any_of / none_ofসবাই পাস? কেউ ফেল? কেউই শূন্য নয়?bool
mismatchদুটি তালিকা প্রথম কোথায় আলাদা?দুই range-এর iterator-এর একটি জোড়া
auto fail = std::find_if(m.begin(), m.end(), [](int x) { return x < 33; });
auto n100 = std::count(m.begin(), m.end(), 100);
bool allPass = std::all_of(m.begin(), m.end(), [](int x) { return x >= 33; });
bool anyZero = std::any_of(m.begin(), m.end(), [](int x) { return x == 0; });

// দুটি উত্তরপত্র মেলানো: key = সঠিক উত্তর, ans = শিক্ষার্থীর উত্তর
auto [k, a] = std::mismatch(key.begin(), key.end(), ans.begin(), ans.end());
if (k != key.end()) std::println("first wrong answer: Q{}", k - key.begin() + 1);

শেষ উদাহরণে std::mismatch দুটি range পাশাপাশি হাঁটে, আর প্রথম যে জায়গায় উপাদান মেলে না, সেখানকার দুটি iterator একসাথে ফেরত দেয় — structured binding (পর্ব ১২) দিয়ে সেগুলো k আর a-তে খুলে নেওয়া হয়েছে। চার-iterator রূপটি (C++14 থেকে) দুই range-এর শেষ-বিন্দুই নেয়, তাই দ্বিতীয় তালিকা ছোট হলেও সীমার বাইরে পড়ে না। পুরোনো তিন-iterator রূপে দ্বিতীয় range অন্তত প্রথমটির সমান লম্বা হতে হয় — সেটি আপনার দায়িত্ব।

খালি range-এর ফাঁদ: all_of খালি range-এ true দেয়, none_of-ও true, আর any_of দেয় false. গণিতের ভাষায় এটা ঠিক — “খালি ক্লাসের সবাই পাস” কথাটি মিথ্যা প্রমাণ করার মতো কেউ নেই। কিন্তু প্রোগ্রামে “কোনো ফল আসেনি” আর “সবাই পাস” এক কথা নয়। তাই “সবাই পাস” ঘোষণার আগে !m.empty()-ও যাচাই করুন।

C++23-এ std::ranges::contains(m, 100) এসেছে, যা সরাসরি bool দেয় — “আছে কি না” প্রশ্নে find(...) != end() লেখার চেয়ে আরও পরিষ্কার। ranges-এর কথা ৮ নম্বর অংশে আর পর্ব ৩৫-এ।

৪ · বদলানো — copy, transform, fill, আর remove কেন মোছে না

দ্বিতীয় পরিবার উপাদান লেখে বা জায়গা বদলায়। এদের বেশিরভাগ একটি গন্তব্য iterator নেয় — “ফল কোথায় লিখব?” — আর সেখানে যথেষ্ট জায়গা থাকা আপনার দায়িত্ব।

std::vector<int> out(m.size());                 // আগে থেকেই m-এর সমান জায়গা
std::copy(m.begin(), m.end(), out.begin());     // সব কপি

std::vector<int> passed;                        // খালি — জায়গা নেই
std::copy_if(m.begin(), m.end(), std::back_inserter(passed),
             [](int x) { return x >= 33; });    // back_inserter: প্রতিবার push_back

std::transform(m.begin(), m.end(), m.begin(),   // নিজের জায়গাতেই বদল
               [](int x) { return x + 5; });    // গ্রেস মার্কস: সবাইকে +৫
std::fill(out.begin(), out.end(), 0);           // সব ঘরে 0
std::replace_if(m.begin(), m.end(),             // ১০০-এর বেশি হলে ১০০
                [](int x) { return x > 100; }, 100);

std::transform প্রতিটি উপাদানে একটি ফাংশন চালিয়ে ফল লেখে — হয় নতুন জায়গায়, নয়তো (উপরের মতো) নিজের জায়গাতেই। এর দুই-range রূপও আছে, যা দুটি তালিকা থেকে জোড়ায় জোড়ায় নেয় — ৭ নম্বর অংশে কেনাকাটার কার্টে দাম × পরিমাণ হিসাবে এটি দেখবেন।

খালি vector-এ copy করবেন না। std::vector<int> passed; std::copy(..., passed.begin()); লিখলে algorithm ধরে নেয় সেখানে জায়গা আছে, আর অন্যের মেমরিতে লিখতে থাকে — undefined behavior. আকার আগে জানা থাকলে vector-টি সেই আকারে বানান; না জানা থাকলে std::back_inserter দিন, যা প্রতিটি লেখাকে একটি push_back-এ বদলে দেয়।

remove কেন মোছে না — আর erase কেন লাগে

ধরুন নম্বরের তালিকায় যারা পরীক্ষায় বসেনি, তাদের জন্য 0 লেখা আছে, আর আপনি সেগুলো বাদ দিতে চান। নামের কারণে স্বাভাবিকভাবে লিখবেন std::remove(m.begin(), m.end(), 0). কিন্তু ২ নম্বর অংশের কথা মনে করুন: algorithm container দেখতে পায় না, শুধু iterator. তাই সে vector ছোট করতে পারে না। remove / remove_if আসলে যা করে: রাখার মতো উপাদানগুলো ক্রম ঠিক রেখে সামনের দিকে সরিয়ে আনে, তারপর “নতুন শেষ”-এর একটি iterator ফেরত দেয়। এর পরের ঘরগুলোতে কী থাকবে, standard তা বলে না। ধাপে ধাপে দেখুন:

1std::vector<int> m{72, 0, 85, 0, 40};2auto it = std::remove(m.begin(), m.end(), 0);3m.erase(it, m.end());
vector-এর অবস্থা
m-এর ঘর [0..4]
পড়ার ঘর (read)
পরের লেখার ঘর (write)
m.size()
  1. শুরু। পাঁচটি ঘর, তার দুটিতে 0. লক্ষ্য: 0-গুলো বাদ দিয়ে 72 85 40 রাখা, ক্রম না বদলে। remove দুটি আঙুল নিয়ে কাজ করে — একটি পড়ে (read), একটি লেখে (write).
  2. 72 রাখার মতো। এটি আগে থেকেই ঠিক জায়গায় (ঘর 0). লেখার আঙুল এগিয়ে গেল ঘর 1-এ: পরের রাখার মতো উপাদান সেখানে বসবে।
  3. 0 বাদ। পড়ার আঙুল এগোল, কিন্তু লেখার আঙুল ঘর 1-এই দাঁড়িয়ে রইল। কিছু মোছা হয়নি — শুধু এই মানটি “গোনার বাইরে” রাখা হলো।
  4. 85 সামনে এল। 85 লেখা হলো ঘর 1-এ (আগের 0-এর ওপরে)। ঘর 2-এ পুরোনো 85 এখনো পড়ে আছে — মানটি সরানো (move) হয়েছে, আর int-এর বেলায় move মানে কপিই, তাই আগের ঘর থেকে মুছে যায়নি। লেখার আঙুল ঘর 2-এ।
  5. আবার 0 বাদ। পড়ার আঙুল ঘর 3-এ, লেখার আঙুল এখনো ঘর 2-এ।
  6. 40 সামনে এল, remove শেষ। ঘর 2-এ 40. প্রথম তিন ঘরে এখন ঠিক উত্তর: 72 85 40. remove ফেরত দিল ঘর 3-এর iterator — “নতুন শেষ এখানে।” কিন্তু size() এখনো 5! শেষের দুই ঘরে কী আছে (এখানে সাধারণত 0 আর 40), standard তার নিশ্চয়তা দেয় না — এগুলো পড়ে কোনো সিদ্ধান্ত নেবেন না।
  7. erase ছেঁটে ফেলল। m.erase(it, m.end()) — এবার container-এর নিজের member function, যে সত্যিই আকার বদলাতে পারে — it থেকে শেষ পর্যন্ত মুছে দিল। size এখন 3. দুই ধাপ মিলে পুরো কাজ: remove সরায়, erase ছাঁটে।

এই দুই ধাপের জোড়াকে বলে erase-remove idiom, আর বহু বছর ধরে C++-এ এটি এক লাইনে লেখা হতো: m.erase(std::remove(m.begin(), m.end(), 0), m.end());. পুরো কাজটা এক পাসে, O(n). লুপের ভেতরে একটি একটি করে erase করলে প্রতিবার পেছনের সব উপাদান সরাতে হয় — সবচেয়ে খারাপ ক্ষেত্রে O(n²), সাথে iterator invalidation-এর ঝুঁকি।

C++20 থেকে কাজটি আরও সহজ: std::erase(m, 0); আর std::erase_if(m, pred); — পর্ব ১৩-এ দেখা std::erase_if. ভেতরে এরা ঠিক এই idiom-ই চালায়, আর মুছে ফেলা উপাদানের সংখ্যাও ফেরত দেয়। নতুন কোডে এটিই লিখুন; কিন্তু idiom-টি চিনে রাখুন, কারণ পুরোনো কোড আর AI-এর কোডে এটি সব জায়গায় পাবেন — অনেক সময় ভুলভাবে, erase ছাড়া।

unique — শুধু পাশাপাশি duplicate

std::unique-ও remove-এর মতো কাজ করে: পরপর বসা একই মানের উপাদানের মধ্যে প্রথমটি রেখে বাকিগুলো সামনে থেকে সরিয়ে দেয়, আর নতুন শেষ ফেরত দেয়। “পরপর” শব্দটি জরুরি — 3 1 3-এ দুটি 3 পাশাপাশি নয়, তাই কোনোটাই বাদ যাবে না। পুরো তালিকা থেকে duplicate সরাতে তাই প্রচলিত তিন ধাপ: আগে sort, তারপর unique, তারপর erase:

std::sort(rolls.begin(), rolls.end());                          // 3 1 3 2  -> 1 2 3 3
rolls.erase(std::unique(rolls.begin(), rolls.end()), rolls.end()); // -> 1 2 3

৫ · সাজানো — sort, stable_sort, partial_sort আর comparator-এর নিয়ম

std::sort সবচেয়ে বেশি ব্যবহৃত algorithm. ডিফল্টভাবে < দিয়ে ছোট থেকে বড় সাজায়। বড় থেকে ছোট চাইলে একটি comparator দিন:

std::sort(m.begin(), m.end());                         // 28 40 72 ... (ছোট থেকে বড়)
std::sort(m.begin(), m.end(), std::greater<>{});       // 85 85 72 ... (বড় থেকে ছোট)
std::sort(s.begin(), s.end(),                          // struct হলে: কোন ফিল্ড দেখে?
          [](const Student& a, const Student& b) { return a.marks > b.marks; });

comparator-এর একটিই নিয়ম — strict weak ordering

comparator যেকোনো bool-ফেরত ফাংশন হতে পারে না। standard চায় এটি একটি strict weak ordering হোক। শুনতে ভারী, কিন্তু ব্যবহারিক অর্থ সহজ — comparator comp(a, b) যেন “a কঠোরভাবে b-এর আগে” প্রশ্নের উত্তর দেয়:

  • নিজের আগে কেউ নয়: comp(a, a) সবসময় false.
  • দুজন দুজনের আগে নয়: comp(a, b) সত্য হলে comp(b, a) মিথ্যা।
  • ক্রম ছড়ায়: a আগে b, আর b আগে c হলে, a আগে c.
  • “সমান”-ও ছড়ায়: কেউ কারও আগে নয় এমন জোড়াকে সমান (equivalent) ধরা হয়; a ≡ b আর b ≡ c হলে a ≡ c.

< আর > এই নিয়ম মানে। <= আর >= মানে না, কারণ a <= a সত্য — প্রথম নিয়মই ভেঙে যায়। দেখতে নিরীহ এই ভুলের ফল undefined behavior: বাস্তবে কিছু বাস্তবায়নে std::sort range-এর সীমা পেরিয়ে মেমরি পড়তে শুরু করে, আর বড় ইনপুটে প্রোগ্রাম crash করে — ছোট পরীক্ষায় হয়তো ঠিকঠাক চলবে, তাই ধরা পড়ে দেরিতে। একই কারণে double-এর তালিকায় NaN থাকলে সাধারণ <-ও নিয়ম ভাঙে (NaN কারও সাথে তুলনায় সত্য দেয় না, ফলে “সমান” আর ছড়ায় না)। এ নিয়ে ভাবনার প্রশ্ন ০৪-এ বিস্তারিত।

মূল নিয়ম

comparator-এ কখনো <= বা >= নয়। সমানে সমানে ক্রম ঠিক করতে চাইলে দ্বিতীয় একটি ফিল্ড দিয়ে tie ভাঙুন — যেমন নম্বর সমান হলে রোল দেখুন: if (a.marks != b.marks) return a.marks > b.marks; return a.roll < b.roll;. এই নিয়ম std::map/std::set-এর comparator-এর জন্যও একই (পর্ব ৩১)।

stable_sort — সমানদের পুরোনো ক্রম রাখা

মেধাতালিকা বানানোর সময় নিয়ম অনেক সময় এমন: “নম্বর অনুযায়ী সাজাও; নম্বর সমান হলে রোল নম্বরের ক্রম যেমন ছিল, তেমন থাকবে।” তালিকাটি যদি আগে থেকেই রোল অনুযায়ী সাজানো থাকে, তাহলে দরকার এমন sort, যা সমান উপাদানগুলোর আপেক্ষিক ক্রম বদলায় না। একে বলে stable sort. std::sort এর নিশ্চয়তা দেয় না; std::stable_sort দেয়। পার্থক্যটি ধাপে ধাপে দেখুন:

ইনপুটরোল অনুযায়ী সাজানো
roll 1  Rafi    72
roll 2  Mita    85
roll 3  Sabbir  40
roll 4  Nusrat  85
roll 5  Tanvir  28
roll 6  Joya    72

comparator:  a.marks > b.marks   (বড় নম্বর আগে)
std::sortএকটি সম্ভাব্য ফল
Nusrat  85   (roll 4)   ┐ সমান নম্বর — ক্রম
Mita    85   (roll 2)   ┘ উল্টে যেতে পারে
Joya    72   (roll 6)   ┐ এখানেও
Rafi    72   (roll 1)   ┘
Sabbir  40
Tanvir  28
নম্বরের ক্রম ঠিক; সমানদের ক্রম standard বলে না
std::stable_sortনিশ্চিত ফল
Mita    85   (roll 2)   ┐ রোল 2 আগে, 4 পরে —
Nusrat  85   (roll 4)   ┘ ইনপুটে যেমন ছিল
Rafi    72   (roll 1)   ┐ রোল 1 আগে, 6 পরে
Joya    72   (roll 6)   ┘
Sabbir  40
Tanvir  28
দুটি সঠিক পথকোনটি কখন
পথ ১: ইনপুট রোল অনুযায়ী আছে  =>  stable_sort(by marks)

পথ ২: যেকোনো ইনপুট  =>  sort, tie ভাঙা comparator দিয়ে
  if (a.marks != b.marks) return a.marks > b.marks;
  return a.roll < b.roll;

কৌশল: আগে গৌণ চাবি দিয়ে সাজান, তারপর মুখ্য চাবি দিয়ে stable_sort
  1. ইনপুট রোল অনুযায়ী। দুটি জোড়ার নম্বর সমান: Mita আর Nusrat (85), Rafi আর Joya (72). comparator শুধু নম্বর দেখে, তাই এর চোখে এই জোড়াগুলো “সমান”।
  2. std::sort — সমানদের ক্রম অনিশ্চিত। নম্বরের ক্রম নিশ্চিতভাবে ঠিক থাকবে, কিন্তু Mita আর Nusrat-এর মধ্যে কে আগে, standard তা বলে না। ভেতরের introsort উপাদান এদিক-ওদিক ছুঁড়ে দেয়; ফল ইনপুটের আকার আর লাইব্রেরি অনুযায়ী বদলাতে পারে। এখানে একটি সম্ভাব্য ফল দেখানো হলো।
  3. std::stable_sort — ক্রম নিশ্চিত। সমান নম্বরের জোড়াগুলো ইনপুটে যে ক্রমে ছিল, সেই ক্রমেই থাকে: রোল 2 আগে, রোল 4 পরে। দাম: সাধারণত বাড়তি মেমরি লাগে (ভেতরে প্রায়ই merge sort), আর জায়গা না পেলে একটু ধীর — O(n log² n).
  4. দুটি সঠিক পথ। ইনপুট আগেই গৌণ চাবিতে (রোল) সাজানো থাকলে stable_sort; নইলে sort-কে tie-ভাঙা comparator দিন। বহু চাবিতে সাজানোর পুরোনো কৌশলও এখান থেকে: আগে গৌণ চাবিতে সাজান, তারপর মুখ্য চাবিতে stable_sort — স্প্রেডশিটে কলামে ক্লিক করে সাজানো এভাবেই কাজ করে।

এবার পুরো ফলাফল-শিটটি একটি প্রোগ্রামে: কতজন পাস (count_if), গড় (accumulate), আর মেধাতালিকা (stable_sort) — একটিও হাতে লেখা হিসাবের লুপ নেই:

#include <algorithm>
#include <numeric>
#include <print>
#include <string>
#include <vector>

struct Student { int roll; std::string name; int marks; };

int main() {
    std::vector<Student> v{{1, "Rafi", 72}, {2, "Mita", 85}, {3, "Sabbir", 40},
                           {4, "Nusrat", 85}, {5, "Tanvir", 28}, {6, "Joya", 72}};
    auto pass = std::count_if(v.begin(), v.end(),
                              [](const Student& s) { return s.marks >= 33; });
    double sum = std::accumulate(v.begin(), v.end(), 0.0,
                     [](double acc, const Student& s) { return acc + s.marks; });
    std::stable_sort(v.begin(), v.end(),
                     [](const Student& a, const Student& b) { return a.marks > b.marks; });
    std::println("pass {} of {}, average {:.2f}", pass, v.size(), sum / v.size());
    for (const auto& s : v) std::println("{:<7} roll {}  {}", s.name, s.roll, s.marks);
}

আউটপুটের প্রথম লাইন pass 5 of 6, average 63.67 — ছয়জনের মোট ৩৮২, ভাগ ৬. তারপর মেধাতালিকা: Mita (roll 2) আর Nusrat (roll 4) দুজনেই 85, Rafi (roll 1) আর Joya (roll 6) দুজনেই 72, তারপর Sabbir 40, Tanvir 28. সমান নম্বরে রোলের ক্রম বজায় আছে, কারণ stable_sort. accumulate-এর চতুর্থ argument একটি lambda: “এতক্ষণের যোগফল আর পরের শিক্ষার্থী নিয়ে নতুন যোগফল দাও।” শুরুর মান 0.0 — কেন 0 নয়, ৭ নম্বর অংশে দেখবেন। {:<7} নামটিকে ৭ ঘর চওড়া, বাঁয়ে-সাঁটা করে ছাপে।

পুরোটা সাজানোর দরকার নেই — partial_sort আর nth_element

ধরুন এক লাখ পরীক্ষার্থীর মধ্যে শুধু সেরা তিনজনকে পুরস্কার দেবেন। পুরো তালিকা সাজানো অপচয়। std::partial_sort শুধু প্রথম k টি ঘর সাজানো অবস্থায় ভরে, বাকিগুলো যেকোনো ক্রমে ফেলে রাখে — খরচ মোটামুটি n log k, যেখানে পুরো sort-এ n log n. আর শুধু “মাঝের মান” (median) বা “১০০তম স্থান কত নম্বরে” জানতে চাইলে std::nth_element: n-তম ঘরে ঠিক সেই উপাদান বসায়, যা পুরো sort করলে সেখানে বসত, আর তার বাঁয়ে সব ছোট-বা-সমান, ডানে সব বড়-বা-সমান রাখে — গড়ে মাত্র O(n).

std::partial_sort(v.begin(), v.begin() + 3, v.end(),    // সেরা ৩ জন, সাজানো
                  [](const Student& a, const Student& b) { return a.marks > b.marks; });

auto mid = m.begin() + m.size() / 2;
std::nth_element(m.begin(), mid, m.end());               // *mid = median (বিজোড় n হলে)
bool ok = std::is_sorted(m.begin(), m.end());            // সাজানো আছে কি?

৬ · সাজানো ডেটায় খোঁজা, set-এর হিসাব আর min/max

সাজানো তালিকায় এক এক করে খোঁজার দরকার নেই — অভিধানে শব্দ খোঁজার মতো মাঝখানে খুলে দেখুন, শব্দটি আগে না পরে, তারপর অর্ধেক বাদ। প্রতি ধাপে খোঁজার জায়গা অর্ধেক হয়, তাই দশ লাখ উপাদানেও মোটামুটি ২০টি তুলনাই যথেষ্ট। এটি binary search, আর STL-এ এর একটি পুরো পরিবার আছে:

algorithmফেরত দেয়কখন
binary_searchশুধু bool — আছে কি নাউপস্থিতি যাচাই; কোথায় আছে বলে না
lower_boundপ্রথম উপাদান, যা মানটির চেয়ে ছোট নয় (≥)কোথায় আছে, বা না থাকলে কোথায় বসত
upper_boundপ্রথম উপাদান, যা মানটির চেয়ে বড় (>)সমানদের ঠিক পরের জায়গা
equal_rangeদুটোর জোড়া: [lower, upper)একই মানের সব উপাদান; সংখ্যা = দূরত্ব

std::lower_bound সবচেয়ে কাজের — এটি ধাপে ধাপে কীভাবে অর্ধেক অর্ধেক করে খোঁজে, দেখুন। প্রশ্ন: সাজানো তালিকায় 55 নেই, তাহলে কোথায় বসত?

1std::vector<int> v{10, 20, 30, 40, 50, 60, 70, 80};2auto it = std::lower_bound(v.begin(), v.end(), 55);3// it - v.begin() == 5,  *it == 60
খোঁজার জায়গা [lo, hi)
lo, hi
বাকি ঘর
mid, v[mid]
সিদ্ধান্ত
তুলনা হলো
  1. শুরু। আটটি সাজানো সংখ্যা। খুঁজছি প্রথম ঘর, যার মান 55-এর চেয়ে ছোট নয়। পুরো range [0, 8) এখনো সম্ভাব্য।
  2. মাঝে দেখা: ঘর 4, মান 50. 50 < 55, তাই উত্তর ঘর 4 বা তার আগে হতে পারে না — তালিকা সাজানো, বাঁয়ের সবাই আরও ছোট। এক তুলনায় অর্ধেকের বেশি বাদ: নতুন range [5, 8).
  3. ঘর 6, মান 70. 70 ≥ 55, তাই ঘর 6 একটি সম্ভাব্য উত্তর, কিন্তু এর বাঁয়ে আরও ভালো (আগের) উত্তর থাকতে পারে। ডান দিক বাদ: range [5, 6).
  4. ঘর 5, মান 60. 60 ≥ 55 — hi নেমে এল 5-এ। range এখন খালি হয়ে গেল।
  5. উত্তর: ঘর 5. lo আর hi মিলে গেছে — সেটাই উত্তর, মান 60. 55 থাকলে ঠিক এখানে থাকত; নতুন 55 যোগ করতে v.insert(it, 55) দিলে তালিকা সাজানোই থাকবে। মাত্র ৩টি তুলনা (log₂ 8 = 3); এক এক করে খুঁজলে লাগত ৬টি। (লাইব্রেরি কোড দেখতে একটু আলাদা, কিন্তু ধারণা এটাই।)

পূর্বশর্ত: range সাজানো থাকতে হবে, আর একই comparator-এ। না-সাজানো ডেটায় binary_search চালালে compile error আসে না, crash-ও সাধারণত হয় না — শুধু ভুল উত্তর আসে, “আছে” জিনিসকে “নেই” বলে। বড় থেকে ছোট সাজানো তালিকায় খুঁজলে std::greater<>{} comparator-টিও দিতে হবে। এমন শর্তকে বলে precondition: algorithm ধরে নেয় এটি সত্য, যাচাই করে না। আর একবার খোঁজার জন্য sort করে binary search অর্থহীন — sort-এর খরচ n log n, এক এক করে খোঁজার খরচ n. লাভ হয় তখনই, যখন একবার সাজিয়ে বহুবার খোঁজেন।

সাজানো তালিকায় সমান মানের সব উপাদান পাশাপাশি থাকে, তাই std::equal_range এক ডাকে তাদের শুরু আর শেষ দুটোই দেয়: auto [lo, hi] = std::equal_range(m.begin(), m.end(), 85); — তারপর hi - lo হলো কতজন 85 পেয়েছে।

set-এর হিসাব — সাজানো দুই তালিকায়

দুটি সাজানো রোল-তালিকা — ক্রিকেট দলের আর বিতর্ক ক্লাবের। কারা দুটোতেই আছে? set operation-এর algorithm দুই তালিকা একসাথে একবার হেঁটে উত্তর দেয়, খরচ O(n + m):

std::vector<int> cricket{2, 5, 7, 9}, debate{1, 5, 9, 12}, both;
std::set_intersection(cricket.begin(), cricket.end(), debate.begin(), debate.end(),
                      std::back_inserter(both));   // both = 5 9
// আরও: set_union (যেকোনোটিতে), set_difference (প্রথমটিতে আছে, দ্বিতীয়টিতে নেই),
//       includes (দ্বিতীয়টির সব কি প্রথমটিতে আছে?)

সবচেয়ে ছোট, সবচেয়ে বড়, আর সীমার ভেতরে রাখা

auto top = std::max_element(m.begin(), m.end());   // iterator! খালি হলে end()
auto [lo, hi] = std::minmax_element(m.begin(), m.end());  // এক পাসে দুটোই
int bigger = std::max(a, b);                        // দুটি মানের মধ্যে
auto [mn, mx] = std::minmax({a, b, c});             // initializer_list: মান ফেরত
int pct = std::clamp(raw, 0, 100);                  // C++17: 0-এর নিচে 0, 100-এর ওপরে 100

লক্ষ করুন, min_element / max_element মান নয়, iterator ফেরত দেয় — যাতে আপনি জানতে পারেন কোথায় আছে (কোন শিক্ষার্থী সর্বোচ্চ)। খালি range-এ এরা end() দেয়, আর তখন *top লেখা undefined behavior. std::clamp একটি মানকে সীমার ভেতরে টেনে আনে; শর্ত হলো নিচের সীমা ওপরেরটির চেয়ে বড় হবে না। আর একটি সূক্ষ্ম ফাঁদ: দুই-argument-এর std::minmax(a, b) মান নয়, reference-এর জোড়া ফেরত দেয় — temporary দিয়ে ডাকলে (std::minmax(f(), g())) সেই reference ঝুলে যায় (পর্ব ১৮)। initializer_list রূপ std::minmax({...}) মান কপি করে দেয়, তাই নিরাপদ।

৭ · হিসাবের algorithm — <numeric>, accumulate-এর ফাঁদ আর shuffle

std::accumulate একটি শুরুর মান নিয়ে range-এর সব উপাদান বাঁ থেকে ডানে একে একে তার সাথে জুড়ে দেয়। ডিফল্টভাবে “জোড়া” মানে +; চতুর্থ argument দিলে অন্য যেকোনো নিয়ম। একটি অনলাইন দোকানের কার্ট ধরুন — প্রতিটি পণ্যের দাম আর পরিমাণ দুটি আলাদা তালিকায়। transform দিয়ে প্রতিটি লাইনের দাম, remove + erase দিয়ে শূন্য লাইন বাদ, আর accumulate দিয়ে মোট:

#include <algorithm>
#include <numeric>
#include <print>
#include <vector>

int main() {
    std::vector<int> price{120, 45, 300, 80};            // taka per item
    std::vector<int> qty{2, 0, 1, 3};
    std::vector<int> line(price.size());
    std::transform(price.begin(), price.end(), qty.begin(), line.begin(),
                   [](int p, int q) { return p * q; });  // 240 0 300 240
    auto it = std::remove(line.begin(), line.end(), 0);
    std::println("after remove: size {}", line.size());
    line.erase(it, line.end());
    std::println("after erase:  size {}", line.size());
    std::println("total = {} taka", std::accumulate(line.begin(), line.end(), 0));

    std::vector<double> kg{0.5, 1.25, 2.0};
    std::println("weight: int init {} vs double init {}",
                 std::accumulate(kg.begin(), kg.end(), 0),
                 std::accumulate(kg.begin(), kg.end(), 0.0));
}

আউটপুট চার লাইন: after remove: size 4, after erase: size 3, total = 780 taka, আর weight: int init 3 vs double init 3.75. দুই-range-এর transform দাম আর পরিমাণ জোড়ায় জোড়ায় নিয়ে লাইনের দাম বানাল (240, 0, 300, 240). remove-এর পরেও size 4 — প্রমাণ যে এটি কিছু মোছে না; erase-এর পরে 3. শেষ লাইনটিই এই অংশের আসল শিক্ষা: একই তিনটি ওজন, কিন্তু শুরুর মান 0 দিলে ফল 3, 0.0 দিলে 3.75. কোনো error নেই, এমনকি -Wall -Wextra-এও কোনো warning নেই।

কেন 3? — শুরুর মানই ফলের টাইপ

accumulate-এর ভেতরের যোগফল-ভ্যারিয়েবলের টাইপ ঠিক হয় শুরুর মান দেখে, উপাদান দেখে নয়। 0 একটি int, তাই প্রতিটি ধাপে int + double-এর ফল আবার একটি int-এ রাখা হয় — দশমিক অংশ কেটে বাদ। ধাপে ধাপে দেখুন:

1std::vector<double> kg{0.5, 1.25, 2.0};2auto a = std::accumulate(kg.begin(), kg.end(), 0);    // T = int3auto b = std::accumulate(kg.begin(), kg.end(), 0.0);  // T = double
ভেতরের যোগফল
এই ধাপের উপাদান
int শুরুতে: sum
double শুরুতে: sum
  1. শুরুর মান ঠিক করে টাইপ। accumulate একটি template: T init-এর T আসে তৃতীয় argument থেকে। 0 লিখলে T = int, 0.0 লিখলে T = double. উপাদানগুলো double কি না, তাতে কিছু যায় আসে না।
  2. প্রথম উপাদান 0.5. 0 + 0.5-এর ফল double 0.5, কিন্তু সেটি রাখা হলো int-এ — দশমিক কেটে 0. double-এর হিসাবে 0.5 ঠিকঠাক রইল।
  3. 1.25. int-এর দিকে 0 + 1.25 = 1.25 → 1. আধা কেজির হিসাব আগেই হারিয়েছে, এবার আরও ০.২৫.
  4. 2.0. int: 1 + 2.0 = 3.0 → 3. double: 3.75. প্রতিটি ধাপে কাটা পড়া অংশ জমে জমে বড় ভুল।
  5. ফল: 3 বনাম 3.75. compiler কোনো সতর্কবার্তা দেয়নি, কারণ রূপান্তরটি ঘটে library-র ভেতরে। দোকানের হিসাবে এমন ভুল মানে ভুল বিল। নিয়ম: শুরুর মান লিখুন ফলের টাইপে — 0.0, 0LL (বড় যোগফলে overflow এড়াতে), বা std::string{}.

একই কারণে অনেক বড় int যোগ করতে 0 দিলে যোগফল int-এর সীমা পেরিয়ে overflow হতে পারে; তখন 0LL দিন, যাতে যোগফল long long-এ থাকে।

accumulate-এর আত্মীয়রা

std::vector<int> runs{6, 4, 9, 12, 3};          // প্রতি ওভারে রান
std::vector<int> score(runs.size());
std::partial_sum(runs.begin(), runs.end(), score.begin());  // 6 10 19 31 34

std::vector<int> roll(5);
std::iota(roll.begin(), roll.end(), 1);          // 1 2 3 4 5 — ক্রমিক নম্বর

int total = std::reduce(runs.begin(), runs.end(), 0);   // C++17: ক্রম না মেনেও যোগ করতে পারে

std::partial_sum চলতি যোগফল বানায় — প্রতি ওভারের রান থেকে প্রতি ওভার শেষের মোট স্কোর, বা প্রতিদিনের লেনদেন থেকে প্রতিদিন শেষের ব্যালান্স। std::iota একটি range-কে শুরুর মান থেকে একে একে বাড়িয়ে ভরে — রোল নম্বর বা index-এর তালিকা বানাতে কাজে লাগে (পর্ব ৮-এর std::views::iota-র মতো, কিন্তু এটি সত্যিই ঘরগুলোতে লেখে)। std::reduce (C++17) দেখতে accumulate-এর মতো, কিন্তু standard তাকে উপাদানগুলো যেকোনো ক্রমে, যেকোনো দলে ভাগ করে জোড়ার অনুমতি দেয়। তাই একে সমান্তরালে চালানো যায়, কিন্তু অপারেশনটি associative আর commutative হতে হয় (যেমন পূর্ণসংখ্যার যোগ)। double-এর যোগে ক্রম বদলালে শেষ দশমিক ঘরে ফল একটু বদলাতে পারে — বাঁ থেকে ডানে নিশ্চিত ক্রম চাইলে accumulate.

shuffle — এলোমেলো, কিন্তু নিয়ন্ত্রিত

পরীক্ষার হলে আসন এলোমেলো করতে, বা কুইজের প্রশ্নের ক্রম বদলাতে std::shuffle. এটি একটি random-number engine নেয়, যেমন std::mt19937:

#include <random>
std::mt19937 rng(2026);                          // নির্দিষ্ট seed: প্রতিবার একই ক্রম
std::shuffle(seats.begin(), seats.end(), rng);

seed নির্দিষ্ট রাখলে প্রতিবার প্রোগ্রাম চালালে একই “এলোমেলো” ক্রম আসে — টেস্ট লেখা আর বাগ আবার ঘটিয়ে দেখার জন্য এটা অমূল্য। আসল ব্যবহারে seed নিন std::random_device থেকে (পর্ব ৪১)। একটি সূক্ষ্ম কথা: mt19937-এর সংখ্যার ধারা standard নির্দিষ্ট করে দেয়, কিন্তু shuffle সেই সংখ্যাগুলো কীভাবে ব্যবহার করবে তা নির্দিষ্ট নয় — তাই একই seed-এ GCC আর MSVC ভিন্ন ক্রম দিতে পারে। আর পুরোনো std::random_shuffle C++14-এ deprecated হয়ে C++17-এ বাদ পড়েছে; পুরোনো কোড বা AI-এর উত্তরে দেখলে বদলে দিন।

৮ · খরচের নিশ্চয়তা, parallel policy আর ranges-এর ঝলক

হাতে লেখা লুপ কত দ্রুত, তা জানতে কোড পড়ে হিসাব করতে হয়। STL algorithm-এর ক্ষেত্রে উত্তর standard-এই লেখা আছে — প্রতিটি algorithm-এর পাশে লেখা থাকে সর্বোচ্চ কতবার তুলনা বা অপারেশন হবে। এটি একটি চুক্তি: যে লাইব্রেরি এর বেশি নেয়, সে standard মানে না (Big-O-র কথা পর্ব ৮-এ)।

algorithmখরচ (n = উপাদান)মন্তব্য
find, count_if, all_of, transform, accumulateO(n)প্রতিটি উপাদান একবার
remove_if, uniqueO(n)এক পাস; erase-সহও O(n)
sortO(n log n) তুলনাC++11 থেকে সবচেয়ে খারাপ ক্ষেত্রেও; আগে শুধু গড়ে
stable_sortO(n log n), মেমরি না পেলে O(n log² n)সমানদের ক্রম নিশ্চিত
partial_sort (প্রথম k)প্রায় n log kk ছোট হলে পুরো sort-এর চেয়ে দ্রুত
nth_elementগড়ে O(n)median, k-তম স্থান
binary_search, lower_bound, equal_rangeO(log n) তুলনাশুধু সাজানো range-এ
set_intersection ইত্যাদিO(n + m)দুটিই সাজানো হতে হবে

একটি সূক্ষ্মতা: lower_bound std::list-এও চলে, আর তুলনা তখনো O(log n) টিই হয় — কিন্তু list-এ মাঝখানে লাফ দেওয়া যায় না, iterator এক এক ঘর হাঁটে, তাই মোট সময় O(n). complexity বুঝতে iterator-এর ধরনও (পর্ব ৩২) দেখতে হয়।

parallel execution policy — এক শব্দে সমান্তরাল

C++17 থেকে অনেক algorithm-এর প্রথম argument হিসেবে একটি execution policy দেওয়া যায় (<execution> header):

#include <execution>
std::sort(std::execution::par, v.begin(), v.end());   // একাধিক thread-এ সাজানোর অনুমতি
auto s = std::reduce(std::execution::par, v.begin(), v.end(), 0LL);

seq মানে সাধারণ ক্রমিক, par মানে একাধিক thread, par_unseq মানে thread-এর সাথে vector instruction-ও (আর C++20-এ এসেছে শুধু unseq). policy একটি অনুমতি, আদেশ নয় — লাইব্রেরি চাইলে ক্রমিকভাবেই চালাতে পারে। GCC-তে আসল সমান্তরাল চালাতে সাধারণত Intel-এর TBB লাইব্রেরি যুক্ত করতে হয় (-ltbb). আর দায়িত্ব আপনার: আপনার lambda যেন একই ডেটায় একসাথে লিখে data race না বাঁধায় (পর্ব ৪৫)। ছোট vector-এ thread চালুর খরচই বেশি পড়ে; মেপে দেখেই ব্যবহার করুন (পর্ব ৪৯)।

ranges সংস্করণ — begin/end আর লিখতে হয় না

C++20-এ প্রায় সব algorithm-এর একটি std::ranges:: সংস্করণ এসেছে, যা পুরো container সরাসরি নেয়:

std::ranges::sort(m);                                         // begin/end ছাড়াই
auto pass = std::ranges::count_if(m, [](int x) { return x >= 33; });
std::ranges::sort(students, std::greater{}, &Student::marks); // projection: কোন ফিল্ড দেখে

std::ranges::sort-এর শেষ উদাহরণে তৃতীয় argument একটি projection: “তুলনার আগে প্রতিটি Student থেকে marks বের করে নাও।” আর lambda লিখতে হয় না। ranges সংস্করণ concepts দিয়ে ভুল টাইপ compile-এর সময়েই ধরে, আর begin একটির আর end আরেকটির — এমন ভুলের সুযোগই রাখে না। views আর pipeline (|) নিয়ে পুরো আলোচনা পর্ব ৩৫-এ।

C জানলে

C-র <stdlib.h>-এ সাজানো আর খোঁজার জন্য আছে শুধু qsort আর bsearch; বাকি সব — গোনা, খোঁজা, যোগফল — হাতে লুপ লিখে করতে হয়। C-এর comparator const void* নিয়ে int (ঋণাত্মক/শূন্য/ধনাত্মক) ফেরত দেয়, আর C++-এর comparator আসল টাইপ নিয়ে শুধু bool বলে: “a কি b-এর আগে?” C++-এ টাইপ ভুলের সুযোগ নেই, আর comparator সাধারণত inline হয়ে যায় (ভাবনার প্রশ্ন ০৩)। sorting algorithm ভেতরে কীভাবে কাজ করে — bubble, insertion, merge, quick — তা হাতে লিখে দেখতে C কোর্সের sorting পর্ব পড়ুন।

পরীক্ষা ও ইন্টারভিউতে
  • remove আর erase-এর পার্থক্য কী? — remove একটি algorithm, রাখার মতো উপাদান সামনে সরিয়ে নতুন শেষ ফেরত দেয়, size বদলায় না; erase container-এর member, যা সত্যিই মোছে। দুটি মিলে erase-remove idiom; C++20-এ std::erase_if.
  • std::sort-এর complexity আর সাধারণ বাস্তবায়ন কী? — C++11 থেকে সবচেয়ে খারাপ ক্ষেত্রেও O(n log n) তুলনা; সাধারণত introsort (quicksort + heapsort + insertion sort). stable নয়।
  • stable sort কাকে বলে, C++-এ কোনটি? — সমান উপাদানের আপেক্ষিক ক্রম অপরিবর্তিত রাখে; std::stable_sort.
  • binary_search আর lower_bound-এর পার্থক্য? — প্রথমটি শুধু bool দেয়; দ্বিতীয়টি প্রথম ≥ উপাদানের iterator দেয়। দুটিরই পূর্বশর্ত সাজানো range, খরচ O(log n).
  • comparator-এ <= দিলে কী হয়? — strict weak ordering ভাঙে (comp(a, a) সত্য), ফল undefined behavior; crash পর্যন্ত হতে পারে।

৯ · AI-এর যুগে — লুপ আর algorithm-এর ভুল ধরা

AI টুলকে “নম্বরের তালিকা থেকে পাসের সংখ্যা বের করো” বললে প্রায়ই একটি হাতে লেখা index-লুপ আসে — কারণ অনলাইনের বিপুল শিক্ষামূলক কোড এভাবেই লেখা। উল্টো দিকে, algorithm ব্যবহার করলেও AI প্রায়ই পূর্বশর্তগুলো ভুলে যায়, কারণ সেগুলো কোডে দেখা যায় না — ডকুমেন্টেশনে লেখা থাকে। ভুলগুলো প্রায় সবই compile হয়, আর ছোট পরীক্ষায় ঠিকঠাক চলে। সাধারণ ভুলগুলো:

AI যা করেসমস্যাযা হওয়া উচিত
সব কাজে index-লুপ, counter, flagউদ্দেশ্য লুকানো; off-by-one-এর সুযোগcount_if, any_of, max_element…
erase ছাড়া শুধু std::remove(...)size বদলায় না; শেষে আবর্জনা থেকে যায়std::erase_if বা পুরো idiom
double-এর তালিকায় accumulate(..., 0)প্রতি ধাপে দশমিক কাটা; ভুল যোগফলশুরুর মান 0.0
comparator-এ <= বা >=undefined behavior; বড় ইনপুটে crash</>, tie ভাঙুন দ্বিতীয় ফিল্ডে
না-সাজানো ডেটায় binary_searchনীরবে ভুল উত্তরআগে sort, একই comparator-এ
সমান নম্বরে ক্রম জরুরি, তবু sortমেধাতালিকা প্রতিবার আলাদা হতে পারেstable_sort বা tie-ভাঙা comparator
max_element-এর ফল যাচাই না করে *itখালি তালিকায় undefined behaviorআগে it != end()

AI-কে কাজ দেওয়ার সময় বলে দিন আপনি কোন ধরনের কোড চান, আর প্রতিটি পূর্বশর্তের ব্যাখ্যা চান:

আমি C++23 শিখছি (g++, -Wall -Wextra). struct Student { int roll; std::string name; double marks; } — এর একটি vector নিয়ে একটি ছোট ফলাফল-প্রোগ্রাম লেখো। নিয়ম: ১. হিসাবের জন্য কোনো হাতে লেখা for/while লুপ নয় — প্রতিটি কাজে নাম-ওয়ালা STL algorithm (count_if, accumulate, stable_sort, partial_sort, max_element, erase_if ...)। শুধু ছাপানোর জন্য range-for চলবে। ২. কাজগুলো: অনুপস্থিত (marks < 0) সবাইকে বাদ দাও; পাসের সংখ্যা (marks ≥ 33); গড় (double-এ); সেরা ৩ জন; আর পুরো মেধাতালিকা, যেখানে নম্বর সমান হলে রোলের ক্রম থাকবে। ৩. comparator-এ কোথাও <= বা >= নয়; accumulate-এর শুরুর মান ফলের টাইপে। ৪. খালি তালিকা হলে কোনো iterator dereference বা শূন্য দিয়ে ভাগ যেন না হয়। কোডের পরে একটি তালিকা দাও: প্রতিটি algorithm কেন বেছেছ, তার complexity কত, আর তার কোনো পূর্বশর্ত (যেমন সাজানো range) থাকলে কোড কীভাবে তা নিশ্চিত করছে।

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

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

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

প্র ০১Sean Parent ২০১৩ সালে বললেন “no raw loops”. একটি লুপকে algorithm দিয়ে বদলালে আসলে কী লাভ হয় — শুধু কোড ছোট হওয়া, নাকি আরও গভীর কিছু?

২০১৩ সালে Microsoft-এর আয়োজিত GoingNative সম্মেলনে Adobe-এর Sean Parent “C++ Seasoning” নামে একটি বক্তৃতা দেন, যা পরে C++ সমাজে সবচেয়ে বেশি উদ্ধৃত বক্তৃতাগুলোর একটি হয়ে ওঠে। তিনি তিনটি লক্ষ্যের কথা বলেন: no raw loops, no raw synchronization primitives, no raw pointers. প্রথমটির ব্যাখ্যায় তিনি একটি বাস্তব প্রজেক্টের বেশ লম্বা, শর্ত-ভরা একটি লুপ দেখান, তারপর ধাপে ধাপে দেখান যে আসলে সেটি একটি find_if আর একটি std::rotate — দুই লাইন। লুপটি দেখে কেউ বুঝতে পারেনি সে কী করছে; algorithm-এর নাম দেখে সবাই সঙ্গে সঙ্গে বোঝে। তিনি দেখান কীভাবে rotate আর stable_partition দিয়ে “slide” (একটি অংশ তালিকার এক জায়গা থেকে আরেক জায়গায় সরানো) আর “gather” (ছড়ানো নির্বাচিত উপাদান এক জায়গায় জড়ো করা) — দুটি নতুন, নামওয়ালা algorithm বানানো যায়।

গভীর লাভটা হলো যুক্তি করার ক্ষমতা। একটি লুপের সঠিকতা প্রমাণ করতে হয় লুপের invariant দিয়ে — প্রতিটি পাকে কোন কথা সত্য থাকে। প্রতিটি নতুন লুপের জন্য সেই যুক্তি নতুন করে করতে হয়, আর পর্যালোচকদেরও করতে হয়। একটি algorithm-এর নাম সেই পুরো যুক্তির একটি সংক্ষিপ্ত রূপ: stable_partition লিখলে সবাই জানে ফল কী, ক্রম কী থাকবে, খরচ কত। কোডটি হয়ে যায় পরিচিত অংশ দিয়ে গড়া, যেমন গণিতে প্রমাণিত উপপাদ্য ব্যবহার করে নতুন প্রমাণ লেখা হয়, প্রতিবার গোড়া থেকে শুরু না করে।

দ্বিতীয় লাভ পরিবর্তনের নিরাপত্তা। নামওয়ালা অংশ আলাদা করে পরীক্ষা করা যায়, ভালো বাস্তবায়ন এলে এক জায়গায় বদলালেই চলে, আর parallel policy-র মতো নতুন ক্ষমতা পুরো কোডে একসাথে আসে। তৃতীয় লাভ শেখা: যিনি algorithm-এর ভাষায় ভাবেন, তিনি নতুন সমস্যাকে পরিচিত টুকরোয় ভাঙতে শেখেন।

তবে কথাটি “লুপ লেখা পাপ” নয়। কখনো কাজটির সত্যিই কোনো নাম নেই, কখনো একটি লুপ তিনটি algorithm-কে এক পাসে সেরে দেয় আর মেপে দেখা গেছে সেটাই দরকার। Parent-এর নিজের পরামর্শ: তখন লুপটিকে একটি ভালো নামের ফাংশনে ভরুন — অর্থাৎ নিজের algorithm বানান।

মূল উপলব্ধি: algorithm-এর আসল দান ছোট কোড নয়, বরং এমন শব্দভান্ডার, যা দিয়ে কোড নিজেই তার উদ্দেশ্য বলে — আর মানুষ ও যন্ত্র দুজনেই তার সঠিকতা নিয়ে সহজে যুক্তি করতে পারে।

প্র ০২std::sort সাধারণত introsort দিয়ে বানানো। quicksort-ই যদি “দ্রুততম”, তাহলে তার সাথে heapsort আর insertion sort মেশানোর দরকার হলো কেন?

quicksort গড়ে খুব দ্রুত: একটি pivot বেছে তালিকাকে দুই ভাগে ভাগ করে, তারপর প্রতিটি ভাগে একই কাজ। মেমরিতে পাশাপাশি পড়ে বলে cache-এর সাথেও এর সম্পর্ক চমৎকার। কিন্তু এর একটি দুর্বলতা আছে: pivot বারবার খারাপ পড়লে (যেমন প্রতিবার সবচেয়ে ছোট উপাদান), প্রতি ভাগে মাত্র একটি উপাদান কমে, আর খরচ হয়ে যায় O(n²)। “প্রথম, মাঝের আর শেষ উপাদানের মধ্যমা” (median-of-3) দিয়ে pivot বাছলে সাধারণ ইনপুটে সমস্যা প্রায় চলে যায়, কিন্তু এমন ইনপুট ইচ্ছা করে বানানো যায়, যা এই কৌশলকেও প্রতিবার ঠকায়।

১৯৯৭ সালে David Musser — Stepanov-এর পুরোনো সহকর্মী — “Introspective Sorting and Selection Algorithms” প্রবন্ধে সমাধান দেন। তিনি নিজেই দেখান কীভাবে median-of-3-কে হারানোর ইনপুট বানানো যায়, তারপর প্রস্তাব করেন: quicksort চালাও, কিন্তু recursion-এর গভীরতা গুনতে থাকো। গভীরতা একটি সীমা (সাধারণত 2·log₂ n-এর কাছাকাছি) পেরোলে বোঝা যায় ইনপুটটি খারাপ — তখন সেই অংশে চলে যাও heapsort-এ, যা সবসময় O(n log n), যদিও গড়ে quicksort-এর চেয়ে ধীর। algorithm নিজের অবস্থা “আত্মদর্শন” করে পথ বদলায়, তাই নাম introsort. আর খুব ছোট টুকরোতে (GCC-র libstdc++-এ ১৬টি উপাদানের মতো) recursion-এর খরচ বাঁচাতে শেষে insertion sort, যা প্রায়-সাজানো ছোট তালিকায় অত্যন্ত দ্রুত।

এর ফল standard-এও দেখা যায়। C++98 শুধু চেয়েছিল std::sort গড়ে O(n log n) হোক — ফলে সাধারণ quicksort-ও বৈধ ছিল। introsort প্রমাণ করল যে গড়ের গতি হারানো ছাড়াই সবচেয়ে খারাপ ক্ষেত্রের নিশ্চয়তাও দেওয়া যায়, আর C++11 শর্তটি কড়া করে: সবচেয়ে খারাপ ক্ষেত্রেও O(n log n) তুলনা। এটি শুধু তাত্ত্বিক নয় — ইচ্ছা করে বানানো খারাপ ইনপুট পাঠিয়ে একটি সার্ভারকে অচল করা (denial of service) বাস্তব আক্রমণের কৌশল। গল্প এখানেই থামেনি: পরে Orson Peters-এর pattern-defeating quicksort (pdqsort) একই মূল ভাবনা আরও উন্নত করে, যা Go ভাষার standard sort-সহ নানা জায়গায় গৃহীত হয়েছে।

মূল উপলব্ধি: বাস্তবের ভালো algorithm প্রায়ই একটি “বিশুদ্ধ” পাঠ্যবইয়ের algorithm নয়, বরং কয়েকটির বুদ্ধিমান মিশ্রণ — প্রতিটি তার শক্তির জায়গায়, আর একটি পাহারাদার, যে খারাপ ক্ষেত্র টের পেলে পথ বদলায়।

প্র ০৩C-র qsort আর C++-এর std::sort একই কাজ করে, তবু std::sort সাধারণত লক্ষণীয়ভাবে দ্রুত। একই মেশিনে, একই ডেটায় এই পার্থক্য আসে কোথা থেকে?

উত্তর লুকিয়ে আছে comparator কীভাবে পৌঁছায়, তাতে। qsort একবার compile হওয়া একটি সাধারণ library ফাংশন। সে জানে না আপনি কী সাজাচ্ছেন — শুধু একটি void*, উপাদানের সংখ্যা, প্রতিটির বাইট-আকার আর একটি function pointer পায়। প্রতিটি তুলনায় সে সেই pointer দিয়ে আপনার ফাংশন ডাকে — একটি পরোক্ষ ডাক, যা compiler সাধারণত inline করতে পারে না, কারণ qsort-এর কোড আগেই compile হয়ে আছে। আর দুটি উপাদান অদলবদল করতে তাকে বাইট ধরে কপি করতে হয়, কারণ সে টাইপ জানে না। এক লাখ উপাদান সাজাতে কয়েক লাখ থেকে কয়েক মিলিয়ন তুলনা হয় — প্রতিটির জন্য একটি পূর্ণ ফাংশন-ডাক।

std::sort একটি template. আপনি যখন int-এর vector আর একটি lambda দিয়ে এটি ডাকেন, compiler ঠিক সেই টাইপ আর ঠিক সেই comparator-এর জন্য একটি নতুন সংস্করণ বানায়। প্রতিটি lambda-র নিজের আলাদা টাইপ, তাই compiler জানে ঠিক কোন কোড চলবে, আর তুলনাটিকে inline করে একটি মাত্র CPU instruction-এ নামিয়ে আনতে পারে। অদলবদলও হয় আসল টাইপে — int-এর জন্য রেজিস্টারে, জটিল object-এর জন্য move দিয়ে। Scott Meyers তাঁর “Effective STL” (২০০১) বইয়ে এ নিয়ে একটি পুরো পরামর্শ লেখেন: algorithm-এ ফাংশনের বদলে function object দিন, কারণ function object inline হয়, ফাংশন pointer সাধারণত হয় না। এখানে আরেকটি শিক্ষা আছে: std::sort-কে সাধারণ ফাংশনের নাম দিলে সেটি pointer হয়ে যায়, আর একই সমস্যা ফিরে আসে — lambda বা function object এই কারণে ভালো।

গতির বাইরেও পার্থক্য আছে। C-র comparator-এ প্রচলিত একটি বাগ: return a - b; — বড় ঋণাত্মক আর বড় ধনাত্মক সংখ্যায় overflow হয়ে উল্টো চিহ্ন দেয়। আর void*-কে ভুল টাইপে cast করলে compiler কিছুই বলে না। C++-এর bool-ফেরত, টাইপ-জানা comparator-এ এই দুই ভুলের জায়গাই নেই। দাম একটি: প্রতিটি নতুন টাইপ-comparator জোড়ার জন্য নতুন কোড তৈরি হয়, তাই প্রোগ্রামের আকার একটু বাড়ে।

মূল উপলব্ধি: abstraction সবসময় ধীর নয়। template-এর মাধ্যমে abstraction compile-এর সময়েই মুছে যায়, আর compiler ফল হিসেবে হাতে লেখা বিশেষ কোডের মতো মেশিন কোড পায় — এটাই C++-এর “zero-overhead” প্রতিশ্রুতির সবচেয়ে চেনা উদাহরণ।

প্র ০৪comparator strict weak ordering না মানলে standard বলে undefined behavior. শুধু “ক্রম একটু ভুল হবে” না বলে এত কঠোর কেন — বাস্তবে কী ঘটতে পারে?

“ক্রম একটু ভুল হবে” কথাটা শুনতে যুক্তিসংগত, কিন্তু দ্রুত sort-এর বাস্তবায়ন comparator-এর গাণিতিক নিয়মগুলোর ওপর ভরসা করে নিজের কাজ বাঁচায়। একটি পরিচিত কৌশল দেখুন। quicksort-এর partition ধাপে বাঁ দিক থেকে একটি আঙুল এগোতে থাকে, যতক্ষণ উপাদান pivot-এর চেয়ে ছোট। সাধারণ কোডে প্রতি ধাপে দেখতে হতো “range-এর শেষে পৌঁছে গেলাম কি?” কিন্তু যদি নিশ্চিত থাকে যে pivot-এর সমান একটি উপাদান সামনে কোথাও আছে, আর comp(pivot, pivot) মিথ্যা, তাহলে আঙুল সেখানে থামবেই — সীমা-যাচাই বাদ দেওয়া যায়। লাখো তুলনায় এই বাঁচানো যাচাইগুলো আসল গতি দেয়। GCC-র libstdc++-এ এমন “unguarded” লুপ সত্যিই আছে।

এবার comparator-এ <= দিন। comp(pivot, pivot) এখন সত্য — আঙুল সমান উপাদানে থামে না, এগিয়ে যায়, আর range শেষ হলেও থামার কারণ পায় না। সে vector-এর বাইরের মেমরি পড়ে চলে, সেই আবর্জনা মানকে উপাদান ভেবে অদলবদলও করতে পারে। ফল: হয় segmentation fault, নয়তো অন্য ডেটা নষ্ট হওয়া — আর সেটা সাধারণত হয় শুধু বড় ইনপুটে, অনেক সমান মান থাকলে। ছোট টেস্টে, যেখানে insertion sort-ই সব কাজ করে, সব ঠিক দেখায়। তাই এমন বাগ প্রায়ই ধরা পড়ে প্রোডাকশনে বা কনটেস্টের লুকানো বড় টেস্টে, “Runtime Error” হয়ে।

অন্য ভাঙনগুলোও একই রকম বিপজ্জনক। NaN-সহ double-এ সাধারণ <: NaN কারও চেয়ে ছোট বা বড় নয়, তাই সে সবার “সমান”, অথচ বাকিরা পরস্পর সমান নয় — “সমান” আর ছড়ায় না। যে comparator এলোমেলো ফল দেয়, বা চলার মাঝে বদলে যাওয়া কোনো বাইরের অবস্থা পড়ে, সেটিও নিয়ম ভাঙে। std::set বা std::map-এ এমন comparator দিলে tree-এর ভেতরের কাঠামো অসংগত হয়ে যায়: ঢোকানো উপাদান খুঁজে পাওয়া যায় না, বা একই চাবি দুবার থাকে।

standard এখানে “অনির্দিষ্ট ক্রম” না বলে undefined behavior বলে, কারণ অন্যথায় প্রতিটি বাস্তবায়নকে প্রতিটি ধাপে সীমা যাচাই করতে হতো — সবার জন্য ধীর, শুধু ভুল কোডের সুরক্ষার জন্য। কিছু লাইব্রেরির debug বা hardened মোড কিছু ক্ষেত্রে ভাঙা comparator ধরে ফেলে, আর পর্ব ৪৩-এর sanitizer সীমার বাইরে পড়া ধরতে পারে — কিন্তু এগুলো নিশ্চয়তা নয়।

মূল উপলব্ধি: algorithm-এর চুক্তি দুই দিকের — লাইব্রেরি গতি আর complexity-র নিশ্চয়তা দেয়, বদলে আপনি comparator-এর গাণিতিক নিয়ম মানার নিশ্চয়তা দেন। আপনার দিকটা ভাঙলে লাইব্রেরির দিকটাও আর টেকে না।

প্র ০৫C++17 এক শব্দে algorithm-কে সমান্তরাল করার সুযোগ দিল: std::execution::par. তাহলে সব sort আর reduce-এ এটি লিখে দিলেই কি প্রোগ্রাম দ্রুত হবে?

আধুনিক CPU-তে একাধিক core থাকে, অথচ সাধারণ algorithm একটি core-এই চলে। Intel, Microsoft, NVIDIA-সহ কয়েকটি প্রতিষ্ঠানের প্রস্তাব থেকে ২০১৫ সালে একটি Parallelism Technical Specification তৈরি হয়, আর তার মূল অংশ C++17-এ standard-এ আসে। ধারণাটি চমৎকারভাবে সরল: algorithm-এর ডাকের প্রথমে একটি execution policy — seq, par, par_unseq (C++20-এ unseq) — আর বাকি কোড একই। এটি “no raw loops”-এর একটি বড় পুরস্কার: যে কোড আগেই algorithm-এ লেখা, তাকে সমান্তরাল করতে প্রায় কিছুই বদলাতে হয় না; হাতে লেখা লুপ হলে thread ভাগ করা, কাজ বণ্টন, ফল জোড়া সব নিজে লিখতে হতো।

কিন্তু “লিখে দিলেই দ্রুত” — না। প্রথমত, thread চালু করা, কাজ ভাগ করা আর ফল জোড়ার একটি নির্দিষ্ট খরচ আছে। কয়েক হাজার উপাদানে সেই খরচ কাজের চেয়েই বেশি পড়তে পারে; লাভ আসে বড় ডেটায়, আর তাও মেপে দেখে বুঝতে হয় (পর্ব ৪৯)। দ্বিতীয়ত, শর্তগুলো কড়া। par-এ আপনার lambda একাধিক thread থেকে একসাথে ডাকা হতে পারে, তাই সে কোনো ভাগ করা ভ্যারিয়েবলে লিখলে data race — undefined behavior (পর্ব ৪৫)। par_unseq-এ এমনকি একই thread-এ একাধিক ডাক মিশে যেতে পারে, তাই lambda-র ভেতরে mutex নেওয়াও নিষেধ। তৃতীয়ত, parallel policy-তে lambda থেকে exception বেরোলে প্রোগ্রাম std::terminate দিয়ে থেমে যায়। চতুর্থত, এ কারণেই accumulate-এর parallel সংস্করণ নেই — তার বাঁ-থেকে-ডানে ক্রমের প্রতিশ্রুতি সমান্তরালের সাথে মেলে না; তার জায়গায় reduce, যার অপারেশন associative আর commutative হতে হয়, আর double-এ শেষ দশমিক ঘরে ফল একটু বদলাতে পারে।

বাস্তবায়নও সমান নয়। MSVC নিজস্ব বাস্তবায়ন দেয়; GCC-র libstdc++ Intel-এর অবদান রাখা কোড ব্যবহার করে আর আসল সমান্তরালের জন্য সাধারণত TBB লাইব্রেরি চায়; NVIDIA-র nvc++ compiler একই standard কোড GPU-তে চালাতে পারে। policy শুধু অনুমতি দেয় — লাইব্রেরি চাইলে ক্রমিকভাবেই চালাতে পারে।

মূল উপলব্ধি: execution policy সমান্তরালকে সহজ করেছে, বিনামূল্যে নয়। এটি “কী” আর “কীভাবে”-কে আলাদা রাখার পুরস্কার — কিন্তু ডেটা কত বড়, lambda নিরাপদ কি না, আর ফলের ক্রম জরুরি কি না, সে বিচার এখনো আপনার।

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. আউটপুট অনুমান: std::vector<int> v{5, 0, 8, 0, 3, 8};. নিচের প্রতিটির ফল কী? (ক) std::count(v.begin(), v.end(), 8) (খ) std::find_if(...) দিয়ে প্রথম 5-এর বড় উপাদানের index (গ) std::remove(v.begin(), v.end(), 0)-এর পরে v.size() (ঘ) std::accumulate(v.begin(), v.end(), 0).

    (ক) 2. (খ) index 2 (মান 8). (গ) এখনো 6 — remove শুধু সামনে সরায়, প্রথম চার ঘরে 5 8 3 8, ফেরত দেওয়া iterator ঘর 4-এ; size বদলায় না। (ঘ) 24 — কিন্তু সাবধান: যদি (গ)-এর পরে চালান আর erase না করে থাকেন, শেষের দুই ঘরের অনির্দিষ্ট মানও যোগ হবে, তাই ফল নির্ভরযোগ্য নয়। এটাই erase না করার আসল বিপদ।

  2. বাগ শিকার: একটি AI এই কোড দিয়েছে। কয়টি সমস্যা পান?
    std::vector<double> price{99.5, 120.25, 0, 45.0};
    std::remove(price.begin(), price.end(), 0);
    double total = std::accumulate(price.begin(), price.end(), 0);
    std::sort(price.begin(), price.end(),
              [](double a, double b) { return a <= b; });
    bool has45 = std::binary_search(price.begin(), price.end(), 45.0);

    তিনটি গুরুতর সমস্যা। (১) remove-এর পরে erase নেই — size এখনো 4, শেষ ঘরে পুরোনো মান থেকে গেছে, তাই total-এ সেটিও যোগ হবে। লিখুন std::erase(price, 0.0);. (২) accumulate-এর শুরুর মান 0 — যোগফল int-এ, প্রতি ধাপে দশমিক কাটা; 0.0 দিন। (৩) comparator-এ <= — strict weak ordering ভাঙে, undefined behavior; a < b লিখুন, বা comparator বাদই দিন (ডিফল্ট <). শেষ লাইনের binary_search নিজে ঠিক আছে, কারণ ঠিক আগেই একই ক্রমে sort করা হয়েছে — কিন্তু (৩)-এর UB থাকলে sort-এর ফলের ওপরই ভরসা নেই।

  3. algorithm বাছুন: প্রতিটি কাজের জন্য সবচেয়ে উপযুক্ত একটি STL algorithm-এর নাম বলুন: (ক) ১০ লাখ বেতনের মধ্যে median (খ) সব অর্ডারের ঠিকানা খালি নয় কি না (গ) প্রতি মাসের আয় থেকে বছরের চলতি মোট (ঘ) সাজানো ফোন নম্বরের তালিকায় নতুন নম্বর কোথায় ঢোকালে তালিকা সাজানো থাকবে (ঙ) ১০০ জনের তালিকা থেকে সেরা ৫ জন, ক্রমসহ।

    (ক) nth_element — গড়ে O(n), পুরো sort লাগে না। (খ) none_of (বা all_of) একটি “খালি কি?” predicate দিয়ে — আর মনে রাখুন, খালি তালিকায় দুটোই true. (গ) partial_sum. (ঘ) lower_bound, তারপর insert(it, x). (ঙ) partial_sort, প্রথম ৫টি ঘর পর্যন্ত — সমান নম্বরে ক্রম জরুরি হলে tie-ভাঙা comparator দিন, কারণ partial_sort-ও stable নয়।

  4. নিজে লিখুন (কোড): উপরের result_sheet.cpp-এ stable_sort-এর জায়গায় partial_sort ব্যবহার করে শুধু সেরা ৩ জন ছাপান। সমান নম্বরে রোল ছোট যে, সে আগে — এটি comparator-এই নিশ্চিত করুন। আগে অনুমান করুন, কোন তিনটি লাইন ছাপা হবে।

    comparator: [](const Student& a, const Student& b) { if (a.marks != b.marks) return a.marks > b.marks; return a.roll < b.roll; }. ডাক: std::partial_sort(v.begin(), v.begin() + 3, v.end(), cmp);, তারপর for (int i = 0; i < 3; ++i) দিয়ে প্রথম তিনটি ছাপান (বা C++20-এ std::views::take(3)). ফল: Mita roll 2 85, Nusrat roll 4 85, Rafi roll 1 72. Joya-ও 72 পেয়েছে, কিন্তু রোল 6 বলে চতুর্থ। partial_sort stable নয়, তাই tie-ভাঙা comparator ছাড়া Rafi আর Joya-র মধ্যে কে তৃতীয় হবে, তার নিশ্চয়তা থাকত না। তালিকায় ৩ জনের কম থাকলে v.begin() + 3 সীমা পেরোবে — আগে std::min<std::size_t>(3, v.size()) নিন।

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

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

ক্রমC++-এ একটি vector থেকে সব duplicate সরিয়ে তারপর তাতে বারবার দ্রুত খোঁজার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. std::sort দিয়ে তালিকা সাজানো, যাতে একই মান পাশাপাশি আসে
  2. std::unique দিয়ে পাশাপাশি duplicate সামনে থেকে সরানো, নতুন শেষ পাওয়া
  3. erase দিয়ে নতুন শেষ থেকে আসল শেষ পর্যন্ত ছেঁটে ফেলা
  4. std::binary_search বা std::lower_bound দিয়ে O(log n)-এ খোঁজা

C++-এ std::vector<int> v{1, 0, 2, 0}; এরপর শুধু std::remove(v.begin(), v.end(), 0); লিখলে v.size() কত হয়?

  • 2
  • 3
  • 4
  • compile error

C++-এ std::vector<double> d{1.5, 2.5}; হলে std::accumulate(d.begin(), d.end(), 0)-এর ফল কী?

  • 4.0
  • 3
  • 4
  • compile error

C++-এ std::sort-এর comparator হিসেবে [](int a, int b) { return a <= b; } দেওয়া কেন ভুল?

  • এতে তালিকা উল্টো ক্রমে সাজে
  • comparator-এর bool ফেরত দেওয়া নিষেধ
  • শুধু sort একটু ধীর হয়ে যায়
  • এটি strict weak ordering ভাঙে, তাই undefined behavior

C++-এ শিক্ষার্থীদের তালিকা রোল অনুযায়ী সাজানো আছে। নম্বর অনুযায়ী সাজাতে চান, কিন্তু সমান নম্বরে রোলের ক্রম অবশ্যই থাকতে হবে। শুধু নম্বর-তুলনার comparator দিয়ে কোনটি ব্যবহার করবেন?

  • std::stable_sort
  • std::sort
  • std::partial_sort
  • std::nth_element

C++-এ একটি না-সাজানো vector-এ std::binary_search চালালে কী হয়?

  • সে আগে নিজে sort করে নেয়, তারপর খোঁজে
  • compile error আসে
  • পূর্বশর্ত ভাঙে; উত্তর নির্ভরযোগ্য নয় — থাকা মানকেও “নেই” বলতে পারে
  • সে নিজে থেকে linear search-এ চলে যায়, তাই উত্তর ঠিক থাকে

C++-এ দশ লাখ নম্বরের তালিকা থেকে শুধু সেরা ১০টি, সাজানো ক্রমে দরকার। কোনটি সবচেয়ে উপযুক্ত?

  • পুরো তালিকায় std::sort, তারপর প্রথম ১০টি
  • std::partial_sort, প্রথম ১০টি ঘর পর্যন্ত
  • std::max_element ১০ বার
  • std::binary_search

C++-এ একটি খালি std::vector<int> v;-এ std::all_of(v.begin(), v.end(), pred) কী ফেরত দেয়?

  • false
  • undefined behavior
  • exception ছোড়ে
  • true

শব্দকোষ

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

মনে রাখুন

  1. লুপ লেখার আগে নাম খুঁজুন: count_if, find_if, any_of, transform, max_element, accumulate — নাম বলে “কী”, লুপ বলে শুধু “কীভাবে”।
  2. algorithm iterator range [first, last)-এ কাজ করে; না পেলে last ফেরত দেয়, আর container-এর আকার বদলাতে পারে না।
  3. remove/unique-এর পরে erase — বা সরাসরি C++20-এর std::erase_if.
  4. comparator-এ <, কখনো <= নয়; সমানে ক্রম জরুরি হলে stable_sort; binary search শুধু একই নিয়মে সাজানো ডেটায়।
  5. accumulate-এর শুরুর মান = ফলের টাইপ (0.0, 0LL); প্রতিটি algorithm-এর complexity standard-এর নিশ্চয়তা।

পরের পর্বে: এই পর্ব জুড়ে আমরা [](int x) { return x >= 33; }-এর মতো ছোট lambda লিখেছি, ভেতরে না তাকিয়ে। এবার পুরো ছবি: capture ([=], [&]), lambda আসলে কী ধরনের object, function object আর std::function — আর কখন কোনটি।

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

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