র‍্যান্ডমাইজড অ্যালগরিদম৭ মিনিট পড়া

র‍্যান্ডমাইজড কুইক সর্ট (Randomized Quick Sort)

একটি র‍্যান্ডম পিভট প্রতিকূল সবচেয়ে খারাপ ক্ষেত্রের ইনপুটগুলো ধ্বংস করে দেয়
expected:দ্রুত · O(n log n)worst case:অত্যন্ত অসম্ভব · O(n²)space:O(log n) প্রত্যাশিত

কল্পনা করুন আপনাকে উচ্চতা অনুযায়ী ১০০ জনকে সাজাতে হবে। আপনি দল থেকে একজনকে রেফারি (referee) হিসেবে বেছে নিলেন। তার চেয়ে খাটো সবাই তার বাঁ দিকে দাঁড়াবে, আর লম্বা সবাই ডান দিকে দাঁড়াবে। তারপর আপনি প্রতিটি অর্ধেকের জন্য এই একই কাজ পুনরাবৃত্তি (recursively) করবেন।

যদি রেফারির উচ্চতা মোটামুটি মাঝামাঝি হয়, তবে প্রতিটি অর্ধে প্রায় ৫০ জন করে থাকবে — এবং প্রতিটি ধাপে কাজ চমৎকারভাবে অর্ধেক হয়ে যাবে: মোট O(n log n)। কিন্তু আপনি যদি সবসময় সবচেয়ে লম্বা মানুষটিকে রেফারি হিসেবে বেছে নেন, তবে একপাশে ৯৯ জন এবং অন্যপাশে ০ জন থাকবে — এটি সময়ের সাথে O(n²) আচরণ করবে।

এর সমাধান খুব সহজ: রেফারিকে এলোমেলোভাবে (randomly) বেছে নিন। একজন র‍্যান্ডম ব্যক্তির সবচেয়ে লম্বা বা সবচেয়ে খাটো হওয়ার সম্ভাবনা নেই বললেই চলে। আর যদি একবার এমন দুর্ভাগ্যজনক বাছাই হয়েও যায়, তবে রিকার্শনের পরবর্তী স্তরে আবার নতুন করে র‍্যান্ডম বাছাই হবে — প্রতিটি বাছাইয়ে খারাপ ফলাফল হওয়ার সম্ভাবনা সূচকীয় হারে (exponentially) কমতে থাকে।

কেন ডিটারমিনিস্টিক কুইক সর্ট সাজানো ইনপুটে ব্যর্থ হয়

চিরায়ত বাগ (classic bug): প্রতিবার শেষ উপাদানটিকে পিভট হিসেবে বেছে নেওয়া। [1, 2, 3, 4, 5] এর মতো আগে থেকেই সাজানো (sorted) অ্যারেতে, শেষ উপাদানটি সবসময়ই সর্বোচ্চ হয়। প্রতিটি বিভাজন (partition) চরমভাবে ভারসাম্যহীন হয়: একপাশে ০টি উপাদান থাকে, অন্যপাশে n-1 টি। আপনি n সংখ্যক রিকার্শন স্তর পাবেন, যার প্রতিটি O(n) কাজ করে — মোট দ্বিঘাত (quadratic) সময়।

এটি কেবল একটি তাত্ত্বিক সমস্যা নয়। একটি সাজানো বা প্রায়-সাজানো তালিকাকে সর্ট করা বাস্তব জগতে খুবই সাধারণ একটি কাজ। অনেক সিস্টেম কুইক সর্টে সাজানো ডেটা দেয় এবং অবাক হয় যে এটি এত ধীরগতিতে কেন চলছে।

র‍্যান্ডম পিভট: কোনো ইনপুটই প্রতিকূল হতে পারে না

র‍্যান্ডম পিভটের সাথে, কোনো নির্দিষ্ট ইনপুট সবচেয়ে খারাপ ক্ষেত্রের (worst-case) আচরণ তৈরি করতে পারে না। O(n²) এ বাধ্য করার জন্য, প্রতিপক্ষকে (adversary) আপনার রিকার্শনের প্রতিটি স্তরে র‍্যান্ডম পছন্দগুলো অনুমান করতে হবে — যা তারা করতে পারে না। সবচেয়ে খারাপ ক্ষেত্রটি ডেটার উপর নির্ভর না করে আপনার র‍্যান্ডম নম্বর জেনারেটরের ফাংশনে পরিণত হয়।

প্রত্যাশিত তুলনার বিশ্লেষণ (The Expected Comparisons Analysis)

