Learn C++ STL

lesson ১ / ৯ · sort, comparator আর lambda: algorithm-কে বলে দাও তুমি কী চাও

Module ১২ · sort, comparator আর lambda: algorithm-কে বলে দাও তুমি কী চাও

sort: জিনিসপত্র সাজাও, আর বলে দাও কোন ক্রমে

Freeপড়া

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

  • sort, greater<int>() আর reverse iterator দিয়ে একটা vector আর একটা array-কে ছোট থেকে বড় আর বড় থেকে ছোট ক্রমে সাজাতে পারবে।
  • বুঝিয়ে বলতে পারবে, একটা comparator শুধু একটা প্রশ্নের উত্তর দেয়, "a কি b-এর আগে আসবে?", আর sort শুধু সেই উত্তর মেনেই জিনিস সরায়।
  • বলতে পারবে, সমান জিনিসের বেলায় stable_sort বাড়তি কী দেয়, আর n-টা জিনিস সাজানোর খরচ কত: O(n log n)-টা তুলনা।

Kenji-র বইয়ের দোকানের নতুন catalogue-এর জন্য 100,000টা দাম সাজাতে হবে। Kenji দ্রুত জিনিস ভালোবাসে, তাই নিজের হাতেই একটা sort লিখে ফেলল: একটা insertion sort, যেটা প্রতিটা দাম তুলে নিয়ে বাঁয়ে সরাতে থাকে, যতক্ষণ না ঠিক জায়গায় বসে। Compiler Explorer-এর GCC 12-এ নয়বার চালিয়ে এটা নিল 872 থেকে 2,432 ms। ওই একই 100,000টা সংখ্যা std::sort এক লাইনেই সাজিয়ে দিল 5.7 থেকে 7.5 ms-এ। Kenji মাপার আগেই optimise করে বসেছিল। এই lesson সেই এক লাইন নিয়ে, আর তুমি কোন ক্রম চাও, সেটা ওকে কীভাবে বলবে, তা নিয়ে।

sort(v.begin(), v.end()): প্রথম জিনিস থেকে end পর্যন্ত

sort থাকে <algorithm> header-এ। ও নেয় দুইটা iterator, মানে জায়গা চিনিয়ে দেওয়ার সেই চিহ্ন, যেটা Module 2-এ দেখেছ। prices.begin() দেখায় প্রথম দামটা। prices.end() দেখায় শেষ দামের ঠিক পরের জায়গাটা, তাই range চলে প্রথম জিনিস থেকে end()-এর ঠিক আগ পর্যন্ত, ওই জায়গাটা বাদ দিয়ে। এই range-এর প্রতিটা জিনিস sort ছোট থেকে বড় ক্রমে বসিয়ে দেয়, ওই vector-এর ভিতরেই।

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

int main() {
    vector<int> prices{40, 15, 55, 20, 35};
    sort(prices.begin(), prices.end());
    cout << "sorted:";
    for (int p : prices) {
        cout << ' ' << p;
    }
    cout << '\n';
    return 0;
}
sorted: 15 20 35 40 55
Run in Compiler begin() থেকে end() পর্যন্ত range Range চলে begin() থেকে end() পর্যন্ত, end() বাদ দিয়ে prices.begin() prices.end() 40 15 55 20 35 0 1 2 3 4 5 জায়গা 0 থেকে 4 সাজানো হয়: পাঁচটা জিনিস। end() হলো জায়গা 5, শেষ জিনিসের এক ধাপ পরে। ওখানে কিছুই নেই।

তাই sort(v.begin(), v.end()) পুরো vector-টাকে জায়গায় বসেই সাজায়, আর end() হলো শেষ জিনিসের এক ধাপ পরের জায়গা, নিজে কখনো কোনো জিনিস না।

sort শুধু একটা প্রশ্নই করে

sort কখনো পুরো vector একসাথে দেখে না। বারবার ও দুইটা জিনিস নেয়, a আর b, আর একটাই প্রশ্ন করে: a কি b-এর আগে আসবে? কোনো নিয়ম না দিলে প্রশ্নটা হলো a < b। যে function এই প্রশ্নের উত্তর দেয়, তাকে বলে comparator। Widget-টা পাঁচটা দামের উপর GCC 12-এর নিজের sort আবার চালিয়ে দেখায়। Compiler Explorer-এ একটা program প্রতিটা প্রশ্ন লিখে রেখেছিল, আর widget ঠিক সেই ক্রমেই প্রশ্নগুলো দেখায়।

GCC 12-এ sort(prices.begin(), prices.end())-এর 17টা ধাপ, প্রতি সারিতে একটা প্রশ্ন বা একটা সরানো।

