লিনিয়ার-টাইম সর্টিং (Linear-Time Sorting)
কল্পনা করুন আপনি এক বান্ডিল চিঠি পোস্ট অফিসের বক্স বা ড্রয়ারে সাজাচ্ছেন। আপনাকে একটি চিঠির সাথে অন্য চিঠির তুলনা করার দরকার নেই — আপনি শুধু প্রতিটি চিঠির নম্বর দেখে তাকে নির্দিষ্ট ড্রয়ারে রেখে দিচ্ছেন। এটিই লিনিয়ার-টাইম সর্টিংয়ের মূল ধারণা: উপাদানগুলোর একে অপরের সাথে তুলনা করার বদলে আমরা তাদের সম্পর্কে থাকা বাড়তি তথ্য (যেমন রেঞ্জ বা সীমা) কাজে লাগাই।
সাধারণ মেথডগুলো (যেমন কুইক সর্ট বা মার্জ সর্ট) \(O(n \log n)\)-এর নিচে নামতে পারে না কারণ তাদের প্রতিটি তুলনা মাত্র ১ বিট তথ্য দেয়। লিনিয়ার সর্টগুলো এই সীমা অতিক্রম করতে পারে কারণ তারা জানে ইনপুটগুলোর মান ঠিক কোন সীমা বা ধরনের মধ্যে থাকবে।
কাউন্টিং সর্ট (Counting Sort) — সহজ ধারণা
যদি আপনার কাছে ০ থেকে k সীমার মধ্যে কিছু পূর্ণসংখ্যা থাকে, তবে কাউন্টিং সর্ট এভাবে কাজ করে: (১) প্রতিটি সংখ্যা কতবার আছে তা গুনে ফেলুন। (২) এই সংখ্যাগুলোকে নিয়ে শুরু হওয়ার পজিশন বা ইনডেক্স ঠিক করুন (যা প্রিফিক্স সাম এর মাধ্যমে করা হয়)। (৩) ইনপুট থেকে প্রতিটি সংখ্যা নিয়ে সরাসরী তার জন্য নির্ধারিত জায়গায় বসিয়ে দিন।
সময়: \(O(n + k)\)। মেমরি: \(O(n + k)\)। এটি একটি স্টেবল (Stable) সর্ট যদি আপনি আউটপুট বসানোর সময় পিছন থেকে কাজ করেন (যা রেডিক্স সর্টের জন্য খুব জরুরি)। যদি k-এর মান n-এর কাছাকাছি হয়, তবে এটি \(O(n)\) বা লিনিয়ার সময়ে কাজ শেষ করে। কিন্তু যদি k অনেক বড় হয় (যেমন ১ থেকে ১ কোটির রেঞ্জ), তবে এটি অনেক বেশি মেমরি অপচয় করবে।
রেডিক্স সর্ট (Radix Sort) — অঙ্ক ধরে সর্ট করা
রেডিক্স সর্ট অনেক বড় ইনটিজার হ্যান্ডেল করতে পারে প্রতিটি অঙ্কের (Digit) ওপর সর্ট করার মাধ্যমে। এটি মূলত একক ঘর থেকে শুরু করে বাম দিকে (LSD) এগোয় এবং প্রতিটি ধাপে একটি স্টেবল সর্ট (যেমন কাউন্টিং সর্ট) ব্যবহার করে। সবগুলো অঙ্ক শেষ হওয়ার পর পুরো অ্যারেটি সর্ট হয়ে যায়।
কেন একক ঘর থেকে শুরু করা হয়? কারণ 'স্ট্যাবিলিটি' পূর্ববর্তী ধাপের পরিশ্রমকে ধরে রাখে। একক ঘর সর্ট করার পর যখন দশকের ঘর সর্ট করা হয়, তখন একই দশকওয়ালা সংখ্যাগুলো তাদের এককের ঘরের ক্রম ঠিক থাকে — এভাবে প্রতিটি ধাপ শেষে নির্ভুলভাবে সর্টিং এগিয়ে চলে।
আপনি যদি ৩২-বিটের ইনটিজার ব্যবহার করেন ২৫৬ বেস (Base) ধরে, তবে মাত্র ৪টি ধাপে আপনি \(O(n)\) সময়ে সর্ট করতে পারবেন, যা বড় ডেটার জন্য \(O(n \log n)\) এর চেয়ে দ্রুত।
বাকেট সর্ট (Bucket Sort) — ভাগ করে বণ্টন করা
বাকেট সর্ট পুরো রেঞ্জকে সমান ভাগে কয়েকটি 'বাকেট' বা বালতিতে ভাগ করে দেয়। প্রতিটি সংখ্যাকে তার নির্দিষ্ট বালতিতে ফেলে দেওয়া হয়, এরপর প্রতিটি বালতিকে ছোট কোনো সর্টিং মেথড (যেমন ইনসারশন সর্ট) দিয়ে সর্ট করা হয়। সবশেষে বালতিগুলোকে ক্রমানুসারে সাজালেই পূর্ণ সর্ট পাওয়া যায়।
শর্ত: যদি ইনপুটগুলো ইউনিফর্মলি ডিস্ট্রিবিউটেড (Uniformly distributed) বা সুষমভাবে ছড়ানো থাকে, তবে প্রতিটি বালতিতে গড়ে ১-২টি উপাদান থাকে, ফলে পুরো কাজ \(O(n)\) সময়ে শেষ হয়। তবে যদি সব উপাদান কোনো একটি নির্দিষ্ট বালতিতে জমা হয়, তবে এটি \(O(n^2)\) জটিলতাও তৈরি করতে পারে।