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

কমপ্লেক্সিটি — Big-O দিয়ে গতি মাপা

Complexity — Measuring How Work Grows with Big-O

Taught in Bangla, this lesson explains algorithm complexity: counting steps as input grows, Big-O notation, the common classes from O(1) to O(2^n), why log n is so fast, reading complexity from code, space and amortized cost, and real-world constants.

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

  • কেন সময়ের বদলে “ধাপ গোনা” আর growth rate দিয়ে অ্যালগরিদম তুলনা করা হয়
  • Big-O, best/average/worst case, আর Ω ও Θ-এর সহজ অর্থ
  • কোড দেখে complexity বলা — লুপ, নেস্টেড লুপ, অর্ধেক-করা লুপ আর রিকার্শন
  • Space complexity আর amortized খরচ — dynamic array কেন গড়ে সস্তা
  • বাস্তবের হিসাব: ধ্রুবক, cache, “সেকেন্ডে ১০ কোটি ধাপ” নিয়ম আর clock() দিয়ে মেপে দেখা

১ · নতুন মডিউল — “চলে” থেকে “দ্রুত চলে”

অভিনন্দন — পাঁচটি মডিউল পেরিয়ে আপনি এখন linked list, stack, queue, tree, hash table আর graph চেনেন। এবার শুরু হলো মডিউল ৬, “অ্যালগরিদম ও প্রবলেম সলভিং”. এতদিন প্রশ্ন ছিল “কোডটা কি ঠিক উত্তর দেয়?” এখন থেকে আরেকটি প্রশ্ন যোগ হবে: “ডেটা ১০ লাখ গুণ বড় হলেও কি এটি সময়মতো উত্তর দেবে?” এই মডিউলে আমরা সর্টিং, সার্চিং, ডায়নামিক প্রোগ্রামিং, greedy আর কনটেস্টের কৌশল দেখব। কিন্তু সবগুলোর আগে দরকার একটি মাপকাঠি — আর সেটিই এই পর্বের বিষয়।

একটি ঘটনা দিয়ে শুরু করি। নাদিয়া একটি কোচিং সেন্টারের জন্য প্রোগ্রাম লিখল। ১০০ জন ছাত্রের তালিকায় একই নাম দুবার আছে কি না, তা খুঁজে বের করে। ল্যাপটপে চোখের পলকে উত্তর এলো। পরে বোর্ডের ফলাফলের ১০ লাখ রেকর্ডে সেই একই প্রোগ্রাম চালানো হলো — আর সারা রাত পেরিয়েও শেষ হলো না। কোডে কোনো bug ছিল না। সমস্যা ছিল অ্যালগরিদমের বৃদ্ধির ধরনে।

সেকেন্ড কেন ভালো মাপকাঠি নয়

“এই প্রোগ্রাম ০.৩ সেকেন্ড নেয়” — কথাটি শুনতে নির্দিষ্ট, কিন্তু আসলে প্রায় অর্থহীন। একই কোড:

  • মেশিনভেদে বদলায়: নতুন গেমিং ল্যাপটপ আর পাঁচ বছরের পুরোনো অফিস কম্পিউটারে সময় কয়েক গুণ আলাদা।
  • চাপভেদে বদলায়: পাশে ব্রাউজারে দশটি ট্যাব খোলা থাকলে, বা অ্যান্টিভাইরাস স্ক্যান চললে সময় বেড়ে যায়।
  • ভাষা ও compiler-ভেদে বদলায়: একই অ্যালগরিদম C-তে -O2 দিয়ে, আর Python-এ — সময়ের পার্থক্য দশ গুণ বা তারও বেশি হতে পারে।

ঢাকায় কেউ জিজ্ঞেস করল, “মিরপুর থেকে মতিঝিল কতক্ষণ?” উত্তর হতে পারে ৩০ মিনিট, আবার জ্যামে দুই ঘণ্টা। সময়টি রাস্তার অবস্থার ওপর নির্ভর করে। কিন্তু দূরত্ব — ধরুন ১৩ কিলোমিটার — সবসময় একই। আর দূরত্ব দ্বিগুণ হলে, ট্রাফিক যেমনই থাকুক, যাত্রা মোটামুটি দ্বিগুণ লম্বা হবে। অ্যালগরিদমেও আমরা “দূরত্ব” মাপি: কতগুলো মৌলিক ধাপ লাগে. মেশিন বা ভাষা হলো ট্রাফিক — সেটি সময় বদলায়, কিন্তু ধাপের হিসাব বদলায় না।

মূল ধারণা

একটি অ্যালগরিদমের time complexity মানে: ইনপুটের আকার n বাড়লে মৌলিক ধাপের (একটি তুলনা, একটি যোগ, একটি মান বসানো) সংখ্যা কোন ধরনে বাড়ে. আমরা নির্দিষ্ট সংখ্যার চেয়ে বৃদ্ধির ধরনটিকে বেশি গুরুত্ব দিই, কারণ বড় ইনপুটে শেষ পর্যন্ত সেটিই সব ঠিক করে দেয়।

২ · ধাপ গোনা — ইনপুট বাড়লে কাজ কত বাড়ে

ধাপ গোনার জন্য গণিতবিদ হতে হয় না। শুধু জিজ্ঞেস করুন: “কোন লাইনটি কতবার চলে?” নাদিয়ার duplicate খোঁজার কোড ছিল এরকম — প্রতিটি নামকে বাকি প্রতিটি নামের সাথে মেলানো। এটি একটি nested loop. একে ছোট n দিয়ে হাতে গুনে দেখি। n = ৪ ধরে “পরে” চাপুন:

1int count = 0;2for (int i = 0; i < n; i++)3    for (int j = 0; j < n; j++)4        count++;      /* basic step */5printf("count = %d\n", count);
ভ্যারিয়েবলের মান (Watch)
n
i
j ঘুরল
count
হিসাব
  1. শুরু। count হলো 0. আমরা প্রতিবার count++ চলাকে একটি “মৌলিক ধাপ” ধরছি। বাইরের লুপ ঠিক করে কোন নামটি ধরা হলো, ভেতরের লুপ সেটিকে সবার সাথে মেলায়।
  2. i = 0. ভেতরের লুপ j = 0, 1, 2, 3 — পুরো ৪ বার ঘুরল। count এখন 4. খেয়াল করুন, বাইরের লুপের একটি ঘোরায় ভেতরের লুপ পুরো n বার চলে।
  3. i = 1. আবার ভেতরের লুপ পুরো ৪ বার। count 8.
  4. i = 2. আরও ৪ বার। count 12. একটি নিয়ম চোখে পড়ছে: বাইরের লুপ যতবার ঘোরে, প্রতিবার ভেতরে n টি ধাপ যোগ হয়।
  5. i = 3. শেষ ৪ বার। count 16 = ৪ × ৪. এরপর i হবে 4, আর 4 < 4 মিথ্যা — বাইরের লুপ শেষ।
  6. সাধারণ সূত্র: n × n = n². n = ৪-এ ১৬ ধাপ। n = ১,০০০ হলে ১০ লাখ ধাপ। n = ১০ লাখ হলে ১০¹² — এক লাখ কোটি ধাপ! নাদিয়ার প্রোগ্রাম কেন সারা রাত চলেছে, এবার বোঝা গেল। ইনপুট ১,০০০ গুণ বড় হয়েছিল, কিন্তু কাজ বেড়েছিল ১০ লাখ গুণ।

একটি ছোট উন্নতি করা যায়: প্রতিটি জোড়া একবারই দেখলে চলে, তাই ভেতরের লুপ j = i + 1 থেকে শুরু করা যায়। তখন ধাপ হয় ৩ + ২ + ১ + ০ = ৬, সাধারণভাবে n(n − 1) / 2. কাজ প্রায় অর্ধেক হলো — কিন্তু n = ১০ লাখে সেটিও প্রায় ৫০ হাজার কোটি। অর্ধেক করা ভালো, কিন্তু বৃদ্ধির ধরন বদলায়নি: n দ্বিগুণ হলে কাজ এখনো চার গুণ।

তাহলে কীভাবে এই সমস্যাকে আসলেই দ্রুত করা যায়? দুটি পথ এই কোর্সেই আছে। পর্ব ৩৭-এর hash table-এ প্রতিটি নাম রাখার আগে দেখে নিন আগেই আছে কি না — গড়ে প্রতিটি কাজ O(1), মোট O(n). অথবা পরের পর্বের মতো আগে নামগুলো সাজিয়ে নিন; তখন একই নাম পাশাপাশি চলে আসে। ঠিক এই ধরনের সিদ্ধান্ত নিতেই complexity জানা দরকার।

৩ · Big-O — শুধু বড় ছবিটা দেখা