ধাপএরপর দামগুলোকী হলো
140 15 55 20 35পাঁচটা দাম, যে ক্রমে এসেছিল সেভাবে। sort একটা প্রশ্নই বারবার করে: বামেরটা কি ডানেরটার চেয়ে ছোট?
240 15 55 20 3515 কি 40-এর চেয়ে ছোট? হ্যাঁ। 15 প্রথম জনেরও আগে বসবে, তাই সোজা সামনে চলে যাবে।
315 40 55 20 3515 চলে যায় ঘর 0-এ, আর ওর আগের সবাই এক ঘর করে ডানে সরে।
415 40 55 20 3555 কি 15-এর চেয়ে ছোট? না। 55 নতুন প্রথম না, তাই এবার বাম পাশের প্রতিবেশীর সাথে দেখা।
515 40 55 20 3555 কি 40-এর চেয়ে ছোট? না। তাই 55 এখানেই থামে।
615 40 55 20 3520 কি 15-এর চেয়ে ছোট? না। 20 নতুন প্রথম না, তাই এবার বাম পাশের প্রতিবেশীর সাথে দেখা।
715 40 55 20 3520 কি 55-এর চেয়ে ছোট? হ্যাঁ। তাই দুজন জায়গা বদলাবে।
815 40 20 55 3520 এক ঘর বামে সরে, 55-কে পার হয়ে।
915 40 20 55 3520 কি 40-এর চেয়ে ছোট? হ্যাঁ। তাই দুজন জায়গা বদলাবে।
1015 20 40 55 3520 এক ঘর বামে সরে, 40-কে পার হয়ে।
1115 20 40 55 3520 কি 15-এর চেয়ে ছোট? না। তাই 20 এখানেই থামে।
1215 20 40 55 3535 কি 15-এর চেয়ে ছোট? না। 35 নতুন প্রথম না, তাই এবার বাম পাশের প্রতিবেশীর সাথে দেখা।
1315 20 40 55 3535 কি 55-এর চেয়ে ছোট? হ্যাঁ। তাই দুজন জায়গা বদলাবে।
1415 20 40 35 5535 এক ঘর বামে সরে, 55-কে পার হয়ে।
1515 20 40 35 5535 কি 40-এর চেয়ে ছোট? হ্যাঁ। তাই দুজন জায়গা বদলাবে।
1615 20 35 40 5535 এক ঘর বামে সরে, 40-কে পার হয়ে।
1715 20 35 40 5535 কি 20-এর চেয়ে ছোট? না। তাই 35 এখানেই থামে। প্রতিটা দাম নিজের জায়গা পেয়ে গেছে: 15 20 35 40 55, মোট 11টা প্রশ্নে।

এগারোটা প্রশ্নেই পাঁচটা দাম সাজানো হয়ে গেল। প্রতিটা "হ্যাঁ"-তে sort একটা জিনিস বাঁয়ে সরিয়েছে, আর প্রতিটা "না"-তে জিনিসটা যেখানে ছিল সেখানেই রেখেছে। 16টা বা তার কম জিনিসে GCC 12-এর sort আসলে একটা insertion sort, Kenji-র মতোই একই idea। বড় vector আগে একটা দ্রুত plan পায়, সেটা দেখাবে lesson 05। তাই "সাজানো" মানে কী, sort নিজে জানেই না: ও শুধু জিজ্ঞেস করে, আর উত্তর যা বলে শুধু সেটুকুই সরায়।

প্রথম দিনেই যে syntax লাগবে

sort, stable_sort আর comparator-এর জায়গা

#include <algorithm>      sort and stable_sort
#include <functional>     greater

sort(first, last);                     increasing order, decided by <
sort(first, last, comp);               the order comp decides
stable_sort(first, last);              increasing, and equal items keep their order
stable_sort(first, last, comp);        the same, with your rule

sort(v.begin(), v.end());              the whole vector
sort(v.begin() + i, v.begin() + j);    places i to j - 1 only
sort(a, a + n);                        the first n items of an array
sort(v.begin(), v.end(), greater<int>());    decreasing order
sort(v.rbegin(), v.rend());            decreasing order, by sorting it backwards
  • first আর last হলো iterator। first দেখায় সাজানোর প্রথম জিনিস, আর last দেখায় শেষ জিনিসের ঠিক পরের জায়গা।
  • comp হলো comparator, মানে নিয়মটা। ও দুইটা জিনিস নেয়, আর প্রথমটাকে দ্বিতীয়টার আগে আসতে হলে true ফেরত দেয়। না দিলে sort নিজে < ব্যবহার করে।
  • greater<int>() হলো <functional>-এর একটা তৈরি comparator, যেটা উত্তর দেয় a > b। শেষের () একটা comparator বানিয়ে দেয়।
  • দুটো call-ই জিনিসগুলো জায়গায় বসেই বদলায়, কিছু ফেরত দেয় না।

তাই প্রতিটা call বলে কোথা থেকে শুরু, কোথায় থামা, আর চাইলে নিয়মটা।

একটা array: sort(a, a + n)

Learn C-তে দেখেছ, array-এর নাম ওর প্রথম জিনিসের pointer-এর মতো কাজ করে। a + 5 দেখায় পাঁচ নম্বর জিনিসের ঠিক পরের জায়গা। একটা pointer iterator-এর কাজ করতে পারে, তাই sort(a, a + 5) প্রথম পাঁচটা জিনিস সাজায়।

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

int main() {
    int a[] = {9, 4, 7, 1, 8};
    sort(a, a + 5);
    cout << "array:";
    for (int x : a) {
        cout << ' ' << x;
    }
    cout << '\n';
    return 0;
}
array: 1 4 7 8 9
Run in Compiler

তাই pointer-ও iterator: a দেখায় জায়গা 0, আর a + 5 দেখায় শেষ জিনিসের পরের জায়গা।

String সাজে character code ধরে

দুইটা string-এ < ওদের তুলনা করে অভিধানের মতো, একটা একটা character করে। প্রথম যে character-টা আলাদা, সেটাই ফয়সালা করে, ASCII table-এ ওর code দিয়ে। অঙ্ক আসে বড় হাতের অক্ষরের আগে, আর বড় হাতের অক্ষর আসে ছোট হাতের আগে।

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

int main() {
    vector<string> names{"zara", "Bob", "amara", "Kenji", "alice", "10", "9"};
    sort(names.begin(), names.end());
    cout << "sorted:";
    for (const string& name : names) {
        cout << ' ' << name;
    }
    cout << '\n';
    return 0;
}
sorted: 10 9 Bob Kenji alice amara zara
Run in Compiler