ধরুন X_{ij} = 1 যদি সাজানো র‍্যাঙ্ক i এবং j এর উপাদানগুলো কখনো সরাসরি তুলনা করা হয়। দুটি উপাদানের তখনই তুলনা হয় যখন তাদের একটি পিভট হয় এবং উভয়েই একই সাব-অ্যারেতে থাকে। এর সম্ভাবনা কত? ঠিক 2 / (j − i + 1) — কারণ সাজানো ক্রমে তাদের মধ্যে j-i+1 টি উপাদান রয়েছে, এবং তুলনাটি কেবল তখনই ঘটবে যদি প্রথমে i বা j কে পিভট হিসেবে বেছে নেওয়া হয়।

সব জোড়ার যোগফল: E[তুলনা] = Σ 2/(j-i+1) ≈ 2n·ln(n) = O(n log n)। এটি উন্মুক্ত যেকোনো ইনপুটের জন্য কাজ করে।

ইন্ট্রোসর্ট (Introsort): উভয়ের সেরা দিক

বাস্তব-জগতের সর্ট ইমপ্লিমেন্টেশনগুলো (যেমন: C++ std::sort, Rust এর sort_unstable) ইন্ট্রোসর্ট (introsort) ব্যবহার করে: র‍্যান্ডমাইজড কুইক সর্ট দিয়ে কাজ শুরু করে কিন্তু রিকার্শনের গভীরতা পর্যবেক্ষণ করে। যদি গভীরতা 2·log n ছাড়িয়ে যায় — যা ধারাবাহিকভাবে খারাপ পিভটের লক্ষণ — তবে এটি হিপ সর্টে (HeapSort) চলে যায়, যা O(n log n) সবচেয়ে খারাপ ক্ষেত্রের গ্যারান্টি দেয়। এটি কুইক সর্টের চমৎকার ক্যাশ পারফরম্যান্সের সাথে নিখুঁত সবচেয়ে খারাপ ক্ষেত্রের গ্যারান্টি যুক্ত করে।

দ্রষ্টব্য: যেকোনো ইনপুটে র‍্যান্ডমাইজড কুইক সর্টের O(n log n) এর চেয়ে বেশি সময় নেওয়ার সম্ভাবনা যেকোনো ধ্রুবক c এর জন্য 1/n^c এর চেয়ে দ্রুতগতিতে হ্রাস পায়। বাস্তবে, আপনি O(n²) আচরণের মুখোমুখি হওয়ার চেয়ে পরপর তিনবার লটারি জেতার সম্ভাবনা বেশি।

র‍্যান্ডমাইজড কুইক সর্ট (Randomized QuickSort)

import random
def quicksort(arr, lo, hi):
if lo >= hi:
return
# Random pivot selection
pivot_idx = random.randint(lo, hi)
arr[pivot_idx], arr[hi] = arr[hi], arr[pivot_idx]
pivot = arr[hi]
# Partition
p = lo
for i in range(lo, hi):
if arr[i] <= pivot:
arr[p], arr[i] = arr[i], arr[p]
p += 1
arr[p], arr[hi] = arr[hi], arr[p]
quicksort(arr, lo, p - 1)
quicksort(arr, p + 1, hi)
# Test on sorted input — the classic worst case for deterministic QS
arr = list(range(10)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
quicksort(arr, 0, len(arr) - 1)
print(arr)
# Test on random input
arr2 = [5, 2, 8, 1, 9, 3]
quicksort(arr2, 0, len(arr2) - 1)
print(arr2)
Output
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 5, 8, 9]

Complexity

🚀 প্রত্যাশিত সময় (Expected time)
প্রতিটি সম্ভাব্য ইনপুট কম্বিনেশনের জন্য প্রযোজ্য — শুধু র‍্যান্ডম ইনপুটের জন্য নয়
প্রতিটি ইনপুটের জন্য দ্রুত O(n log n)
💀 সবচেয়ে খারাপ-সময়ের (Worst-case time)
এর জন্য প্রয়োজন র‍্যান্ডম নম্বর জেনারেটরকে প্রতিটি রিকার্সিভ কলে সবচেয়ে খারাপ পিভট বেছে নেওয়া
জ্যোতির্বিজ্ঞানের মতো অসম্ভব O(n²)
🌟 সবচেয়ে ভালো সময়ের (Best-case time)
যখন প্রতিটি পিভট সাব-অ্যারেকে পুরোপুরি দুভাগ করে — প্রত্যাশিত সময়ের সাথে মিলে যায়
প্রতিবার নিখুঁত বিভাজন O(n log n)
📚 স্পেস (কল স্ট্যাক)
প্রত্যাশিত রিকার্শন গভীরতা; সবচেয়ে খারাপ ক্ষেত্রে O(n) — নিরাপদে থাকতে ছোট পার্টিশনের উপর টেইল (tail) রিকার্শন ব্যবহার করুন
গড়পরতা অগভীর রিকার্শন O(log n)

ছোট কুইজ

ডিটারমিনিস্টিক কুইক সর্ট (শেষ-উপাদানের পিভট) কেন সাজানো ইনপুটে সবচেয়ে খারাপ কাজ করে?

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