ধরুন হিসাব করে পেলেন, একটি প্রোগ্রাম মোট 3n² + 100n + 5000 টি ধাপ নেয়। এই পুরো সূত্র মনে রাখা ঝামেলার। Big-O notation একে ছোট করে লেখে: O(n²). কীভাবে? দুটি নিয়মে:

  1. সবচেয়ে দ্রুত বাড়া পদটি রাখুন, বাকিগুলো বাদ দিন। n = ১০ হলে 3n² = ৩০০ আর 5000 বড় দেখায়। কিন্তু n = ১০ লাখে 3n² = ৩ × ১০¹², আর 100n মাত্র ১০⁸ — তুলনায় প্রায় শূন্য। n যত বাড়ে, সবচেয়ে বড় পদটিই সব দখল করে নেয়।
  2. ধ্রুবক গুণ বাদ দিন। 3n² আর n²-এর পার্থক্য তিন গুণ — ঠিক যেমন দ্রুত আর ধীর কম্পিউটারের পার্থক্য। মেশিন বদলালে এটি বদলায়, তাই এটি অ্যালগরিদমের মৌলিক বৈশিষ্ট্য নয়। কিন্তু n দ্বিগুণ হলে কাজ চার গুণ হওয়া — এটি যেকোনো মেশিনে সত্য।
Big-O প্রশ্ন করে না “কত সেকেন্ড?”
Big-O প্রশ্ন করে “ইনপুট দ্বিগুণ হলে কাজ কত গুণ?”

একটু আনুষ্ঠানিকভাবে বললে: f(n) = O(g(n)) মানে এমন একটি ধ্রুবক c আর একটি শুরুর বিন্দু n₀ আছে, যার পর থেকে সবসময় f(n) ≤ c · g(n). অর্থাৎ যথেষ্ট বড় n-এ, কোনো এক ধ্রুবক গুণ বাদে, g(n) হলো f(n)-এর ওপরের সীমা. ইনপুট খুব বড় হলে খরচ কেমন আচরণ করে, শুধু সেদিকে তাকানোর এই পদ্ধতিকে বলে asymptotic analysis.

Best, average আর worst case

একই অ্যালগরিদম ইনপুটভেদে ভিন্ন সময় নিতে পারে। ক্লাসের হাজিরা খাতায় রোল নম্বর না জেনে “রাফি” নামটি খুঁজছেন, ওপর থেকে একজন একজন করে — অর্থাৎ linear search:

🍀 Best case

রাফির নাম প্রথম লাইনেই। একটি তুলনা — O(1). শুনতে ভালো, কিন্তু এর ওপর ভরসা করে সিস্টেম বানানো যায় না।

best case

⚖️ Average case

নাম এলোমেলো জায়গায় থাকলে গড়ে অর্ধেক খাতা দেখতে হয় — n/2, অর্থাৎ এখনো O(n).

average case

🛡️ Worst case

নাম শেষ লাইনে, বা খাতায় নেই। n টি তুলনা — O(n). “যা-ই হোক, এর বেশি লাগবে না” — এই নিশ্চয়তাই সবচেয়ে কাজের।

worst case

সাধারণত “এই অ্যালগরিদম O(n)” বললে worst case বোঝানো হয়। ব্যতিক্রম আছে — hash table-এর “O(1)” আসলে average case; worst case O(n). কোনটির কথা হচ্ছে, তা সবসময় জিজ্ঞেস করে নেওয়া ভালো অভ্যাস।

Ω আর Θ — দুটি ভাইবোন চিহ্ন

চিহ্নমানেদৈনন্দিন তুলনা
O (Big-O)ওপরের সীমা — এর চেয়ে দ্রুত হারে বাড়বে না“বাস ভাড়া ১০০ টাকার বেশি হবে না”
Ω (Big-Omega)নিচের সীমা — অন্তত এই হারে বাড়বেই“কম করে হলেও ৫০ টাকা লাগবে”
Θ (Big-Theta)দুই সীমা একই — ঠিক এই হারে বাড়ে“ভাড়া ঠিক ৮০ টাকার আশেপাশে”

যেমন linear search-এর worst case Θ(n): n টির কম তুলনায় নিশ্চিত হওয়া যায় না যে নামটি নেই, আবার n-এর বেশিও লাগে না। কঠোর গণিতে O(n²) লেখাও ভুল নয় (ওপরের সীমা তো!), কিন্তু তাতে তথ্য কমে যায়। বাস্তবে প্রোগ্রামাররা “O” বলেন, কিন্তু প্রায়ই মনে মনে Θ বোঝান।

O চিহ্নটি প্রোগ্রামিংয়ের চেয়ে অনেক পুরোনো। ১৮৯৪ সালে জার্মান গণিতবিদ Paul Bachmann সংখ্যাতত্ত্বের (number theory) একটি বইয়ে এটি ব্যবহার করেন। পরে Edmund Landau-র লেখায় চিহ্নটি এত জনপ্রিয় হয় যে আজও এদের “Landau symbols” বলা হয়। কম্পিউটারের অ্যালগরিদমে এর ব্যবহার ছড়িয়ে দেন Donald Knuth — ১৯৭৬ সালে SIGACT News-এ “Big Omicron and Big Omega and Big Theta” লেখায় তিনি O, Ω আর Θ-এর আজকের সংজ্ঞা প্রস্তাব করেন। আর ১৯৬৫ সালে Juris Hartmanis ও Richard Stearns-এর গবেষণাপত্র থেকে “computational complexity” একটি আলাদা গবেষণা-ক্ষেত্র হয়ে ওঠে; এজন্য তাঁরা ১৯৯৩ সালে Turing Award পান।

৪ · ছয়টি পরিচিত শ্রেণি — O(1) থেকে O(2ⁿ)

বাস্তবে যত অ্যালগরিদম দেখবেন, তার বেশিরভাগই নিচের ছয়টি শ্রেণির কোনো একটিতে পড়ে। প্রতিটির জন্য একটি চেনা দৃশ্য মনে রাখুন:

O(1) — ধ্রুব

রোল নম্বর জানা থাকলে সরাসরি সেই সিটে যাওয়া। ক্লাসে ৩০ জন হোক বা ৩০০ — একটিই ধাপ। Array-র a[i] পড়া এমন।

constant time

O(log n) — অর্ধেক করে

অভিধানে “নদী” শব্দ খোঁজা — মাঝখানে খুলে দেখেন আগে না পরে, তারপর সেই অর্ধেকের মাঝখানে। পর্ব ২-এর সংখ্যা-আন্দাজের খেলাটিও এটাই।

logarithmic

O(n) — একবার করে সবাই

ক্লাসে হাজিরা ডাকা — প্রত্যেকের নাম একবার। ছাত্র দ্বিগুণ হলে সময়ও দ্বিগুণ। Array-র যোগফল বের করা এমন।

linear time

O(n log n) — ভাগ করে সাজানো

বোর্ড পরীক্ষার হাজার হাজার খাতা রোল অনুযায়ী সাজানো — কয়েকজনকে ভাগ করে দিয়ে, প্রত্যেকে নিজের গাদা সাজিয়ে, শেষে মিলিয়ে নেওয়া। ভালো সর্টিং এই শ্রেণির।

linearithmic

O(n²) — সবাই সবার সাথে

বিয়েবাড়িতে প্রত্যেক অতিথি প্রত্যেকের সাথে হাত মেলালেন। ২০ জনে ১৯০টি হ্যান্ডশেক, ২০০ জনে ১৯,৯০০টি। নাদিয়ার duplicate খোঁজা এমন।

quadratic

O(2ⁿ) — প্রতিটি নতুন জিনিসে দ্বিগুণ

দোকানে n টি জিনিস — কোনগুলো ব্যাগে নেবেন, তার সব সম্ভাব্য বাছাই। প্রতিটি জিনিস “নেব/নেব না”, তাই ১০টিতে ১,০২৪, ৩০টিতে ১০০ কোটির বেশি।

exponential

সংখ্যাগুলো পাশাপাশি রাখলে পার্থক্যটা গায়ে লাগে। নিচের টেবিলে মোটামুটি কত ধাপ লাগে, দেখুন (log মানে এখানে log₂, আর মান গোল করা):

শ্রেণিn = ১০n = ১,০০০n = ১০ লাখ (১০⁶)
O(1)১১১
O(log n)প্রায় ৩প্রায় ১০প্রায় ২০
O(n)১০১,০০০১০ লাখ
O(n log n)প্রায় ৩৩প্রায় ১০ হাজারপ্রায় ২ কোটি
O(n²)১০০১০ লাখ১০¹² (এক লাখ কোটি)
O(2ⁿ)১,০২৪প্রায় ১০³⁰¹ (৩০২ অঙ্কের সংখ্যা)৩ লাখেরও বেশি অঙ্কের সংখ্যা