"10" আসে "9"-এর আগে, কারণ '1' (49) ছোট '9' (57)-এর চেয়ে, আর দ্বিতীয় character পর্যন্ত প্রশ্ন যায়ই না। Bob আর Kenji আসে alice-এর আগে, কারণ 'B' হলো 66, 'K' 75, আর 'a' 97। alice আর amara-র ফয়সালা হয় দ্বিতীয় অক্ষরে, 'l' আসে 'm'-এর আগে। তাই সাজানো string থাকে character code-এর ক্রমে, আর সেটা বর্ণমালার ক্রম হয় শুধু তখনই, যখন সব শব্দ একই case-এ লেখা।

বড় থেকে ছোট: greater<int>() নয়তো reverse iterator

বড় থেকে ছোট সাজাতে প্রশ্নটাই বদলে দাও: "a কি b-এর চেয়ে বড়?" greater<int>() ঠিক এটাই জিজ্ঞেস করে। আরেকটা উপায় হলো vector-টাকে উল্টো দিক থেকে পড়ে সাজানো: rbegin() আর rend() হলো reverse iterator, যেগুলো শেষ জিনিস থেকে প্রথমটার দিকে হাঁটে।

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

void show(const char* label, const vector<int>& v) {
    cout << label;
    for (int x : v) {
        cout << ' ' << x;
    }
    cout << '\n';
}

int main() {
    vector<int> v{40, 15, 55, 20, 35};
    sort(v.begin(), v.end(), greater<int>());
    show("greater:", v);

    vector<int> w{40, 15, 55, 20, 35};
    sort(w.rbegin(), w.rend());
    show("rbegin: ", w);
    return 0;
}
greater: 55 40 35 20 15
rbegin:  55 40 35 20 15
Run in Compiler

দুই লাইন একই। দ্বিতীয় call উল্টো দিকের পড়াটাকে বাড়তির দিকে সাজিয়েছে, তাই সামনে থেকে পড়লে সেটা কমতে থাকে। তাই ছোট থেকে বড় সাজিয়ে তারপর উল্টো করে print করার দরকার কখনো নেই: "বড় থেকে ছোট" কথাটা sort-কেই বলে দাও।

vector-এর শুধু একটা অংশ

দুইটা iterator যেকোনো range দেখাতে পারে, পুরো vector-ই হতে হবে এমন না। p.begin() + 1 দেখায় জায়গা 1, আর p.begin() + 5 দেখায় জায়গা 5, যেটা বাদ যায়। তাই এই call জায়গা 1 থেকে 4 সাজায়, আর কিছুতে হাত দেয় না।

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

int main() {
    vector<int> p{5, 1, 4, 2, 3, 9, 0};
    sort(p.begin() + 1, p.begin() + 5);
    cout << "places 1 to 4 sorted:";
    for (int x : p) {
        cout << ' ' << x;
    }
    cout << '\n';
    return 0;
}
places 1 to 4 sorted: 5 1 2 3 4 9 0
Run in Compiler

1, 4, 2 আর 3 হয়ে গেল 1 2 3 4। সামনের 5, আর পেছনের 9 আর 0, যেখানে ছিল সেখানেই রইল। Bob তাড়াহুড়ো করে "জায়গা 4 পর্যন্ত" বোঝাতে লিখেছিল p.begin() + 4, আর জায়গা 4 সাজানোই হলো না। তাই দ্বিতীয় iterator সবসময় দেখায় প্রথম সেই জায়গা, যেটা সাজানো হবে না।

তোমার নিজের নিয়ম: প্রথম lambda

sort-এর তৃতীয় argument হলো নিয়মটা। এখানে নিয়ম হলো "a আগে আসবে, যদি a শূন্যের বেশি কাছে থাকে"। এটা লেখা হয়েছে একটা lambda দিয়ে, মানে নামহীন ছোট্ট একটা function, যেটা যেখানে লাগে ঠিক সেখানেই লেখা হয়।

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

int main() {
    vector<int> d{-7, 3, -1, 5, -4};
    sort(d.begin(), d.end(), [](int a, int b) { return abs(a) < abs(b); });
    cout << "closest to zero first:";
    for (int x : d) {
        cout << ' ' << x;
    }
    cout << '\n';
    return 0;
}
closest to zero first: -1 3 -4 5 -7
Run in Compiler

[](int a, int b) { return abs(a) < abs(b); }-কে পড়ো sort-এর সেই একটা প্রশ্নের উত্তর হিসেবে। sort যতবার জিজ্ঞেস করে "a কি b-এর আগে আসবে?", lambda ততবার শূন্য থেকে দুইটার দূরত্ব তুলনা করে। <cstdlib>-এর abs minus চিহ্নটা ফেলে দেয়। Lambda-কে টুকরো টুকরো করে খুলে দেখাবে lesson 02। তাই ক্রম ঠিক করে নিয়ম, আর sort নিজে কখনো বদলায় না।

সমান জিনিস, আর stable_sort বাড়তি কী দেয়

দুইটা জিনিস tie, মানে সমানে সমান, যখন নিয়ম বলে কেউই অন্যটার আগে আসে না। Amara-র তাকে ছয়টা বই, প্রতিটার একটা দাম আর তাকের একটা অক্ষর, A থেকে F। দাম ধরে সাজালে তিনটা বইয়ের দাম 30, আর দুইটার 20। stable_sort কথা দেয়, সমান জিনিসগুলো ওদের পুরোনো ক্রমেই থাকবে। সাধারণ sort ওদের নিয়ে কোনো কথাই দেয় না।

struct Book {
    int price;
    char shelf;
};

bool byPrice(const Book& a, const Book& b) {
    return a.price < b.price;
}

stable_sort(books.begin(), books.end(), byPrice);

byPrice হলো একটা সাধারণ, নাম-ওয়ালা function হিসেবে লেখা comparator। ও দাম তুলনা করে, অক্ষরের দিকে তাকায়ই না। নিচের Example 3 এটাকে পুরোটা চালায়, আর widget সেটাই GCC 12-এ আবার চালিয়ে দেখায়।

