সরাসরি পর্বে যান
পর্ব ৫৫ · ৫৫-এর মধ্যে · মডিউল ৮

Python কোডিং ইন্টারভিউ — সমস্যা থেকে সমাধান

Python Coding Interviews — From Problem to Solution

Taught in Bangla, this lesson prepares you for Python coding interviews: a seven-step problem-solving method, 17 problems with Pythonic solutions on arrays, strings, hashing, stacks, linked lists, trees, heaps, binary search and DP, Python design problems like an LRU cache, a decorator, a transaction context manager and a generator pipeline, deep questions on dicts and the GIL, and mock interviews with AI.

এই পর্বে যা শিখবেন

  • Python কোডিং ইন্টারভিউতে ইন্টারভিউয়ার আসলে কী কী দেখেন
  • যেকোনো নতুন সমস্যায় প্রয়োগ করা যায় এমন সাত ধাপের পদ্ধতি, আর জোরে ভেবে সমাধান করা
  • অ্যারে, স্ট্রিং, হ্যাশিং, স্ট্যাক, কিউ, লিংকড লিস্ট, ট্রি, হিপ, বাইনারি সার্চ ও DP-র ক্লাসিক সমস্যা — built-in আর collections দিয়ে
  • LRU cache, @retry decorator, transaction context manager আর generator pipeline-এর মতো Python ডিজাইন সমস্যা
  • ক্যাপস্টোন প্রজেক্ট সৎভাবে ব্যাখ্যা করা, আর AI দিয়ে নিজের সমাধান যাচাই করা

১ · Python কোডিং ইন্টারভিউ কী যাচাই করে

পর্ব ৫৪-এ দেখেছেন লিখিত ও মৌখিক পরীক্ষায় কীভাবে উত্তর গোছাতে হয়। সফটওয়্যার কোম্পানির technical interview একটু আলাদা। এখানে শুধু “উত্তর জানেন কি না” দেখা হয় না। দেখা হয়, অচেনা একটি সমস্যা পেলে আপনি কীভাবে ভাবেন, কীভাবে কথা বলেন, আর কতটা পরিষ্কার ও নির্ভরযোগ্য কোড লেখেন।

Python এখন ইন্টারভিউর সবচেয়ে জনপ্রিয় ভাষাগুলোর একটি। কারণ সহজ: কম লাইনে ভাবনাটা কোডে আনা যায়, আর ইন্টারভিউয়ার আপনার যুক্তিতে মন দিতে পারেন। কিন্তু এর একটি উল্টো দিকও আছে। Python-এ sorted(), Counter বা heapq এক লাইনে অনেক কাজ করে দেয় — তাই ইন্টারভিউয়ার প্রায়ই জিজ্ঞেস করেন, “এই এক লাইনের খরচ কত?” built-in ব্যবহার করা ভালো, কিন্তু তার ভেতরের জটিলতা জানা আরও জরুরি।

দেশি স্টার্টআপ, সফটওয়্যার ফার্ম, ডেটা ও AI টিম, ব্যাংকের আইটি বিভাগ বা বহুজাতিক কোম্পানি — প্রতিষ্ঠানভেদে ধাপের নাম ও সংখ্যা আলাদা। তবে কাঠামোটা সাধারণত এমন:

ধাপকী হয়প্রস্তুতি
Phone / HR screen১৫–৩০ মিনিটের কথা: পড়াশোনা, প্রজেক্ট, আগ্রহ। কখনো একটি সহজ টেকনিক্যাল প্রশ্ন।নিজের প্রজেক্ট দুই মিনিটে গুছিয়ে বলা
Online assessmentসময় বাঁধা অনলাইন পরীক্ষা: ২–৪টি কোডিং সমস্যা, কখনো Python-এর MCQ. লুকানো test case দিয়ে যন্ত্র নম্বর দেয়।Online judge-এ সময় ধরে সমাধান (পর্ব ৪৫)
Live coding৪৫–৬০ মিনিট, একজন ইন্টারভিউয়ার, শেয়ার করা editor বা হোয়াইটবোর্ড। ১–২টি সমস্যা আর আলোচনা।জোরে ভেবে সমাধান — এই পাতার মূল অনুশীলন
Python গভীরতাভাষার প্রশ্ন: mutability, generator, decorator, context manager, GIL, dataclass. কখনো ছোট একটি class ডিজাইন।অংশ ৬–৭, আর পর্ব ৫২–৫৪
Behaviouralদলে কাজ, মতবিরোধ, ব্যর্থতা থেকে শেখা, নিজের প্রজেক্টের সিদ্ধান্ত।STAR কাঠামোয় ৪–৫টি সত্যি গল্প (অংশ ৮)

ইন্টারভিউয়ারের মূল্যায়ন-পত্রে কী থাকে

অনেকে ভাবেন, “সঠিক কোড লিখলেই পাস।” বাস্তবে মূল্যায়নে সাধারণত কয়েকটি আলাদা ঘর থাকে। ঠিক কোড লিখেও কেউ পিছিয়ে পড়েন, আবার কোড শেষ না করেও কেউ ভালো করেন — যদি বাকি ঘরগুলো শক্ত হয়।

সমস্যা বোঝা ও যোগাযোগ

লেখার আগে প্রশ্ন করে অস্পষ্টতা দূর করেছেন কি? ভাবনা জোরে বলেছেন কি? ইন্টারভিউয়ার আপনার মাথার ভেতর দেখতে পান না।

সঠিকতা, জটিলতা, edge case

সব ইনপুটে ঠিক কি? খালি list, এক উপাদান, ডুপ্লিকেট, বাংলা বা Unicode ইনপুট ভেবেছেন কি? সময় ও মেমরি Big-O-তে নিজে বলেছেন কি?

কোডের মান

পরিষ্কার নাম, ছোট ফাংশন, দরকারি জায়গায় type hint, docstring, ঠিক data structure — প্রোডাকশনের মতো কোড, কনটেস্টের শর্টকাট নয়।

Python-এ সাবলীলতা

built-in ফাংশন, comprehension, collections, generator, OOP, পরিষ্কার ফাংশন — এগুলো স্বাভাবিকভাবে, অর্থাৎ Pythonic ভাবে ব্যবহার করেন কি?

Python-নির্দিষ্ট প্রশ্ন দেখতে কেমন

সাধারণ অ্যালগরিদমের প্রশ্ন যেকোনো ভাষায় করা যায়। কিন্তু Python পদের ইন্টারভিউতে প্রায়ই এমন প্রশ্নও আসে, যেখানে ভাষাটাই আসল বিষয়। এ ধরনের প্রশ্ন প্রায়ই আসে:

  • “একটি LRU cache লিখুন” — get ও put দুটোই O(1); OrderedDict চেনেন কি না, আর functools.lru_cache ভেতরে কী করে (সমস্যা ১৪; পর্ব ২৭, ৩৮)।
  • “একটি decorator লিখুন” — যেমন @retry বা @timer; closure আর functools.wraps বোঝেন কি না (সমস্যা ১৫; পর্ব ২০, ৩৫)।
  • “একটি context manager লিখুন” — সময় মাপা বা transaction-এর জন্য; __enter__/__exit__ আর contextlib (সমস্যা ১৬; পর্ব ৩৬)।
  • “একটি iterator class লিখুন” আর “একটি dataclass মডেল বানান” — __iter__/__next__, @dataclass(frozen=True), যাচাই (সমস্যা ১৭; পর্ব ৩২, ৩৪)।
  • “GIL কী, আর কেন গুরুত্বপূর্ণ?” — thread, process আর free-threaded Python-এর পার্থক্য (অংশ ৭, প্র ০৬; পর্ব ৪১)।
মূল ধারণা

ইন্টারভিউ একটি কাজের নমুনা। ইন্টারভিউয়ার মনে মনে ভাবেন: “এই মানুষটির সাথে একটি কঠিন বাগ নিয়ে এক ঘণ্টা বসলে কেমন লাগবে?” তাই চুপচাপ নিখুঁত কোডের চেয়ে কথা বলে ধাপে ধাপে এগোনো ভালো কোড প্রায়ই বেশি মূল্য পায়।

এই পাতার ১৭টি সমস্যা

বিষয়সমস্যারিভিশনের পর্ব
পদ্ধতি (অংশ ২)১. Two sum১৬
অ্যারে, স্ট্রিং, হ্যাশিং (অংশ ৩)২. শব্দের ক্রম উল্টানো · ৩. Anagram ও group anagrams · ৪. পুনরাবৃত্তিহীন দীর্ঘতম substring · ৫. Palindrome যাচাই · ৬. প্রথম অপুনরাবৃত্ত অক্ষর৯, ১৪, ১৬, ২৭
স্ট্যাক, কিউ, লিংকড লিস্ট (অংশ ৪)৭. বন্ধনী মেলানো · ৮. Min-stack ও deque দিয়ে queue · ৯. লিংকড লিস্ট উল্টানো১৪, ২৭, ৩২
ট্রি, হিপ, সার্চ, DP (অংশ ৫)১০. BST ও level-order · ১১. k-তম বৃহত্তম · ১২. ঘোরানো list-এ সার্চ · ১৩. সিঁড়ি ও কয়েন চেঞ্জ৩৪, ৩৮, ৪৫
Python ডিজাইন (অংশ ৬)১৪. LRU cache · ১৫. @retry decorator · ১৬. Transaction context manager · ১৭. Generator pipeline২০, ৩৪, ৩৫, ৩৬

২ · সমস্যা থেকে সমাধান — সাত ধাপের পদ্ধতি

ইন্টারভিউর চাপে মাথা ফাঁকা হয়ে যাওয়া খুব স্বাভাবিক। এর সবচেয়ে ভালো ওষুধ একটি বাঁধা পদ্ধতি — যাতে “এখন কী করব” ভাবতে না হয়, শুধু পরের ধাপে যেতে হয়। ক্রিকেটে ব্যাটসম্যান যেমন প্রতিটি বলের আগে একই রুটিন মানেন — গার্ড নেওয়া, ফিল্ডিং দেখা, তারপর স্টান্স — এটাও তেমন।

বুঝুন → উদাহরণ → brute force → উন্নত পদ্ধতি → কোড → পরীক্ষা → জটিলতা ও ফলো-আপ

পুরো সময় জুড়ে একটি নিয়ম: জোরে ভাবুন (think aloud). চুপ করে দুই মিনিট ভাবার চেয়ে অর্ধেক-গোছানো ভাবনা বলাও ভালো — ইন্টারভিউয়ার পথ দেখে সাহায্য করতে পারেন। এবার পদ্ধতিটি একটি বিখ্যাত সমস্যায় প্রয়োগ করি — সমস্যা ১ · Two sum (সহজ): “একটি list[int] আর একটি target দেওয়া আছে। এমন দুটি ভিন্ন index বের করুন, যাদের মানের যোগফল target.” নিচের ডায়াগ্রামে “পরে” চাপুন:

  1. ১ · বুঝুনপ্রশ্ন করে অস্পষ্টতা দূর
  2. ২ · উদাহরণছোট ইনপুট ও edge case
  3. ৩ · Brute forceআগে একটি সঠিক সমাধান
  4. ৪ · উন্নত পদ্ধতিবারবার হওয়া কাজ কমান
  5. ৫ · কোডপরিষ্কার, Pythonic
  6. ৬ · পরীক্ষাউদাহরণ দিয়ে dry run
  7. ৭ · জটিলতা ও ফলো-আপBig-O আর নতুন শর্ত
আপনি জিজ্ঞেস করছেনলেখা শুরুর আগে
- উত্তর সবসময় থাকবে, নাকি নাও থাকতে পারে?
- একই index দুবার নেওয়া যাবে? (না)
- list কি সাজানো? (না)
- n কত বড়? ঋণাত্মক সংখ্যা আছে?
- index ফেরত দেব, নাকি মান?
উদাহরণ ও edge caseখাতায় লেখা
[2, 7, 11, 15], target 9  ->  (0, 1)
[3, 3],         target 6  ->  (0, 1)  একই মান, ভিন্ন index
[-3, 4, 3, 90], target 0  ->  (0, 2)  ঋণাত্মক
[5],            target 10 ->  None    5+5 চলবে না
Brute forceমুখে বলুন, কোড নয়
for i in range(n):
    for j in range(i + 1, n):
        if nums[i] + nums[j] == target: return i, j
সময় O(n²), বাড়তি মেমরি O(1)
উন্নত পদ্ধতিবারবার কী খুঁজছি?
ভেতরের লুপ শুধু একটা প্রশ্ন করে:
  "target - x কি আগে দেখেছি?"
-> দেখা মানগুলো dict-এ রাখি
   (মান -> index), খোঁজা গড়ে O(1)
কোডের কাঠামোপুরো কোড নিচে, চালানো যায়
for j, x in enumerate(nums):
    if target - x in seen: return seen[target - x], j
    seen[x] = j       # আগে খুঁজি, পরে রাখি