একটি সাধারণ কম্পিউটার সেকেন্ডে মোটামুটি ১০⁸ (১০ কোটি) সরল ধাপ করতে পারে — এটি একটি মোটা দাগের আন্দাজ, যা অংশ ৮-এ আবার দেখব। সেই হিসাবে n = ১০ লাখে O(n log n) লাগে এক সেকেন্ডের অনেক কম, O(n²) প্রায় ১০,০০০ সেকেন্ড — প্রায় পৌনে তিন ঘণ্টা। আর n = ১,০০০-এ O(2ⁿ)? মহাবিশ্বের বয়স সেকেন্ডে হিসাব করলে ১০¹⁸-এরও কম; এখানে ধাপ ১০³⁰¹. কোনো কম্পিউটারই কখনো এটি শেষ করতে পারবে না।

এবার একটি দৌড় দেখুন। প্রতিটি ধাপে n দ্বিগুণ হচ্ছে — কোন শ্রেণির কাজ কতটা লাফ দেয়, খেয়াল করুন:

ধাপের সংখ্যা (log মানে log₂)
n
O(1)
O(log n)
O(n)
O(n log n)
O(n²)
O(2ⁿ)
  1. n = 8. ছোট ইনপুটে সবাই কাছাকাছি — সবচেয়ে বেশি 2ⁿ = 256. এই অবস্থায় যেকোনো অ্যালগরিদম “দ্রুত” মনে হয়। এজন্যই ছোট ইনপুটে পরীক্ষা করে complexity-র সমস্যা ধরা যায় না।
  2. n = 16. log n বাড়ল মাত্র ১, n² চার গুণ হলো, আর 2ⁿ লাফিয়ে ৬৫ হাজার ছাড়াল। পার্থক্য এবার চোখে পড়ছে।
  3. n = 32. 2ⁿ এখন প্রায় ৪২৯ কোটি — সেকেন্ডে ১০⁸ ধাপ হারে প্রায় ৪৩ সেকেন্ড। অথচ n² মাত্র ১,০২৪. ইনপুট মাত্র ৩২!
  4. n = 64. 2ⁿ প্রায় ১.৮ × ১০¹⁹ — সেকেন্ডে ১০⁸ ধাপ হারে কয়েক হাজার বছর। এদিকে log n মাত্র ৬. একই n, অথচ এক শ্রেণি চোখের পলক, আরেকটি কয়েক হাজার বছর।
  5. নিয়মটি মনে রাখুন। n দ্বিগুণ হলে: O(1) একই, O(log n) মাত্র এক ধাপ বেশি, O(n) দ্বিগুণ, O(n log n) দ্বিগুণের সামান্য বেশি, O(n²) চার গুণ, আর O(2ⁿ) নিজের বর্গ হয়ে যায়। এই “দ্বিগুণ করলে কী হয়” প্রশ্নটিই অংশ ৮-এ আমাদের মাপার যন্ত্র হবে।

৫ · log n-এর জাদু — প্রতি ধাপে অর্ধেক

log n শব্দটি অনেকের কাছে স্কুলের গণিতের ভয় ফিরিয়ে আনে। কিন্তু প্রোগ্রামিংয়ে এর অর্থ খুব সহজ: log₂ n = n-কে কতবার অর্ধেক করলে ১-এ পৌঁছায়. ১৬ → ৮ → ৪ → ২ → ১: চারবার, তাই log₂ ১৬ = ৪. পর্ব ২-এ ১ থেকে ১০০-এর মধ্যে সংখ্যা আন্দাজের খেলায় আপনি দেখেছিলেন, সবচেয়ে বেশি ৭টি প্রশ্নই যথেষ্ট। এবার সেই binary search চালাই ১০ লাখ সাজানো রোল নম্বরের ওপর:

সাজানো তালিকায় একটি রোল নম্বর খোঁজা
বাকি সম্ভাব্য ঘর
তুলনা হয়েছে
2 এর ঘাত
Linear search হলে
  1. শুরু: ১০ লাখ ঘর। তালিকাটি সাজানো — এটিই শর্ত। খুঁজছি একটি নির্দিষ্ট রোল। মাঝের ঘরটি দেখব।
  2. প্রথম তুলনা। মাঝের রোল আমাদেরটির চেয়ে বড়। তাই ডান দিকের পুরো অর্ধেক — ৫ লাখ ঘর — একটি তুলনাতেই বাদ! linear search-এ এই একটি তুলনা বাদ দিত মাত্র একটি ঘর।
  3. দ্বিতীয় তুলনা। আবার মাঝখান — আরও অর্ধেক বাদ। বাকি ২.৫ লাখ।
  4. দশটি তুলনা পরে। বাকি মাত্র ৯৭৬টি ঘর। কারণ ২¹⁰ = ১,০২৪ — দশবার অর্ধেক করলে প্রায় হাজার ভাগের এক ভাগ থাকে। মনে রাখার সহজ সূত্র: প্রতি ১০টি তুলনায় প্রায় ১,০০০ গুণ ছোট.
  5. ১৭টি তুলনা পরে। বাকি ৭টি ঘর। আর মাত্র কয়েক ধাপ।
  6. ১৯টি তুলনা পরে একটি ঘর। শেষ তুলনায় নিশ্চিত হওয়া যায় রোলটি আছে কি না — সবচেয়ে খারাপেও মোট ২০টি তুলনা, কারণ ২²⁰ ≈ ১০.৫ লাখ। তালিকা ১০০ কোটি হলেও লাগত মাত্র ৩০টি। এটাই O(log n)-এর শক্তি: ডেটা হাজার গুণ বাড়লে কাজ বাড়ে মাত্র ১০ ধাপ।

Binary search-এর শর্তটি ভুলবেন না: তালিকা সাজানো থাকতে হবে. অসাজানো তালিকা আগে সাজাতে লাগে O(n log n) — একবার খোঁজার জন্য সেটি linear search-এর O(n)-এর চেয়ে বেশি। কিন্তু একই তালিকায় হাজারবার খুঁজলে একবার সাজিয়ে নেওয়াই লাভজনক। পর্ব ৩৫-এর ভারসাম্যপূর্ণ BST-ও ঠিক এই কারণে O(log n): প্রতিটি নোডে অর্ধেক গাছ বাদ পড়ে।

log-এর base কেন লেখা হয় না

O(log n) লেখার সময় কেউ log₂ না log₁₀, তা বলে না। কারণ base বদলালে শুধু একটি ধ্রুবক গুণ বদলায়: log₂ n = ৩.৩২ × log₁₀ n (প্রায়)। আর Big-O ধ্রুবক গুণ বাদ দেয়। তাই সব log একই শ্রেণির। তিন ভাগে ভাগ করা অ্যালগরিদমও O(log n), আধাআধি ভাগ করাটিও O(log n).

৬ · কোড দেখে complexity পড়া

বেশিরভাগ ক্ষেত্রে complexity বের করতে কোনো সূত্র লাগে না — কয়েকটি প্যাটার্ন চিনলেই চলে। প্রশ্ন একটাই: “n-এর সাথে কোন লাইনটি কতবার চলে?”

প্যাটার্নউদাহরণComplexity
লুপ নেই, নির্দিষ্ট কয়েকটি কাজx = a[5] + a[9];O(1)
একটি লুপ, ধাপ ১ করে বাড়েfor (i = 0; i < n; i++)O(n)
একটি লুপ, ধাপ ২ বা ১০ করেfor (i = 0; i < n; i += 2)এখনো O(n) — n/2 বার
প্রতি ধাপে অর্ধেক বা দ্বিগুণfor (k = n; k > 0; k /= 2)O(log n)
লুপের ভেতরে পূর্ণ লুপfor i … for j < nO(n²)
পরপর দুটি আলাদা লুপfor i < n {…} তারপর for j < n {…}O(n + n) = O(n)
বাইরে n, ভেতরে অর্ধেক-করা লুপfor i < n … for (k = n; k > 0; k /= 2)O(n log n)
মূল নিয়ম

পরপর থাকলে যোগ, একটির ভেতরে আরেকটি থাকলে গুণ। দুটি O(n) লুপ পাশাপাশি = ২n = O(n). একটি O(n) লুপের ভেতরে আরেকটি O(n) লুপ = n × n = O(n²). আর যোগের ক্ষেত্রে বড়টিই জেতে: O(n²) + O(n) = O(n²). দুটি আলাদা ইনপুট থাকলে (n জন ছাত্র, m টি বিষয়) দুটিই রাখুন — O(n + m) বা O(n × m).

কথাগুলো নিজে মেপে দেখুন। নিচের প্রোগ্রাম তিন রকম লুপে মৌলিক ধাপ গোনে, আর প্রতিবার n দ্বিগুণ করে:

#include <stdio.h>