GCC 12-এ ছয়টা বইয়ের ওপর stable_sort-এর ধাপগুলো। প্রতিটা item হলো একটা দাম আর বইয়ের অক্ষর, যেটা দেখায় তাকে বইটা কোথায় ছিল।

ধাপএরপর বইগুলোকী হলো
130 A, 20 B, 30 C, 10 D, 20 E, 30 Fতাকের ক্রমে ছয়টা বই, A থেকে F, প্রতিটার সাথে দাম। byPrice শুধু দাম মেলায়। stable_sort প্রথমে বাম অর্ধেকটা (ঘর 0 থেকে 2) আলাদা করে সাজায়।
230 A, 20 B, 30 C, 10 D, 20 E, 30 F20 B কি 30 A-এর চেয়ে কম? হ্যাঁ। 20 B প্রথম জনেরও আগে বসবে, তাই সোজা সামনে চলে যাবে।
320 B, 30 A, 30 C, 10 D, 20 E, 30 F20 B চলে যায় ঘর 0-এ, আর ওর আগের সবাই এক ঘর করে ডানে সরে।
420 B, 30 A, 30 C, 10 D, 20 E, 30 F30 C কি 20 B-এর চেয়ে কম? না। 30 C নতুন প্রথম না, তাই এবার বাম পাশের প্রতিবেশীর সাথে দেখা।
520 B, 30 A, 30 C, 10 D, 20 E, 30 F30 C কি 30 A-এর চেয়ে কম? না। দুটো সমান, আর সমান হলে আগে বসার প্রশ্ন ওঠে না, তাই 30 C থাকে 30 A-এর পেছনেই।
620 B, 30 A, 30 C, 10 D, 20 E, 30 Fএবার ডান অর্ধেক, ঘর 3 থেকে 5, আলাদা করে। 20 E কি 10 D-এর চেয়ে কম? না। 20 E নতুন প্রথম না, তাই নিজের জায়গা থেকে বামে হাঁটা শুরু করবে।
720 B, 30 A, 30 C, 10 D, 20 E, 30 Fবামে হাঁটার loop একই প্রশ্ন আবার করে: 20 E কি 10 D-এর চেয়ে কম? না। তাই 20 E এখানেই থামে।
820 B, 30 A, 30 C, 10 D, 20 E, 30 F30 F কি 10 D-এর চেয়ে কম? না। 30 F নতুন প্রথম না, তাই এবার বাম পাশের প্রতিবেশীর সাথে দেখা।
920 B, 30 A, 30 C, 10 D, 20 E, 30 F30 F কি 20 E-এর চেয়ে কম? না। তাই 30 F এখানেই থামে।
1020 B, 30 A, 30 C, 10 D, 20 E, 30 Fদুই অর্ধেকই সাজানো, এবার দুটো মিলবে। 10 D কি 20 B-এর চেয়ে কম? হ্যাঁ। তাই ডান অর্ধেকের 10 D পরের জায়গাটা পায়।
1110 D, 20 B, 30 A, 30 C, 20 E, 30 F10 D বসে ঘর 0-এ, আর বাম অর্ধেকের বাকিরা এক ঘর ডানে সরে।
1210 D, 20 B, 30 A, 30 C, 20 E, 30 F20 E কি 20 B-এর চেয়ে কম? না। শুধু সমান, তাই বাম অর্ধেকের 20 B আগে যায়: stable_sort এভাবেই সমান দুজনকে আগের ক্রমে রাখে।
1310 D, 20 B, 30 A, 30 C, 20 E, 30 F20 E কি 30 A-এর চেয়ে কম? হ্যাঁ। তাই ডান অর্ধেকের 20 E পরের জায়গাটা পায়।
1410 D, 20 B, 20 E, 30 A, 30 C, 30 F20 E বসে ঘর 2-এ, আর বাম অর্ধেকের বাকিরা এক ঘর ডানে সরে।
1510 D, 20 B, 20 E, 30 A, 30 C, 30 F30 F কি 30 A-এর চেয়ে কম? না। শুধু সমান, তাই বাম অর্ধেকের 30 A আগে যায়: stable_sort এভাবেই সমান দুজনকে আগের ক্রমে রাখে।
1610 D, 20 B, 20 E, 30 A, 30 C, 30 F30 F কি 30 C-এর চেয়ে কম? না। শুধু সমান, তাই বাম অর্ধেকের 30 C আগে যায়: stable_sort এভাবেই সমান দুজনকে আগের ক্রমে রাখে। দাম অনুযায়ী সাজানো, আর সমান দামের বইগুলো তাকের ক্রমেই আছে: B-র পরে E, আর A, C, F পরপর।

GCC 12 তিনটা করে বইয়ের প্রতিটা অর্ধেক আলাদা করে সাজিয়েছে, তারপর দুই অর্ধেক মিশিয়েছে, যাকে বলে merge। দুইটা দাম সমান হলে merge আগে বাঁ অর্ধেকের বইটা নিয়েছে। তাই B রইল E-এর আগে, আর A, C আর F রইল পরপর। তাই "stable" মানে ঠিক এটুকুই: সমান জিনিসগুলো যে ক্রমে ঢুকেছিল, সেই ক্রমেই বেরোয়।

sort সমান জিনিসের ক্রম রাখবে, এই ভরসা কোরো না

ছোট input-এ GCC 12-এর সাধারণ sort ঘটনাচক্রে সমান জিনিসের ক্রম রেখে দেয়। 16টা বা তার কম জিনিসে এটা একটা insertion sort, যেটা কোনো জিনিসকে ওর সমান কাউকে পার করায় না। 17টা থেকে আগে একটা ধাপ চলে, যেটা একটা মানের দুই পাশে জিনিসগুলো ভাগ করে, আর তখন সমান জিনিস জায়গা বদলাতে পারে। এই program record-গুলোকে 60, 70 আর 80 mark দেয়, আর input-এর ক্রমে অক্ষর দেয় a, b, c, এভাবে।

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