return None           # "নেই" স্পষ্টভাবে
Dry run[3, 3], target 6
j=0: need=3, seen-এ নেই    -> seen {3: 0}
j=1: need=3, seen-এ আছে(0) -> (0, 1)
উল্টো ক্রমে (আগে রাখি, পরে খুঁজি) [5], 10
-> নিজের সাথেই জোড়া (0, 0) — ভুল!
জটিলতা ও ফলো-আপশেষ কথা
dict: সময় O(n) গড়ে, মেমরি O(n)
- list সাজানো হলে? -> দুই পয়েন্টার, O(1) মেমরি
- সব জোড়া চাই? 3-sum? -> সাজিয়ে + দুই পয়েন্টার, O(n²)
- ডেটা stream-এ এলে? -> একই dict, এক পাসেই
  1. বুঝুন। সমস্যার বিবরণ প্রায়ই ইচ্ছা করেই অসম্পূর্ণ রাখা হয় — আপনি প্রশ্ন করেন কি না, সেটাই দেখা হয়। এই clarifying question-গুলোর উত্তর পুরো সমাধান বদলে দিতে পারে। যেমন list সাজানো থাকলে একদম অন্য, আরও সহজ পদ্ধতি চলে।
  2. উদাহরণ। দেওয়া উদাহরণ ছাড়াও নিজে কয়েকটি লিখুন: একই মান দুবার, ঋণাত্মক, উত্তর নেই, একটিমাত্র উপাদান, খালি list. এগুলোই পরে আপনার test case হবে। Edge case আগে লিখলে কোডে সেগুলো ভুলে যাওয়ার সম্ভাবনা কমে।
  3. Brute force. সবচেয়ে সরল brute force সমাধানটি মুখে বলুন, যদিও সেটি ধীর। এতে দেখান যে অন্তত একটি সঠিক উত্তর আপনার হাতে আছে। সময় না থাকলে এটাই কোড করবেন — কিছু না থাকার চেয়ে সঠিক ধীর সমাধান অনেক ভালো।
  4. উন্নত পদ্ধতি। জিজ্ঞেস করুন: “কোন কাজটি বারবার হচ্ছে?” এখানে ভেতরের লুপ শুধু একটি মান খোঁজে। খোঁজা দ্রুত করার যন্ত্র hash table, অর্থাৎ Python-এর dict (পর্ব ১৬)। O(n²) থেকে গড়ে O(n) — বিনিময়ে O(n) বাড়তি মেমরি।
  5. কোড। ইন্টারভিউয়ার রাজি হলে তবেই কোড লিখুন। প্রথমে ফাংশনের signature: type hint দিয়ে ইনপুট আর ফেরত টাইপ, আর “উত্তর নাও থাকতে পারে” বোঝাতে tuple[int, int] | None. একটি খুঁটিনাটি: আগে খুঁজি, তারপর রাখি — এতে একটি উপাদান নিজের সাথে জোড়া হয় না।
  6. পরীক্ষা। “শেষ” বলার আগে নিজের উদাহরণ দিয়ে কোডটি লাইন ধরে চালান — এটাই dry run, যা পর্ব ৫২-এ অনুশীলন করেছেন। এখানে দেখা যাচ্ছে, ক্রম উল্টালে [5], target 10-এ ভুল উত্তর আসত।
  7. জটিলতা ও ফলো-আপ। শেষে সময় ও মেমরি আবার পরিষ্কার করে বলুন। dict-এর O(1) “গড়ে” — খারাপ ক্ষেত্রে collision বাড়লে ধীর হয় (অংশ ৭)। এরপর ইন্টারভিউয়ার শর্ত বদলান; প্রতিটি follow-up-এ আবার একই সাত ধাপ, শুধু দ্রুত।
def two_sum(nums: list[int], target: int) -> tuple[int, int] | None:
    seen: dict[int, int] = {}              # value -> index
    for j, x in enumerate(nums):
        need = target - x
        if need in seen:                   # look first ...
            return seen[need], j
        seen[x] = j                        # ... then remember
    return None


tests = [([2, 7, 11, 15], 9), ([3, 3], 6), ([-3, 4, 3, 90], 0), ([5], 10), ([], 1)]
for nums, target in tests:
    print(f"{nums}, {target} -> {two_sum(nums, target)}")

প্রত্যাশিত আউটপুট: পাঁচ লাইন — (0, 1), (0, 1), (0, 2), None, None. লক্ষ করুন কয়েকটি “প্রোডাকশন” অভ্যাস: enumerate দিয়ে index আর মান একসাথে (পর্ব ১২), ফেরত টাইপেই “উত্তর নেই” সম্ভাবনা — -1-এর মতো জাদুকরী মান নয়, আর test ইনপুটগুলো কোডের সাথেই লেখা।

যা নিজে থেকে বলবেন: C বা Java-র মতো এখানে target - x overflow করে না — Python-এর int যত বড় দরকার তত বড় হয় (পর্ব ০৭)। ডুপ্লিকেটে seen[x] = j শেষ index রাখে; প্রথম index চাইলে seen.setdefault(x, j). খালি list-এ লুপই চলে না, তাই None — আলাদা if লাগে না।

C/C++ জানলে

C-তে এই সমস্যার বড় অংশ ছিল নিজে একটি hash table লেখা; C++-এ std::unordered_map আছে, কিন্তু const&, overflow আর iterator নিয়ে সতর্ক থাকতে হয়। Python-এ dict ভাষারই অংশ, তাই ইন্টারভিউয়ার আপনার যুক্তি আর খরচ বোঝায় বেশি মন দেন — আর জিজ্ঞেস করেন built-in-এর ভেতরে কী হয়। একই সমস্যাগুলোর অন্য ভাষার সংস্করণ পাশাপাশি দেখতে পারেন C++ কোর্সের কোডিং ইন্টারভিউ পর্বে আর C কোর্সের কোডিং ইন্টারভিউ পর্বে.

সময় ভাগের মোটা হিসাব (৪৫ মিনিট): বোঝা ও উদাহরণ ৫ মিনিট, পদ্ধতি ১০ মিনিট, কোড ১৫–২০ মিনিট, পরীক্ষা ৫ মিনিট, বাকিটা ফলো-আপ। কোড শুরুর আগে সম্মতি নিন: “এই পদ্ধতিতে লিখি?” আর ইন্টারভিউতে এক লাইনের জাদু এড়িয়ে চলুন — কনটেস্টে চলে (পর্ব ৪৫), কিন্তু পড়তে কষ্ট হয় এমন কোড প্রোডাকশন-মানের নয় (পর্ব ৪৪)।

৩ · অ্যারে, স্ট্রিং ও হ্যাশিং — প্রথম সারির প্রশ্ন

প্রায় প্রতিটি ইন্টারভিউর প্রথম প্রশ্ন এই গোত্রের। দেখতে সহজ, কিন্তু এখানেই বোঝা যায় আপনি slicing, str-এর method, dict, Counter আর defaultdict স্বাভাবিকভাবে ব্যবহার করেন কি না। রিভিশনের জন্য: পর্ব ০৯ · string ও বাংলা Unicode, পর্ব ১৪ · list, পর্ব ১৬ · dictionary, পর্ব ১৭ · set, পর্ব ১৮ · comprehension, পর্ব ২৭ · collections.

সমস্যা ২ · বাক্যের শব্দগুলোর ক্রম উল্টানো (সহজ)

প্রশ্ন: “the sky is blue” থেকে “blue is sky the” বানান। শুরু, শেষ বা মাঝের বাড়তি space বাদ যাবে, শব্দের মাঝে থাকবে ঠিক একটি space.

জিজ্ঞেস করে নিন: space ছাড়া অন্য whitespace (tab, নতুন লাইন) আছে? খালি বা শুধু space থাকা ইনপুটে কী ফেরত দেব? built-in ব্যবহার করা যাবে, নাকি নিজে লিখতে হবে?

পদ্ধতি ও জটিলতা: argument ছাড়া split() যেকোনো পরিমাণ whitespace দিয়ে ভাগ করে আর খালি টুকরো বাদ দেয় — এটাই সমস্যার অর্ধেক। তারপর reversed আর " ".join. সময় O(n), মেমরি O(n).

def reverse_words(s: str) -> str:
    return " ".join(reversed(s.split()))     # split() drops every extra space

# reverse_words("  hello   world  ")  ->  "world hello"
# reverse_words("   ")                ->  ""

Edge case ও ফলো-আপ: split(" ") (argument সহ) লিখলে ভুল হয় — পরপর দুটি space-এর মাঝে খালি string তৈরি হয়। এটা ইন্টারভিউয়ারের প্রিয় ফাঁদ। ফলো-আপে প্রায়ই বলা হয়: “built-in ছাড়া লিখুন।” তখন একটি index দিয়ে string পার হয়ে শব্দ জমান। আর “in-place করুন” বললে মনে করিয়ে দিন — Python-এর str immutable, তাই আগে list(s) বানাতে হবে; তারপর পুরো list উল্টে প্রতিটি শব্দ আবার উল্টানোর two pointers কৌশল।

সমস্যা ৩ · Anagram যাচাই, আর group anagrams (সহজ → মাঝারি)

প্রশ্ন: (ক) দুটি শব্দ পরস্পরের anagram কি না বলুন — “listen” আর “silent” হ্যাঁ। (খ) একগুচ্ছ শব্দকে anagram-দলে ভাগ করুন।

জিজ্ঞেস করে নিন: বড়-ছোট হাতের পার্থক্য ধরব? space বা যতিচিহ্ন গুনব? দলগুলোর ক্রম কি গুরুত্বপূর্ণ?

পদ্ধতি ও জটিলতা: (ক) দুই রকম: দুটোই sorted করে তুলনা — O(n log n); অথবা Counter দিয়ে প্রতিটি অক্ষর গোনা — O(n). (খ) প্রতিটি শব্দের অক্ষর সাজালে একটি “পরিচয়” (key) পাওয়া যায় — “eat”, “tea”, “ate” সবার key “aet”. key থেকে শব্দ-তালিকার defaultdict(list) বানালেই দল হয়ে যায়। k দৈর্ঘ্যের n টি শব্দে সময় O(n · k log k).

from collections import Counter, defaultdict


def is_anagram(a: str, b: str) -> bool:
    return len(a) == len(b) and Counter(a) == Counter(b)


def group_anagrams(words: list[str]) -> list[list[str]]:
    groups: defaultdict[str, list[str]] = defaultdict(list)
    for w in words:
        groups["".join(sorted(w))].append(w)   # "eat", "tea" -> key "aet"
    return list(groups.values())


print(is_anagram("listen", "silent"), is_anagram("rat", "car"), is_anagram("", ""))
print(is_anagram("Listen", "Silent"), is_anagram("Listen".casefold(), "Silent".casefold()))
for group in group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"]):
    print(group)

প্রত্যাশিত আউটপুট: True False True, তারপর False True, তারপর তিনটি দল: ['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']. দলের ক্রম নির্দিষ্ট, কারণ Python 3.7 থেকে dict ঢোকানোর ক্রম মনে রাখার নিশ্চয়তা দেয় — প্রথম দেখা key-এর দল আগে। দ্বিতীয় লাইন দেখায় কেন প্রশ্ন ২ জরুরি: “L” আর “l” আলাদা অক্ষর; casefold() (lower()-এর চেয়ে শক্তিশালী, পর্ব ০৯) দিয়ে মিলিয়ে নিলে True.

যা নিজে থেকে বলবেন: len-এর আগাম যাচাই সস্তা “short-circuit” — দৈর্ঘ্য না মিললে গোনাই লাগে না। dict-এর key হতে হলে মান hashable হতে হয়; তাই key হিসেবে sorted(w)-এর list নয়, "".join(...)-এর string বা tuple(sorted(w)). ফলো-আপ: sort না করে key বানান — ২৬টি অক্ষরের গণনার একটি tuple — তাহলে সময় O(n · k).

সমস্যা ৪ · পুনরাবৃত্তিহীন দীর্ঘতম substring (মাঝারি)

প্রশ্ন: একটি string-এর এমন দীর্ঘতম টানা অংশের দৈর্ঘ্য বের করুন, যেখানে কোনো অক্ষর দুবার নেই। “abcabcbb” → 3 (“abc”), “pwwkew” → 3 (“wke”).

জিজ্ঞেস করে নিন: substring (টানা অংশ) নাকি subsequence (মাঝে বাদ দেওয়া চলে)? “অক্ষর” মানে কী — code point, নাকি চোখে দেখা একটি বাংলা অক্ষর? খালি string-এ উত্তর 0 তো?

পদ্ধতি ও জটিলতা: Brute force: প্রতিটি শুরু থেকে যতদূর যাওয়া যায় — O(n²) বা তার বেশি। উন্নত: sliding window. একটি জানালা s[left:right + 1] ডানে বাড়ান; নতুন অক্ষরটি জানালার ভেতরে আগে থাকলে left-কে তার আগের অবস্থানের ঠিক পরে সরান। প্রতিটি অক্ষরের শেষ দেখা index একটি dict-এ রাখুন। সময় O(n), মেমরি O(ভিন্ন অক্ষরের সংখ্যা). নিচে “abba” দিয়ে ধাপে ধাপে দেখুন — এখানে একটি সূক্ষ্ম ফাঁদ আছে:

1def longest_unique(s):2    last = {}3    best = left = 04    for right, ch in enumerate(s):5        if last.get(ch, -1) >= left:6            left = last[ch] + 17        last[ch] = right8        best = max(best, right - left + 1)9    return best
ভ্যারিয়েবল
right
ch
left
last
best
  1. প্রস্তুতি। last খালি dict — কোনো অক্ষর এখনো দেখা হয়নি। জানালা খালি, best = 0. best = left = 0 একসাথে দুটি নামকে একই মান দেয়।
  2. right = 0, 'a'. last.get('a', -1) = -1, যা left-এর চেয়ে ছোট — তাই লাফ নেই। জানালা “a”, দৈর্ঘ্য ১, best = 1.
  3. right = 1, 'b'. 'b' আগে দেখা হয়নি। জানালা “ab”, best = 2.
  4. right = 2, আবার 'b'. আগের 'b' ছিল index 1-এ, যা জানালার ভেতরে (1 ≥ 0). তাই left লাফিয়ে 2-এ যায় — জানালা এখন শুধু “b”. প্রতিটি অক্ষর জানালায় একবার ঢোকে, left শুধু সামনে যায় — তাই মোট কাজ O(n).
  5. right = 3, আবার 'a' — ফাঁদ। আগের 'a' ছিল index 0-এ, অর্থাৎ জানালার বাইরে (0 < 2). শর্তে >= left না থাকলে left পিছিয়ে 1-এ যেত, আর জানালা হতো “bba” — যেখানে 'b' দুবার! শর্তটি নিশ্চিত করে left কখনো পেছায় না। জানালা “ba”, দৈর্ঘ্য ২।
  6. শেষ। উত্তর 2. এই ধরনের ছোট, চতুর test (“abba”) নিজে থেকে বানানো ইন্টারভিউতে খুব ভালো সংকেত — এটি দেখায় আপনি কোডের invariant (জানালায় কোনো ডুপ্লিকেট নেই) নিয়ে ভাবছেন।