int main(void) {
    printf("%6s %8s %10s %8s\n", "n", "linear", "nested", "halving");
    for (int n = 1000; n <= 8000; n *= 2) {
        long long lin = 0, nest = 0, half = 0;
        for (int i = 0; i < n; i++) lin++;              /* O(n)     */
        for (int i = 0; i < n; i++)                     /* O(n^2)   */
            for (int j = 0; j < n; j++) nest++;
        for (int k = n; k > 0; k /= 2) half++;          /* O(log n) */
        printf("%6d %8lld %10lld %8lld\n", n, lin, nest, half);
    }
    return 0;
}

আউটপুটে চারটি সারি আসবে — n = 1000, 2000, 4000, 8000. linear কলাম n-এর সমান: প্রতিবার দ্বিগুণ। nested কলাম 1000000, 4000000, 16000000, 64000000 — প্রতিবার চার গুণ। আর halving কলাম 10, 11, 12, 13 — n দ্বিগুণ হলেও মাত্র এক বাড়ে। গ্রোথ রেসের ডায়াগ্রামে যা দেখেছিলেন, এখানে আসল সংখ্যায় তা-ই ঘটল। একটি সূক্ষ্ম বিষয়: counter-গুলো long long, কারণ বড় n-এ n² সহজেই int-এর সীমা (প্রায় ২১৫ কোটি) পেরিয়ে যায় — complexity-র হিসাব করতে গিয়ে overflow-এর কথাও মাথায় রাখতে হয়।

রিকার্শন — call tree গুনে দেখা

রিকার্সিভ ফাংশনে লুপ চোখে পড়ে না, তাই complexity বুঝতে call tree আঁকতে হয়। পর্ব ১৫-এর সেই সরল Fibonacci মনে করুন: fib(n) = fib(n-1) + fib(n-2). প্রতিটি কল আরও দুটি কল করে। fib(5)-এর গাছে মোট ১৫টি কল, fib(40)-এ ৩৩ কোটিরও বেশি (৩৩,১১,৬০,২৮১টি)।

fib(5)-এর call tree

Each call branches into two — the same work is repeated again and again

                    fib(5)
            /                  \
        fib(4)                fib(3)
       /      \              /      \
   fib(3)    fib(2)      fib(2)    fib(1)
   /    \     /   \      /   \
fib(2) fib(1) f(1) f(0) f(1) f(0)
 /  \
f(1) f(0)                 মোট কল: 15
ছবি ৩৯.১ — fib(3) দুবার, fib(2) তিনবার হিসাব হচ্ছে — একই কাজের পুনরাবৃত্তি।

n এক বাড়লে কলের সংখ্যা প্রায় ১.৬ গুণ হয় (ঠিক মানটি সোনালি অনুপাত, প্রায় ১.৬১৮)। তাই সরল fib-এর সময় সূচকীয় — ওপরের সীমা হিসেবে O(2ⁿ) বলা হয়, আরও সূক্ষ্মভাবে Θ(১.৬১৮ⁿ). পর্ব ১৫-এ দেখা memoization প্রতিটি fib(k) একবারই হিসাব করে মনে রাখে — তখন কল হয় মাত্র n-এর কাছাকাছি, অর্থাৎ O(n). একই উত্তর, অথচ fib(40)-এ ৩৩ কোটি কলের বদলে প্রায় ৪০টি হিসাব। পর্ব ৪২-এর ডায়নামিক প্রোগ্রামিং পুরোপুরি এই ধারণার ওপর দাঁড়িয়ে।

রিকার্শনের একটি সাধারণ সূত্র মনে রাখুন: মোট কাজ ≈ কলের সংখ্যা × প্রতিটি কলের কাজ. যে রিকার্শন সমস্যাকে অর্ধেক করে একটিমাত্র কল করে (যেমন রিকার্সিভ binary search), তার গাছ আসলে একটি সরু লাইন — O(log n). আর যেটি অর্ধেক করে দুটি কল করে এবং প্রতিটি স্তরে মোট n কাজ করে, সেটি O(n log n) — পরের দুই পর্বের merge sort ঠিক এমন।

৭ · Space complexity আর amortized খরচ

মেমরিও একটি সম্পদ

সময়ের মতো মেমরিও সীমিত। Space complexity মাপে: ইনপুটের বাইরে অ্যালগরিদম বাড়তি কত মেমরি নেয়, আর n বাড়লে সেটি কীভাবে বাড়ে।

  • Array-র যোগফল: শুধু একটি sum আর একটি i — n যত বড়ই হোক। বাড়তি মেমরি O(1).
  • Array-র একটি কপি বানানো: n টি নতুন ঘর — O(n).
  • n × n টেবিল (যেমন পর্ব ৩৮-এর adjacency matrix): O(n²). ১ লাখ vertex-এর matrix মানে ১০¹⁰ ঘর — প্রতিটি এক byte হলেও ১০ GB!
  • রিকার্শন: প্রতিটি অসমাপ্ত কল call stack-এ একটি frame দখল করে রাখে। সরল fib(n)-এর সময় সূচকীয় হলেও stack-এর গভীরতা সর্বোচ্চ n — তাই space O(n). পর্ব ২০-এ দেখেছেন, stack-এর জায়গা সাধারণত মাত্র কয়েক MB (Linux-এ প্রায় ৮, Windows-এ প্রায় ১); গভীরতা খুব বেশি হলে stack overflow.

রান্নাঘরের কথা ভাবুন। কেউ একটি কড়াইয়েই সব রান্না করেন (O(1) বাড়তি জায়গা), কেউ প্রতিটি উপকরণের জন্য আলাদা বাটি নেন (O(n)). দ্বিতীয়জন হয়তো দ্রুত রাঁধেন — কিন্তু ছোট রান্নাঘরে বাটি রাখার জায়গা ফুরিয়ে যায়। সময় আর মেমরির এই দর-কষাকষি (time–space trade-off) প্রকৌশলের সবখানে: hash table মেমরি খরচ করে সময় বাঁচায়, memoization-ও তাই।

Amortized — মাঝে মাঝে দামি, গড়ে সস্তা

পর্ব ২০-এর dynamic array মনে করুন: জায়গা ফুরোলে realloc দিয়ে capacity দ্বিগুণ, আর পুরোনো সব উপাদান নতুন জায়গায় কপি। পর্ব ৩৭-এ hash table-এর resizing-ও একই কৌশল। একটি push কখনো কখনো n টি কপি ঘটায় — তাহলে কি push O(n)? নিচে capacity ১ থেকে শুরু করে ৯টি push গুনে দেখি। প্রতিটি নতুন উপাদান লেখা = ১ কাজ, প্রতিটি কপি = ১ কাজ:

push-এর হিসাব (শুরুতে capacity = 1)
মোট push (n)
capacity
এই push-এ কপি
মোট কাজ
গড় কাজ (মোট ÷ n)
  1. push ১. একটি ঘর, ফাঁকা — সরাসরি লেখা। মোট কাজ ১।
  2. push ২ — প্রথম দ্বিগুণ। ঘর ভরা, তাই capacity ২ করা হলো আর পুরোনো ১টি উপাদান কপি হলো। এই push-এর খরচ ১ কপি + ১ লেখা = ২. মোট ৩।
  3. push ৩ — আবার দ্বিগুণ। capacity ৪, কপি ২টি। এই push-এর খরচ ৩. মোট ৬।
  4. push ৪ — সস্তা। খালি ঘর আছে, শুধু লেখা। মোট ৭. গড় নেমে এলো ১.৭৫-এ।
  5. push ৫ — দামি। capacity ৮, কপি ৪টি। এই একটি push-এর খরচ ৫. মোট ১২।
  6. push ৬, ৭, ৮ — তিনটিই সস্তা। প্রতিটি মাত্র ১. মোট ১৫, গড় আবার নেমে ১.৮৭৫. লক্ষ করুন: দামি push যত বড়, তার পরের সস্তা push-এর সারিও তত লম্বা — ২টি কপির পর ১টি সস্তা push, ৪টি কপির পর ৩টি।
  7. push ৯ — capacity ১৬. কপি ৮টি, মোট ২৪. এ পর্যন্ত মোট কপি ১ + ২ + ৪ + ৮ = ১৫ — যা n-এর দ্বিগুণের কম। তাই মোট কাজ সবসময় ৩n-এর কম, আর গড়ে প্রতিটি push O(1). একেই বলে amortized O(1).

কেন দ্বিগুণ করাই জরুরি? ধরুন capacity প্রতিবার শুধু ১০ করে বাড়ানো হলো। তাহলে প্রতি ১০টি push-এ একবার পুরো array কপি — ১০, ২০, ৩০… মোট কপি প্রায় n²/২০, অর্থাৎ O(n²). গুণ করে বাড়ালে (দ্বিগুণ, বা ১.৫ গুণ) মোট কপি থাকে O(n). বাস্তবের অনেক dynamic array ১.৫ থেকে ২ গুণের মধ্যে কোনো গুণক ব্যবহার করে।