struct Rec {
    int mark;
    char tag;
};

bool byMark(const Rec& a, const Rec& b) {
    return a.mark < b.mark;
}

void show(int n, const char* label, const vector<Rec>& v) {
    cout << n << ' ' << label;
    for (const Rec& r : v) {
        cout << ' ' << r.mark << r.tag;
    }
    cout << '\n';
}

int main() {
    for (int n : {16, 17}) {
        vector<Rec> v;
        for (int i = 0; i < n; i++) {
            v.push_back({60 + ((i * 5 + 1) % 3) * 10, char('a' + i)});
        }
        vector<Rec> s = v;
        vector<Rec> t = v;
        sort(s.begin(), s.end(), byMark);
        stable_sort(t.begin(), t.end(), byMark);
        show(n, "sort:       ", s);
        show(n, "stable_sort:", t);
    }
    return 0;
}
16 sort:        60b 60e 60h 60k 60n 70a 70d 70g 70j 70m 70p 80c 80f 80i 80l 80o
16 stable_sort: 60b 60e 60h 60k 60n 70a 70d 70g 70j 70m 70p 80c 80f 80i 80l 80o
17 sort:        60b 60q 60n 60k 60h 60e 70g 70j 70d 70m 70p 70a 80f 80i 80l 80c 80o
17 stable_sort: 60b 60e 60h 60k 60n 60q 70a 70d 70g 70j 70m 70p 80c 80f 80i 80l 80o
Run in Compiler

16টা record-এ দুই লাইন মিলে যায়। 17টায় sort 60q-কে বসিয়েছে দ্বিতীয় জায়গায়, আর 70a-কে 70-গুলোর একদম শেষে, অথচ stable_sort প্রতিটা সমান জিনিস অক্ষরের ক্রমে রেখেছে। ঠিক এই কারণেই Zara সবার আগে 17টা বা তার বেশি সমান key দিয়ে test করে। তাই সমান জিনিসের ক্রম রাখতে হলে stable_sort দিয়ে সেটা বলে দাও: অল্প কয়টা জিনিসের test-এ পার্থক্যটা ধরা পড়বে না।

সাজানোর খরচ: O(n log n)

Kenji-র insertion sort প্রতিটা নতুন জিনিসকে ওর আগের প্রতিটা বড় জিনিস পার করে সরায়। এলোমেলো সংখ্যায় সেটা মোটামুটি n2/4 বার সরানো, তাই জিনিস দশগুণ হলে সময় লাগে মোটামুটি একশো গুণ। std::sort-এর লাগে মোটামুটি n log2 n-টা তুলনা, যেখানে log2 n হলো n-কে কতবার অর্ধেক করা যায়: 100,000-এর জন্য প্রায় 17। এটাকে লেখা হয় O(n log n)। C++11 থেকে standard এটা worst case-এও দাবি করে। Compiler Explorer-এর GCC 12-এ আটবার চালিয়ে Kenji-র সংখ্যাগুলো এমন এল:

জিনিসহাতে লেখা insertion sortstd::sort
1,0000.099 থেকে 0.274 ms0.036 থেকে 0.173 ms
10,0009.0 থেকে 26.4 ms0.45 থেকে 0.56 ms
100,000872 থেকে 2,199 ms5.65 থেকে 7.49 ms
1,000,000চালানো হয়নি66.7 থেকে 86.1 ms
হাতে লেখা insertion sort বনাম std::sort, GCC 12 এলোমেলো int সাজানোর সময়, log scale (প্রথম তিন run-এর সবচেয়ে কম) হাতে লেখা insertion sort std::sort 10,000 ms 1,000 ms 100 ms 10 ms 1 ms 0.1 ms 0.01 ms 0.099 0.037 9.0 0.46 872 5.7 1,000টা জিনিস 10,000টা জিনিস 100,000টা জিনিস জিনিস দশগুণ হলে হাতে লেখা sort-এর সময় বাড়ে প্রায় 100 গুণ, std::sort-এর প্রায় 12 গুণ।

100,000টা জিনিসে হাতে লেখা sort নিয়েছে 150 গুণেরও বেশি সময়। আরেকটা run, একদম প্রথমটা, হাতে লেখা sort-এ নিয়েছিল 2,432 ms, সেজন্যই মাপাটা বারবার করা হয়েছে। Buffer-এর জন্য memory ধার পেলে stable_sort-এর খরচও O(n log n), ওর সংখ্যাগুলো দেবে lesson 02। তাই বড় vector-এ machine-এর চেয়ে plan বেশি জরুরি: std::sort নাও, আর ওকে ওর প্রশ্নগুলো করতে দাও।

Example 1: পাঁচটা দাম, দুই দিকেই

সবচেয়ে ছোট program: একই দামগুলো, প্রথমে ছোট থেকে বড়, তারপর বড় থেকে ছোট। একটা দাম দুইবার আছে, আর দুটো copy-ই থেকে যায়।

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

int main() {
    vector<int> prices{120, 45, 300, 45, 80};
    sort(prices.begin(), prices.end());
    cout << "low to high:";
    for (int p : prices) {
        cout << ' ' << p;
    }
    cout << '\n';

    sort(prices.begin(), prices.end(), greater<int>());
    cout << "high to low:";
    for (int p : prices) {
        cout << ' ' << p;
    }
    cout << '\n';
    return 0;
}
low to high: 45 45 80 120 300
high to low: 300 120 80 45 45

দুইটা 45 হলো tie। সাধারণ int-এ ওদের আলাদা করে চেনার উপায় নেই, তাই এখানে ওদের ক্রমে কিছু যায় আসে না।

