মার্জ সর্ট ও এর অ্যানালাইসিস
এই পাঠে যা শিখবেন
- মার্জ সর্টের রানটাইম রিকারেন্স $T(n) = 2T(n/2) + O(n)$ পুরোপুরি ডেরাইভ করতে পারা
- মাস্টার থিওরেমের কেস ২ প্রয়োগ করে $\Theta(n \log n)$ সিদ্ধান্তে পৌঁছাতে পারা
- মার্জ ধাপের সঠিক তুলনা-সংখ্যার একটি পৃথক রিকারেন্স ডেরাইভ করতে পারা
- একটি জেনুইন কোড দিয়ে প্রকৃত তুলনার সংখ্যা গুনে $\Theta(n \log n)$ দাবিটি অভিজ্ঞতালব্ধভাবে যাচাই করতে পারা
১ · মার্জ সর্ট — সংক্ষিপ্ত রিক্যাপ
মার্জ সর্টের মেকানিক্স (ডিভাইড: অ্যারেকে মাঝখান থেকে দুই ভাগ করা; কনকার: প্রতিটি অর্ধেক রিকার্সিভভাবে সর্ট করা; কমবাইন: দুটি সর্টেড অর্ধেক একত্রে মার্জ করা) ইতিমধ্যে DSA কোর্সে ইমপ্লিমেন্টেশনসহ কভার করা হয়েছে — এখানে সেই মেকানিক্স পুনরায় ব্যাখ্যা না করে সরাসরি এর রানটাইম অ্যানালাইসিসে যাওয়া হচ্ছে, যা L14-এ সাধারণভাবে দেখা D&C টেমপ্লেটের একটি নির্দিষ্ট প্রয়োগ।
২ · রিকারেন্স ডেরিভেশন — $T(n) = 2T(n/2) + O(n)$
মার্জ সর্টের রানটাইম $T(n)$-কে তিনটি অংশে ভাঙা যায়:
- ডিভাইড: অ্যারেকে মাঝখান থেকে ভাগ করা — $O(1)$ (শুধু একটি ইনডেক্স হিসাব)।
- কনকার: দুটি অর্ধেক, প্রতিটির আকার $n/2$, রিকার্সিভভাবে সর্ট করা — $2T(n/2)$।
- কমবাইন: দুটি সর্টেড অর্ধেক মার্জ করা — মোট $n$টি এলিমেন্টের প্রতিটি একবার করে দেখতে হয়, তাই $O(n)$।
এই তিনটি একত্র করলে:
$$T(n) = 2T(n/2) + O(n), \qquad T(1) = O(1)$$এটি ঠিক সেই ফর্ম $T(n) = aT(n/b) + f(n)$ যার জন্য L13-এ মাস্টার থিওরেম শেখা হয়েছে, যেখানে $a = 2$, $b = 2$, এবং $f(n) = \Theta(n)$। মাস্টার থিওরেম প্রয়োগ করতে প্রথমে $n^{\log_b a}$ হিসাব করা হয়:
$$n^{\log_b a} = n^{\log_2 2} = n^1 = n$$যেহেতু $f(n) = \Theta(n) = \Theta(n^{\log_b a})$ (অর্থাৎ $f(n)$ এবং $n^{\log_b a}$ একই গ্রোথ রেটের, $k = 0$ সহ), এটি মাস্টার থিওরেমের কেস ২-এর সাথে মেলে, যা বলে:
$$T(n) = \Theta\!\left(n^{\log_b a} \log n\right) = \Theta(n \log n)$$অর্থাৎ মার্জ সর্ট $\Theta(n \log n)$ সময়ে চলে — সবসময়, ইনপুট যেভাবেই সাজানো থাক না কেন (এমনকি ইতিমধ্যে সর্টেড ইনপুটেও, কারণ রিকার্সন ও মার্জ ধাপ ইনপুটের ক্রম নির্বিশেষে একইভাবে ঘটে) — এটি L16-এ দেখা কুইক সর্টের সাথে একটি গুরুত্বপূর্ণ পার্থক্য, যেখানে ওয়ার্স্ট ও অ্যাভারেজ কেস ভিন্ন হয়।
রিকার্সন ট্রি-র প্রতিটি স্তরে (level) মোট কাজের পরিমাণ $\Theta(n)$ — কারণ প্রতিটি স্তরে সাব-প্রবলেমের সংখ্যা দ্বিগুণ হয় কিন্তু প্রতিটির আকার অর্ধেক হয়, ফলে প্রতি-স্তর মোট আকার (এবং তাই মোট মার্জ-খরচ) সবসময় $n$-এর সমান থাকে। গাছের গভীরতা (depth) $\log_2 n$ (কারণ প্রতি স্তরে আকার অর্ধেক হয়ে $1$-এ পৌঁছাতে $\log_2 n$ ধাপ লাগে)। তাই মোট কাজ $= (\text{প্রতি-স্তর কাজ}) \times (\text{স্তর সংখ্যা}) = n \times \log_2 n$ — মাস্টার থিওরেম থেকে পাওয়া ফলাফলের সাথে হুবহু মেলে।
৩ · সঠিক তুলনার সংখ্যা — একটি পৃথক রিকারেন্স
"$\Theta(n)$" শুধু গ্রোথ-ক্লাস বলে, প্রকৃত সংখ্যা নয়। মার্জ ধাপে ঠিক কতটি তুলনা (comparison) হয় তা নির্ভুলভাবে গুনতে পারলে বিশ্লেষণ আরও কংক্রিট হয়। দুটি সর্টেড অর্ধেক, আকার $p$ ও $q$ ($p + q = n$), মার্জ করতে সর্বোচ্চ $p + q - 1$টি তুলনা লাগে — কারণ প্রতিটি তুলনায় একটি এলিমেন্ট চূড়ান্ত ফলাফলে বসে যায়, এবং যখন একটি অর্ধেক পুরোপুরি শেষ হয়ে যায় তখন বাকি অর্ধেকের অবশিষ্ট এলিমেন্টগুলো আর কোনো তুলনা ছাড়াই সরাসরি যোগ হয়ে যায় (তাই $n$টি এলিমেন্টে ঠিক $n-1$টি নয়, সর্বোচ্চ $n-1$টি তুলনা)। তাই তুলনার সংখ্যা $C(n)$-এর নিজস্ব রিকারেন্স:
$$C(n) = 2\,C(n/2) + (n - 1), \qquad C(1) = 0$$এই রিকারেন্সও $f(n) = \Theta(n)$ সহ একই মাস্টার থিওরেম কেস ২-তে পড়ে, তাই $C(n) = \Theta(n \log n)$ — $T(n)$-এর মতোই। নিচের কোড সেলে এই $C(n)$ সত্যিকারের কোড চালিয়ে গুনে দেখানো হচ্ছে।
import random
import math
def merge_sort_counted(arr):
"""মার্জ সর্ট চালায় এবং মার্জ ধাপে ঠিক কতটি তুলনা হলো তা ফেরত দেয়।"""
counter = [0]
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
counter[0] += 1
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def sort(a):
if len(a) <= 1:
return a
mid = len(a) // 2
left = sort(a[:mid])
right = sort(a[mid:])
return merge(left, right)
sorted_arr = sort(arr)
return sorted_arr, counter[0]
random.seed(42)
sizes = [100, 200, 400, 800]
print(f"{'n':>5} | {'সঠিক?':>7} | {'তুলনা':>8} | {'n*log2(n)':>10} | {'রেশিও':>8}")
for n in sizes:
arr = list(range(n))
random.shuffle(arr)
sorted_arr, comparisons = merge_sort_counted(arr)
correct = (sorted_arr == sorted(arr))
n_log_n = n * math.log2(n)
ratio = comparisons / n_log_n
print(f"{n:>5} | {str(correct):>7} | {comparisons:>8} | {n_log_n:>10.1f} | {ratio:>8.4f}")
True দেখায় (মার্জ সর্ট সঠিকভাবে কাজ
করছে, Python-এর বিল্ট-ইন sorted()-এর সাথে মিলিয়ে যাচাই করা), এবং রেশিও
কলামটি — অর্থাৎ $\frac{\text{comparisons}}{n \log_2 n}$ — $n$ বাড়ার সাথে সাথে একটি ধ্রুবকের দিকে স্থির হতে
থাকে (উঠানামা কমতে থাকে)। এটাই ঠিক $\Theta(n \log n)$-এর সংজ্ঞাগত অর্থ — যদি তুলনার সংখ্যা $\Theta(n \log n)$
হতো না (ধরুন এটি $\Theta(n^2)$ হতো), তাহলে এই রেশিও $n$ বাড়ার সাথে সাথে ক্রমাগত বাড়তেই থাকত, স্থির
হতো না।
মার্জ সর্টের $\Theta(n \log n)$ বাউন্ড শুধু একটি "মুখস্থ করা তথ্য" নয় — এটি রিকারেন্স $T(n) = 2T(n/2) + O(n)$ থেকে মাস্টার থিওরেমের কেস ২ প্রয়োগ করে সরাসরি ডেরাইভ করা যায়, রিকার্সন ট্রি দিয়ে ক্রস-চেক করা যায়, এবং প্রকৃত তুলনার সংখ্যা গুনে অভিজ্ঞতালব্ধভাবে যাচাই করা যায়। এই তিন-স্তরের যাচাই (আনুষ্ঠানিক প্রমাণ + বিকল্প ডেরিভেশন + কম্পিউটেশনাল যাচাই) এই কোর্সের প্রতিটি অ্যালগরিদম অ্যানালাইসিসে ফিরে আসবে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ মার্জ সর্টের কমবাইন ধাপ $O(n)$ কেন — অন্য কিছু D&C অ্যালগরিদমে (যেমন বাইনারি সার্চ) এটি $O(1)$ হয়?
মার্জ সর্টে প্রতিটি সাব-প্রবলেমের সমাধান (দুটি সর্টেড অর্ধেক) মূল সমস্যার সমাধানের সাথে সরাসরি সমান নয় — সেগুলোকে একত্রে নতুন করে সাজিয়ে মূল সর্টেড অ্যারে তৈরি করতে হয়, যা পুরো $n$টি এলিমেন্ট একবার করে দেখা দাবি করে। বাইনারি সার্চে বিপরীত — একটি সাব-প্রবলেমের উত্তরই (হয় "পাওয়া গেছে এই ইনডেক্সে" অথবা "পাওয়া যায়নি") সরাসরি মূল সমস্যার উত্তর, তাই কমবাইন করার কিছুই থাকে না।
প্র ০২ যদি ইনপুট অ্যারে ইতিমধ্যে সর্টেড থাকে (best-case-looking ইনপুট), তাহলে কি মার্জ সর্টের জটিলতার ক্লাস পাল্টায়?
না। মার্জ সর্টের রিকার্সন কাঠামো (কতবার ভাগ হবে, প্রতিটি মার্জে সর্বোচ্চ কতটি তুলনা হতে পারে) ইনপুটের প্রাথমিক ক্রম নির্বিশেষে একই থাকে — তাই ওয়ার্স্ট, অ্যাভারেজ ও (প্রায়) বেস্ট কেস সবই $\Theta(n \log n)$। এটি L16-এর কুইক সর্টের সাথে একটি গুরুত্বপূর্ণ বৈসাদৃশ্য তৈরি করে, যেখানে ইনপুটের ক্রম (এবং পিভট বাছাইয়ের পদ্ধতি) সরাসরি জটিলতার ক্লাস পাল্টে দিতে পারে।
প্র ০৩ উপরের কোডে চারটি ভিন্ন $n$ ব্যবহার করে রেশিও দেখানো হয়েছে — মাত্র একটি $n$-এর জন্য একবার চালালেই কি যথেষ্ট হতো না?
না। একটি মাত্র $n$-এর জন্য একটি সংখ্যা দেখে গ্রোথ-রেট বোঝা অসম্ভব — $\Theta(n \log n)$ একটি দাবি ইনপুট বাড়ার সাথে সাথে সম্পর্ক নিয়ে, একটি নির্দিষ্ট বিন্দুতে মান নিয়ে নয়। $n$ দ্বিগুণ করতে করতে একাধিকবার পরিমাপ করেই বোঝা যায় রেশিওটি সত্যিই স্থির হচ্ছে (Theta) নাকি বাড়তে/কমতেই থাকছে (ভিন্ন কোনো জটিলতার ক্লাস) — এই প্যাটার্নটাই এই কোর্সের প্রতিটি এম্পিরিক্যাল যাচাইয়ে বারবার ব্যবহৃত হবে।
অনুশীলন
-
চিন্তা করুন: উপরের প্যাটার্ন অনুযায়ী $n = 1600$-এ তুলনার সংখ্যা কেমন হবে বলে আপনার
ধারণা — মোটামুটি $n \log_2 n$-এর কত কাছাকাছি?
$n = 1600$-এ $n \log_2 n = 1600 \times \log_2 1600 \approx 1600 \times 10.64 \approx 17{,}024$। প্রকৃত তুলনার সংখ্যা এই মানের কাছাকাছি হবে (উপরের রেশিও কলামে যে ধ্রুবকটির দিকে স্থির হচ্ছে সেটি দিয়ে গুণ করলে আরও নির্ভুল অনুমান পাওয়া যাবে) — নিচে রান করে যাচাই করুন।
-
পরীক্ষা করুন: উপরের কোড সেলে
sizesলিস্টে1600যোগ করে (sizes = [100, 200, 400, 800, 1600]) Run চেপে আপনার অনুমান যাচাই করুন। রেশিও কলামটি আগের মানগুলোর তুলনায় আরও কাছাকাছি স্থির থাকছে কি না লক্ষ্য করুন।নতুন লাইনে
n=1600-এর তুলনা-সংখ্যা ও রেশিও দেখা যাবে, এবং রেশিওটি আগের চারটি মানের সাথে একই ধ্রুবকের কাছাকাছি থাকবে — $n$ আরও বড় হলেও প্যাটার্নটি ভাঙে না, যা $\Theta(n \log n)$ দাবির সাথে সামঞ্জস্যপূর্ণ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাসিম্পটোটিক অ্যানালাইসিস, রিকারেন্স, ডিভাইড অ্যান্ড কনকার, গ্রিডি, DP, গ্রাফ অ্যালগরিদম, ব্যাকট্র্যাকিং, স্ট্রিং অ্যালগরিদম, অ্যামর্টাইজড অ্যানালাইসিস, NP-কমপ্লিটনেস ও অ্যাপ্রক্সিমেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
- ডিভাইড অ্যান্ড কনকার প্যারাডাইম আগের পাঠ D&C-এর সাধারণ তিন-ধাপ টেমপ্লেট, যার একটি নির্দিষ্ট প্রয়োগ এই পাঠের মার্জ সর্ট।
- Data Structures & Algorithms কোর্স সহোদর কোর্স মার্জ সর্টের ইমপ্লিমেন্টেশন ওয়াকথ্রু সেই কোর্সেই আছে।