Amortized মানে “গড়ে সস্তা”, “প্রতিবার সস্তা” নয়। যে push-টি capacity বাড়ায়, সেটি একাই O(n). গেম বা লাইভ ভিডিওর মতো যেখানে প্রতিটি ফ্রেম সময়মতো হওয়া জরুরি, সেখানে হঠাৎ একটি লম্বা থেমে যাওয়াও সমস্যা। তাই এমন সিস্টেমে প্রায়ই শুরুতেই যথেষ্ট capacity নিয়ে রাখা হয়।

৮ · বাস্তবের হিসাব — ধ্রুবক, cache, সময়ের বাজেট আর মেপে দেখা

Big-O ধ্রুবক বাদ দেয় — কিন্তু আপনার কম্পিউটার বাদ দেয় না। একই O(n) হয়েও দুটি প্রোগ্রামের গতিতে দশ গুণ পার্থক্য থাকতে পারে।

  • Cache-এর প্রভাব: array আর linked list — দুটিতেই সব উপাদান একবার করে দেখা O(n). কিন্তু array-র উপাদান মেমরিতে পাশাপাশি, তাই CPU একবারে কয়েকটি একসাথে cache-এ টেনে আনে (spatial locality). Linked list-এর node ছড়ানো থাকে, প্রায় প্রতিটি ধাপে cache miss. ফলে বাস্তবে array অনেক গুণ দ্রুত হতে পারে।
  • ছোট n-এ ধ্রুবকই রাজা: ধরুন অ্যালগরিদম A নেয় 2n² ধাপ, আর B নেয় 50 n log₂ n. n = ১০০-এ A লাগে ২০,০০০, B প্রায় ৩৩,০০০ — O(n²) জিতল! n প্রায় ১৯০ পেরোলে তবেই B এগিয়ে যায়, আর n = ১,০০০-এ B প্রায় চার গুণ দ্রুত। এজন্য অনেক লাইব্রেরির সর্টিং ছোট অংশে এসে সরল O(n²) পদ্ধতিতে চলে যায় — পরের পর্বে দেখবেন।

সময়ের বাজেট — “সেকেন্ডে ১০⁸” নিয়ম

প্রোগ্রামিং কনটেস্টে সাধারণত সময়সীমা ১–২ সেকেন্ড, আর ইনপুটের সর্বোচ্চ আকার (constraint) আগেই বলা থাকে। একটি মোটা দাগের নিয়ম: C-তে সেকেন্ডে মোটামুটি ১০⁸ সরল কাজ. এটি নিখুঁত নয় — CPU, কাজের ধরন আর মেমরি-প্যাটার্নভেদে কয়েক গুণ কমবেশি হয়। কিন্তু constraint দেখে কোন complexity চলবে, তা আন্দাজ করার জন্য যথেষ্ট:

n-এর সর্বোচ্চ মান (প্রায়)যে complexity সাধারণত চলেউদাহরণ
১০–১১O(n!)সব ক্রম চেষ্টা করা
২০–২৫O(2ⁿ) বা O(2ⁿ × n)সব উপসেট (subset) চেষ্টা করা
৫০০O(n³)তিনটি নেস্টেড লুপ
৫,০০০O(n²)সব জোড়া দেখা
১০⁶ (১০ লাখ)O(n log n)সর্টিং, binary search বারবার
১০⁸O(n)একবার পড়ে যাওয়া
১০¹⁸O(log n) বা O(1)গাণিতিক সূত্র, অর্ধেক করা

উল্টো দিক থেকে ভাবুন: constraint যদি বলে n ≤ ২ লাখ, তাহলে O(n²) মানে ৪ × ১০¹⁰ — প্রায় ৪০০ সেকেন্ড। সমাধান লেখার আগেই বোঝা যায়, এই পথ চলবে না; O(n log n) খুঁজতে হবে। এই হিসাব কনটেস্টে কীভাবে কাজে লাগে, তা পর্ব ৪৪-এ বিস্তারিত দেখব।

নিজে মেপে দেখা — n দ্বিগুণ করার পরীক্ষা

তত্ত্বের হিসাব বাস্তবে মেলে কি না, তা যাচাইয়ের সবচেয়ে সহজ উপায় একটি ছোট benchmark. কৌশলটি হলো: n দ্বিগুণ করুন, আর দেখুন সময় কত গুণ হলো। প্রায় ২ গুণ মানে O(n), প্রায় ৪ গুণ মানে O(n²), প্রায় ৮ গুণ মানে O(n³). নিচের প্রোগ্রাম time.h-এর clock() দিয়ে দুটি কাজের সময় মাপে — একটি O(n) যোগফল, আর একটি O(n²) কাজ: কতগুলো জোড়া উল্টো ক্রমে আছে (বাঁয়ের সংখ্যা ডানেরটির চেয়ে বড়), তা গোনা।

#include <stdio.h>
#include <time.h>
#define MAX 16000
int a[MAX];
int main(void) {
    for (int i = 0; i < MAX; i++) a[i] = (i * 7919) % 10007;   /* mixed-up values */
    for (int n = 4000; n <= MAX; n *= 2) {
        clock_t t0 = clock();
        long long sum = 0, inv = 0;
        for (int i = 0; i < n; i++) sum += a[i];                 /* O(n)   */
        clock_t t1 = clock();
        for (int i = 0; i < n; i++)                               /* O(n^2) */
            for (int j = i + 1; j < n; j++)
                if (a[i] > a[j]) inv++;
        clock_t t2 = clock();
        printf("n=%5d  sum=%lld %.4fs  pairs=%lld %.4fs\n", n, sum, (double)(t1 - t0) / CLOCKS_PER_SEC,
               inv, (double)(t2 - t1) / CLOCKS_PER_SEC);
    }
    return 0;
}

উল্টো-ক্রমের জোড়া আসবে 4005106, 16017216 আর 64019381 — n দ্বিগুণ হলে প্রায় ঠিক ৪ গুণ, কারণ জোড়া দেখা হয় n(n − 1)/2 বার। সময়ের কলাম মেশিনভেদে আলাদা হবে, কিন্তু অনুপাতটি খেয়াল করুন: pairs-এর সময়ও প্রতিবার মোটামুটি ৪ গুণ. আর O(n) যোগফল এত দ্রুত যে clock() প্রায়ই 0.0000 দেখায় — ১৬,০০০ সংখ্যা যোগ করতে একটি সাধারণ CPU-র এক মিলিসেকেন্ডও লাগে না। এখানে তিনটি শিক্ষা: (১) একবারের মাপ নয়, অনুপাত দেখুন; (২) খুব ছোট সময় মাপতে হলে কাজটি বহুবার চালিয়ে গড় নিন; (৩) clock() মাপে CPU সময় — অপেক্ষা বা ইনপুটের সময় এতে ধরা পড়ে না।

৯ · AI-এর যুগে complexity — “এটি O(n)” দাবি যাচাই

AI-কে “এই কোডের time complexity কত?” জিজ্ঞেস করলে সে আত্মবিশ্বাসের সাথে উত্তর দেয় — বেশিরভাগ সময় ঠিকও হয়। কিন্তু AI প্রায়ই দৃশ্যমান লুপ গোনে, আর লুপের ভেতরে ডাকা ফাংশনের খরচ উপেক্ষা করে। সবচেয়ে বিখ্যাত ফাঁদটি দেখুন:

for (int i = 0; i < strlen(s); i++) {      /* AI: "single loop, so O(n)" */
    if (s[i] == ' ') spaces++;
}

দেখতে একটি লুপ। কিন্তু পর্ব ১৯-এ দেখেছেন, strlen নিজেই পুরো string হেঁটে '\0' খোঁজে — O(n). আর শর্তটি লুপের প্রতিটি ঘোরায় আবার যাচাই হয়। তাই n বার × n ধাপ = O(n²). ১০ লাখ অক্ষরের একটি ফাইলে এটি এক লাখ কোটি ধাপের কাছাকাছি! সমাধান সহজ: লুপের আগে একবার size_t len = strlen(s); লিখে i < len ব্যবহার করুন। কখনো কখনো compiler নিজেই এই হিসাব লুপের বাইরে সরিয়ে নেয় (hoisting), কিন্তু লুপের ভেতরে string বদলালে বা optimization বন্ধ থাকলে পারে না — এর ওপর ভরসা করবেন না।

লুপ দেখে নয়, প্রতিটি লাইনের আসল খরচ দেখে complexity বলুন।
লুপের শর্ত আর ভেতরের ফাংশনও “লাইন”।

AI সাধারণত complexity-তে যেসব ভুল করে