Run in Compiler
Example 2: বর্ণমালার ক্রমে নাম, আর বড় হাতের অক্ষরের ফাঁদ

Maria club-এর নামগুলো type করে, কোনোটা বড় হাতের অক্ষর দিয়ে শুরু, কোনোটা না। প্রথম sort Zara-কে বসায় bob-এর আগে, আর Maria জিজ্ঞেস করে: কিন্তু কেন? কারণ 'Z' হলো 90, আর 'b' 98। সমাধান হলো আগে প্রতিটা নাম ছোট হাতের করে নেওয়া, <cctype>-এর tolower দিয়ে।

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

void show(const char* label, const vector<string>& names) {
    cout << label;
    for (const string& name : names) {
        cout << ' ' << name;
    }
    cout << '\n';
}

int main() {
    vector<string> names{"maria", "Zara", "bob", "Amara", "kenji"};
    sort(names.begin(), names.end());
    show("as typed:  ", names);

    for (string& name : names) {
        for (char& c : name) {
            c = tolower(c);
        }
    }
    sort(names.begin(), names.end());
    show("lowercase: ", names);
    return 0;
}
as typed:   Amara Zara bob kenji maria
lowercase:  amara bob kenji maria zara

প্রথম লাইন character code-এর ক্রমে, বড় হাতের দুইটা নামই আগে। দ্বিতীয় লাইন বর্ণমালার ক্রমে, কারণ এখন প্রতিটা নাম একই case-এ।

Run in Compiler
Example 3: Amara-র তাক, stable_sort দিয়ে দাম ধরে সাজানো

Widget যে program আবার চালিয়ে দেখিয়েছে, এটাই সেটা। byPrice শুধু দাম তুলনা করে, আর stable_sort সমান দামের বইগুলো তাকের ক্রমে রাখে।

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

struct Book {
    int price;
    char shelf;
};

bool byPrice(const Book& a, const Book& b) {
    return a.price < b.price;
}

int main() {
    vector<Book> books{{30, 'A'}, {20, 'B'}, {30, 'C'}, {10, 'D'}, {20, 'E'}, {30, 'F'}};
    stable_sort(books.begin(), books.end(), byPrice);
    for (const Book& b : books) {
        cout << b.price << ' ' << b.shelf << '\n';
    }
    return 0;
}
10 D
20 B
20 E
30 A
30 C
30 F

দুইটা 20 বেরোল B, তারপর E, আর তিনটা 30 বেরোল A, C, F: ওদের তাকের ক্রমেই। ছয়টা বইয়ে GCC 12-এ সাধারণ sort-ও একই জিনিস print করত, কিন্তু শুধু কপালগুণে, 17টা record সেটাই দেখিয়েছে।

Run in Compiler
Example 4: সেরা তিনটা score, আর মাত্র দুইজন player-এর একটা খেলা

Zara একটা খেলার সেরা তিনটা score print করে। আগে ও edge case test করে নেয়: মাত্র দুইজন player হলে দুইটা score-ই print হবে, vector-এর শেষ পেরিয়ে কিছু পড়া হবে না।

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

void topThree(vector<int> scores) {
    sort(scores.begin(), scores.end(), greater<int>());
    int shown = min(3, (int)scores.size());
    cout << "top " << shown << ':';
    for (int i = 0; i < shown; i++) {
        cout << ' ' << scores[i];
    }
    cout << '\n';
}

int main() {
    topThree({70, 95, 60, 95, 88, 40});
    topThree({50, 75});
    return 0;
}
top 3: 95 95 88
top 2: 75 50

topThree ওর vector নেয় by value, মানে একটা copy, তাই sort কখনো caller-এর score বদলায় না। Player তিনজনের কম হলে min(3, size) loop-কে vector-এর ভিতরেই রাখে।

Run in Compiler

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

  • GNU sort, command line-এর tool. GNU coreutils-এর এই tool-এর -s (--stable) option পুরো লাইন তুলনা করার শেষ ধাপটা বন্ধ করে দেয়। তাই বাছাই করা key-তে সমান লাইনগুলো input-এর ক্রমেই থাকে, মানে চাইলেই stable_sort।
  • Python-এর sorted(). Python-এর documentation কথা দেয়, built-in sorted() stable। তাই record-গুলো এক key দিয়ে সাজিয়ে, তারপর আরও জরুরি একটা key দিয়ে আবার সাজালে, দুই key-এর ঠিক ক্রম পাওয়া যায়।
  • PostgreSQL-এর ORDER BY. ওর documentation বলে, ORDER BY-এর প্রতিটা column-এ সমান row-গুলো ফেরত আসে implementation-এর উপর নির্ভর করা একটা ক্রমে। এটাই সাধারণ sort-এর নিয়ম: সমান row প্রতিবার একই ক্রমে চাইলে আরেকটা column জুড়ে দাও, যেমন row-এর id।

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

১. Header লিখতে ভুলে যাওয়া।

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

int main() {
    vector<int> v{3, 1, 2};
    sort(v.begin(), v.end());
    return 0;
}

Runner-এর flag-এ error: error: 'sort' was not declared in this scope; did you mean 'short'?। Container-এর header ভুলে গেলে GCC 12 যেমন header-এর নাম বলে একটা note দেয়, এখানে তেমন কিছু দেয় না। #include <algorithm> যোগ করো। তুমি এটা ভুলবে, কারণ <iostream> আর <vector>-কেই যথেষ্ট মনে হয়।

২. end-এর বন্ধনী ছাড়া লেখা।

vector<int> v{3, 1, 2};
sort(v.begin(), v.end);