def longest_unique(s: str) -> int:
    last: dict[str, int] = {}          # char -> last index seen
    best = left = 0                    # window is s[left:right + 1]
    for right, ch in enumerate(s):
        if last.get(ch, -1) >= left:   # repeat inside the window?
            left = last[ch] + 1        # jump past the previous copy
        last[ch] = right
        best = max(best, right - left + 1)
    return best


for s in ["abcabcbb", "bbbbb", "pwwkew", "", "abba", "আমার আমি"]:
    print(f"{s!r} -> {longest_unique(s)}")

প্রত্যাশিত আউটপুট: ছয়টি ইনপুটে ক্রমে 3, 1, 3, 0, 2, 6. শেষেরটি মনোযোগ দিয়ে দেখুন। “আমার আমি” চোখে ৮টি “অক্ষর” মনে না-ও হতে পারে, কিন্তু Python-এর str গোনে code point: আ, ম, া, র, space, আ, ম, ি — ৮টি। এর মধ্যে “ার আমি” অংশে (া, র, space, আ, ম, ি) কোনো code point দুবার নেই, তাই ৬। ইন্টারভিউতে এটা নিজে থেকে বলা — “আমি code point গুনছি; দেখা অক্ষর ধরে গুনতে হলে আগে grapheme cluster-এ ভাগ করতে হবে” — খুব ভালো সংকেত।

ফলো-আপ: শুধু দৈর্ঘ্য নয়, substring-টিই ফেরত দিন (best-এর সাথে শুরুর index রাখুন)। “সর্বোচ্চ k টি ভিন্ন অক্ষর” রূপভেদে একই জানালা, শুধু শর্ত বদলায় — Counter রেখে বাম প্রান্ত সরান, আর গণনা শূন্য হলে key মুছুন (পর্ব ৪৫)।

সমস্যা ৫ · Palindrome যাচাই — যতিচিহ্ন বাদ, আর বাংলার কথা (সহজ)

প্রশ্ন: বড়-ছোট হাতের পার্থক্য, space আর যতিচিহ্ন বাদ দিলে একটি বাক্য সামনে-পেছনে একই কি না বলুন। “A man, a plan, a canal: Panama” → True.

জিজ্ঞেস করে নিন: “অক্ষর” মানে শুধু ইংরেজি অক্ষর ও সংখ্যা? Unicode ইনপুট (বাংলা) আসবে? বাড়তি মেমরি O(1) রাখতে হবে?

পদ্ধতি ও জটিলতা: সহজ পথ: পরিষ্কার করা অক্ষরের list বানিয়ে উল্টোটার সাথে তুলনা — O(n) সময়, O(n) মেমরি। ইন্টারভিউয়ার সাধারণত দেখতে চান two pointers: দুই প্রান্ত থেকে ভেতরে আসুন, অপ্রাসঙ্গিক অক্ষর লাফিয়ে যান — O(n) সময়, O(1) বাড়তি মেমরি।

import unicodedata


def is_palindrome(s: str) -> bool:
    """Two pointers; ignore case, spaces and punctuation."""
    i, j = 0, len(s) - 1
    while i < j:
        if not s[i].isalnum():
            i += 1
        elif not s[j].isalnum():
            j -= 1
        elif s[i].casefold() != s[j].casefold():
            return False
        else:
            i, j = i + 1, j - 1
    return True


def is_palindrome_unicode(s: str) -> bool:
    s = unicodedata.normalize("NFC", s)
    keep = [c.casefold() for c in s if unicodedata.category(c)[0] in "LNM"]
    return keep == keep[::-1]          # L = letter, N = number, M = mark


print(is_palindrome("A man, a plan, a canal: Panama"), is_palindrome("race a car"), is_palindrome(""))
print(is_palindrome("কাকি"), is_palindrome_unicode("কাকি"))
print("য়" == "য়",
      unicodedata.normalize("NFC", "য়") == unicodedata.normalize("NFC", "য়"))

প্রত্যাশিত আউটপুট: True False True, তারপর True False, শেষে False True. প্রথম লাইন ইংরেজির জন্য ঠিকঠাক। দ্বিতীয় লাইনেই বাগ: “কাকি” palindrome নয়, তবু is_palindrome বলছে True! কারণ া আর ি হলো স্বরচিহ্ন — Unicode-এর ভাষায় “mark” (category Mc), অক্ষর বা সংখ্যা নয় — তাই isalnum() এদের False দেয়, আর ফাংশন এদের যতিচিহ্নের মতো বাদ দিয়ে শুধু “কক” তুলনা করে।

সমাধান ও শিক্ষা: is_palindrome_unicode অক্ষর (L), সংখ্যা (N) আর চিহ্ন (M) — তিনটিই রাখে। তৃতীয় লাইন আরেকটি ফাঁদ দেখায়: “য়” একটিমাত্র code point (U+09DF) হিসেবেও লেখা যায়, আবার য + নুক্তা (দুটি code point) হিসেবেও — চোখে একই, কিন্তু == বলে আলাদা। তুলনার আগে একই রূপে আনা, অর্থাৎ normalization (NFC), দুটোকে মিলিয়ে দেয়। বাংলা palindrome-এর সংজ্ঞাই আসলে প্রশ্নসাপেক্ষ — code point উল্টালে স্বরচিহ্ন ভুল অক্ষরে বসে যায়। তাই এখানে সেরা উত্তর একটি clarifying question: “ইনপুট কি শুধু ASCII?”

সমস্যা ৬ · প্রথম অপুনরাবৃত্ত অক্ষর (সহজ)

প্রশ্ন: string-এর প্রথম যে অক্ষর ঠিক একবারই আছে, সেটি বের করুন। “swiss” → 'w', “aabb” → নেই।

পদ্ধতি ও জটিলতা: দুবার পার হোন — প্রথমবার Counter দিয়ে গুনুন, দ্বিতীয়বার মূল string-এর ক্রমে প্রথম যার গণনা ১, সেটি ফেরত দিন। সময় O(n), মেমরি O(ভিন্ন অক্ষর)। next()-এর দ্বিতীয় argument “না পেলে কী দেব” বলে দেয় — আলাদা লুপ আর flag লাগে না।

from collections import Counter


def first_unique(s: str) -> str | None:
    counts = Counter(s)
    return next((ch for ch in s if counts[ch] == 1), None)

# first_unique("swiss") -> "w"     first_unique("aabb") -> None

Edge case ও ফলো-আপ: খালি string → None; “leetcode” → 'l'. Python 3.7 থেকে Counter-ও ঢোকানোর ক্রম রাখে, তাই counts-এর ওপর লুপ চালালেও এখানে ঠিক উত্তর আসত — তবে মূল string-এর ওপর লুপ উদ্দেশ্যটা বেশি স্পষ্ট করে। ফলো-আপে প্রায়ই আসে: অক্ষরগুলো একটি stream-এ একটি একটি করে আসছে, প্রতিবার “এ পর্যন্ত প্রথম অপুনরাবৃত্ত” বলতে হবে। তখন গণনার সাথে একটি deque রাখুন, আর সামনের অক্ষরের গণনা ১-এর বেশি হলে popleft() — প্রতি অক্ষরে গড়ে O(1).

৪ · স্ট্যাক, কিউ ও লিংকড লিস্ট

এই গোত্রে দুটি জিনিস একসাথে যাচাই হয়: ঠিক data structure চেনা, আর Python-এর কোন built-in কোন কাজে দ্রুত তা জানা। list-এর শেষে append/pop সস্তা, কিন্তু শুরুতে নয় — এই একটি তথ্যই অনেক প্রশ্নের চাবি। রিভিশনের জন্য: পর্ব ১৪ · list, পর্ব ১৯ · mutability ও alias, পর্ব ২৭ · collections, পর্ব ২৯ · class, পর্ব ৩২ · dataclass.

সমস্যা ৭ · বন্ধনী ঠিকঠাক মেলানো কি না (সহজ)

প্রশ্ন: (), [], {} মেশানো একটি string-এ প্রতিটি খোলা বন্ধনী কি সঠিক ক্রমে সঠিক ধরনের বন্ধনী দিয়ে বন্ধ হয়েছে?

জিজ্ঞেস করে নিন: অন্য অক্ষর থাকবে কি (যেমন একটি গাণিতিক রাশি)? থাকলে উপেক্ষা করব তো? খালি string-কে balanced ধরব?

পদ্ধতি ও জটিলতা: শেষে যে খুলেছে, তাকেই আগে বন্ধ হতে হবে — এটাই LIFO, অর্থাৎ stack. Python-এ আলাদা stack class লাগে না — সাধারণ list-এর append আর pop দুটোই শেষ প্রান্তে, গড়ে O(1). সময় O(n), মেমরি O(n).

PAIRS = {")": "(", "]": "[", "}": "{"}


def is_balanced(s: str) -> bool:
    stack: list[str] = []
    for ch in s:
        if ch in "([{":
            stack.append(ch)
        elif ch in PAIRS:
            if not stack or stack.pop() != PAIRS[ch]:
                return False
    return not stack                  # a leftover opener = unbalanced

# "()[]{}" True · "([)]" False · "{[()]}" True · "((" False · ")(" False · "" True

Edge case ও ফলো-আপ: দুটি ফাঁদ এখানে ধরা পড়ে: ")(" — খালি list-এ pop() করলে IndexError, তাই আগে not stack যাচাই (or-এর short-circuit দ্বিতীয় অংশ চালায়ই না); আর "((" — লুপ শেষে stack খালি না হলে উত্তর False. ফলো-আপ: শুধু এক ধরনের বন্ধনী হলে stack-ও লাগে না — একটি গণনা যথেষ্ট (O(1) মেমরি)। প্রথম ভুলের অবস্থান ফেরত দিন। সবচেয়ে কম কয়টি বন্ধনী মুছলে balanced হবে?

সমস্যা ৮ · Min-stack, আর deque দিয়ে queue (মাঝারি)

প্রশ্ন: (ক) একটি stack class ডিজাইন করুন, যেখানে push, pop, top আর get_min — চারটিই O(1). (খ) মেট্রো রেলের টিকিট কাউন্টারের লাইনের মতো একটি queue বানান: যে আগে আসে, সে আগে যায়; দুই কাজই O(1).

জিজ্ঞেস করে নিন: খালি stack বা queue থেকে নিতে চাইলে কী হবে — exception, নাকি None? মানগুলো কি শুধু সংখ্যা?

পদ্ধতি ও জটিলতা: (ক) প্রতিটি উপাদানের পাশে “এখন পর্যন্ত নিচের সবার সর্বনিম্ন” জমা রাখুন — (value, min_so_far) tuple. pop করলে সেই তথ্যও চলে যায়, আগের সর্বনিম্ন নিজে থেকে ফিরে আসে। সব কাজ O(1), মেমরি ২n. (খ) list-এর pop(0) O(n) — প্রথমটি সরালে বাকি সবাইকে এক ঘর বাঁয়ে সরাতে হয়। collections.deque দুই প্রান্তেই O(1).

from collections import deque


class MinStack:
    def __init__(self) -> None:
        self._items: list[tuple[int, int]] = []     # (value, min of it and below)

    def push(self, x: int) -> None:
        low = min(x, self._items[-1][1]) if self._items else x
        self._items.append((x, low))

    def pop(self) -> int:
        if not self._items:
            raise IndexError("pop from empty MinStack")
        return self._items.pop()[0]

    def top(self) -> int:
        return self._items[-1][0]                   # IndexError when empty

    def get_min(self) -> int:
        return self._items[-1][1]


class TicketQueue:
    def __init__(self) -> None:
        self._q: deque[str] = deque()

    def join(self, name: str) -> None:
        self._q.append(name)                        # O(1) at the back

    def serve(self) -> str:
        if not self._q:
            raise IndexError("queue is empty")
        return self._q.popleft()                    # O(1); list.pop(0) is O(n)

    def __len__(self) -> int:
        return len(self._q)

Edge case ও ফলো-আপ: 5, 3, 7, 3, 1 push করলে প্রতিবারের get_min: 5, 3, 3, 3, 1; ডুপ্লিকেট সর্বনিম্ন (দুটি 3) কোনো বিশেষ ব্যবস্থা ছাড়াই ঠিক চলে। __len__ লিখলে len(q) আর if q: দুটোই স্বাভাবিকভাবে কাজ করে — এটাই Python-এর data model (পর্ব ৩০)। এক ধাপ এগিয়ে নিজেকে জিজ্ঞেস করুন: self._items-এ underscore কেন? — বাইরের কোডকে বলা “এটা ছুঁয়ো না”, যাতে invariant ভাঙা না যায় (পর্ব ২৯)। ফলো-আপ: “দুটি stack দিয়ে queue বানান” — একটিতে ঢোকান, অন্যটি খালি হলে প্রথমটির সব উল্টে ঢেলে দিন; প্রতিটি উপাদান মোট দুবার সরে, তাই গড়ে (amortized) O(1). thread-এর মধ্যে কাজ ভাগ করতে queue.Queue (পর্ব ৪১)।

সমস্যা ৯ · লিংকড লিস্ট উল্টানো (সহজ, কিন্তু খুব জনপ্রিয়)

প্রশ্ন: একমুখী linked list-এর head দেওয়া আছে। নতুন node না বানিয়ে লিস্টটি উল্টে নতুন head ফেরত দিন।

জিজ্ঞেস করে নিন: Node class কি দেওয়া আছে, নাকি আমি বানাব? খালি লিস্ট (None) আসতে পারে? (আসতে পারে।)

পদ্ধতি ও জটিলতা: Python-এ built-in linked list নেই, তাই ছোট একটি dataclass দিয়ে Node বানাই — __init__ আর __repr__ নিজে লিখতে হয় না (পর্ব ৩২)। তিনটি হাত — উল্টানো অংশ (prev), বর্তমান node (head), বাকি অংশ (rest). সময় O(n), মেমরি O(1).

from dataclasses import dataclass


@dataclass
class Node:
    val: int
    next: "Node | None" = None


def reverse(head: Node | None) -> Node | None:
    prev = None
    while head is not None:
        rest = head.next       # 1. hold on to the rest
        head.next = prev       # 2. turn the arrow around
        prev = head            # 3. grow the reversed part
        head = rest            # 4. step forward
    return prev