AI-এর দাবিলুকানো সত্যকীভাবে ধরবেন
“একটি লুপ, তাই O(n)”লুপের শর্তে বা ভেতরে strlen, strcat, linear search — আসলে O(n²)প্রতিটি ফাংশন কল নিজে কত খরচ, জিজ্ঞেস করুন
“Hash table, তাই O(1)”গড়ে O(1); খারাপ hash বা আক্রমণে O(n); লম্বা চাবির hash নিজেই O(চাবির দৈর্ঘ্য)“worst case কত?” আলাদা করে জিজ্ঞেস করুন
“রিকার্শন, কিন্তু দ্রুত”একই উপ-সমস্যা বারবার — সূচকীয় সময়ছোট n-এ কলের সংখ্যা গুনুন
“Space O(1)”রিকার্শনের stack গভীরতা O(n), বা গোপনে array কপিরিকার্শনের গভীরতা আর malloc খুঁজুন
শুধু best case বলাসাজানো ইনপুটে দ্রুত, এলোমেলোতে ধীরসবচেয়ে খারাপ ইনপুট নিজে বানিয়ে চালান

AI-কে complexity জিজ্ঞেস করার সময় তাকে হিসাব দেখাতে বাধ্য করুন — শুধু উত্তর নয়। তাহলে কোন ধাপে ভুল, তা আপনি নিজে ধরতে পারবেন:

নিচের C ফাংশনের time complexity আর space complexity বের করো। [এখানে কোড পেস্ট করুন] ১. প্রতিটি লুপ ও প্রতিটি ফাংশন কল (strlen, strcmp, malloc ইত্যাদিসহ) কতবার চলে এবং প্রতিবার কত খরচ, লাইন ধরে দেখাও। ২. best case, average case আর worst case আলাদা করে বলো; কোন ইনপুটে worst case হয়, তার একটি উদাহরণ দাও। ৩. রিকার্শন থাকলে stack-এর সর্বোচ্চ গভীরতা কত, বলো। ৪. শেষে একটি ছোট পরীক্ষা প্রস্তাব করো: কোন n-গুলোতে চালালে সময়ের অনুপাত দেখে আমি তোমার উত্তর যাচাই করতে পারব।

AI-এর দেওয়া complexity হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১O(n²) অ্যালগরিদম কি কখনো O(n log n)-কে হারাতে পারে? পারলে কখন?

হ্যাঁ — এবং বাস্তবে প্রায় প্রতিদিনই হারায়। Big-O একটি দীর্ঘমেয়াদি প্রতিশ্রুতি: “n যথেষ্ট বড় হলে”। কিন্তু “যথেষ্ট বড়” কত, তা বলে দেয় বাদ দেওয়া ধ্রুবকগুলো।

  • ছোট ইনপুট: insertion sort-এর মতো O(n²) পদ্ধতির ভেতরের কাজ অত্যন্ত সরল — একটি তুলনা, একটি সরানো, সব পাশাপাশি মেমরিতে। Merge sort বা quicksort-এর রিকার্শন, বাড়তি array আর ভাগাভাগির খরচ ছোট n-এ সেই সুবিধা খেয়ে ফেলে। তাই অনেক বাস্তব সর্টিং লাইব্রেরি বড় অংশে O(n log n) পদ্ধতি চালায়, কিন্তু টুকরো খুব ছোট (কয়েক ডজনের কম) হয়ে এলে insertion sort-এ চলে যায়। এমন মিশ্র পদ্ধতিকে বলা হয় hybrid algorithm.
  • প্রায়-সাজানো ইনপুট: insertion sort-এর best case O(n). যে ডেটা আগে থেকেই প্রায় সাজানো (যেমন প্রতিদিনের হাজিরা তালিকায় দু-একজন নতুন), সেখানে সে দ্রুততম।

অংশ ৮-এর হিসাবটি মনে করুন: 2n² বনাম 50 n log₂ n — প্রায় n = ১৯০ পর্যন্ত O(n²) জেতে। কিন্তু n = ১০ লাখে 2n² = ২ × ১০¹², আর 50 n log n প্রায় ১০⁹ — দুই হাজার গুণ পার্থক্য। দীর্ঘমেয়াদে growth rate-ই জেতে, সবসময়।

তাই পেশাদার প্রকৌশলী দুটি প্রশ্ন করেন: “আমার n সাধারণত কত বড়?” আর “সবচেয়ে বড় কত হতে পারে?” প্রথমটি ঠিক করে ধ্রুবক কতটা গুরুত্বপূর্ণ; দ্বিতীয়টি ঠিক করে growth rate কতটা বিপজ্জনক। আর দুটির উত্তর অনুমানে নয়, মেপে দেখে নেন।

মূল উপলব্ধি: Big-O বলে কোন অ্যালগরিদম বড় হলে টিকবে; benchmark বলে আপনার ইনপুটে কোনটি দ্রুত। ভালো সিদ্ধান্তের জন্য দুটিই লাগে।

প্র ০২Hash table-এ খোঁজা “O(1)” — তাহলে কি এটি সবসময় দ্রুত?

না। “O(1)” কথাটির পেছনে অন্তত চারটি লুকানো শর্ত আছে, যা পর্ব ৩৭-এ আমরা কাছ থেকে দেখেছি।

  • এটি average case: hash function ভালো আর load factor একটি সীমার নিচে থাকলে প্রতিটি শিকল গড়ে ছোট। কিন্তু সব চাবি এক ঘরে পড়লে খোঁজা হয়ে যায় linear search — worst case O(n).
  • চাবির দৈর্ঘ্য: “O(1)” ধরে নেয় hash হিসাব করা ধ্রুব সময়ের কাজ। কিন্তু ১০ KB লম্বা একটি URL-এর hash বের করতে প্রতিটি অক্ষর ছুঁতে হয় — খরচ চাবির দৈর্ঘ্যের সমানুপাতিক। ঘর খুঁজে পাওয়ার পর চাবি মেলাতে strcmp-ও তাই।
  • Resize-এর ঝাঁকুনি: সাধারণ insert O(1), কিন্তু যেটি resizing ঘটায় সেটি O(n). গড়ে সস্তা, কিন্তু মাঝে মাঝে একটি লম্বা বিরতি — amortized খরচের সেই কথা।
  • Cache miss: hash ইচ্ছে করে চাবিগুলো ছড়িয়ে দেয়, তাই প্রায় প্রতিটি খোঁজায় মেমরির নতুন জায়গায় যেতে হয়। ছোট, সাজানো array-তে binary search (O(log n)) অনেক সময় বড় hash table-এর চেয়ে দ্রুত হয়, কারণ সব ডেটা cache-এ এঁটে যায়।

আরেকটি সূক্ষ্ম বিষয়: O(1) মানে “n-এর ওপর নির্ভর করে না”, “খুব কম সময়” নয়। এক কোটি ধাপের একটি কাজও O(1), যদি n যা-ই হোক সবসময় ঠিক এক কোটি ধাপ লাগে। তাই Big-O দিয়ে দুটি অ্যালগরিদমের বৃদ্ধি তুলনা করা যায়, কিন্তু একটির আসল গতি বলা যায় না।

তাই hash table বেছে নেওয়ার আগে প্রকৌশলীরা জিজ্ঞেস করেন: চাবি কারা পাঠায়, কত লম্বা, আর ক্রম লাগবে কি না।

মূল উপলব্ধি: প্রতিটি complexity দাবির সাথে জিজ্ঞেস করুন — কোন case, কোন অনুমানে, আর কী কী খরচ হিসাবের বাইরে রাখা হয়েছে।

প্র ০৩O(log n) লেখার সময় log-এর base কেন বলা হয় না? অথচ O(2ⁿ) আর O(3ⁿ) কেন আলাদা?

দুটি প্রশ্নের উত্তর একই নিয়মে লুকিয়ে আছে: Big-O শুধু ধ্রুবক গুণ বাদ দেয়, এর বেশি কিছু নয়।

log-এর ক্ষেত্রে: গণিতের একটি সূত্র বলে log₂ n = log₁₀ n ÷ log₁₀ ২ ≈ ৩.৩২ × log₁₀ n. অর্থাৎ base বদলালে পুরো ফলটি একটি নির্দিষ্ট সংখ্যা দিয়ে গুণ হয় — n যা-ই হোক, গুণকটি একই। Big-O এমন গুণক বাদ দেয়, তাই log₂ n, log₁₀ n, ln n — সব O(log n). বাস্তব অর্থেও এটি যুক্তিসংগত: প্রতি ধাপে অর্ধেক করা আর প্রতি ধাপে দশ ভাগের এক ভাগ রাখা — দুটিতেই ইনপুট দশ গুণ বাড়লে কাজ বাড়ে মাত্র কয়েকটি নির্দিষ্ট ধাপ।