Runner-এর flag-এ error: error: no matching function for call to 'sort(std::vector<int>::iterator, <unresolved overloaded function type>)', তারপর লম্বা লম্বা note। v.end শুধু function-টার নাম; ওকে call করে iterator পাওয়া যায় শুধু v.end() লিখলে। প্রথম লাইনের <unresolved overloaded function type> অংশটা খোঁজো। দ্রুত type করতে গিয়ে তুমি এটা করবে, কারণ এক মুহূর্ত আগেই begin() ওর বন্ধনী পেয়েছিল।

৩. greater<int>-এর বন্ধনী ছাড়া লেখা।

vector<int> v{3, 1, 2};
sort(v.begin(), v.end(), greater<int>);

Runner-এর flag-এ error: error: expected primary-expression before ')' token। greater<int> একটা type, আর sort-এর লাগে ওই type-এর একটা value। লেখো greater<int>(), যেটা একটা বানিয়ে দেয়। তুমি বন্ধনী ফেলে দেবে, কারণ Module 7-এ priority_queue-এর angle bracket-এর ভিতরে greater<int> বন্ধনী ছাড়াই লেখা হয়েছিল, যেখানে type-টাই ঠিক।

মাথা খাটাও

প্রথম widget-এর প্রশ্নগুলো গোনো: পাঁচটা দাম সাজাতে GCC 12-এর sort করেছে 11টা প্রশ্ন। Kenji ভাবে, আরও চালাক কোনো sort কি সবসময় এর চেয়ে কম প্রশ্নে পারত? যেকোনো 5টা আলাদা জিনিস সাজাতে সবচেয়ে কম কয়টা প্রশ্ন সবসময় যথেষ্ট? আর যে sort শুধু "a কি b-এর আগে আসবে?" জিজ্ঞেস করে, সে কেন সবসময় এর চেয়ে কম প্রশ্নে পারবে না?

5টা জিনিস কত রকম ক্রমে আসতে পারে, গুনে দেখো। তারপর ভাবো, একটা হ্যাঁ-না উত্তর তার কয়টাকে আলাদা করতে পারে, দুইটা উত্তর কয়টাকে, আর k-টা উত্তর কয়টাকে।

অনুশীলন ১সহজ

Maria ক্লাস test-এর খাতা দেখে roll-এর ক্রমে। এবার ও notice board-এ ফলাফল টাঙাবে, সবচেয়ে বেশি mark সবার উপরে। সমান mark পাওয়া ছাত্রছাত্রীরা থাকবে roll-এর ক্রমেই, যাতে কেউ বলতে না পারে board কোনো বন্ধুর পক্ষ নিয়েছে।

Input. এক লাইনে n, তারপর n-টা লাইন। প্রতিটা লাইনে একজনের নাম আর mark, মাঝে একটা space, roll-এর ক্রমে।

Output. Board-এর ক্রমে n-টা লাইন: নাম আর mark, মাঝে একটা space। বেশি mark আগে আসে, আর সমান mark পাওয়া ছাত্রছাত্রীরা roll-এর ক্রম রাখে।

Constraints. 1 <= n <= 60000। একটা নাম 1 থেকে 10টা ছোট হাতের ইংরেজি অক্ষর, আর দুইজনের নাম কখনো এক না। Mark হলো 0 থেকে 100-এর একটা পূর্ণসংখ্যা।

Sample. Input 7, তারপর maria 70, bob 85, kenji 70, zara 92, david 85, amara 70 আর alice 60 দিলে আসে zara 92, bob 85, david 85, maria 70, kenji 70, amara 70 আর alice 60, প্রতি লাইনে একটা। Bob আর David দুজনেরই 85, আর Bob-এর খাতা আগে দেখা হয়েছে, তাই Bob আগে।

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

struct Student {
    string name;
    int mark;
};

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

    int n = 0;
    cin >> n;
    vector<Student> roll(n);   // in roll order, the order the papers were marked
    for (Student& s : roll) {
        cin >> s.name >> s.mark;
    }

    // Put roll in board order: the highest mark first, and students
    // with equal marks in roll order. Then print one student per line.

    return 0;
}

notice-board নামে grade হয়। এটা এই module-এর problem set-এর একটা free problem। Hidden test-এ আছে একজন মাত্র ছাত্র, একই mark পাওয়া 60000 জন, আর শুধু 0 আর 100 mark। আরও আছে আগে থেকেই board-এর ক্রমে থাকা একটা roll, আর উল্টো ক্রমের একটা। আর আছে এমন 17 জন, যাদের সমান mark-এর ক্রম সাধারণ sort উল্টেপাল্টে দেয়।

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

David-এর gymnastics club-এ n জন judge একটা routine-কে একটা করে score দেন। একজন কড়া বা একজন উদার judge যাতে একাই ফল ঠিক করে দিতে না পারেন, তাই club সবচেয়ে কম k-টা আর সবচেয়ে বেশি k-টা score বাদ দেয়। বাকি score-গুলোর যোগফলই routine-এর ফল।

Input. এক লাইনে n আর k। তারপর এক লাইনে n-টা score, মাঝে একটা করে space।

Output. এক লাইন: সবচেয়ে কম k-টা আর সবচেয়ে বেশি k-টা বাদ দেওয়ার পর বাকি score-গুলোর যোগফল।

Constraints. 1 <= n <= 100000 আর 0 <= 2k < n। প্রতিটা score 0 থেকে 100-এর একটা পূর্ণসংখ্যা।

Sample. Input 7 2 আর 9 6 8 10 7 8 3 দিলে 23। ক্রমে সাজালে score-গুলো 3 6 7 8 8 9 10। 3, 6, 9 আর 10 বাদ দিলে থাকে 7 + 8 + 8।

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

int main() {
    int n, k;
    cin >> n >> k;
    vector<int> score(n);
    for (int& s : score) {
        cin >> s;
    }

    // Drop the k lowest and the k highest scores, and print the sum of
    // the scores that are left.

    return 0;
}

