এক সময় ছিল যখন "Big-O নোটেশন সেটি কী?" শুনলে আমার চেয়ে বেশি ভয় পেতাম কোনো জিনিস। স্কুলে এই টপিক পড়েছিলাম, কিন্তু যেহেতু এর ম্যাথের সাথে সম্পর্ক ছিল (যা কখনো আমার সুত렀다 বিষয় নয়), আমি এটিকে ব্ল্যাক আউট করিয়ে ফেলেছিলাম।
তবে, যেনে মেনে আমার ক্যারিয়ার এগিয়ে গেল, আমি নিজেকে পেলাম:
- পারফরম্যান্স চার্টে তাকিয়ে থাকতে
- স্লো কোয়েরি ডিবাগ করতে চেষ্টা করতে
- জিজ্ঞেস করা হচ্ছে কি আমি কনসিডার করেছি যে আমার কোড ইনক্রিজড লোড দিয়ে কীভাবে টিকবে
যখন আমি সিদ্ধান্ত নিলাম Big-O শিখতে ফিরে যাব (গেট ইট?), আমি এর হাই-লেভেল সিম্প্লিসিটি দেখে বিস্মিত হলাম। আমি যা শিখলাম সেটি এই আর্টিকেলে শেয়ার করছি যাতে আপনি, আমার ফেলো ইঞ্জিনিয়াররা, শুধু ইন্টারভিউতে ফ্লাইং কলার্স দিয়ে পাস করুন না, বরং পারফরম্যান্ট, স্কেলেবল সিস্টেমও বানাতে পারেন।
আমি প্রতিশ্রুতি দিচ্ছি, Big-O তত ভয়ঙ্কর নয় যেন মনে হয়। একবার আপনি এটকে ধরে ফেললে, আপনি কোনো অ্যালগরিদম দেখে সহজেই এর ইফিশিয়েন্সি বুঝতে পারবেন, প্রোফাইলিং টুল চালানো ছাড়াই!
Big-O নোটেশন কী?
Big-O নোটেশন হলো একটি ফ্যান্সি উপায় বলতে: "ওরে, এই অ্যালগরिदমের ওৰ্স্ট কেস পারফরম্যান্স কী?" আপনি হয়তো একটি ফাংশনকে O(n) বা O(1) হিসেবে ডেসক্রাইব করা দেখেছেন। এর মানে:
- O(n) - ওৰ্স্ট-কেস রান টাইম ইনপুট সাইজ (n) বাড়ার সাথে সাথে লিনিয়ারলি বাড়ে
- O(1) - যেকোনো সাইজ ইনপুটের জন্য ওৰ্স্ট-কেস রান টাইম কনস্ট্যান্ট
...এবং এটি বুঝতে হলে আমাদের অ্যাসিম্পটোট (asymptotes) সম্পর্কে শিখতে হবে
অ্যাসিম্পটোট?
হাই স্কুল অ্যালজেব্রাতে ফিরে যাই, টেক্সটবুক থেকে ধুলো ঝাড়িয়ে লিমিটস এবং অ্যাসিম্পটোট চ্যাপ্টার খুলি।
- লিমিট অ্যানালিসিস: একটি ফাংশন কীভাবে বিহেভ করে সেটি দেখা যাতে এটি কোনো ভ্যালুতে আপ্রোচ করে
- অ্যাসিম্পটোটিক অ্যানালিসিস: f(x) আনন্ততফের দিকে আপ্রোচ করলে কী হয় সেটি দেখা
উদাহরণস্বরূপ, কল্পনা করুন f(x) = x² + 4x ফাংশনটি প্লট করি।
আমরা নিচের অ্যানালিসিসগুলো করতে পারি: - লিমিট অ্যানালিসিস: x বাড়লে f(x) আনন্ততফের দিকে আপ্রোচ করে, তাই আমরা বলতে পারি f(x) = x² + 4x-এর লিমিট x আনন্ততফের দিকে আপ্রোচ করলে আনন্ততফ। - অ্যাসিম্পটোটিক অ্যানালিসিস: x খুব বড় হলে, 4x টার্ম x² টার্মের তুলনায় নগণ্য হয়ে যায়। তাই আমরা বলতে পারি f(x) = x² + 4x আনন্ততফের দিকে আপ্রোচ করলে x-এর ভ্যালুর জন্য প্রায় f(x) = x²-এর সমতুল্য হয়।
কীভাবে একটি ফাংশনের অংশ "নগণ্য" হয় সেটি বুঝতে, ओरিজিনাল ফাংশনে বিভিন্ন সংখ্যা প্লাগ করুন। উদাহরণস্বরূপ, x = 1 হলে, ফাংশন রিটার্ন করে 1 + 4 (যা ৫)। কিন্তু x = ২,০০০ হলে, ফাংশন রিটার্ন করে ৪,০০০,০০০ + ৮,০০০ (যা ৪,০০৮,০০০) — x² টার্ম টোটালের চেয়ে অনেক বেশি কন্ট্রিবিউট করছে।
Big-O নোটেশন একটি উপায় হলো বর্ণনা করার জন্য কীভাবে একটি অ্যালগরিদমের রান টাইম পরিবর্তন হয় ইনপুট সাইজ পরিবর্তন হওয়ার সাথে সাথে।
একটি অ্যালগরিদমের রান টাইম কী নির্ধারণ করে?
যদি আমি আপনাকে জিজ্ঞেস করি একটি সুইয়েয়ের স্ট্যাক থেকে সুই নিডেল খুঁজে বের করতে কতক্ষণ লাগবে, আমি কল্পনা করি আপনি জানতে চাবেন স্টাকে কত সুইয়ে আছে। যদি আমি বলি "১০ টুকরো" তবে আমি মনে করি আপনি খুব কনফিডেন্ট হবেন আপনি এক বা দুটি মিনিটের মধ্যে সুই খুঁজে পাবেন, কিন্তু যদি আমি বলি "১,০০০ টুকরো", তবে আপনি সেটা শুনে সেজয়ादा এক্সাইটেড হবেন না।
এখানে আরেকটি তথ্য জানা দরকার। প্রতিটি সুইয়ে যোগ হলে সার্চ করতে কত বেশি সময় লাগে? এবং সুইয়ের পরিমাণ আনন্ততফের দিকে যাওয়ার সাথে সাথে কী হয়?
এটি উপরের অ্যাসিম্পটোটিক অ্যানালিসিস উদাহরণের जैसा। একটু আর উদাহরণ দেখি শুধু নিশ্চিত হতে। f(x) = 5x² + 100x + 50 ফাংশনটি বিবেচনা করুন। আমরা এই ফাংশনের দুটি অংশ আলাদা করে প্লট করতে পারি:
পূর্বের উদাহরণের মতো, 5x² টার্ম आखিরে 100x + 50 টার্মের চেয়ে বড় হয়ে যায়, তাই আমরা সেগুলো ড্রপ করে বলতে পারি f(x) = 5x² + 100x + 50-এর রানটাইম x² হিসেবে বাড়ে।
নিশ্চিতভাবেই বলা দরকার, রান টাইমে প্রভাব ফেলে অন্যান্য ফ্যাক্টরও আছে, যেমন প্রোগ্রাম চালানো কম্পিউটারের স্পীড, এবং ব্যবহৃত ভাষা।
লিনিয়ার সার্চের Big-O বের করা
লিনিয়ার সার্চ অ্যালগরিদমের Big-O অ্যানালিসিস করি। লিনিয়ার সার্চ ডেটাসেটের শুরু থেকে শুরু করে টার্গেট এলিমেন্ট পাওয়া পর্যন্ত ট্রাভার্স করে।
Ruby-এ একটি ইমপ্লিমেন্টেশন:
def find_number_via_linear_search(array, target)
counter = 0
# দেওয়া অ্যারেটে ইন্ডেক্স ০ থেকে শুরু করে এন্ড পর্যন্ত ইটারেট
while counter < array.length
if array[counter] == target
# টার্গেট এলিমেন্ট পাওয়া গেলে এক্সিট
return "linear search took: #{counter} iterations"
else
counter += 1
end
end
return "#{target} not found"
end
এই মেথডকে এভাবে স্পিন দিতে পারি:
# ৫০ ইন্টিজার সহ একটি অ্যারে বানাই
# এবং অর্ডার রি-আরেঞ্জ করি
# যাতে জিনিসগুলো ইন্টারেস্টিং হয়
array = [*1..50].shuffle
find_number_via_linear_search(array, 24)
এটি কয়েকবার রান করালাম এবং নিচের রেজাল্ট পেলাম:
=> "linear search took: 10 iterations"
=> "linear search took: 11 iterations"
=> "linear search took: 26 iterations"
একটি ফাংশনের Big-O নোটেশন অ্যানালাইজ করার সময়, আমরা ওৰ্স্ট-কেস সিনেরিও (অথবা আপার অ্যাসিম্পটোটিক বাউন্ড) কে কেয়ার করি।
ইনটুইটিভলি চিন্তা করলে, সর্বনিম্ন ইটারেশন হবে ১। এটা ঘটে যখন টার্গেট এলিমেন্ট অ্যারেতে পজিশন ০-তে থাকে। সর্বোচ্চ ইটারেশন (বা ওৰ্স্ট-কেস সিনেরিও) হবে ৫০। এটি ঘটে যখন টার্গেট এলিমেন্ট অ্যারেতে পাওয়া যায় না।
আমাদের অ্যারেতে ১০০ এলিমেন্ট থাকলে, ওৰ্স্ট-কেস হবে ১০০ ইটারেশন। ২০০ এলিমেন্ট? ২০০ ইটারেশন। সুতরাং, লিনিয়ার সার্চের Big-O নোটেশন সিম্পলি O(n), যেখানে n হলো এলিমেন্টের সংখ্যা।
বাইনারি সার্চ দিয়ে একটি আরও জটিল উদাহরণ!
বাইনারি সার্চ বিবেচনা করি। এখানে কীভাবে একটি প্রি-সোর্টেড অ্যারেতে বাইনারি সার্চ করা যায়:
১. মিডেল এলিমেন্ট নিন
২. যদি element == target হয় তবে কাজ শেষ
৩. যদি element > target হয় তবে অ্যারের টপ হাফ ডিসকার্ড করুন
৪. যদি element < target হয় তবে বটম হাফ ডিসকার্ড করুন
৫. বাকি অ্যারেতে স্টেপ ১ থেকে পুনরায় শুরু করুন
নোট: আপনি যদি Rubyist হন, তবে বিল্ট-ইন b-search মেথড আছে যা এই অ্যালগরিদম ইমপ্লিমেন্ট করে!
উদাহরণস্বরূপ, ধরুন আপনার একটি ডিকশনারি আছে এবং আপনি "pineapple" শব্দটি খুঁজছেন। আমরা ডিকশনারির মিডেল পেজে যাব। যদি সেটিতে "pineapple" থাকে, তবে কাজ শেষ!
আমার অনুমান, ডিকশনারির মিডেল "p"-তে হবেনা, তাই হয়তো "llama" শব্দটি পাব। "L" "P"-এর আগে আসে তাই আমরা ডিকশনারির পুরো লোয়ার হাফ ডিসকার্ড করব। পরবর্তীতে, বাকি অংশে প্রসেস রিপিট করব।
লিনিয়ার সার্চের মত, বাইনারি সার্চের বেস্ট-কেস রান টাইম এক ইটারেশন। কিন্তু ওৰ্স্ট কেস কী? ১৬ এলিমেন্ট বিশিষ্ট একটি উদাহরণ অ্যারে — কল্পনা করুন ২৩ নম্বরটি খুঁজতে বাইনারি সার্চ ব্যবহার করছি:
[2, 3, 15, 18, 22, 23, 24, 50, 65, 66, 88, 90, 92, 95, 100, 200]
প্রথম স্টেপ হবে ইন্ডেক্স ৭-এ থাকা নম্বরটি দেখা, যা ৫০। যেহেতু ৫০, ২৩-এর চেয়ে বড়, আমরা ডান পাশের সব কিছু ডিসকার্ড করি। এখন আমাদের অ্যারে দেখাচ্ছে:
[2, 3, 15, 18, 22, 23, 24, 50]
মিডেল এলিমেন্ট এখন ১৮, যা ২৩-এর চেয়ে ছোট, তাই এই বার লোয়ার হাফ ডিসকার্ড করি।
[22, 23, 24, 50]
যা হয়ে যায়
[22, 23]
যা ফাইনালি হয়ে যায়
[23]
মোটে, আমাদের অ্যারেটি ৪ বার হাফ করতে হয়েছিল টার্গেট নম্বরটি পাওয়ার জন্য, যার লেন্থ ছিল ১৬।
এটিকে জেনারালাইজ করলে, আমরা বলতে পারি বাইনারি সার্চের ওৰ্স্ট কেস সিনেরিও হচ্ছে অ্যারেটি হাফ করার সর্বোচ্চ সংখ্যা।
গণিতে, লগারিদম ব্যবহার করে এই প্রশ্নের জবাব দেওয়া হয়: "এই সংখ্যা কতবার গুণ করলে সেটা সংখ্যা मिलবে?" আমরা কীভাবে লগারিদম আপ্লাই করি আমাদের সমস্যাটে:
সুতরাং আমরা বলতে পারি বাইনারি সার্চের Big-O, বা ওৰ্স্ট-কেস রান টাইম হলো log(base 2) n।
র্যাপিং আপ
Big-O নোটেশন হলো একটি ফ্যান্সি উপায় বলতে: "ওরে, এইটার ওৰ্স্ট কেস কী?" কম্পিউটার সাইন্স বাদ দিয়ে, একটি রিয়েল-ওয়ার্ল্ড উদাহরণ হতে পারে আপনার প্লাম্বারকে জিজ্ঞেস করলে টুটি খরাব রিপেয়ার করতে কত খরচ হবে। সে হয়তো جواب দেবে, "ওরে, আমি গ্যারান্টি দিতে পারি ২,০০০ ডলার থেকে বেশি হবে না।" এটি একটি আপার বাউন্ড, যদিও আপনার জন্য খুব হেল্পফুল নয়।
এই কারণে অন্যান্য Big-O নোটেশনগুলো প্রায়শই ব্যবহার করা হয়। উদাহরণস্বরূপ, Big-Theta লোয়ার বাউন্ড এবং আপার বাউন্ড দুটোই কেয়ার করে। এই কেসে প্লাম্বার বলে, "ওরে, ১,০০০ ডলার থেকে কম হবে না কিন্তু ২,০০০ ডলার থেকে বেশিও হবে না।" এটি অনেক বেশি ইউজফুল।
পড়ার জন্য ধন্যবাদ, এবং আশা করি এই পোস্ট Big-O নোটেশনকে কমপক্ষে একটু কম ভয়ঙ্কর করে তুলতে পেরেছে!