সূচকের ক্ষেত্রে: 3ⁿ ÷ 2ⁿ = (১.৫)ⁿ. এই অনুপাত কোনো নির্দিষ্ট সংখ্যা নয় — n-এর সাথে নিজেই সূচকীয় হারে বাড়ে। n = ১০-এ প্রায় ৫৮ গুণ, n = ৫০-এ ৬০ কোটিরও বেশি গুণ। তাই এখানে base-এর পার্থক্য ধ্রুবক নয়, বাদ দেওয়া যায় না। একই কারণে সরল fib-এর Θ(১.৬১৮ⁿ) আর O(2ⁿ) এক জিনিস নয় — 2ⁿ শুধু একটি ঢিলেঢালা ওপরের সীমা।

একই যুক্তিতে, O(n²) আর O(n³) আলাদা (অনুপাত n — বাড়ে), কিন্তু O(n²) আর O(5n²) একই (অনুপাত ৫ — ধ্রুব)। আর log(n²) = 2 log n, তাই O(log n²) আসলে O(log n). প্রতিটি ক্ষেত্রে একটিই প্রশ্ন করুন: “দুটির অনুপাত কি n বাড়লেও একটি নির্দিষ্ট সংখ্যায় আটকে থাকে?”

ঐতিহাসিকভাবেও এই “ধ্রুবক বাদ দেওয়া” একটি সচেতন সিদ্ধান্ত। Hartmanis ও Stearns-এর ১৯৬৫ সালের গবেষণাপত্রেই দেখানো হয়, তাত্ত্বিক মেশিনের নকশা বদলে প্রায় যেকোনো হিসাবকে একটি ধ্রুবক গুণে দ্রুত করা যায় (linear speedup). তাই ধ্রুবক গুণটি অ্যালগরিদমের নয়, মেশিনের বৈশিষ্ট্য।

মূল উপলব্ধি: Big-O-র প্রতিটি নিয়ম একটি প্রশ্ন থেকে আসে — “এই পার্থক্য কি মেশিন বদলালে মুছে যায়?” মুছে গেলে বাদ, না মুছলে রাখুন।

প্র ০৪P vs NP প্রশ্নটি কী, আর আপনার bKash বা অনলাইন ব্যাংকিংয়ের নিরাপত্তার সাথে এর সম্পর্ক কোথায়?

কিছু সমস্যার উত্তর খুঁজে বের করা কঠিন মনে হয়, কিন্তু কেউ উত্তর দিলে তা যাচাই করা সহজ। যেমন একটি কঠিন সুডোকু মেলাতে ঘণ্টা লাগে, কিন্তু মেলানো সুডোকু ঠিক আছে কি না দেখতে এক মিনিট। কম্পিউটার বিজ্ঞানে:

  • P: যেসব সমস্যা polynomial সময়ে (যেমন n², n³) সমাধান করা যায় — সর্টিং, shortest path, binary search.
  • NP: যেসব সমস্যার একটি প্রস্তাবিত উত্তর polynomial সময়ে যাচাই করা যায় — সুডোকু, “সব শহর একবার করে ঘুরে আসার এমন রুট আছে কি, যা ৫০০ কিলোমিটারের কম?”, একটি বড় সংখ্যার উৎপাদক।

প্রশ্নটি হলো P vs NP: যা দ্রুত যাচাই করা যায়, তা কি সবসময় দ্রুত সমাধানও করা যায়? ১৯৭১ সালে Stephen Cook প্রশ্নটিকে আনুষ্ঠানিক রূপ দেন (প্রায় একই সময়ে সোভিয়েত ইউনিয়নে Leonid Levin স্বাধীনভাবে)। ২০০০ সালে Clay Mathematics Institute একে সাতটি “Millennium Prize Problems”-এর একটি ঘোষণা করে — সমাধানে ১০ লাখ ডলার পুরস্কার। আজও কেউ প্রমাণ করতে পারেননি; বেশিরভাগ গবেষকের ধারণা P ≠ NP, কিন্তু ধারণা প্রমাণ নয়।

নিরাপত্তার সাথে সম্পর্ক: আধুনিক cryptography দাঁড়িয়ে আছে এমন সমস্যার ওপর, যার উত্তর যাচাই সহজ কিন্তু খুঁজে বের করা (আমাদের জানামতে) অসম্ভব রকম ধীর। RSA-এর নিরাপত্তা নির্ভর করে বিশাল একটি সংখ্যাকে দুটি মৌলিক উৎপাদকে ভাঙা কঠিন হওয়ার ওপর — উৎপাদক দুটি দিলে গুণ করে মেলানো এক মুহূর্তের কাজ। এই উৎপাদক-খোঁজা সমস্যা NP-তে আছে। তাই যদি কেউ প্রমাণ করেন P = NP, এবং সাথে একটি বাস্তবে দ্রুত অ্যালগরিদম দেন, তাহলে আজকের অনেক এনক্রিপশন ভেঙে পড়বে। উল্টো দিকে, P = NP হলে ওষুধ নকশা, সময়সূচি তৈরি আর অপটিমাইজেশনের হাজারো কঠিন সমস্যা সহজ হয়ে যেত।

একটি সূক্ষ্মতা: উৎপাদক-খোঁজা “সবচেয়ে কঠিন” NP সমস্যাগুলোর একটি বলে প্রমাণিত নয়। আর ১৯৯৪ সালে Peter Shor দেখান, যথেষ্ট বড় quantum computer এটি দ্রুত করতে পারবে — এজন্যই এখন “post-quantum” cryptography নিয়ে কাজ চলছে।

মূল উপলব্ধি: Complexity শুধু “প্রোগ্রাম কত দ্রুত” নয় — কিছু সমস্যা কঠিন হওয়াটাই আমাদের নিরাপত্তার ভিত্তি। ধীরগতিও কখনো কখনো একটি সম্পদ।

প্র ০৫অনেকে বলেন “গড়ে দ্রুত হলেই হলো।” তাহলে সার্ভারের প্রকৌশলীরা worst case নিয়ে এত চিন্তিত কেন?

কারণ ইন্টারনেটে আপনার ইনপুট শুধু সাধারণ ব্যবহারকারী পাঠায় না — আক্রমণকারীও পাঠায়। আর আক্রমণকারী ইচ্ছে করে ঠিক সেই ইনপুট বানায়, যা আপনার worst case ঘটায়।

  • Hash flooding: পর্ব ৩৭-এ দেখেছেন, hash function আগে থেকে জানা থাকলে আক্রমণকারী হাজার হাজার চাবি বানাতে পারে যা সব একই ঘরে পড়ে। প্রতিটি insert তখন O(1)-এর বদলে O(n), মোট O(n²). ২০১১ সালে গবেষকেরা দেখান, অনেক জনপ্রিয় ওয়েব প্ল্যাটফর্মে একটি মাত্র বড় অনুরোধ দিয়ে সার্ভারের CPU দীর্ঘক্ষণ আটকে রাখা যায় — একটি hash flooding আক্রমণ। এর পর অনেক ভাষা SipHash-এর মতো গোপন চাবিযুক্ত hash-এ চলে যায়।
  • Regular expression: কিছু regex ইঞ্জিন নির্দিষ্ট ধরনের প্যাটার্নে সূচকীয় হারে পিছু হটে চেষ্টা করে। ২০১৯ সালের জুলাইয়ে Cloudflare-এর একটি নতুন firewall নিয়মের এমন একটি regex তাদের সার্ভারের CPU প্রায় পুরোপুরি দখল করে ফেলে, আর বিশ্বজুড়ে বহু ওয়েবসাইট প্রায় আধা ঘণ্টা বিঘ্নিত হয়।

তাই নিরাপদ সিস্টেমে প্রকৌশলীরা গড়ের পাশাপাশি নিশ্চয়তা চান: ভারসাম্যপূর্ণ tree (worst case O(log n)), ইনপুটের আকারে সীমা, এলোমেলো গোপন চাবিযুক্ত hash, আর প্রতিটি অনুরোধের জন্য সময়সীমা (timeout). এটি একটি denial of service প্রতিরক্ষা — প্রোগ্রাম ভুল উত্তর দিচ্ছে না, কিন্তু এত ধীর যে কার্যত অচল।

মূল উপলব্ধি: নিজের কম্পিউটারে “গড়ে দ্রুত” যথেষ্ট; কিন্তু অচেনা মানুষের ইনপুট নেওয়া যেকোনো সিস্টেমে worst case-ই আপনার আসল complexity, কারণ কেউ না কেউ ঠিক সেটিই খুঁজে বের করবে।