আলাদা করে grade হয় না। Sort করার পর যে score-গুলো রাখতে হবে, সেগুলো মাঝখানে এক টানা বসে থাকে: জায়গা k থেকে জায়গা n - k পর্যন্ত, n - k বাদ দিয়ে।

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

Bob n-টা বই উচ্চতা ধরে তাকে সাজিয়েছিল, সবচেয়ে ছোটটা আগে। তারপর তাকে ধাক্কা লাগল, আর ওর ধারণা একটা টানা অংশের বইগুলো উল্টে গেছে, শেষেরটা আগে। বই অদলবদল শুরু করার আগে ও জানতে চায়, একটা অংশ আবার উল্টে দিলেই তাক সত্যিই ঠিক হয় কি না।

Input. এক লাইনে n, তারপর এক লাইনে তাকের ক্রমে n-টা উচ্চতা, মাঝে একটা করে space।

Output. উচ্চতাগুলো আগে থেকেই ক্রমে থাকলে print করো sorted। নইলে বের করো, তাক সাজানো ক্রম থেকে প্রথম আর শেষ কোন জায়গায় আলাদা, 1 থেকে গুনে। ওই দুই জায়গার মাঝের বইগুলো উল্টে দিলে তাক সাজানো হয়ে গেলে print করো yes আর জায়গা দুইটা। না হলে print করো no।

Constraints. 1 <= n <= 100000। প্রতিটা উচ্চতা 1 থেকে 1000000000-এর একটা পূর্ণসংখ্যা, আর দুইটা বইয়ের উচ্চতা সমান হতে পারে।

Sample. Input 6 আর 1 2 6 5 3 7 দিলে yes 3 5: 6 5 3 উল্টালে হয় 1 2 3 5 6 7। Input 5 আর 3 1 2 4 5 দিলে no: 3 1 2 উল্টালে হয় 2 1 3।

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

int main() {
    int n;
    cin >> n;
    vector<int> height(n);
    for (int& h : height) {
        cin >> h;
    }

    // Print sorted if the shelf is already in order. Otherwise find the
    // first and the last place that differ from the sorted order. Print
    // yes and those two places, counted from 1, if reversing that part
    // sorts the shelf, and no if it does not.

    return 0;
}

আলাদা করে grade হয় না। Lesson যে কথাটা শুধু ইঙ্গিতে বলেছে: একটা copy sort করো, তাহলে copy-টাই বলে দেবে প্রতিটা বইয়ের আসল জায়গা কোথায়। তারপর <algorithm>-এর reverse একটা range উল্টে দেয়।

Run in Compiler

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

  • sort সাজানো vector-টা ফেরত দেয় না কেন?

    জিনিসগুলো যেখানে আছে, ও সেখানেই সাজায়, কিছু ফেরত দেয় না। পুরোনো ক্রমটাও লাগলে আগে vector-টা copy করো, তারপর copy-টা sort করো, যেমন Example 4 vector-টা by value নিয়ে করেছে।

  • একটা string-এর অক্ষরগুলো কি sort করা যায়?

    যায়। Vector-এর মতো string-এরও begin() আর end() আছে, তাই sort(s.begin(), s.end()) ওর character-গুলো code ধরে সাজায়। GCC 12-এ "banana" হয়ে গেছে aaabnn।

  • খালি vector বা একটা মাত্র জিনিস পেলে sort কী করে?

    কিছুই না, আর এতে কোনো বিপদ নেই। GCC 12-এ একটা গোনার comparator দুই ক্ষেত্রেই দেখিয়েছে 0টা প্রশ্ন, কারণ জিজ্ঞেস করার মতো কোনো জোড়াই নেই।

  • Range কেন "end পর্যন্ত, end বাদ দিয়ে"?

    কারণ তাহলে end() - begin() হয় জিনিসের সংখ্যা, আর খালি range মানে সোজা begin() == end()। পাশাপাশি দুইটা range, যেমন জায়গা 0 থেকে 3 আর 3 থেকে 6, মাঝের iterator-টা ভাগাভাগি করে নেয়: কোনো জায়গা দুইবার পড়ে না, কোনোটা বাদও যায় না।

মূল কথা

  • sort(first, last) first থেকে শুরু করে last-এর ঠিক আগ পর্যন্ত range-টা জায়গায় বসেই ছোট থেকে বড় ক্রমে সাজায়: sort(v.begin(), v.end()) বা sort(a, a + n)।
  • একটা comparator শুধু একটা প্রশ্নের উত্তর দেয়, "a কি b-এর আগে আসবে?", আর sort উত্তর যা বলে শুধু সেটুকুই সরায়।
  • বড় থেকে ছোট মানে greater<int>(), বা rbegin() আর rend(); string সাজে character code ধরে, তাই case-এ ফারাক পড়ে।
  • stable_sort সমান জিনিসগুলোকে পুরোনো ক্রমে রাখে; সাধারণ sort কোনো কথা দেয় না, আর GCC 12 17টা জিনিস থেকেই সমান জিনিসের ক্রম বদলে ফেলে।
  • n-টা জিনিস সাজানোর খরচ O(n log n)-টা তুলনা, C++11 থেকে worst case-এও, যেখানে insertion sort-এ লাগে মোটামুটি n2/4 বার সরানো।
  • আরও গভীরে: ভেতরের কথা, introsort, comparator-এর call আর একটা lambda-র খরচ (Pro)।

এরপর lesson 02 সাজানোর প্রতিটা call আর নিয়ম লেখার প্রতিটা উপায় একটা একটা করে দেখাবে, প্রতিটার খরচসহ।

lesson ১ শেষ

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

পরেরটা: সাজানোর প্রতিটা call আর নিয়ম লেখার প্রতিটা উপায়, খরচসহ