def from_list(values: list[int]) -> Node | None:
    head = None
    for v in reversed(values):
        head = Node(v, head)
    return head


def to_list(head: Node | None) -> list[int]:
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out


for values in ([1, 2, 3, 4, 5], [], [9]):
    print(values, "->", to_list(reverse(from_list(values))))

প্রত্যাশিত আউটপুট: [1, 2, 3, 4, 5] -> [5, 4, 3, 2, 1], [] -> [], [9] -> [9]. annotation-এ "Node | None" উদ্ধৃতিচিহ্নে, কারণ class-এর ভেতরে Node নামটি তখনো পুরো তৈরি হয়নি — এটি forward reference. Python 3.14 annotation দেরিতে মূল্যায়ন করে, তাই সেখানে উদ্ধৃতি ছাড়াও চলে; পুরোনো সংস্করণেও চালাতে চাইলে উদ্ধৃতিই নিরাপদ। কোথাও মেমরি মুক্ত করার কোড নেই — কোনো নাম আর পুরোনো node-কে না দেখালে Python-এর reference counting নিজেই সেটি মুছে দেয় (পর্ব ০৬)।

লুকানো ফাঁদ যা নিজে বলবেন: এক লাইনে head.next, prev, head = prev, head, head.next লেখা যায় — ডান দিক আগে পুরো মূল্যায়িত হয়, তারপর বাঁ থেকে ডানে বসানো হয়। কিন্তু বাঁ দিকের ক্রম বদলালে (head আগে বসালে) head.next ভুল node-এ লেখা হয়। চারটি স্পষ্ট লাইন ইন্টারভিউতে নিরাপদ ও পড়তে সহজ। ফলো-আপ: recursive সংস্করণ লিখুন — আর বলুন দশ হাজার node-এ সেটি RecursionError দেবে (অংশ ৭, প্র ০২); k-টি করে দলে উল্টান; লিস্টে চক্র আছে কি না (ধীর ও দ্রুত দুই pointer).

৫ · ট্রি, হিপ, বাইনারি সার্চ ও DP

এই অংশের সমস্যাগুলোতে কৌশল চেনাই আসল: “সাজানো” শুনলে binary search, “স্তরে স্তরে” শুনলে queue, “সবচেয়ে বড় k টি” শুনলে heap, “কত উপায়ে / সর্বনিম্ন কত” শুনলে DP. রিভিশনের জন্য: পর্ব ১৩ · ফাংশন, পর্ব ৩৪ · generator ও yield from, পর্ব ৩৫ · decorator ও cache, পর্ব ৩৮ · functools ও itertools, পর্ব ৪৫ · অ্যালগরিদম ও কনটেস্ট.

সমস্যা ১০ · BST-তে insert, inorder আর level-order (সহজ → মাঝারি)

প্রশ্ন: একটি binary search tree-তে key যোগ করুন। তারপর (ক) সব key সাজানো ক্রমে, আর (খ) স্তর ধরে বের করুন: [[50], [30, 70], [20, 40, 60, 80], [35]].

জিজ্ঞেস করে নিন: ডুপ্লিকেট key কোথায় যাবে — বাদ, নাকি ডানে? tree কি balanced রাখতে হবে? (না হলে খারাপ ক্ষেত্রে উচ্চতা n.)

পদ্ধতি ও জটিলতা: insert: ছোট হলে বাঁয়ে, নইলে ডানে, খালি জায়গা পেলে সেখানে নতুন node. Inorder traversal (বাঁ → root → ডান) BST-তে সবসময় সাজানো ক্রম দেয়; yield from দিয়ে এটি একটি generator হয়। Level-order মানে BFS: আগে দেখা node-এর সন্তানদের আগে দেখা — FIFO, তাই deque. স্তর আলাদা করার কৌশল: প্রতিটি স্তর শুরুর সময় queue-তে কয়টি node আছে গুনে ঠিক ততগুলোই বের করুন। insert O(h), দুই traversal O(n); h হলো উচ্চতা।

from collections import deque
from collections.abc import Iterator
from dataclasses import dataclass


@dataclass
class Tree:
    key: int
    left: "Tree | None" = None
    right: "Tree | None" = None


def insert(t: Tree | None, key: int) -> Tree:
    if t is None:
        return Tree(key)
    if key < t.key:
        t.left = insert(t.left, key)
    else:
        t.right = insert(t.right, key)
    return t


def inorder(t: Tree | None) -> Iterator[int]:      # left, root, right = sorted
    if t:
        yield from inorder(t.left)
        yield t.key
        yield from inorder(t.right)


def level_order(root: Tree | None) -> list[list[int]]:
    levels, q = [], deque([root] if root else [])
    while q:
        level = []
        for _ in range(len(q)):                    # exactly one level per pass
            t = q.popleft()
            level.append(t.key)
            q.extend(c for c in (t.left, t.right) if c)
        levels.append(level)
    return levels


root = None
for k in [50, 30, 70, 20, 40, 60, 80, 35]:
    root = insert(root, k)
print(list(inorder(root)))
print(level_order(root), level_order(None))

প্রত্যাশিত আউটপুট: [20, 30, 35, 40, 50, 60, 70, 80], তারপর [[50], [30, 70], [20, 40, 60, 80], [35]] []. range(len(q)) লুপের শুরুতে একবারই হিসাব হয় — তাই লুপের ভেতরে নতুন সন্তান যোগ হলেও এই স্তরের সংখ্যা বদলায় না। খালি tree-তে deque([]) দিয়ে শুরু হয়, তাই None কখনো queue-তে ঢোকে না।

Edge case ও ফলো-আপ: সাজানো ইনপুট (1, 2, 3, …) দিলে tree একটি লম্বা লাইন হয়ে যায় — উচ্চতা n, আর recursive insert ও inorder-এর গভীরতাও n; প্রায় হাজারের কাছে গেলেই RecursionError. তখন লুপ দিয়ে insert লিখুন। বাস্তবে সাজানো ক্রম রাখতে প্রায়ই সাজানো list + bisect যথেষ্ট। ফলো-আপ: একটি tree বৈধ BST কি না যাচাই করুন (প্রতিটি node-এর জন্য অনুমোদিত সীমা নিচে পাঠিয়ে); zigzag ক্রম; প্রতিটি স্তরের ডানদিকের node; tree-এর সর্বনিম্ন গভীরতা — BFS প্রথম পাতায় পৌঁছেই থামতে পারে।

সমস্যা ১১ · k-তম বৃহত্তম সংখ্যা (মাঝারি)

প্রশ্ন: একটি অসাজানো list-এর k-তম বৃহত্তম মান বের করুন। [3, 2, 1, 5, 6, 4], k = 2 → 5.

পদ্ধতি ও জটিলতা: তিনটি উত্তর, তিনটি trade-off — তিনটিই বলুন: (ক) sorted(nums)[-k]: O(n log n). (খ) k আকারের min-heap: প্রতিটি মান ঢোকান, আকার k ছাড়ালে সবচেয়ে ছোটটি ফেলুন; শেষে heap[0]-ই উত্তর — O(n log k) সময়, O(k) মেমরি, আর ডেটা stream-এ এলেও চলে। (গ) quickselect: গড়ে O(n), কিন্তু খারাপ ক্ষেত্রে O(n²) আর লেখা কঠিন।

import heapq


def kth_largest(nums: list[int], k: int) -> int:
    if not 1 <= k <= len(nums):
        raise ValueError("k out of range")
    heap: list[int] = []                   # min-heap of the k largest so far
    for x in nums:
        heapq.heappush(heap, x)
        if len(heap) > k:
            heapq.heappop(heap)            # drop the smallest
    return heap[0]

# Library one-liner, also O(n log k):  heapq.nlargest(k, nums)[-1]

Edge case ও ফলো-আপ: heapq সবসময় min-heap — heap[0] সবচেয়ে ছোট। দীর্ঘদিন max-heap-এর প্রচলিত কৌশল ছিল মান ঋণাত্মক করে রাখা (-x); Python 3.14-এ heapq.heappush_max-এর মতো max-heap ফাংশনও যোগ হয়েছে — তবে অনলাইন জাজে পুরোনো সংস্করণ থাকতে পারে, তাই দুটোই জানুন। k অবৈধ হলে স্পষ্ট ValueError — নীরবে ভুল উত্তর নয়। ডুপ্লিকেট গোনা হবে কি না ([3, 3, 2], k = 2 → 3 নাকি 2?) জিজ্ঞেস করুন। ফলো-আপ: সবচেয়ে ঘন ঘন আসা k টি শব্দ — Counter(words).most_common(k), আর তার ভেতরের খরচ।

সমস্যা ১২ · ঘোরানো সাজানো list-এ সার্চ (মাঝারি)

প্রশ্ন: একটি সাজানো list কোনো অজানা বিন্দুতে ঘোরানো হয়েছে, যেমন [4, 5, 6, 7, 0, 1, 2]. O(log n)-এ target-এর index বের করুন, না থাকলে -1.

জিজ্ঞেস করে নিন: মানগুলো কি ভিন্ন? (ডুপ্লিকেট থাকলে খারাপ ক্ষেত্রে O(n) লাগে।) খালি list আসতে পারে?

পদ্ধতি: Binary search-এর রূপভেদ। যেকোনো mid-এ দুই অর্ধেকের অন্তত একটি পুরোপুরি সাজানো। সাজানো অর্ধেকের সীমার ভেতরে target থাকলে সেদিকে যান, নইলে অন্যদিকে। প্রতি ধাপে খোঁজার জায়গা অর্ধেক — O(log n), মেমরি O(1). সাধারণ সাজানো list-এ নিজে না লিখে bisect ব্যবহার করুন।

from bisect import bisect_left


def search_rotated(a: list[int], target: int) -> int:
    """Index of target in a rotated sorted list of distinct values, or -1."""
    lo, hi = 0, len(a) - 1
    while lo <= hi:
        mid = (lo + hi) // 2           # Python int never overflows
        if a[mid] == target:
            return mid
        if a[lo] <= a[mid]:            # left half is sorted
            if a[lo] <= target < a[mid]:
                hi = mid - 1
            else:
                lo = mid + 1
        else:                          # right half is sorted
            if a[mid] < target <= a[hi]:
                lo = mid + 1
            else:
                hi = mid - 1
    return -1


def index_of(sorted_a: list[int], x: int) -> int:   # plain sorted list: use bisect
    i = bisect_left(sorted_a, x)
    return i if i < len(sorted_a) and sorted_a[i] == x else -1


a = [4, 5, 6, 7, 0, 1, 2]
print([search_rotated(a, t) for t in (0, 4, 2, 7, 3)], search_rotated([], 1), search_rotated([1], 1))
print(index_of([0, 1, 2, 4, 5, 6, 7], 5), index_of([0, 1, 2], 3))

প্রত্যাশিত আউটপুট: [4, 0, 6, 3, -1] -1 0, তারপর 4 -1. Python-এর chained comparison (a[lo] <= target < a[mid]) গাণিতিক লেখার মতোই পড়া যায় (পর্ব ০৮)। C/Java-তে (lo + hi) / 2 overflow-এর ভয়ে lo + (hi - lo) / 2 লেখা হয়; Python-এর int-এ এই সমস্যা নেই — এটা বললে বোঝা যায় আপনি দুই জগতেই সচেতন।

bisect কেন: bisect_left সাজানো list-এ x বসানোর প্রথম জায়গা দেয়; তারপর সেখানে x সত্যিই আছে কি না যাচাই — না করলে “না থাকা” মানও একটি index পেত। নিজে binary search লিখলে off-by-one-এর ঝুঁকি; standard library-রটা পরীক্ষিত। ফলো-আপ: ঘোরানোর বিন্দু (সর্বনিম্ন উপাদান) খুঁজুন, তারপর সঠিক অর্ধেকে bisect; ডুপ্লিকেট থাকলে কী বদলায়; “উত্তরের ওপর binary search” (যেমন সর্বনিম্ন কত গতিতে কাজ শেষ হবে)।

সমস্যা ১৩ · সিঁড়ি বেয়ে ওঠা ও কয়েন চেঞ্জ (মাঝারি)

প্রশ্ন: (ক) n ধাপের সিঁড়িতে একবারে ১ বা ২ ধাপ উঠা যায় — কত উপায়ে উপরে পৌঁছানো যায়? (খ) নির্দিষ্ট মানের কয়েন (প্রতিটি অসীম সংখ্যক) দিয়ে একটি টাকার পরিমাণ বানাতে সবচেয়ে কম কয়টি কয়েন লাগে?

পদ্ধতি: দুটোই dynamic programming. (ক) শেষ পদক্ষেপ ১ বা ২ ধাপের, তাই climb(n) = climb(n-1) + climb(n-2) — Fibonacci. সরাসরি recursion করলে একই উপসমস্যা বহুবার হিসাব হয় — exponential; functools.cache ফল মনে রাখে (memoization), ফলে O(n). (খ) dp[x] = x বানানোর সর্বনিম্ন কয়েন; প্রতিটি কয়েন c-এর জন্য dp[x - c] + 1 চেষ্টা — নিচ থেকে টেবিল ভরা। সময় O(amount · কয়েনের সংখ্যা)।

from functools import cache


@cache
def climb(n: int) -> int:              # 1 or 2 steps at a time
    if n <= 1:
        return 1
    return climb(n - 1) + climb(n - 2)


def min_coins(coins: list[int], amount: int) -> int:
    """Fewest coins that make amount, or -1 (bottom-up DP)."""
    INF = amount + 1                   # larger than any real answer
    dp = [0] + [INF] * amount
    for x in range(1, amount + 1):
        for c in coins:
            if c <= x:
                dp[x] = min(dp[x], dp[x - c] + 1)
    return -1 if dp[amount] == INF else dp[amount]


print("stairs:", climb(1), climb(2), climb(5), climb(50))
print("coins: ", min_coins([1, 3, 4], 6), min_coins([2], 3), min_coins([1, 2, 5], 11), min_coins([7], 0))
print(climb.cache_info())