অনুশীলন

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

  1. কোড পড়ে বলুন: নিচের চারটির complexity কত? (ক) for (i = 0; i < n; i += 3) (খ) for (i = 1; i < n; i *= 3) (গ) for (i = 0; i < n; i++) {…} তারপর আলাদাভাবে for (j = 0; j < m; j++) {…} (ঘ) for (i = 0; i < n; i++) for (j = 0; j < i; j++)

    (ক) O(n) — লুপ চলে প্রায় n/3 বার; ৩ একটি ধ্রুবক গুণ, বাদ।

    (খ) O(log n) — i প্রতিবার ৩ গুণ হয়: ১, ৩, ৯, ২৭… n-এ পৌঁছাতে লাগে log₃ n ধাপ; base বাদ দিলে O(log n).

    (গ) O(n + m) — দুটি লুপ পরপর, তাই যোগ। n আর m দুটি আলাদা ইনপুট হলে কোনোটিই বাদ দেওয়া যায় না।

    (ঘ) O(n²) — ভেতরের লুপ ০, ১, ২, … , n − ১ বার চলে; মোট n(n − 1)/2. অর্ধেক হলেও বৃদ্ধির ধরন বর্গ।

  2. ভবিষ্যদ্বাণী করুন: একটি O(n²) প্রোগ্রাম n = ১০,০০০-এ ২ সেকেন্ড নিল। n = ২০,০০০ আর n = ১ লাখে কত নেবে আন্দাজ করুন। প্রোগ্রামটি O(n log n) হলে উত্তর কী হতো?

    O(n²): n দ্বিগুণ → সময় ৪ গুণ, তাই ২০,০০০-এ প্রায় ৮ সেকেন্ড। n দশ গুণ → সময় ১০০ গুণ, তাই ১ লাখে প্রায় ২০০ সেকেন্ড — তিন মিনিটের বেশি।

    O(n log n): n দ্বিগুণ হলে সময় দ্বিগুণের সামান্য বেশি — প্রায় ৪.৩ সেকেন্ড। ১ লাখে প্রায় ১০ × (log ১ লাখ ÷ log ১০ হাজার) ≈ ১০ × ১.২৫ = সাড়ে ১২ গুণ, অর্থাৎ প্রায় ২৫ সেকেন্ড। একই শুরুর সময়, অথচ বড় ইনপুটে পার্থক্য আট গুণ — আর n যত বাড়বে, পার্থক্য তত বাড়বে।

  3. Doubling পরীক্ষা নিজে চালান: উপরের doubling.c চালান। pairs-এর সময়ের পরপর দুটি অনুপাত বের করুন। তারপর ভেতরের লুপটি j = i + 1-এর বদলে j = 0 থেকে শুরু করুন — সময় আর অনুপাত কী বদলাল?

    সাধারণত অনুপাত ৩.৫ থেকে ৪.৫-এর মধ্যে আসে — একটি O(n²) কাজের পরিচয়। অনুপাত ঠিক ৪ না হওয়ার কারণ: cache, অন্য প্রোগ্রামের চাপ, CPU-র গতি বদলানো (turbo), আর ছোট সময়ে clock()-এর অসূক্ষ্মতা।

    j = 0 থেকে শুরু করলে প্রতিটি সময় প্রায় দ্বিগুণ হবে (এখন n(n − 1)/2 নয়, n² বার তুলনা), কিন্তু পরপর দুটি সময়ের অনুপাত সেই ৪-এর কাছাকাছিই থাকবে। এটাই Big-O-র মূল কথা হাতে-কলমে: ধ্রুবক গুণ সময় বদলায়, বৃদ্ধির ধরন বদলায় না। (pairs-এর মানও বদলাবে, কারণ এখন i > j জোড়াগুলোও তুলনা হচ্ছে।)

  4. AI অডিট: যেকোনো AI-কে নিচের ফাংশনের complexity জিজ্ঞেস করুন: একটি string-এর প্রতিটি অক্ষর strcat দিয়ে আরেকটি buffer-এর শেষে জুড়ে দেওয়া হচ্ছে, লুপ চলছে i < strlen(src) পর্যন্ত। AI কী বলল? আপনি নিজে হিসাব করে কী পান?

    এখানে দুটি লুকানো O(n) খরচ আছে। প্রথমত, লুপের শর্তে strlen(src) প্রতিবার পুরো src হাঁটে। দ্বিতীয়ত, strcat জুড়ে দেওয়ার আগে dst-এর শেষ খুঁজতে তার শুরু থেকে হাঁটে — আর dst প্রতিবার লম্বা হচ্ছে। দুটিই n বার × O(n), তাই মোট O(n²).

    অনেক AI প্রথমে “একটি লুপ, তাই O(n)” বলে; “strlen আর strcat-এর নিজের খরচ কত?” জিজ্ঞেস করলে নিজেকে শুধরে নেয়। সমাধান: দৈর্ঘ্য একবার বের করে রাখুন, আর dst-এর শেষ কোথায় তা একটি index বা pointer-এ মনে রাখুন — তখন O(n).

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

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

ক্রমসবচেয়ে ধীরে বাড়া থেকে সবচেয়ে দ্রুত বাড়া — শ্রেণিগুলো সাজান (▲▼ চেপে সরান)

  1. O(1) — রোল জানা থাকলে সরাসরি সিট
  2. O(log n) — অভিধানে মাঝখান থেকে খোঁজা
  3. O(n) — হাজিরা ডাকা
  4. O(n log n) — ভাগ করে খাতা সাজানো
  5. O(n²) — সবাই সবার সাথে হ্যান্ডশেক
  6. O(2ⁿ) — সব সম্ভাব্য বাছাই

অ্যালগরিদমের গতি তুলনায় “কত সেকেন্ড লাগল” কেন ভালো মাপকাঠি নয়?

  • সেকেন্ড মাপা খুব কঠিন
  • একই কোড ভিন্ন মেশিন ও পরিস্থিতিতে ভিন্ন সময় নেয়, আর একটি সংখ্যা বড় ইনপুটের কথা বলে না
  • সময় আসলে কোনো গুরুত্বপূর্ণ বিষয় নয়
  • C প্রোগ্রাম সবসময় এক সেকেন্ডের কম নেয়

একটি প্রোগ্রাম মোট 3n² + 100n + 5000 ধাপ নেয়। Big-O-তে এটি কত?

  • O(5000)
  • O(n)
  • O(n²)
  • O(3n²)

১০ লাখ সাজানো রোল নম্বরে binary search সবচেয়ে খারাপে মোটামুটি কতটি তুলনা করে?

  • প্রায় ২০টি
  • প্রায় ১,০০০টি
  • প্রায় ৫ লাখটি
  • ১০ লাখটি

একটি O(n²) প্রোগ্রাম n = ৫,০০০-এ ৩ সেকেন্ড নেয়। n = ১০,০০০-এ আন্দাজ কত?

  • ৩ সেকেন্ড
  • ৬ সেকেন্ড
  • ৯ সেকেন্ড
  • প্রায় ১২ সেকেন্ড

for (k = n; k > 0; k /= 2) { … } — এই লুপের complexity কত (ভেতরের কাজ O(1) ধরে)?

  • O(n)
  • O(log n)
  • O(n²)
  • O(1)

Capacity দ্বিগুণ করা dynamic array-তে একটি push-এর খরচ সম্পর্কে কোনটি ঠিক?

  • প্রতিটি push নিশ্চিতভাবে O(1)
  • প্রতিটি push গড়ে O(n)
  • মাঝে মাঝে একটি push দামি, কিন্তু গড়ে (amortized) O(1)
  • Capacity প্রতিবার ১০ করে বাড়ালে আরও সস্তা হতো

শব্দকোষ

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

মনে রাখুন

  1. গতি মাপা হয় ধাপ গুনে, সেকেন্ডে নয় — প্রশ্ন একটাই: n দ্বিগুণ হলে কাজ কত গুণ?
  2. Big-O সবচেয়ে বড় পদ রাখে, ধ্রুবক ও ছোট পদ বাদ দেয়; worst case-ই সাধারণত আসল নিশ্চয়তা।
  3. O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) — n = ১০ লাখে এই পার্থক্য মুহূর্ত থেকে অসম্ভব পর্যন্ত।
  4. কোড পড়ার নিয়ম: পরপর হলে যোগ, ভেতরে হলে গুণ; অর্ধেক করা লুপ log n; লুপের শর্ত ও ভেতরের ফাংশনের খরচও গুনুন।
  5. Space complexity আর amortized খরচ ভুলবেন না; শেষ কথা বলে মেপে দেখা — n দ্বিগুণ করে সময়ের অনুপাত।

পরের পর্বে: হাতে এখন মাপকাঠি আছে। এবার প্রথম বড় পরীক্ষা — সর্টিং। কেন সরল পদ্ধতিগুলো O(n²), কীভাবে ভাগ করে সাজিয়ে O(n log n)-এ নামা যায়, আর তুলনা করে সাজানোর একটি সীমা কেন কেউ কখনো ভাঙতে পারবে না।

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

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