সর্টিং৮ মিনিট পড়া

মার্জ সর্ট (Merge Sort)

ভাগ করুন, প্রতি অর্ধেক সাজান, তারপর একত্রিত (merge) করুন — প্রতিবারই \(O(n \log n)\) এর নিশ্চয়তা
time (all cases):\(O(n \log n)\)space:\(O(n)\) অক্সিলিয়ারি (auxiliary)stable:হ্যাঁrecursion depth:\(O(\log n)\)

আপনার ডেস্কে থাকা কাগজপত্রের একটি বিশাল অগোছালো স্তূপের কথা ভাবুন। এটিকে দুটি ছোট স্তূপে ভাগ করা বেশ সহজ। যদি প্রতিটি ছোট স্তূপ আগে থেকেই সাজানো (sorted) থাকে, তবে সেগুলোকে একত্রিত (merge) করে আবার একটি সাজানো স্তূপে পরিণত করাও সহজ — শুধু প্রতিটি স্তূপের সবচেয়ে ওপরের কাগজটি তুলনা করুন এবং ছোটটি তুলে নিন। ঠিক এভাবেই মার্জ সর্ট (Merge Sort) কাজ করে।

অবশ্যই, ছোট স্তূপগুলো প্রথম দিকে সাজানো থাকে না — তাই আপনি সেগুলোকে আবারও ভাগ করেন, এবং ততক্ষণ পর্যন্ত ভাগ করতে থাকেন যতক্ষণ না প্রতিটি স্তূপে কেবল একটি করে কাগজ থাকে। একটিমাত্র কাগজ সর্বদা সাজানোই থাকে। এরপর জোড়ায় জোড়ায় স্তূপগুলোকে আবার একত্রিত (merge) করে ওপরের দিকে নিয়ে আসুন। পুরো প্রক্রিয়াটি \(O(n \log n)\) সময়ে কাজ করে — এবং এটি শতভাগ নিশ্চিত, ইনপুট যেমনই হোক না কেন।

তিনটি ধাপ (The Three Phases)

১. ভাগ করুন (Divide): অ্যারেটিকে সমান দুই ভাগে ভাগ করুন। প্রতিটি অর্ধাংশের ওপর রিকার্শন (recursion) প্রয়োগ করুন। যতক্ষণ না অ্যারের সাইজ ১ (base case) হচ্ছে, ততক্ষণ ভাগ করতে থাকুন।

২. জয় করুন (Conquer): ১ সাইজের অ্যারেগুলো স্বাভাবিকভাবেই সাজানো (sorted) থাকে। আসল কাজটি এরপরই শুরু হয়।

৩. একত্রিত করুন (Merge): দুটি সাজানো অর্ধাংশকে একত্রিত করে একটি সম্পূর্ণ সাজানো অ্যারে তৈরি করুন। দুটি পয়েন্টার ব্যবহার করুন — প্রতিটি অর্ধাংশের জন্য একটি। প্রতিটি ধাপে, সামনের ছোট উপাদানটি (element) বেছে নিন এবং সেই পয়েন্টারটিকে এক ধাপ এগিয়ে দিন। যখন একটি অর্ধাংশ শেষ হয়ে যাবে, তখন অন্য অর্ধাংশের বাকি অংশটুকু শেষে যোগ করে দিন। প্রতিটি মার্জের জন্য \(O(n)\) সময় লাগে, এবং প্রতিটি উপাদান ঠিক log n সংখ্যক মার্জে অংশ নেয় (রিকার্শন ট্রির প্রতিটি লেভেলে একবার করে)।

কেন এটি সর্বদা \(O(n \log n)\)?

রিকার্শন ট্রিতে log n সংখ্যক লেভেল থাকে (কারণ আমরা প্রতিবার সমস্যাটিকে অর্ধেক করে ফেলি)। প্রতিটি লেভেলে, সমস্ত নোড মিলিয়ে মোট মার্জের কাজ হয় \(O(n)\) — অর্থাৎ অনেকগুলি ছোট ছোট মার্জ মিলে প্রতিটি উপাদানকে একবার স্পর্শ করে। গুণ করলে পাই: \(O(n)\) × \(O(\log n)\) = \(O(n \log n)\), এটি সর্বদা সত্যি — কোনো খারাপ ইনপুট বা ভুল পিভট (pivot) বাছাইয়ের ভয় এখানে নেই।

স্থায়িত্ব (Stability) এবং মেমরি বা স্পেস ট্রেডঅফ

মার্জ সর্ট একটি স্টেবল (stable) অ্যালগরিদম: একত্রিত বা মার্জ করার সময় যখন দুটি উপাদান সমান হয়, তখন আমরা সর্বদা বাম দিকের (বা আগের) অর্ধাংশ থেকে উপাদানটি আগে নিই। ফলে সমান উপাদানগুলো নিজেদের মধ্যে কখনো স্থান পরিবর্তন করে না।

এর একমাত্র নেতিবাচক দিকটি হলো মার্জ বাফারের জন্য এর \(O(n)\) অতিরিক্ত মেমরি (extra space) প্রয়োজন হয়। মেমরি-সীমাবদ্ধ ডিভাইসে কুইক সর্টের কাছে মার্জ সর্টের হেরে যাওয়ার এটিই মূল কারণ। তবে এর একটি ব্যতিক্রম আছে: লিংকড লিস্ট (linked lists)। দুটি সাজানো লিংকড লিস্ট একত্রিত করার জন্য কোনো অতিরিক্ত অ্যারের প্রয়োজন হয় না — শুধু পয়েন্টারগুলো রিলিংক (relink) করলেই হয়। তাই লিংকড লিস্টে মার্জ সর্ট ব্যবহার করলে কেবল স্ট্যাক স্পেসের জন্য \(O(\log n)\) মেমরি লাগে।

মার্জ সর্ট হলো টিমসর্ট বা Timsort এর মূল ভিত্তি (যা অবজেক্ট সাজানোর জন্য Python এবং Java ব্যবহার করে) এবং এটি এক্সটার্নাল সর্টিং বা external sorting এর (সাধারণ র‍্যামের চেয়ে বড় সাইজের ডেটা যা হার্ডডিস্কে থাকে) স্ট্যান্ডার্ড অ্যালগরিদম।

দ্রষ্টব্য: মার্জ সর্ট হলো একমাত্র কম্প্যারিজন সর্ট (comparison sort) যা স্টেবল (stable), সব ক্ষেত্রেই \(O(n \log n)\) সময় নেয়, এবং এক্সটার্নাল সর্টিংয়ের জন্য প্রাকৃতিকভাবেই মানানসই। যদি আপনার ডেটা হার্ডডিস্কে থাকে বা বড় ডেটাসেটের জন্য আপনার স্ট্যাবিলিটির প্রয়োজন হয় — তবে মার্জ সর্টই হলো আপনার উত্তর।

মার্জ সর্টের ইমপ্লিমেন্টেশন (Merge Sort Implementation)

def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
# Take from left on tie — preserves stability
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
data = [38, 27, 43, 3, 9, 82, 10]
print(merge_sort(data))
Output
[3, 9, 10, 27, 38, 43, 82]

Complexity

⏱️ সর্ট বা সাজানো — সমস্ত ক্ষেত্র (all cases)
সবচেয়ে ভালো (best), সবচেয়ে খারাপ (worst), এবং গড়ের (average) ক্ষেত্রে এর মান সমান — \(\Theta(n \log n)\)
লিনিয়ারিদমিক (Linearithmic), কোনো খারাপ ইনপুট নেই \(O(n \log n)\)
🔀 মার্জ (Merge) ধাপ
টু-পয়েন্টার (Two-pointer) মার্জ: প্রতিটি মার্জে প্রতিটি উপাদানকে ঠিক একবার স্পর্শ করা হয়
মার্জ করা সাইজের সাপেক্ষে লিনিয়ার \(O(n)\)
💾 অক্সিলিয়ারি স্পেস (Auxiliary space)
মার্জ করার জন্য বাফার প্রয়োজন হয় — কুইক সর্টের তুলনায় এটিই এর মূল অসুবিধা
লিনিয়ার অতিরিক্ত মেমরি \(O(n)\)
📚 রিকার্সন ডেপথ (Recursion depth)
সাইজ 1 এর বেস কেসে (base case) পৌঁছানোর আগে পর্যন্ত \(\log_2 n\) সংখ্যক লেভেল তৈরি হয়
লগারিদমিক স্ট্যাক ডেপথ (Logarithmic stack depth) \(O(\log n)\)

ছোট কুইজ

মার্জ সর্টকে কোন জিনিসটি স্টেবল (stable) করে তোলে?

পড়া চালিয়ে যান