প্রত্যাশিত আউটপুট: stairs: 1 2 8 20365011074, coins: 2 -1 3 0, আর শেষে CacheInfo(hits=51, misses=51, maxsize=None, currsize=51) — ০ থেকে ৫০ পর্যন্ত ৫১টি n প্রতিটি একবারই আসলে হিসাব হয়েছে, বাকি ৫১টি ডাক cache থেকে উত্তর পেয়েছে। ৫০ ধাপের উত্তর C-র ৩২-বিট int-এ ধরত না; Python-এ চিন্তা নেই। কয়েনে {1, 3, 4} দিয়ে 6: “বড়টা আগে” (greedy) নেয় 4 + 1 + 1 = ৩টি, অথচ 3 + 3 = ২টিই যথেষ্ট — এটাই দেখায় কেন এখানে DP লাগে।

যা নিজে থেকে বলবেন: @cache সুন্দর, কিন্তু recursion থেকেই যায় — খালি cache-এ climb(5000) ডাকলে গভীরতা হাজার ছাড়িয়ে RecursionError (অংশ ৭, প্র ০২)। বড় n-এ শুধু শেষ দুটি মান রেখে লুপ লিখুন: O(n) সময়, O(1) মেমরি। ফলো-আপ: সর্বনিম্ন নয়, কত উপায়ে বানানো যায় (লুপের ক্রম বদলালে বিন্যাস বনাম সমাবেশ গোনা হয়); একবারে ১, ২ বা ৩ ধাপ উঠা গেলে।

৬ · Python ডিজাইন সমস্যা — LRU cache, decorator, context manager, generator

এখানে অ্যালগরিদম ছোট, কিন্তু ভাষার জ্ঞান গভীরভাবে যাচাই হয়: কোন collections টাইপ কোন কাজে, closure, exception কীভাবে ছড়ায়, আর lazy মূল্যায়ন। রিভিশনের জন্য: পর্ব ২০ · closure, পর্ব ২৪ · exception, পর্ব ৩০ · dunder method, পর্ব ৩২ · dataclass, পর্ব ৩৪ · generator, পর্ব ৩৫ · decorator, পর্ব ৩৬ · context manager, পর্ব ৩৮ · functools.

সমস্যা ১৪ · LRU cache — get আর put দুটোই O(1) (মাঝারি)

প্রশ্ন: নির্দিষ্ট ধারণক্ষমতার একটি cache বানান। get(key) মান দেয় (না থাকলে None); put(key, value) যোগ বা বদল করে। জায়গা ভরে গেলে সবচেয়ে দীর্ঘ সময় ব্যবহার না হওয়া (least recently used) key বাদ যাবে।

জিজ্ঞেস করে নিন: get-ও কি “ব্যবহার” ধরা হবে? (হ্যাঁ।) ধারণক্ষমতা 0 হলে? একাধিক thread থেকে ডাকা হবে কি? OrderedDict ব্যবহার করা যাবে, নাকি নিজে doubly linked list লিখতে হবে?

পদ্ধতি: দুটি চাহিদা — key দিয়ে দ্রুত খোঁজা (hash), আর ব্যবহারের ক্রম, যেখানে যেকোনো key O(1)-এ “সবচেয়ে নতুন” প্রান্তে সরানো যায়। OrderedDict ঠিক এই দুটি একসাথে দেয়: ভেতরে dict আর একটি doubly linked list. move_to_end(key) আর popitem(last=False) দুটোই O(1).

from collections import OrderedDict


class LRUCache:
    def __init__(self, capacity: int) -> None:
        self.capacity = capacity
        self._data: OrderedDict[int, int] = OrderedDict()   # oldest first

    def get(self, key: int) -> int | None:
        if key not in self._data:
            return None
        self._data.move_to_end(key)            # now the most recent, O(1)
        return self._data[key]

    def put(self, key: int, value: int) -> None:
        if self.capacity <= 0:
            return
        self._data[key] = value
        self._data.move_to_end(key)
        if len(self._data) > self.capacity:
            self._data.popitem(last=False)     # evict the least recent, O(1)


c = LRUCache(2)
c.put(1, 10)
c.put(2, 20)
print(c.get(1), end=" ")                       # 10; key 1 is now most recent
c.put(3, 30)                                   # evicts key 2
print(c.get(2), c.get(3), end=" ")
c.put(1, 11)
c.put(4, 40)                                   # evicts key 3
print(c.get(1), c.get(3), c.get(4))

প্রত্যাশিত আউটপুট: 10 None 30 11 None 40. ধাপগুলো খাতায় মিলিয়ে নিন: get(1)-এর পর 1 সবচেয়ে নতুন, তাই put(3) বাদ দেয় 2-কে; put(1, 11) 1-কে আবার নতুন প্রান্তে আনে, তাই put(4) বাদ দেয় 3-কে। সাধারণ dict-ও ঢোকানোর ক্রম রাখে, কিন্তু তাতে move_to_end নেই — pop করে আবার ঢোকানো যায়, তবে OrderedDict এই কাজের জন্যই বানানো।

functools.lru_cache কী করে: এটি একই নীতির একটি তৈরি LRU cache, কিন্তু ফাংশনের জন্য — argument-গুলো মিলে key, ফেরত মান হলো value. @lru_cache(maxsize=128) সর্বোচ্চ ১২৮টি ফল রাখে; maxsize=None (বা @cache) সীমাহীন, তাই ক্রম রাখার খরচও নেই। argument-গুলো hashable হতে হয় — list দিলে TypeError. CPython-এ এটি C-তে লেখা, আর এর ভেতরেও dict + linked list. ফলো-আপ: thread-safe করুন (get-ও ক্রম বদলায়, তাই সেখানেও একটি threading.Lock লাগে); নিজে doubly linked list দিয়ে লিখুন (sentinel head/tail node সীমানার if কমায়); মেয়াদ (TTL) যোগ করুন।

সমস্যা ১৫ · @retry আর @timer decorator (মাঝারি)

প্রশ্ন: নেটওয়ার্কের ডাক মাঝে মাঝে ব্যর্থ হয়। একটি @retry(times=3) decorator লিখুন, যা নির্দিষ্ট exception হলে ফাংশনটি আবার চেষ্টা করে; শেষ চেষ্টাও ব্যর্থ হলে আসল exception বাইরে যায়।

জিজ্ঞেস করে নিন: কোন কোন exception-এ আবার চেষ্টা হবে — সব, নাকি শুধু নেটওয়ার্কের? চেষ্টার মাঝে অপেক্ষা (backoff) লাগবে? ফাংশনের নাম ও docstring অটুট থাকতে হবে তো?

পদ্ধতি: argument-সহ decorator তিন স্তরের: retry(3) একটি decorator ফেরত দেয়, সেই decorator ফাংশন নিয়ে wrapper ফেরত দেয়। wrapper *args, **kwargs নিয়ে মূল ফাংশনকে হুবহু পাঠায়; functools.wraps নাম ও docstring কপি করে (পর্ব ৩৫)।

import functools


