কুইক সর্ট ও পিভট সিলেকশন
এই পাঠে যা শিখবেন
- কেন একটি ফিক্সড-পিভট কুইক সর্টের ওয়ার্স্ট কেস $\Theta(n^2)$, এবং কোন ইনপুট সেই ওয়ার্স্ট কেস তৈরি করে
- ওয়ার্স্ট-কেস রিকারেন্স $T(n) = T(n-1) + O(n)$ থেকে $\Theta(n^2)$ পুরোপুরি ডেরাইভ করতে পারা
- কেন র্যান্ডম ইনপুটে অ্যাভারেজ কেস $\Theta(n \log n)$ (ইনটুইশন স্তরে, সম্পূর্ণ প্রমাণ L54-এ)
- একটি জেনুইন কোড দিয়ে সর্টেড বনাম র্যান্ডম ইনপুটে প্রকৃত তুলনা গুনে এই দুই ক্লাসের পার্থক্য যাচাই করতে পারা
১ · কুইক সর্ট — সংক্ষিপ্ত রিক্যাপ
কুইক সর্টের মেকানিক্স ইতিমধ্যে DSA কোর্সে কভার করা হয়েছে: একটি পিভট বেছে অ্যারেকে পিভটের চেয়ে ছোট ও বড় — এই দুই ভাগে পার্টিশন করা হয় (ডিভাইড ধাপেই আসল কাজ, L14 দ্রষ্টব্য), তারপর প্রতিটি ভাগ রিকার্সিভভাবে সর্ট করা হয় (কমবাইন ধাপে কিছুই করার নেই)। এই পাঠে মেকানিক্স নয়, বরং পিভট কীভাবে বাছাই করা হচ্ছে তার উপর জটিলতা কতটা নির্ভরশীল তা বিশ্লেষণ করা হবে।
২ · ওয়ার্স্ট কেস — $\Theta(n^2)$
ধরা যাক পিভট বাছাইয়ের নিয়মটি নেইভ ও ফিক্সড — প্রতিবার সবসময় শেষ এলিমেন্টকে পিভট হিসেবে নেওয়া হয় (বা সমতুল্যভাবে, সবসময় প্রথম এলিমেন্ট)। এখন ইনপুট যদি ইতিমধ্যে ঊর্ধ্বক্রমে সর্টেড থাকে, তাহলে প্রতিটি পার্টিশনে পিভট (শেষ এলিমেন্ট) সেই সাব-অ্যারের সর্বোচ্চ মান — অর্থাৎ বাকি সব এলিমেন্টই পিভটের চেয়ে ছোট, ফলে পার্টিশন সম্পূর্ণ অসামঞ্জস্যপূর্ণ হয়: একটি ভাগে $n-1$টি এলিমেন্ট, অন্য ভাগে $0$টি।
প্রতিটি পার্টিশন ধাপে $O(n)$ সময় লাগে (পুরো সাব-অ্যারে একবার স্ক্যান করতে হয়), এবং প্রতিবার সমস্যার আকার মাত্র $1$ কমে। তাই রিকারেন্স:
$$T(n) = T(n-1) + O(n), \qquad T(0) = T(1) = O(1)$$এটি সরাসরি সমষ্টি (summation) আকারে খুলে ফেলা যায়:
$$T(n) = O(n) + O(n-1) + O(n-2) + \cdots + O(1) = O\!\left(\sum_{i=1}^{n} i\right) = O\!\left(\frac{n(n+1)}{2}\right) = O(n^2)$$এবং যেহেতু প্রতিটি পদই ধনাত্মক ও নিচের দিকেও একই আর্গুমেন্ট প্রযোজ্য (প্রতিটি পার্টিশনে অন্তত $\Omega(n-i)$ কাজ হয়), তাই আসলে $T(n) = \Theta(n^2)$ — শুধু আপার বাউন্ড নয়, টাইট বাউন্ড। মনে রাখবেন, এই একই রিকারেন্স ফর্ম $T(n) = T(n-1) + O(n)$ M3-এর সাবস্টিটিউশন বা রিকার্সন ট্রি মেথড দিয়েও একই ফলাফলে পৌঁছায় (মাস্টার থিওরেম সরাসরি প্রযোজ্য নয় এখানে, কারণ সাব-প্রবলেমগুলোর আকার সমান ভাগে ভাগ হয় না — $a=1$ কিন্তু $b$ ধ্রুবক নয়)।
সর্টেড (বা প্রায়-সর্টেড) ইনপুট বাস্তব জীবনে অস্বাভাবিক কিছু নয় — লগ ফাইল, টাইমস্ট্যাম্প-অর্ডারড ডেটা, বা ইতিমধ্যে একবার সর্ট করা ডেটাসেটে আবার সর্ট চালানো, ইত্যাদি ক্ষেত্রে এটি ঘটতেই পারে। তাই "ফার্স্ট/লাস্ট এলিমেন্ট পিভট" পদ্ধতি শুধু তাত্ত্বিকভাবে খারাপ নয় — এটি বাস্তব ইনপুটেও ট্রিগার হতে পারে এমন একটি প্র্যাক্টিক্যাল দুর্বলতা।
৩ · অ্যাভারেজ কেস — $\Theta(n \log n)$
এখন একই ফিক্সড-পিভট কুইক সর্ট যদি একটি র্যান্ডম ক্রমে সাজানো ইনপুটে চালানো হয়, তাহলে পিভট (শেষ এলিমেন্ট) হওয়ার সম্ভাবনা প্রতিটি র্যাংকের জন্য সমান — অর্থাৎ পিভট কখনো সবচেয়ে ছোট/বড় হতে পারে (খারাপ স্প্লিট), কিন্তু বেশিরভাগ সময় এটি মাঝামাঝি কোনো র্যাংকে পড়ে (মোটামুটি সুষম স্প্লিট)। যদি প্রতিটি পার্টিশন অন্তত একটি ধ্রুবক ভগ্নাংশ (যেমন ২৫%/৭৫% স্প্লিটও) অনুপাতে হয়, তাহলে রিকার্সনের গভীরতা তখনও $O(\log n)$ থাকে (L13-এর "অসম কিন্তু ধ্রুবক-ভগ্নাংশ" স্প্লিট আলোচনার মতো) — ফলাফল $\Theta(n \log n)$।
এই আর্গুমেন্টটি এখানে ইনটুইশন স্তরে রাখা হচ্ছে ইচ্ছাকৃতভাবে — একটি সম্পূর্ণ রিগোরাস প্রমাণ (এক্সপেক্টেড তুলনার সংখ্যা ইনডিকেটর ভ্যারিয়েবল ও লিনিয়ারিটি অফ এক্সপেক্টেশন দিয়ে গণনা) দাবি করে যে পিভট র্যান্ডমভাবে বাছাই করা হচ্ছে, ইনপুট নয় (কারণ ইনপুট সবসময় "র্যান্ডম" হবে এই ধরে নেওয়া নিরাপদ নয়!) — এটাই M12/L54 (র্যান্ডোমাইজড কুইকসর্ট)-এর বিষয়, যা এই ওয়ার্স্ট-কেস দুর্বলতার রিগোরাস সমাধান দেয়: প্রতিবার পিভট এলোমেলোভাবে বাছাই করে, যেকোনো নির্দিষ্ট ইনপুট (এমনকি সর্টেড) আর আলাদাভাবে খারাপ থাকে না, এবং প্রত্যাশিত (expected) রানটাইম সবসময় $\Theta(n \log n)$ প্রমাণ করা যায়।
৪ · কম্পিউটেশনাল ডেমো — একই কোড, দুই ইনপুট
নিচের কোডে একটি নেইভ ফিক্সড-পিভট (সবসময় শেষ এলিমেন্ট) কুইক সর্ট ইমপ্লিমেন্ট করা হয়েছে, যা তুলনার সংখ্যা গুনে রাখে। এটি একবার সর্টেড ইনপুটে (ওয়ার্স্ট কেস) এবং একবার র্যান্ডম ইনপুটে (অ্যাভারেজ কেস) চালানো হয়েছে — একই কোড, শুধু ইনপুট ভিন্ন।
import random
import math
import sys
sys.setrecursionlimit(3000)
def quicksort_counted(arr):
"""নেইভ ফিক্সড-পিভট (সবসময় শেষ এলিমেন্ট) কুইক সর্ট — তুলনার সংখ্যা গুনে রাখে।"""
a = list(arr)
counter = [0]
def partition(lo, hi):
pivot = a[hi] # নেইভ ফিক্সড পিভট
i = lo - 1
for j in range(lo, hi):
counter[0] += 1
if a[j] <= pivot:
i += 1
a[i], a[j] = a[j], a[i]
a[i + 1], a[hi] = a[hi], a[i + 1]
return i + 1
def sort(lo, hi):
if lo < hi:
p = partition(lo, hi)
sort(lo, p - 1)
sort(p + 1, hi)
sort(0, len(a) - 1)
return a, counter[0]
random.seed(42)
sizes = [50, 100, 200, 400]
results = []
for n in sizes:
sorted_input = list(range(n))
sorted_result, c_sorted = quicksort_counted(sorted_input)
assert sorted_result == sorted(sorted_input)
random_input = list(range(n))
random.shuffle(random_input)
random_result, c_random = quicksort_counted(random_input)
assert random_result == sorted(random_input)
results.append((n, c_sorted, c_random))
print(f"{'n':>5} | {'সর্টেড ইনপুট (worst)':>22} | {'র্যান্ডম ইনপুট (avg)':>22} | {'n(n-1)/2':>10} | {'n*log2(n)':>10}")
for n, c_sorted, c_random in results:
worst_predicted = n * (n - 1) // 2
avg_predicted = n * math.log2(n)
print(f"{n:>5} | {c_sorted:>22} | {c_random:>22} | {worst_predicted:>10} | {avg_predicted:>10.1f}")
print("\nn দ্বিগুণ হলে তুলনার সংখ্যা কত গুণ বাড়ে:")
for i in range(1, len(results)):
n_prev, cs_prev, cr_prev = results[i - 1]
n_cur, cs_cur, cr_cur = results[i]
print(f"n={n_prev}->{n_cur}: সর্টেড ইনপুটে {cs_cur / cs_prev:.2f}x, র্যান্ডম ইনপুটে {cr_cur / cr_prev:.2f}x বেড়েছে")
assert পাস করে নিশ্চিত করে যে দুই ইনপুটেই ফলাফল সঠিকভাবে সর্টেড। এরপর লক্ষ্য করুন
"n দ্বিগুণ হলে..." অংশে — সর্টেড ইনপুট-এর ঘরে অনুপাতটি $4$-এর দিকে অগ্রসর হয় (ঠিক
$\Theta(n^2)$-এর প্রত্যাশিত আচরণ, L01-এর নেইভ ডুপ্লিকেট-চেক ডেমোর মতোই), কিন্তু র্যান্ডম
ইনপুট-এর ঘরে অনুপাতটি $2$-এর কাছাকাছি থেকে যায় (সামান্য বেশি, কারণ $n \log n$-এ $\log$ ফ্যাক্টরও
সামান্য বাড়ে) — $\Theta(n^2)$ ও $\Theta(n \log n)$-এর মধ্যে এই স্পষ্ট বৈসাদৃশ্যই এই পাঠের মূল বক্তব্য।
একই অ্যালগরিদম, একই কোড — কিন্তু ইনপুটের উপর নির্ভর করে জটিলতার ক্লাস সম্পূর্ণ পাল্টে যেতে পারে, যদি পিভট বাছাইয়ের নিয়ম ডিটারমিনিস্টিক (ফিক্সড) হয়। এটিই কুইক সর্টকে মার্জ সর্ট থেকে আলাদা করে (L15 — মার্জ সর্টের জটিলতা ইনপুট নির্বিশেষে সবসময় একই)। এই দুর্বলতা দূর করতে দুটি সাধারণ কৌশল আছে — র্যান্ডম পিভট (M12/L54-এ রিগোরাসভাবে দেখানো হবে) অথবা মিডিয়ান-অফ-থ্রি হিউরিস্টিক (তিনটি এলিমেন্টের মিডিয়ান পিভট হিসেবে বাছাই করা) — উভয়ই সহজ সাজানো ইনপুটকে আর বিশেষভাবে খারাপ থাকতে দেয় না।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ "শেষ এলিমেন্ট পিভট" নিয়মের জন্য নির্দিষ্টভাবে সর্টেড ইনপুট কেন ওয়ার্স্ট কেস — অন্য কোনো এলোমেলো ইনপুট কেন নয়?
ওয়ার্স্ট কেস তৈরি হয় যখন পিভট বারবার প্রতিটি সাব-অ্যারের চরম মান (সর্বোচ্চ বা সর্বনিম্ন) হয়, কারণ তখনই পার্টিশন সবচেয়ে অসামঞ্জস্যপূর্ণ (এক ভাগ খালি) হয়। ঊর্ধ্বক্রমে সর্টেড ইনপুটে শেষ এলিমেন্ট সবসময়ই সেই মুহূর্তের সাব-অ্যারের সর্বোচ্চ মান — তাই এটি একটি সুনির্দিষ্ট, সহজে-ঘটতে-পারা প্যাটার্ন যা প্রতিবারই সবচেয়ে খারাপ স্প্লিট নিশ্চিত করে। এলোমেলো ইনপুটে পিভট মাঝেমধ্যে চরম মান হতে পারে, কিন্তু সবসময় নয় — তাই এটি নিশ্চিতভাবে ওয়ার্স্ট কেস তৈরি করে না।
প্র ০২ যদি পিভট বাছাইয়ের নিয়ম "সবসময় মাঝের এলিমেন্ট" হতো, তাহলে কি সর্টেড ইনপুট তখনও ওয়ার্স্ট কেস তৈরি করত?
না — মাঝের এলিমেন্ট পিভট হলে একটি সর্টেড অ্যারেতে প্রতিবার প্রায় $n/2, n/2$ সুষম স্প্লিট হবে, যা আসলে $\Theta(n \log n)$ (ভালো কেস) দেবে। কিন্তু এর মানে এই নয় যে মাঝের-এলিমেন্ট পিভট নিরাপদ — এটির জন্য একটি ভিন্ন আডভার্সারিয়াল ইনপুট ঠিকই বানানো যায় (এমন একটি অ্যারে যেখানে মাঝের এলিমেন্ট বারবার চরম মান হয়ে যায়)। মূল কথা হলো: যেকোনো ফিক্সড, ডিটারমিনিস্টিক পিভট-বাছাই নিয়মের জন্যই কোনো-না-কোনো আডভার্সারিয়াল ইনপুট বানানো সম্ভব — শুধু কোন নিয়মে কোন নির্দিষ্ট ইনপুট সেই ভূমিকা পালন করে তা পাল্টায়। এই সাধারণ দুর্বলতা এড়াতেই র্যান্ডমাইজেশন (L54) দরকার হয়, যেখানে পিভট নিজেই ইনপুট-নির্বিশেষ এলোমেলো।
প্র ০৩ এই পাঠে অ্যাভারেজ-কেস $\Theta(n \log n)$ দাবিটি সম্পূর্ণ গাণিতিকভাবে প্রমাণ না করে শুধু ইনটুইশন দিয়ে ব্যাখ্যা করা হলো কেন?
একটি সম্পূর্ণ রিগোরাস অ্যাভারেজ-কেস প্রমাণের জন্য ইনডিকেটর র্যান্ডম ভ্যারিয়েবল, লিনিয়ারিটি অফ এক্সপেক্টেশন, এবং "কোন দুটি এলিমেন্ট তুলনা হবে তার সম্ভাবনা" সংক্রান্ত একটি সূক্ষ্ম গণনা দরকার — যা র্যান্ডমাইজড অ্যালগরিদম অ্যানালাইসিসের একটি নির্দিষ্ট টুলকিট (M12-এ শেখানো হবে)। এই পাঠে D&C মডিউলের অংশ হিসেবে শুধু ফলাফল ও তার পেছনের সরল যুক্তি দেখানো হয়েছে; সম্পূর্ণ প্রমাণ, র্যান্ডম পিভট সহ, নির্দিষ্টভাবে L54-এর জন্য রাখা হয়েছে, যেখানে এটি স্বতন্ত্রভাবে প্রাপ্য গুরুত্ব পাবে।
অনুশীলন
-
চিন্তা করুন: সর্টেড ইনপুটে $n = 800$-এ তুলনার সংখ্যা কত হবে বলে আপনার ধারণা? (হিন্ট:
$n(n-1)/2$ সূত্র প্রয়োগ করুন।)
$\frac{800 \times 799}{2} = 319{,}600$টি তুলনা হবে — $n=400$-এর মানের প্রায় $4$ গুণ, যা $\Theta(n^2)$ প্যাটার্নের সাথে মেলে।
-
পরীক্ষা করুন: উপরের কোড সেলে
sizesলিস্টে800যোগ করে (sizes = [50, 100, 200, 400, 800]) Run চেপে আপনার অনুমান যাচাই করুন — সর্টেড ও র্যান্ডম দুই কলামের অনুপাত কীভাবে ভিন্নভাবে বাড়ে তা লক্ষ্য করুন।নতুন লাইনে
n=800-এর সর্টেড-ইনপুট তুলনা প্রায়319600(গণনার সাথে মিলে যাবে) দেখাবে, এবং "n দ্বিগুণ হলে..." অংশে সর্টেড কলামের অনুপাত $4$-এর আরও কাছাকাছি পৌঁছাবে, অথচ র্যান্ডম কলামের অনুপাত $2$-এর কাছাকাছিই থেকে যাবে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাসিম্পটোটিক অ্যানালাইসিস, রিকারেন্স, ডিভাইড অ্যান্ড কনকার, গ্রিডি, DP, গ্রাফ অ্যালগরিদম, ব্যাকট্র্যাকিং, স্ট্রিং অ্যালগরিদম, অ্যামর্টাইজড অ্যানালাইসিস, NP-কমপ্লিটনেস ও অ্যাপ্রক্সিমেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
- মার্জ সর্ট ও এর অ্যানালাইসিস আগের পাঠ একটি D&C অ্যালগরিদম যার জটিলতা ইনপুট নির্বিশেষে সবসময় $\Theta(n \log n)$ — এই পাঠের কুইক সর্টের সাথে বৈসাদৃশ্য তৈরি করে।
- Data Structures & Algorithms কোর্স সহোদর কোর্স কুইক সর্টের ইমপ্লিমেন্টেশন ওয়াকথ্রু সেই কোর্সেই আছে।