def retry(times: int, on: tuple[type[Exception], ...] = (ConnectionError,)):
    """Decorator factory: call the function up to `times` times."""
    def decorate(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except on as e:
                    print(f"attempt {attempt}: {e}")
                    if attempt == times:
                        raise                  # give up: re-raise the last error
        return wrapper
    return decorate


replies = iter([None, None, 121.5])            # fake network: fails twice


@retry(times=3)
def fetch_rate(currency: str) -> float:
    rate = next(replies)
    if rate is None:
        raise ConnectionError(f"{currency}: timeout")
    return rate


print(fetch_rate("USD"), fetch_rate.__name__)

প্রত্যাশিত আউটপুট: attempt 1: USD: timeout, attempt 2: USD: timeout, তারপর 121.5 fetch_rate. আসল নেটওয়ার্ক নয়, একটি iterator থেকে উত্তর আসছে — তাই আউটপুট প্রতিবার একই, আর পরীক্ষা করা সহজ। ইন্টারভিউতে এভাবে “নকল নির্ভরতা” দিয়ে test করার কথা বলা ভালো সংকেত (পর্ব ৩৯)। fetch_rate নামটি প্রমাণ যে তিন স্তরের ভেতরেও wraps কাজ করেছে।

যা নিজে থেকে বলবেন: খালি raise মূল traceback-সহ একই exception আবার ছোড়ে — নতুন exception বানালে আসল কারণ হারাত। except Exception দিয়ে সব ধরলে প্রোগ্রামিংয়ের ভুলও (TypeError) তিনবার চলত — তাই কোন exception ধরব, সেটি argument. বাস্তবে চেষ্টার মাঝে বাড়তে থাকা অপেক্ষা (exponential backoff) দিন। একই কাঠামোয় @timer: wrapper-এ start = time.perf_counter(), তারপর try-তে মূল ডাক, আর finally-তে সময় লগ — এতে exception হলেও সময় লেখা হয়। মাপার জন্য time.time() নয়, perf_counter (পর্ব ৪৩)।

সমস্যা ১৬ · Transaction context manager — সব হবে, নয়তো কিছুই না (মাঝারি)

প্রশ্ন: wallet-এর টাকা পাঠানোয় দুটি ধাপ: একজনের থেকে কাটা, আরেকজনকে দেওয়া। মাঝপথে ভুল হলে আগের অবস্থায় ফিরতে হবে (rollback). একটি with transaction(accounts): context manager লিখুন।

জিজ্ঞেস করে নিন: ভুল হলে exception কি বাইরে যাবে, নাকি চাপা পড়বে? (বাইরে যাবে — নীরবে ব্যর্থতা সবচেয়ে বিপজ্জনক।) ডেটা কত বড় — পুরো কপি রাখা সম্ভব?

পদ্ধতি: contextlib.contextmanager দিয়ে একটি generator: yield-এর আগের অংশ __enter__, পরের অংশ __exit__. with-ব্লকে exception হলে সেটি yield-এর জায়গাতেই ওঠে, তাই try/except দিয়ে ধরে rollback করে আবার raise.

from contextlib import contextmanager


@contextmanager
def transaction(accounts: dict[str, int]):
    snapshot = dict(accounts)              # copy taken on enter
    try:
        yield accounts
    except Exception:
        accounts.clear()
        accounts.update(snapshot)          # rollback on any error
        raise                              # never swallow the error


def transfer(acc: dict[str, int], src: str, dst: str, amount: int) -> None:
    acc[src] -= amount                     # step 1
    if acc[src] < 0:
        raise ValueError(f"{src}: insufficient balance")
    acc[dst] += amount                     # step 2


wallet = {"rahim": 500, "karim": 100}
with transaction(wallet):
    transfer(wallet, "rahim", "karim", 200)
print(wallet)
try:
    with transaction(wallet):
        transfer(wallet, "karim", "rahim", 900)
except ValueError as e:
    print("failed:", e)
print(wallet)

প্রত্যাশিত আউটপুট: {'rahim': 300, 'karim': 300}, তারপর failed: karim: insufficient balance, তারপর আবার {'rahim': 300, 'karim': 300}. দ্বিতীয় লেনদেনে ধাপ ১ (karim থেকে 900 কাটা) হয়ে গিয়েছিল, কিন্তু ধাপ ২-এর আগেই ভুল — rollback সেই অর্ধেক কাজ মুছে দিয়েছে। clear() + update() একই dict object বদলায়; accounts = snapshot লিখলে শুধু local নাম বদলাত, বাইরের wallet নয় (পর্ব ১৯-এর alias).

যা নিজে থেকে বলবেন: dict(accounts) হলো shallow copy — মান যদি list বা অন্য mutable হতো, copy.deepcopy লাগত। বড় ডেটায় পুরো কপি ব্যয়বহুল; তখন পরিবর্তনের লগ রেখে উল্টো দিকে চালানো, বা আসল database-এর transaction. সময় মাপার context manager class দিয়ে লেখা যায়: __enter__-এ perf_counter(), __exit__(self, exc_type, exc, tb)-এ সময় হিসাব আর False ফেরত — অর্থাৎ exception চাপা দেব না। ফলো-আপ: নেস্টেড transaction; একাধিক resource একসাথে — contextlib.ExitStack.

সমস্যা ১৭ · Generator pipeline — বড় লগ ফাইল এক সারি করে (মাঝারি)

প্রশ্ন: কয়েক GB-এর একটি লেনদেনের লগ ফাইল আছে। মেমরিতে পুরো ফাইল না এনে প্রতিটি প্রোভাইডারের সফল লেনদেনের মোট বের করুন। মন্তব্যের লাইন আর ভাঙা সারি বাদ যাবে।

জিজ্ঞেস করে নিন: লাইনের format কী? ভাঙা সারি কী করব — বাদ, নাকি লগ করে বাদ? ফল কি একবারই লাগবে, নাকি একাধিকবার পড়তে হবে?

পদ্ধতি: ছোট ছোট generator ধাপের শিকল — প্রতিটি একটি করে সারি নেয়, প্রক্রিয়া করে, পরের ধাপে দেয়। পুরো pipeline-এ একসাথে মাত্র একটি সারি মেমরিতে থাকে: মেমরি O(1), সময় O(n). প্রতিটি ধাপ আলাদাভাবে test করা যায় — এটাই lazy evaluation-এর সৌন্দর্য।

from collections.abc import Iterable, Iterator

LOG = """2026-10-01 bKash 500 OK
2026-10-01 Nagad 200 FAIL
# a comment line
2026-10-02 bKash 1200 OK
broken line
2026-10-03 Nagad 700 OK"""

Row = tuple[str, int, str]


def read_lines(text: str) -> Iterator[str]:
    for line in text.splitlines():     # an open file works the same way
        yield line.strip()


def parse(lines: Iterable[str]) -> Iterator[Row]:
    for line in lines:
        parts = line.split()
        if line.startswith("#") or len(parts) != 4:
            continue                   # skip comments and broken rows
        _, provider, amount, status = parts
        yield provider, int(amount), status


def only_ok(rows: Iterable[Row]) -> Iterator[Row]:
    return (row for row in rows if row[2] == "OK")


pipeline = only_ok(parse(read_lines(LOG)))     # nothing has run yet
print(next(pipeline))                          # pulls just enough lines
totals: dict[str, int] = {}
for provider, amount, _ in pipeline:           # the rest, one row at a time
    totals[provider] = totals.get(provider, 0) + amount
print(totals)

প্রত্যাশিত আউটপুট: ('bKash', 500, 'OK'), তারপর {'bKash': 1200, 'Nagad': 700}. লক্ষ করুন, totals-এ bKash 1700 নয়, 1200! প্রথম সারিটি next() আগেই নিয়ে গেছে, আর generator একবারই চলে — যা খরচ হয়েছে, তা আর ফেরত আসে না। ইন্টারভিউতে এই “একবার-চলা” স্বভাবটি নিজে থেকে বললে ইন্টারভিউয়ার বোঝেন আপনি generator সত্যিই বোঝেন। আসল ফাইলে read_lines-এর বদলে with open(path, encoding="utf-8") as f: আর সরাসরি f-এর ওপর লুপ — file object নিজেই লাইনের iterator (পর্ব ২৫)।

Iterator class আর dataclass মডেল — দুটি জনপ্রিয় ফলো-আপ: “generator ছাড়া লিখুন” বললে একটি class বানান, যার __iter__ নিজেকে ফেরত দেয় আর __next__ পরের মান দেয় বা শেষে StopIteration ছোড়ে — এটাই iterator protocol (পর্ব ৩৪)। আর tuple-এর row[2]-এর মতো অস্পষ্ট index-এর বদলে একটি মডেল: @dataclass(frozen=True, slots=True) class Txn: provider: str; amount: int; ok: bool, সাথে __post_init__-এ যাচাই (amount ঋণাত্মক হলে ValueError). frozen=True মান বদলাতে দেয় না আর object-কে hashable করে; slots=True (Python 3.10 থেকে) মেমরি বাঁচায় (পর্ব ৩২)।

৭ · ভাবনার প্রশ্ন — Python ইন্টারভিউর গভীর আলোচনা

কোড লেখার পর ইন্টারভিউয়ার প্রায়ই জিজ্ঞেস করেন, “এটা কেন এভাবে লিখলেন?” নিচের প্রশ্নগুলো সেই আলোচনার প্রস্তুতি। উত্তরের ভালো কাঠামো: এক লাইনে সারকথা → কারণ → একটি ফাঁদ বা বাস্তব উদাহরণ। প্রতিটি আগে নিজে মুখে উত্তর দিন, তারপর খুলুন। রিভিশনের জন্য: পর্ব ১৬, পর্ব ২০, পর্ব ৩৪, পর্ব ৩৯, পর্ব ৪১, পর্ব ৪৩.

প্র ০১dict-এ খোঁজা কেন গড়ে O(1)? খারাপ ক্ষেত্রে কী হয়, আর ইন্টারভিউতে কীভাবে বলবেন? (মাঝারি)

Python-এর dict একটি hash table. key-এর hash() থেকে টেবিলের একটি ঘর ঠিক হয়; সেখানে key পেলে কাজ শেষ। ঘরটি অন্য key-এ ভরা থাকলে (hash collision) CPython একটি নির্দিষ্ট নিয়মে পরের সম্ভাব্য ঘরগুলো দেখে — একে বলে open addressing; C++-এর মতো প্রতিটি ঘরে আলাদা তালিকা রাখে না। টেবিল দুই-তৃতীয়াংশের মতো ভরে গেলে CPython বড় টেবিল বানিয়ে সব নতুন করে বসায়। ফলে ঘরগুলো যথেষ্ট ফাঁকা থাকে, আর গড়ে অল্প কয়েকটি ঘর দেখেই উত্তর মেলে — তাই গড়ে O(1). মাঝে মাঝে বড় হওয়ার সেই একটি insert O(n), কিন্তু সব মিলিয়ে গড়ে (amortized) O(1).

খারাপ ক্ষেত্রে সব key একই ঘরের দিকে গেলে প্রতিটি খোঁজা O(n) — n টি insert মিলে O(n²). এটা শুধু তত্ত্ব নয়। ২০১১ সালের শেষে নিরাপত্তা গবেষকেরা দেখিয়েছিলেন, অনেক জনপ্রিয় web platform-এ ইচ্ছাকৃত collision-ভরা একটি request সার্ভারকে দীর্ঘ সময় ব্যস্ত রাখতে পারে — একে বলে hash flooding. Python এর উত্তরে string ও bytes-এর hash-এ এলোমেলো seed যোগ করে (hash randomization); Python 3.3 থেকে এটি ডিফল্ট, আর পরে hash ফাংশন হিসেবে SipHash-ভিত্তিক অ্যালগরিদম আসে। এ কারণেই একটি string-এর hash() প্রতিবার প্রোগ্রাম চালালে আলাদা হতে পারে, আর string-এর set-এর ক্রম চালানো-ভেদে বদলায়। তবে ছোট int-এর hash সংখ্যাটিই (hash(5) == 5), তাই বেছে বেছে বানানো int ইনপুটে collision ঘটানো তুলনামূলক সহজ — কনটেস্টে “anti-hash” test এভাবেই বানানো হয়।

দুটি জরুরি পার্শ্বকথা। এক, key-কে hashable হতে হয়, আর তার hash জীবনভর একই থাকতে হয় — তাই list key হতে পারে না, tuple পারে (যদি ভেতরের সব কিছু hashable হয়)। নিজের class-এ __eq__ লিখলে __hash__-ও ভাবতে হয় (পর্ব ৩০)। দুই, Python 3.6-এ CPython-এর dict “compact” নকশায় যায় — মেমরি কমে, আর ঢোকানোর ক্রম রক্ষা পায়; Python 3.7 থেকে এই ক্রম ভাষার নিশ্চয়তা।

ইন্টারভিউতে বলুন: “dict ও set-এ খোঁজা গড়ে O(1), খারাপ ক্ষেত্রে O(n); list-এ in সবসময় O(n). তাই লুপের ভেতরে membership test হলে list-কে set বানিয়ে নিই।” এই এক বাক্যে বোঝা যায় আপনি Big-O-কে শুধু মুখস্থ সংখ্যা ভাবেন না।

মূল উপলব্ধি: জটিলতা বলার সময় “গড়ে” আর “খারাপ ক্ষেত্রে” আলাদা করে বলুন — আর সেই পার্থক্য কখন বাস্তবে গুরুত্বপূর্ণ, সেটাও।

প্র ০২Python-এ recursion-এর সীমা কেন আছে? RecursionError এলে ইন্টারভিউতে কী করবেন? (মাঝারি → কঠিন)

প্রতিটি ফাংশন-ডাক একটি frame তৈরি করে — local ভ্যারিয়েবল, কোথায় ফিরতে হবে ইত্যাদি। recursion অসীম হলে বা খুব গভীরে গেলে এই frame জমতে জমতে মেমরি ফুরিয়ে interpreter crash করতে পারত। তাই CPython একটি সীমা রাখে — recursion limit, সাধারণত ১০০০; sys.getrecursionlimit() দিয়ে দেখা যায়। সীমা পেরোলে crash নয়, একটি পরিষ্কার exception: RecursionError: maximum recursion depth exceeded. এটি ধরা যায়, traceback পড়া যায়।

কোথায় গভীরতা n হয়? সাজানো ইনপুটে বানানো BST (একটি লম্বা লাইন), লিংকড লিস্টে recursive reverse, আর top-down DP যেখানে f(n) ডাকে f(n-1)-কে। @cache-যুক্ত climb(5000) খালি cache-এ ডাকলেও RecursionError — memoization ডাকের সংখ্যা কমায়, গভীরতা নয়। balanced tree-তে গভীরতা log n — দশ লাখ node-এও মাত্র ২০-এর মতো — তাই সেখানে recursion নিরাপদ।

sys.setrecursionlimit(10**6) দিয়ে সীমা বাড়ানো যায়, আর কনটেস্টে অনেকে তাই করেন। কিন্তু সীমাটা নিরাপত্তার বেড়া — বেশি বাড়ালে নিচের আসল (C) stack ফুরিয়ে প্রক্রিয়াটি কোনো exception ছাড়াই মরে যেতে পারে। নতুন CPython সংস্করণগুলোতে এর সুরক্ষা উন্নত হয়েছে, তবু প্রোডাকশন কোডে এর ওপর ভরসা করা ভালো অভ্যাস নয়।

অনেকে জিজ্ঞেস করেন, Python কেন tail-call optimization করে না, যাতে শেষ লাইনের recursive ডাক লুপের মতো চলে? Python-এর স্রষ্টা Guido van Rossum ২০০৯ সালে তাঁর ব্লগে এর বিরুদ্ধে যুক্তি দিয়েছিলেন: এতে traceback-এর frame হারিয়ে যায় আর ডিবাগ কঠিন হয়, আর Python-এ সাধারণ কাজের জন্য লুপই স্বাভাবিক পথ। তাই সমাধান একটাই: গভীর হতে পারে এমন recursion-কে লুপে, বা নিজের list-কে stack বানিয়ে লুপে রূপান্তর করা — সেই stack heap-এ থাকে, আর অনেক বড় হতে পারে।

ইন্টারভিউতে recursive সমাধান লেখা দোষের নয় — প্রায়ই সেটাই সবচেয়ে পরিষ্কার। কিন্তু নিজে থেকে বলুন: “গভীরতা n পর্যন্ত যেতে পারে; ইনপুট হাজারের বেশি হলে iterative করব।”

মূল উপলব্ধি: জটিলতা বলার সময় call stack-এর মেমরিও গুনুন — recursive DFS-এর বাড়তি মেমরি O(1) নয়, O(গভীরতা); আর Python-এ সেই গভীরতার একটি কঠিন সীমা আছে।

প্র ০৩কখন list ফেরত দেবেন, আর কখন generator? (মাঝারি)

Generator Python-এ আসে Python 2.2-এ (২০০১, PEP 255), yield শব্দ দিয়ে। এর মূল ধারণা: মান আগে থেকে বানিয়ে রাখা নয়, যখন চাওয়া হবে তখন একটি একটি করে বানানো। ফাংশনটি yield-এ থেমে যায়, তার local অবস্থা মনে রাখে, আর পরের next()-এ ঠিক সেখান থেকে চলে। পরে Python 3.3-এ yield from (PEP 380) এসে generator জোড়া লাগানো সহজ করে — সমস্যা ১০-এর inorder যেমন।

generator বেছে নিন যখন: (১) ডেটা বড় বা সীমাহীন — লগ ফাইল, নেটওয়ার্ক stream, অসীম সংখ্যা-ক্রম। list-এ সব একসাথে রাখতে O(n) মেমরি, generator-এ O(1). (২) সব মান লাগবে কি না জানা নেই — প্রথম মিল পেলেই থামবেন (next(...), any(...)), তাহলে বাকিগুলো বানানোই হয় না। (৩) ধাপে ধাপে প্রক্রিয়া — সমস্যা ১৭-এর pipeline. David Beazley ২০০৮ সালের PyCon-এ “Generator Tricks for Systems Programmers” বক্তৃতায় এই pipeline-ধারণা জনপ্রিয় করেন — Unix-এর | দিয়ে command জোড়ার মতো।

list বেছে নিন যখন: (১) ফল একাধিকবার পড়তে হবে — generator একবারই চলে, দ্বিতীয়বার লুপ চালালে কিছুই আসে না, কোনো error-ও নয়। এটা নীরব বাগের বড় উৎস। (২) len(), index ([3]), slicing বা উল্টানো লাগবে — generator এগুলো পারে না। (৩) ডেটা ছোট আর ফাংশনের ফল সরাসরি ব্যবহারকারীকে দেখাবেন বা test-এ তুলনা করবেন — list পরিষ্কার।

একটি মাঝামাঝি রূপও জানুন: generator expression — sum(x * x for x in nums) — list comprehension-এর মতো দেখতে, কিন্তু বর্গাকার কোনো list তৈরি হয় না। আর একটি সূক্ষ্মতা: generator ফাংশনের ভেতরের কোড প্রথম next()-এর আগে একদমই চলে না। তাই ইনপুট যাচাই (যেমন ValueError) generator-এর ভেতরে রাখলে ভুলটা দেরিতে, অন্য জায়গায় ধরা পড়ে — যাচাই একটি সাধারণ ফাংশনে করে তারপর ভেতরের generator ফেরত দিন।

মূল উপলব্ধি: generator মানে “দরকার হলে বানাও” — মেমরি বাঁচায় আর অসীম ডেটা সামলায়, কিন্তু একবারই চলে। একাধিকবার বা এলোমেলোভাবে পড়তে হলে list.

প্র ০৪ইন্টারভিউর কোডে mutable default argument কেন ফাঁদ? (সহজ → মাঝারি)

এটি Python-এর সবচেয়ে বিখ্যাত ফাঁদগুলোর একটি, আর ইন্টারভিউয়ার প্রায়ই আপনার কোডে এটা খোঁজেন। def add(x, items=[]): — এখানে [] প্রতিবার ডাকার সময় নতুন করে তৈরি হয় না। default মান একবারই মূল্যায়িত হয়: def লাইনটি চলার মুহূর্তে, অর্থাৎ ফাংশন তৈরির সময়। তারপর সেই একটি list object ফাংশনের সাথে লেগে থাকে (add.__defaults__-এ দেখা যায়), আর প্রতিটি ডাক সেটিকেই বদলায় (mutable default, পর্ব ২০)।

ফল: items.append(x); return items হলে প্রথম ডাক add(1) দেয় [1], দ্বিতীয় ডাক add(2) দেয় [1, 2] — যদিও কেউ আগের list পাঠায়নি। আরও বিভ্রান্তিকর: print(add(1), add(2)) ছাপে [1, 2] [1, 2], কারণ দুটি ডাকই একই object ফেরত দেয়, আর ছাপার আগে দুটি ডাকই শেষ হয়ে যায়।

প্রচলিত সমাধান হলো sentinel হিসেবে None: def add(x, items=None): তারপর if items is None: items = []. এখানে is None লিখুন, if not items নয় — নইলে কেউ ইচ্ছা করে খালি list পাঠালে সেটিও বদলে নতুন list হয়ে যেত। dataclass-এ ভাষা নিজেই রক্ষা করে: xs: list = [] লিখলে class তৈরির সময়েই ValueError আসে আর default_factory ব্যবহার করতে বলে — field(default_factory=list) (পর্ব ৩২)। একই ধরনের ভুল class attribute-এ: class-এর গায়ে members = [] লিখলে সব object একটিই list ভাগ করে।

মজার ব্যাপার, কনটেস্টে কেউ কেউ ইচ্ছা করেই এই আচরণ কাজে লাগান — def fib(n, memo={}): — যাতে ডাকের মাঝে ফল মনে থাকে। এটি চলে, কিন্তু ইন্টারভিউতে লুকানো অবস্থা হিসেবে খারাপ সংকেত; একই কাজ @functools.cache পরিষ্কারভাবে করে। immutable default (0, "", None, tuple) নিয়ে চিন্তা নেই, কারণ সেগুলো বদলানোই যায় না।

মূল উপলব্ধি: default মান তৈরি হয় একবার, def-এর সময় — তাই mutable default মানে সব ডাকের মধ্যে ভাগ করা একটি object. None দিয়ে শুরু করে ভেতরে নতুন বানান।

প্র ০৫কোড লেখা শেষ। ইন্টারভিউতে চালানো ছাড়া, জোরে বলে কীভাবে নিজের কোড পরীক্ষা করবেন? (সহজ)

হোয়াইটবোর্ড বা শেয়ার করা editor-এ প্রায়ই কোড চালানো যায় না। তাই “পরীক্ষা” মানে নিজের মাথায় আর মুখে চালানো — এবং এটা ইন্টারভিউয়ারের সামনে করাটাই মূল্যায়নের অংশ। একটি বাঁধা ক্রম রাখুন:

১. একটি সাধারণ উদাহরণ, লাইন ধরে। ভ্যারিয়েবলের মানগুলো পাশে একটি ছোট টেবিলে লিখুন — ঠিক অংশ ৩-এর stepper-এর মতো। জোরে বলুন: “right = 2, 'b' আগে ছিল 1-এ, যা left-এর সমান বা বেশি, তাই left হয় 2…”

২. Edge case-এর তালিকা। খালি ইনপুট ([], "", None), এক উপাদান, সব একই, সব ভিন্ন, ঋণাত্মক, ডুপ্লিকেট, বিশাল আকার, Unicode. প্রতিটির জন্য কোডের কোন লাইন কী করবে, মুখে বলুন। বেশিরভাগ বাগ এখানেই ধরা পড়ে।

৩. সীমানা। range(n) না range(n + 1)? slicing-এর শেষ index বাদ যায় — s[i:j]-এ j নেই। খালি list-এ a[0], a[-1] বা pop() ডাকা হচ্ছে কি না।

৪. Python-নির্দিষ্ট চেক। লুপ চলাকালীন একই list বা dict বদলানো হচ্ছে কি না (dict-এ RuntimeError, list-এ উপাদান লাফিয়ে যাওয়া); একটি generator দুবার পড়া হচ্ছে কি না; mutable default বা alias-এর কারণে অন্য কোথাও ডেটা বদলাচ্ছে কি না (পর্ব ১৯)।

নিজের বাগ নিজে ধরলে বলুন, “এখানে off-by-one আছে, ঠিক করছি” — এটা দুর্বলতা নয়, খুব ইতিবাচক সংকেত। চালানোর সুযোগ পেলে ফাংশনের নিচে কয়েকটি assert লিখে চালান: assert two_sum([3, 3], 6) == (0, 1). মনে রাখবেন, python -O দিলে assert বাদ পড়ে, তাই আসল যাচাইয়ের জন্য নয়, শুধু test-এর জন্য। বাড়িতে অনুশীলনের সময় pytest-এর @pytest.mark.parametrize দিয়ে অনেক ইনপুট একসাথে পরীক্ষা করুন (পর্ব ৩৯)। আরও এক ধাপ: ধীর কিন্তু নিশ্চিত brute force-এর সাথে এলোমেলো ইনপুটে ফল মিলিয়ে দেখা — stress testing (পর্ব ৪৫)।

মূল উপলব্ধি: “শেষ” বলার আগে পরীক্ষা করা অভ্যাস — ইন্টারভিউয়ার সেই অভ্যাসটাই নিয়োগ দিতে চান, কারণ বাস্তব কাজে কেউ আপনার পেছনে বসে test case দেবে না।

প্র ০৬GIL কী? থাকলে thread কি অকেজো? free-threaded Python কী বদলাচ্ছে? (মাঝারি → কঠিন)

GIL (Global Interpreter Lock) হলো CPython-এর ভেতরের একটি তালা: একটি প্রক্রিয়ায় একসাথে মাত্র একটি thread Python bytecode চালাতে পারে। কেন? CPython প্রতিটি object-এর reference গোনে, আর সেই গণনা একাধিক thread একসাথে বদলালে ভুল হতো — হয় memory leak, নয়তো ব্যবহৃত object মুছে যাওয়া। প্রতিটি object-এ আলাদা তালা দেওয়ার চেয়ে পুরো interpreter-এ একটি তালা সহজ ও single-thread কোডে দ্রুত ছিল। মনে রাখুন, GIL ভাষার নিয়ম নয়, CPython-এর বাস্তবায়নের অংশ — Jython বা IronPython-এ GIL ছিল না।

তাহলে thread কি অকেজো? না। ফাইল, নেটওয়ার্ক বা database-এর জন্য অপেক্ষার সময় thread GIL ছেড়ে দেয়, তাই অনেকগুলো ওয়েবসাইট থেকে ডেটা আনার মতো I/O-নির্ভর কাজে thread (বা asyncio, পর্ব ৪২) দারুণ কাজ করে। NumPy-র মতো C-তে লেখা লাইব্রেরিও ভারী হিসাবের সময় প্রায়ই GIL ছেড়ে দেয়। সমস্যা হয় খাঁটি Python-এ CPU-নির্ভর কাজে — চারটি thread চালালেও সাধারণ build-এ সময় প্রায় কমে না। তখন multiprocessing বা ProcessPoolExecutor: আলাদা প্রক্রিয়া, প্রত্যেকের নিজস্ব interpreter ও GIL (পর্ব ৪১)।

একটি সাধারণ ভুল ধারণা: “GIL আছে, তাই আমার কোড thread-safe.” না। counter += 1 কয়েকটি bytecode-এ ভাঙে — পড়া, যোগ, লেখা — আর মাঝখানে thread বদলাতে পারে, ফলে বৃদ্ধি হারাতে পারে। ভাগ করা অবস্থা বদলাতে threading.Lock লাগে; কাজ ভাগ করতে queue.Queue.

free-threaded Python: Sam Gross-এর প্রস্তাব PEP 703 গৃহীত হওয়ার পর Python 3.13-এ GIL ছাড়া একটি আলাদা build পরীক্ষামূলকভাবে আসে, আর Python 3.14-এ সেটি আনুষ্ঠানিকভাবে সমর্থিত (PEP 779) — তবে এখনো ডিফল্ট নয়; আলাদা করে install করতে হয়। এতে CPU-নির্ভর thread সত্যিই একাধিক core-এ একসাথে চলতে পারে, বিনিময়ে single-thread কোড কিছুটা ধীর হতে পারে, আর সব C extension এখনো প্রস্তুত নয়। ইন্টারভিউতে এই সূক্ষ্মতা বললে বোঝা যায় আপনার জ্ঞান হালনাগাদ।

মূল উপলব্ধি: I/O-নির্ভর কাজে thread বা asyncio, CPU-নির্ভর কাজে process (বা free-threaded build); আর GIL থাকলেও ভাগ করা ডেটায় তালা লাগে।

৮ · কথা বলা, ক্যাপস্টোন ব্যাখ্যা আর AI

একই সমাধান জানা দুজন প্রার্থীর একজন নির্বাচিত হন, অন্যজন হন না — প্রায়ই পার্থক্যটা যোগাযোগে। এই দক্ষতাও অনুশীলনে বাড়ে, ঠিক কোডের মতো।

ইন্টারভিউর ঘরে পাঁচটি অভ্যাস

  • জোরে ভাবুন: “প্রথমে brute force ভাবছি… এটা O(n²), n যদি এক লাখ হয় তাহলে ধীর… তাই dict ভাবছি।” ইন্টারভিউয়ার আপনার পথ দেখতে পেলে সাহায্যও করতে পারেন।
  • প্রশ্ন করুন, ধরে নেবেন না: “ইনপুট কি সাজানো?”, “খালি list আসতে পারে?”, “বাংলা text আসবে?” — এগুলো দুর্বলতা নয়, পেশাদারিত্ব।
  • Hint-কে উপহার ভাবুন: ইন্টারভিউয়ার “যদি list-টা সাজানো থাকত?” বললে সেটা একটা দরজা। ধন্যবাদ দিয়ে ধরুন, আর বলুন hint থেকে কী বুঝলেন।
  • ভুল স্বীকার করুন, দ্রুত: dry run-এ নিজের বাগ ধরলে সাথে সাথে বলুন। নিজের ভুল নিজে ধরা ইন্টারভিউতে খুব ইতিবাচক সংকেত।
  • না জানলে সৎ থাকুন: “asyncio-র TaskGroup নিয়ে বড় কাজ করিনি, তবে যতটুকু জানি…” — বানিয়ে বলা অভিজ্ঞ ইন্টারভিউয়ার সাথে সাথে ধরে ফেলেন।

নিজের ক্যাপস্টোন প্রজেক্ট কীভাবে ব্যাখ্যা করবেন

“আপনার একটি প্রজেক্টের কথা বলুন” — এই প্রশ্নটি প্রায় নিশ্চিত। পর্ব ৫১-এর ক্যাপস্টোন (বা পর্ব ২১-এর CLI টুল) এখানে আপনার সবচেয়ে বড় সম্পদ, কারণ এটি আপনি নিজে বানিয়েছেন। পাঁচ মিনিটের একটি কাঠামো তৈরি রাখুন:

  1. সমস্যা ও ব্যবহারকারী (৩০ সেকেন্ড): প্রজেক্টটি কী করে, কার জন্য।
  2. কাঠামো (১ মিনিট): প্রধান module ও class-গুলো, ডেটা কোথা থেকে কোথায় যায় — ফাইল, API, database. খাতায় তিন-চারটি বাক্স এঁকে দেখাতে পারলে আরও ভালো।
  3. দুটি সিদ্ধান্ত ও তার কারণ (২ মিনিট): “ডেটা মডেল dataclass-এ রেখেছি, যাতে type hint আর যাচাই এক জায়গায় থাকে”; “বড় ফাইল generator দিয়ে পড়েছি, যাতে মেমরি না ফুরায়।” trade-off বলুন — কী ছেড়ে দিয়েছেন।
  4. একটি কঠিন বাগ (১ মিনিট): লক্ষণ, কীভাবে খুঁজলেন (traceback, logging, debugger, test), আসল কারণ, কী শিখলেন।
  5. পরের উন্নতি (৩০ সেকেন্ড): আরও সময় পেলে কী বদলাতেন — আরও test, mypy, ভালো error message. এটা দেখায় আপনি নিজের কাজের দুর্বলতা চেনেন।

সততার নিয়ম: যে অংশ টিউটোরিয়াল, বন্ধু বা AI-এর সাহায্যে লিখেছেন, সেটা লুকাবেন না — বলুন কোন অংশ নিজে বুঝে বদলেছেন। ইন্টারভিউয়ার প্রায়ই প্রজেক্টের যেকোনো লাইন দেখিয়ে জিজ্ঞেস করেন, “এটা কেন?” যে কোড আপনি ব্যাখ্যা করতে পারেন না, তা আপনার পক্ষে নয়, বিপক্ষে কাজ করে। “দলগত প্রজেক্টে আমার অংশ ছিল…” — নিজের অবদান ঠিক যতটুকু, ততটুকুই বলুন।

আচরণগত প্রশ্নে STAR কাঠামো

“এমন একটা সময়ের কথা বলুন যখন…” — এ ধরনের প্রশ্নে STAR কাঠামোয় উত্তর গোছান: Situation (পরিস্থিতি), Task (আপনার দায়িত্ব), Action (আপনি নিজে কী করলেন), Result (ফল, আর কী শিখলেন)। ৪–৫টি সত্যি গল্প আগে থেকে তৈরি রাখুন: একটি কঠিন বাগ, একটি দলগত মতবিরোধ, একটি ব্যর্থতা ও তা থেকে শেখা, নিজে থেকে নেওয়া একটি উদ্যোগ। বানানো গল্প নয় — পরের প্রশ্নেই ফাঁক বেরিয়ে যায়।

একটি নমুনা: “ক্যাপস্টোন প্রজেক্টে আমার রিপোর্টে মাঝে মাঝে আগের ব্যবহারকারীর ডেটা ঢুকে যাচ্ছিল (S). কারণ খুঁজে ঠিক করা আমার দায়িত্ব ছিল (T). logging দিয়ে দেখলাম, একটি ফাংশনের default argument ছিল খালি list — সব ডাক একই list ভাগ করছিল। None দিয়ে শুরু করে ভেতরে নতুন list বানালাম, আর দুবার ডাকার একটি pytest test যোগ করলাম (A). বাগ বন্ধ হলো; তারপর থেকে code review-তে আমি প্রথমেই mutable default খুঁজি (R).”

AI-এর যুগে ইন্টারভিউ: নিয়ম আর সততা

ইন্টারভিউ চলাকালীন AI-এর সাহায্য অনেক প্রতিষ্ঠানেই নিষিদ্ধ — live coding-এ, online assessment-এ, অনেক সময় take-home-এও। কিছু প্রতিষ্ঠান আবার AI ব্যবহারের অনুমতি দিয়ে দেখতে চায় আপনি সেটি কীভাবে যাচাই করেন। নিয়ম প্রতিষ্ঠানভেদে আলাদা আর সময়ের সাথে বদলায় — তাই আগে জিজ্ঞেস করে নিন, আর অনুমতি না থাকলে ব্যবহার করবেন না। নিষেধ ভেঙে ধরা পড়লে শুধু ওই ইন্টারভিউ নয়, ভবিষ্যতের সুযোগও নষ্ট হতে পারে।

কেন মুখস্থ নয়, বোঝা? জনপ্রিয় সমস্যার সমাধান অনলাইনে আছে, ইন্টারভিউয়ার তা জানেন। তাই আসল পরীক্ষা হয় ফলো-আপে: “এবার thread-safe করুন”, “ইনপুট stream-এ এলে?”, “বাংলা text হলে?” মুখস্থ সমাধান এখানেই ভেঙে পড়ে। প্রস্তুতির সময় অবশ্য AI হতে পারে চমৎকার mock interview-এর সঙ্গী আর code review-এর সাহায্যকারী — শর্ত: উত্তর নয়, প্রশ্ন আর মূল্যায়ন চাইবেন।

নিচের prompt দিয়ে একটি পূর্ণ mock interview চালান। AI-কে স্পষ্ট বলা হয়েছে সমাধান না দিতে, আর শেষে কঠোরভাবে মূল্যায়ন করতে:

তুমি একজন অভিজ্ঞ Python ইঞ্জিনিয়ার, আমার কোডিং ইন্টারভিউ নিচ্ছ। আমি জুনিয়র পদের প্রার্থী, Python 3.12 বা নতুন সংস্করণ ব্যবহার করব। নিয়ম: ১. একটি মাঝারি মানের সমস্যা দাও (অ্যারে, স্ট্রিং, হ্যাশিং, স্ট্যাক/কিউ, ট্রি, অথবা decorator বা context manager-এর মতো একটি Python ডিজাইন প্রশ্ন)। ইচ্ছা করে কিছু তথ্য অস্পষ্ট রাখো, যাতে আমাকে প্রশ্ন করতে হয়। ২. আমি না চাইলে কোনো hint বা সমাধান দেবে না। আটকে গেলে একবারে শুধু একটি ছোট hint. ৩. আমি পদ্ধতি বললে জিজ্ঞেস করবে সময় ও মেমরির জটিলতা কত, আর কেন — built-in ফাংশনের খরচসহ। ৪. আমার কোডে Python-এর প্রশ্ন করবে: কোন data structure কেন (list, dict, set, deque, heapq), mutable default বা alias-এর ঝুঁকি আছে কি না, generator দুবার পড়া হচ্ছে কি না, কোড Pythonic কি না। ৫. নিজে কোড চালানোর বদলে আমাকে একটি edge case দিয়ে dry run করতে বলবে। ৬. এরপর অন্তত দুটি ফলো-আপ (শর্ত বদলে)। ৭. শেষে ছয়টি দিকে ১ থেকে ৫ নম্বর দেবে: সমস্যা বোঝা, যোগাযোগ, সঠিকতা, জটিলতা, টেস্টিং, Python কোডের মান — প্রতিটির জন্য একটি নির্দিষ্ট উন্নতির পরামর্শ। প্রশংসা নয়, সৎ মূল্যায়ন চাই। প্রথম সমস্যা দিয়ে শুরু করো।

নিজের লেখা সমাধান যাচাই করাতে চাইলে আলাদা prompt দিন: “এই Python সমাধানের বাগ, অনাবৃত edge case আর জটিলতার ভুল খুঁজে দাও; ঠিক করা কোড লিখবে না, শুধু কোন লাইনে কী সমস্যা বলবে।” এতে ঠিক করার কাজটা আপনার হাতেই থাকে।

AI দিয়ে অনুশীলনের চেকলিস্ট

৯ · কোর্স সমাপ্তি — ৫৫টি পর্বের পরে আপনি কোথায়

এটি ৮টি মডিউল আর ৫৫টি পর্বের এই কোর্সের শেষ পর্ব। একবার পেছনে তাকান। পর্ব ০১-এ প্রশ্ন ছিল, “Python কেন?” পর্ব ০৪-এ লিখেছিলেন প্রথম print, আর পর্ব ০৫-এ দেখেছিলেন Python আপনার কোড কীভাবে bytecode বানিয়ে চালায়। আজ আপনি একটি LRU cache-এর জন্য ঠিক data structure বাছতে পারেন, mutable default-এর ফাঁদ ব্যাখ্যা করতে পারেন, GIL নিয়ে হালনাগাদ উত্তর দিতে পারেন, আর একটি অচেনা সমস্যাকে সাত ধাপে ভেঙে সমাধান করতে পারেন। আন্তরিক অভিনন্দন — পুরো Python কোর্স শেষ করা সত্যিই বড় অর্জন।

এখন আপনি যা পারেন

  • নাম, object, mutability আর alias দিয়ে Python-এর মেমরির ছবিটা নিজের ভাষায় ব্যাখ্যা করতে।
  • list, tuple, dict, set, comprehension আর collections দিয়ে কম কোডে নির্ভুল সমাধান লিখতে — আর প্রতিটির খরচ বলতে।
  • module, package, virtual environment, exception, ফাইল ও JSON দিয়ে বড় প্রোগ্রাম গোছাতে।
  • class, dunder method, dataclass, Protocol, generator, decorator আর context manager দিয়ে Pythonic ডিজাইন করতে।
  • type hint, pytest, logging, concurrency, asyncio আর profiling দিয়ে পেশাদার মানের একটি প্রজেক্ট দাঁড় করাতে।
  • AI-এর লেখা Python কোড যাচাই করতে — আর দরকারে তার ভুল ধরতে।

পরীক্ষা ও সার্টিফিকেট

লগইন করলে ড্যাশবোর্ডে শেখার সাতটি মডিউলের (পর্ব ০১–৫১) প্রতিটির একটি টেস্ট আর একটি ফাইনাল পরীক্ষা পাবেন। শেখার এই সব পর্ব (০১–৫১) পড়া শেষ করে ফাইনালে ৬০% বা তার বেশি পেলে একটি ফ্রি কোর্স-সমাপ্তির সার্টিফিকেট পাবেন। প্রতিটি পর্বের “পড়েছি” বোতাম চাপা থাকলে আপনার অগ্রগতি সেখানে দেখা যাবে। পরীক্ষার আগে পর্ব ৫২–৫৪-এর প্রশ্নব্যাংক একবার ঘুরে আসুন।

অনুশীলন চালিয়ে যাওয়ার উপায়

  • প্রতিদিন অল্প: দিনে একটি সমস্যা, পুরো সাত ধাপে — সপ্তাহে একদিন বড় সেশনের চেয়ে বেশি কাজে দেয়। LeetCode, HackerRank, Codeforces আর বাংলাদেশে জনপ্রিয় Toph-এর মতো প্ল্যাটফর্মে এ ধরনের অনেক সমস্যা পাবেন।
  • একটি ভুলের খাতা: প্রতিটি ভুলের পর তিন লাইন — কোন ধারণা মিস করেছিলাম, কোন edge case ভুলেছিলাম, পরেরবার কোন সংকেত দেখে চিনব।
  • ক্যাপস্টোন বড় করুন: নতুন একটি feature, আরও test, একটি পরিষ্কার README — GitHub-এ রাখলে portfolio নিজে থেকেই বড় হবে।
  • অন্যকে শেখান: বন্ধুকে generator বা decorator বোঝাতে গেলেই বুঝবেন নিজের কোথায় ফাঁক আছে।
  • মাঝে মাঝে ফিরে আসুন: কোর্স হোম থেকে যে পর্ব দুর্বল লাগে সেটি আবার পড়ুন।

এরপর কোন পথে

Python for AI

Python জানা হলো — এবার NumPy, pandas আর AI-এর কাজে Python. ডেটা ও AI পথের স্বাভাবিক পরের ধাপ।

Data Structures & Algorithms

ইন্টারভিউর জন্য আরও গভীর ডেটা স্ট্রাকচার, গ্রাফ অ্যালগরিদম আর DP-র ধরন।

Machine Learning

ডেটা থেকে শেখা মডেল — Python-এর সবচেয়ে বড় ব্যবহারক্ষেত্রগুলোর একটি।

Software Engineering & Git

দলে কাজ, code review, version control — প্রজেক্টকে পেশাদার মানে নিয়ে যাওয়া।

ব্যাকএন্ড বা বড় সিস্টেমের দিকে যেতে চাইলে System Design, আর ডেটার দিকে Data Science কোর্সও দেখতে পারেন।

কোড লেখা শুরু মাত্র।
বোঝা, যাচাই করা আর ব্যাখ্যা করতে পারা — এগুলোই আপনাকে ইঞ্জিনিয়ার বানায়।
শেষ কথা

৫৫টি পর্ব শেষ করা সহজ কাজ নয় — আপনার ধৈর্য আর পরিশ্রমকে আন্তরিক অভিনন্দন। প্রথম IndentationError, প্রথম অদ্ভুত alias-বাগ, প্রথম লম্বা traceback — সেগুলোর সামনে যে বিভ্রান্তি ছিল, তা-ই আজ আপনার শক্তি। ইন্টারভিউতে একবার বা দুবার ব্যর্থ হওয়া খুব স্বাভাবিক; প্রতিটি থেকে একটি শিক্ষা খাতায় লিখে নিন, আর পরেরটিতে আরও প্রস্তুত হয়ে যান। শুভকামনা।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমকোডিং ইন্টারভিউতে একটি সমস্যা পেলে ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ❓ প্রশ্ন করে সমস্যা ও সীমা পরিষ্কার করা
  2. 📝 নিজে উদাহরণ ও edge case লেখা
  3. 🐢 একটি সঠিক brute force সমাধান মুখে বলা
  4. 🧭 বারবার হওয়া কাজ খুঁজে উন্নত পদ্ধতি বাছা
  5. ⌨️ ইন্টারভিউয়ারের সম্মতি নিয়ে কোড লেখা
  6. 🔍 নিজের উদাহরণ দিয়ে dry run, তারপর জটিলতা বলা

(মাঝারি) Python-এ get ও put দুটোই O(1) রেখে একটি LRU cache বানাতে standard library-র কোন টাইপ সবচেয়ে উপযুক্ত?

  • list, প্রতিটি উপাদান (key, value) tuple
  • heapq দিয়ে বানানো heap
  • collections.OrderedDict
  • set

(মাঝারি) Python-এর dict-এ একটি key খোঁজার গড় ও খারাপ-ক্ষেত্রের সময়-জটিলতা কত?

  • গড়ে O(1), খারাপ ক্ষেত্রে O(log n)
  • গড়ে O(1), খারাপ ক্ষেত্রে O(n)
  • গড়ে O(log n), খারাপ ক্ষেত্রে O(n)
  • সবসময় O(1)

(মাঝারি) n টি সংখ্যা থেকে k-তম বৃহত্তম বের করতে heapq দিয়ে k আকারের min-heap রাখলে (আকার k ছাড়ালেই heappop) সময় কত?

  • O(n)
  • O(n log n)
  • O(n · k)
  • O(n log k)

(সহজ) Python-এ একটি queue-এর সামনে থেকে বারবার উপাদান বের করতে হবে। কোনটি সবচেয়ে উপযুক্ত?

  • collections.deque-এর popleft()
  • list-এর pop(0)
  • list-এর pop()
  • set-এর pop()

(সহজ) একটি লুপের ভেতরে বারবার জানতে হবে “x কি এই এক লাখ সংখ্যার মধ্যে আছে?” সংখ্যাগুলো কোন রূপে রাখা উচিত?

  • list
  • set
  • tuple
  • কমা দিয়ে জোড়া একটি string

(মাঝারি) পুনরাবৃত্তিহীন দীর্ঘতম substring-এর sliding window সমাধানে (left শুধু সামনে যায়, শেষ অবস্থান dict-এ) সময়-জটিলতা কত?

  • O(n²)
  • O(n log n)
  • O(n)
  • O(1)

(মাঝারি) ডিফল্ট সেটিংসে CPython-এ একটি recursive ফাংশন ১০,০০০ স্তর গভীরে যেতে চাইলে সাধারণত কী হয়?

  • Python নিজে থেকে লুপে রূপান্তর করে চালিয়ে দেয়
  • কোনো বার্তা ছাড়াই প্রোগ্রাম crash করে
  • ধীরে হলেও ঠিকঠাক শেষ হয়
  • RecursionError ওঠে

(কঠিন) def add(x, items=[]): items.append(x); return items — এরপর print(add(1)) আর তারপর print(add(2)) চালালে দুই লাইনে কী ছাপা হবে?

  • [1] এবং [2]
  • [1] এবং [1, 2]
  • TypeError
  • [1, 2] এবং [1, 2]

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব শব্দ এখানে একসাথে:

মনে রাখুন

  1. ইন্টারভিউয়ার দেখেন বোঝা, যোগাযোগ, সঠিকতা, জটিলতা, টেস্টিং আর Python কোডের মান — শুধু শেষ কোড নয়।
  2. প্রতিটি সমস্যায় সাত ধাপ: বুঝুন → উদাহরণ → brute force → উন্নত পদ্ধতি → কোড → পরীক্ষা → জটিলতা ও ফলো-আপ.
  3. “কোন কাজ বারবার হচ্ছে?” — dict/set, sliding window, stack/deque, heapq, bisect বা DP দিয়ে সেটি দ্রুত করুন, আর built-in-এর খরচসহ trade-off বলুন।
  4. Python-এর নিজস্ব ফাঁদ চিনুন: mutable default, alias, একবার-চলা generator, recursion limit, Unicode-এর code point; ডিজাইনে OrderedDict, decorator, context manager আর dataclass.
  5. মুখস্থ নয়, বুঝুন — ফলো-আপে বোঝাই কাজে আসে; নিজের প্রজেক্ট সৎভাবে ব্যাখ্যা করুন, আর ইন্টারভিউর সময় AI-এর নিয়ম আগে জেনে নিন।

এরপর: এটিই কোর্সের শেষ পর্ব — আর কোনো পরের পর্ব নেই। কোর্স হোম-এ ফিরে পুরো রোডম্যাপ দেখুন, ড্যাশবোর্ডে মডিউল টেস্ট ও ফাইনাল পরীক্ষা দিন, আর বেছে নিন ABCL TECH-এর পরের কোর্স — যেমন Python for AI.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান