এই পর্বে যা শিখবেন
- টেকনিক্যাল ইন্টারভিউ কোন কোন ধাপে হয়, আর ইন্টারভিউয়ার আসলে কী কী দেখেন
- যেকোনো নতুন সমস্যায় প্রয়োগ করা যায় এমন একটি সাত ধাপের পদ্ধতি
- অ্যারে, স্ট্রিং, পয়েন্টার, লিংকড লিস্ট, স্ট্যাক, হিপ, সার্চিং, বিট ও DP-র ১৬টি ক্লাসিক সমস্যা
- C-নির্দিষ্ট ধারণার প্রশ্নে সংক্ষিপ্ত অথচ গভীর উত্তর দেওয়া
- ভাবনা জোরে বলা, hint নেওয়া, ভুল স্বীকার করা — আর একটি বাস্তব অনুশীলন পরিকল্পনা
১ · টেকনিক্যাল ইন্টারভিউ আসলে কীভাবে হয়
পর্ব ৫৫-এ দেখেছেন লিখিত ও মৌখিক পরীক্ষায় কীভাবে উত্তর গোছাতে হয়। সফটওয়্যার কোম্পানির technical interview আরেকটু আলাদা। এখানে শুধু “উত্তর জানেন কি না” দেখা হয় না। দেখা হয়, অচেনা একটি সমস্যা পেলে আপনি কীভাবে ভাবেন, কীভাবে কথা বলেন, আর কতটা নির্ভরযোগ্য কোড লেখেন।
দেশি স্টার্টআপ, সফটওয়্যার ফার্ম, ব্যাংকের আইটি বিভাগ বা বহুজাতিক কোম্পানি — প্রতিষ্ঠানভেদে ধাপের নাম ও সংখ্যা আলাদা। তবে সারা বিশ্বেই কাঠামোটা মোটামুটি এক রকম। সাধারণত যে ধাপগুলো দেখা যায়:
| ধাপ | কী হয় | প্রস্তুতি |
|---|---|---|
| Phone / HR screen | ১৫–৩০ মিনিটের কথা: আপনার পড়াশোনা, প্রজেক্ট, আগ্রহ। কখনো একটি সহজ টেকনিক্যাল প্রশ্ন। | নিজের প্রজেক্ট দুই মিনিটে গুছিয়ে বলার অনুশীলন |
| Online assessment (OA) | সময় বাঁধা অনলাইন পরীক্ষা: ২–৪টি কোডিং সমস্যা, কখনো C, OS, DBMS-এর MCQ. লুকানো test case দিয়ে যন্ত্র নম্বর দেয়। অনেক প্রতিষ্ঠান এর বদলে অফিসে লিখিত পরীক্ষা নেয়। | Online judge-এ সময় ধরে সমাধান; edge case-এর অভ্যাস |
| Live coding | ৪৫–৬০ মিনিট, একজন ইন্টারভিউয়ার, শেয়ার করা editor বা হোয়াইটবোর্ড। ১–২টি সমস্যা, সাথে আলোচনা। | জোরে ভেবে সমাধান করা — এই পাতার মূল অনুশীলন |
| System design (lite) | নতুনদের জন্য হালকা সংস্করণ: একটি ring buffer, logger, rate limiter বা ছোট key-value store-এর নকশা। | পর্ব ৫২-এর ক্যাপস্টোন নিজের মুখে ব্যাখ্যা করা |
| Behavioural | দলে কাজ, মতবিরোধ, ব্যর্থতা থেকে শেখা, কেন এই প্রতিষ্ঠান। | STAR কাঠামোয় ৪–৫টি সত্যি গল্প তৈরি রাখা |
| Take-home assignment | কিছু প্রতিষ্ঠান কয়েক দিনের সময় দিয়ে ছোট একটি প্রজেক্ট বানাতে দেয়, পরে সেটি নিয়ে আলোচনা। | README, test আর পরিষ্কার commit — পর্ব ৩২ ও ৫২ |
ইন্টারভিউয়ার শুধু শেষ কোড দেখেন না
অনেকে ভাবেন, “সঠিক কোড লিখলেই পাস।” বাস্তবে একটি ইন্টারভিউর মূল্যায়ন-পত্রে সাধারণত কয়েকটি আলাদা ঘর থাকে। ঠিক কোড লিখেও কেউ বাদ পড়তে পারেন, আবার কোড শেষ না করেও কেউ ভালো নম্বর পেতে পারেন — যদি বাকি ঘরগুলোতে তিনি ভালো করেন।
সমস্যা বোঝা
লেখা শুরুর আগে প্রশ্ন করে অস্পষ্টতা দূর করেছেন কি? ইনপুটের সীমা, খালি ইনপুট, ঋণাত্মক সংখ্যা — এগুলো নিজে তুলেছেন কি?
যোগাযোগ
ভাবনা জোরে বলেছেন কি? ইন্টারভিউয়ার আপনার মাথার ভেতর দেখতে পান না — আপনি যা বলেন, সেটুকুই তিনি মূল্যায়ন করতে পারেন।
সঠিকতা ও জটিলতা
সমাধান সব ইনপুটে ঠিক কি? সময় ও মেমরির জটিলতা নিজে বলতে পেরেছেন কি? আরও ভালো পদ্ধতি আছে কি না ভেবেছেন?
টেস্টিং ও কোডের মান
নিজে উদাহরণ দিয়ে কোড চালিয়ে দেখেছেন কি? নাম পরিষ্কার, ফাংশন ছোট, মেমরি ফেরত দেওয়া হয়েছে কি?
ইন্টারভিউ একটি পরীক্ষা নয়, একটি কাজের নমুনা। ইন্টারভিউয়ার মনে মনে প্রশ্ন করেন: “এই মানুষটির সাথে একটি কঠিন বাগ নিয়ে এক ঘণ্টা বসলে কেমন লাগবে?” তাই চুপচাপ নিখুঁত কোডের চেয়ে, কথা বলে ধাপে ধাপে এগোনো ভালো কোড প্রায়ই বেশি মূল্য পায়।
এই পাতার ১৬টি সমস্যা
| বিষয় | সমস্যা | রিভিশনের পর্ব |
|---|---|---|
| অ্যারে ও স্ট্রিং (অংশ ৩) | ১. স্ট্রিং উল্টানো · ২. palindrome · ৩. প্রথম অপুনরাবৃত্ত অক্ষর · ৪. Two sum · ৫. ডুপ্লিকেট বাদ ও শূন্য সরানো · ৬. অ্যারে ও ম্যাট্রিক্স ঘোরানো | ১৬, ১৯, ২১, ৩৭ |
| পয়েন্টার ও মেমরি (অংশ ৪) | ৭. strlen, strcpy, memcpy, memmove · ৮. ওভারফ্লো-নিরাপদ atoi | ১৮, ৪৫, ৮ |
| লিংকড লিস্ট, স্ট্যাক, হিপ (অংশ ৫) | ৯. লিস্ট উল্টানো · ১০. cycle শনাক্ত · ১১. দুটি সাজানো লিস্ট জোড়া · ১২. বন্ধনী মেলানো · ১৩. k-তম বৃহত্তম | ৩৩, ৩৪, ৩৬ |
| সার্চিং, বিট, DP (অংশ ৬) | ১৪. binary search-এর রূপভেদ · ১৫. set bit গোনা ও ২-এর ঘাত · ১৬. সিঁড়ি ও কয়েন চেঞ্জ | ৪১, ৯, ৪২ |
২ · সমস্যা থেকে সমাধান — সাত ধাপের পদ্ধতি
ইন্টারভিউর চাপে মাথা ফাঁকা হয়ে যাওয়া খুব স্বাভাবিক। এর সবচেয়ে ভালো ওষুধ হলো একটি বাঁধা পদ্ধতি — যাতে কী করবেন ভাবতে না হয়, শুধু পরের ধাপে যেতে হয়। ক্রিকেটে ব্যাটসম্যান যেমন প্রতিটি বলের আগে একই রুটিন মানেন — গার্ড নেওয়া, ফিল্ডিং দেখা, তারপর স্টান্স — এটাও তেমন।
আন্তর্জাতিকভাবে এ ধরনের পদ্ধতি নানা নামে পরিচিত, যেমন UMPIRE (Understand, Match, Plan, Implement, Review, Evaluate). আমরা সহজ বাংলায় সাতটি ধাপ নিই:
এবার পদ্ধতিটি একটি বিখ্যাত সমস্যায় প্রয়োগ করি — Two sum: “একটি int অ্যারে আর একটি target দেওয়া আছে। এমন দুটি ভিন্ন index বের করুন, যাদের মানের যোগফল target.” নিচের ডায়াগ্রামে “পরে” চাপুন:
- ১ · বুঝুনপ্রশ্ন করে অস্পষ্টতা দূর
- ২ · উদাহরণ ও edge caseছোট ইনপুট হাতে লেখা
- ৩ · পদ্ধতিআগে brute force, তারপর উন্নত
- ৪ · জটিলতাসময় ও মেমরি, Big-O
- ৫ · কোডপরিষ্কার নাম, ছোট ফাংশন
- ৬ · পরীক্ষাউদাহরণ দিয়ে dry run
- ৭ · উন্নতিফলো-আপ ও trade-off
- ঠিক একটি জোড়া থাকবে, নাকি নাও থাকতে পারে? - একই index দুবার নেওয়া যাবে? (না) - অ্যারে কি সাজানো? (না) - n কত বড়? মান কত বড়? ঋণাত্মক আছে? - index ফেরত দেব, নাকি মান?
[2, 7, 11, 15], target 9 -> (0, 1) [3, 3], target 6 -> (0, 1) একই মান, ভিন্ন index [-3, 4, 3, 90], target 0 -> (0, 2) ঋণাত্মক [1, 2, 3], target 7 -> জোড়া নেই [5], target 10 -> জোড়া নেই 5+5 নেওয়া যাবে না
ক) Brute force: প্রতিটি জোড়া (i, j) যাচাই খ) সাজিয়ে দুই দিক থেকে দুই পয়েন্টার (আসল index মনে রাখতে হবে) গ) Hash table: প্রতিটি a[j]-এর জন্য খুঁজি "target - a[j] কি আগে দেখেছি?"
সময় বাড়তি মেমরি ক) Brute O(n²) O(1) খ) Sort + 2p O(n log n) O(n) (index জোড়া রাখতে) গ) Hash O(n) গড়ে O(n)
for j = 0 .. n-1:
need = target - a[j]
if need আছে hash-এ: return (hash[need], j)
hash[a[j]] = j // আগে খুঁজি, পরে রাখি
return "জোড়া নেই"
j=0: need=3, hash-এ নেই -> hash{3:0}
j=1: need=3, hash-এ আছে(0) -> (0, 1)
যদি আগে রাখতাম, পরে খুঁজতাম: [5], target 10
-> নিজের সাথেই জোড়া (0, 0) — ভুল!
- অ্যারে আগে থেকেই সাজানো হলে? -> দুই পয়েন্টার, O(1) মেমরি - সব জোড়া চাই? -> ডুপ্লিকেট সামলানো - তিনটি সংখ্যা (3-sum)? -> সাজিয়ে + দুই পয়েন্টার, O(n²) - target - a[j] কি overflow করতে পারে?
- বুঝুন। সমস্যার বিবরণ প্রায়ই ইচ্ছা করেই অসম্পূর্ণ রাখা হয় — আপনি প্রশ্ন করেন কি না, সেটাই দেখা হয়। এই clarifying question-গুলোর উত্তর পুরো সমাধান বদলে দিতে পারে। যেমন অ্যারে সাজানো থাকলে একদম অন্য, আরও সহজ পদ্ধতি চলে।
- উদাহরণ ও edge case. দেওয়া উদাহরণ ছাড়াও নিজে কয়েকটি লিখুন: একই মান দুবার, ঋণাত্মক, জোড়া নেই, একটিমাত্র উপাদান। এগুলোই পরে আপনার test case হবে। Edge case আগে লিখলে কোডে সেগুলো ভুলে যাওয়ার সম্ভাবনা কমে।
- পদ্ধতি — আগে brute force. সবচেয়ে সরল সমাধান মুখে বলুন, যদিও সেটি ধীর। এতে দেখান যে আপনি অন্তত একটি সঠিক উত্তর জানেন। তারপর জিজ্ঞেস করুন: “কোন কাজটি বারবার হচ্ছে?” এখানে “জোড়া খোঁজা” কাজটিকে hash table দ্রুত করে দেয় (পর্ব ৩৭)।
- জটিলতা। প্রতিটি পদ্ধতির সময় ও মেমরি Big-O-তে বলুন (পর্ব ৩৯)। লক্ষ করুন, দ্রুততম পদ্ধতিটি বাড়তি মেমরি খরচ করছে — এই trade-off নিজে থেকে বলা ভালো সংকেত। Hash-এর O(n) “গড়ে”, খারাপ ক্ষেত্রে collision বাড়লে ধীর হতে পারে।
- কোড। ইন্টারভিউয়ার রাজি হলে তবেই কোড লিখুন। প্রথমে কাঠামো, তারপর বিস্তারিত। একটি গুরুত্বপূর্ণ খুঁটিনাটি: আগে খুঁজি, তারপর রাখি — এতে একটি উপাদান নিজের সাথে জোড়া হয় না।
- পরীক্ষা। “শেষ” বলার আগে নিজের লেখা উদাহরণ দিয়ে কোডটি লাইন ধরে চালান — এটাই dry run, যা পর্ব ৫৩-এ অনুশীলন করেছেন। এখানে দেখা যাচ্ছে, ক্রম উল্টালে
[5], target 10-এ ভুল উত্তর আসত। - উন্নতি। ভালো ইন্টারভিউ এখানে শেষ হয় না — ইন্টারভিউয়ার শর্ত বদলে দেন। প্রতিটি follow-up-এ আবার একই সাত ধাপ, শুধু দ্রুত। এই পর্বের প্রতিটি সমস্যার শেষে এমন ফলো-আপ দেওয়া আছে।
C-তে কোনো তৈরি hash table নেই — এটাই C-ইন্টারভিউর বিশেষত্ব। ছোট ইনপুটের জন্য নিচের মতো একটি open addressing টেবিল নিজেই লিখে ফেলা যায়। অনেক ইন্টারভিউয়ার বলেন, “ধরে নিন একটি hash map আছে।” তখন শুধু মূল যুক্তিটি লিখলেই চলে।
#include <stdio.h>
#include <string.h>
#define CAP 64 /* power of two, more than 2 * n */
static int keys[CAP], idx[CAP], used[CAP];
static unsigned slot(int key) { /* linear probing */
unsigned h = (unsigned)key & (CAP - 1);
while (used[h] && keys[h] != key) h = (h + 1) & (CAP - 1);
return h;
}
int two_sum(const int *a, int n, int target, int *i_out, int *j_out) {
memset(used, 0, sizeof used);
for (int j = 0; j < n; j++) {
unsigned h = slot(target - a[j]); /* seen the partner before? */
if (used[h]) { *i_out = idx[h]; *j_out = j; return 1; }
h = slot(a[j]);
if (!used[h]) { used[h] = 1; keys[h] = a[j]; idx[h] = j; }
}
return 0;
}
int main(void) {
int a[] = {2, 7, 11, 15}, b[] = {3, 3}, c[] = {-3, 4, 3, 90}, d[] = {1, 2, 3}, i, j;
if (two_sum(a, 4, 9, &i, &j)) printf("%d %d\n", i, j);
if (two_sum(b, 2, 6, &i, &j)) printf("%d %d\n", i, j);
if (two_sum(c, 4, 0, &i, &j)) printf("%d %d\n", i, j);
if (!two_sum(d, 3, 7, &i, &j)) printf("no pair\n");
return 0;
}প্রত্যাশিত আউটপুট: 0 1, 0 1, 0 2, no pair. টেবিলের আকার ২-এর ঘাত (64) রাখায় % CAP-এর বদলে দ্রুত & (CAP - 1) ব্যবহার করা গেছে। ঋণাত্মক key-কে unsigned-এ রূপান্তর করায় index কখনো ঋণাত্মক হয় না।
সীমাবদ্ধতা যা নিজে বলবেন: টেবিল ভরে গেলে খোঁজার লুপ শেষ হবে না, তাই n-কে CAP-এর অর্ধেকের নিচে রাখতে হবে (load factor, পর্ব ৩৭)। আর target - a[j] চরম মানে signed overflow করতে পারে — সেটি undefined behavior, তাই বড় মানের জন্য long long-এ হিসাব করা নিরাপদ।
সময় ভাগ করার একটি মোটা হিসাব (৪৫ মিনিটের ইন্টারভিউ): বোঝা ও উদাহরণ ৫ মিনিট, পদ্ধতি ও জটিলতা ১০ মিনিট, কোড ১৫–২০ মিনিট, পরীক্ষা ৫ মিনিট, বাকিটা ফলো-আপ। কোড লিখতে শুরু করার আগে ইন্টারভিউয়ারের সম্মতি নিন: “এই পদ্ধতিতে কোড লিখি?”
৩ · অ্যারে ও স্ট্রিং — প্রথম সারির প্রশ্ন
প্রায় প্রতিটি ইন্টারভিউর প্রথম প্রশ্ন এই গোত্রের। দেখতে সহজ, কিন্তু C-তে null terminator, size_t-এর underflow আর সীমার বাইরে পড়ার ফাঁদ এখানেই সবচেয়ে বেশি। রিভিশন: পর্ব ১৬ · অ্যারে, পর্ব ১৯ · স্ট্রিং।
সমস্যা ১ · স্ট্রিং উল্টানো, বাড়তি মেমরি ছাড়া (সহজ)
প্রশ্ন: একটি C স্ট্রিং in-place উল্টে দিন — নতুন অ্যারে না বানিয়ে।
জিজ্ঞেস করে নিন: স্ট্রিংটি কি পরিবর্তনযোগ্য মেমরিতে আছে, নাকি string literal? NULL আসতে পারে? শুধু ASCII, নাকি UTF-8-এর বাংলা অক্ষরও? (বাংলা অক্ষর একাধিক byte-এ থাকে, byte উল্টালে অক্ষর ভেঙে যায়।)
পদ্ধতি ও জটিলতা: দুই প্রান্তে দুটি পয়েন্টার রাখুন, অদলবদল করে মাঝের দিকে আসুন। সময় O(n), বাড়তি মেমরি O(1). এটাই two-pointer কৌশলের সবচেয়ে সরল রূপ।
#include <stdio.h>
#include <string.h>
void reverse(char *s) {
if (s == NULL) return; /* defensive: NULL input */
size_t n = strlen(s);
if (n < 2) return; /* "" and "x" need no work */
char *l = s, *r = s + n - 1;
while (l < r) {
char t = *l;
*l++ = *r;
*r-- = t;
}
}
int main(void) {
char a[] = "Dhaka", b[] = "ab", c[] = "x", d[] = "";
reverse(a); reverse(b); reverse(c); reverse(d);
printf("[%s] [%s] [%s] [%s]\n", a, b, c, d);
reverse(NULL);
printf("NULL ok\n");
return 0;
}প্রত্যাশিত আউটপুট: [akahD] [ba] [x] [] তারপর NULL ok. n < 2 পরীক্ষাটি জরুরি: খালি স্ট্রিংয়ে s + n - 1 হতো অ্যারের শুরুর আগের ঠিকানা, যা বানানোই undefined behavior.
Edge case ও টেস্ট: খালি, এক অক্ষর, জোড় ও বিজোড় দৈর্ঘ্য, NULL. আর reverse("Dhaka") — literal-এ লেখা undefined behavior, প্রায়ই segfault (পর্ব ১৯)।
ফলো-আপ: একটি বাক্যের শব্দগুলোর ক্রম উল্টান (“I love Dhaka” → “Dhaka love I”). কৌশল: পুরো বাক্য উল্টে তারপর প্রতিটি শব্দ আবার উল্টানো — সমস্যা ৬-এও এই কৌশল কাজে লাগবে।
সমস্যা ২ · Palindrome যাচাই, যতিচিহ্ন বাদ দিয়ে (সহজ)
প্রশ্ন: একটি বাক্য palindrome কি না বলুন — শুধু অক্ষর ও অঙ্ক গুনবেন, বড়-ছোট হাতের পার্থক্য উপেক্ষা করবেন। “A man, a plan, a canal: Panama” হলো palindrome.
জিজ্ঞেস করে নিন: অঙ্কও গোনা হবে, নাকি শুধু অক্ষর? খালি স্ট্রিং বা শুধু যতিচিহ্ন থাকলে উত্তর কী? (সাধারণ রীতি: palindrome.)
পদ্ধতি ও জটিলতা: নতুন “পরিষ্কার” স্ট্রিং বানালে O(n) বাড়তি মেমরি লাগে। তার বদলে দুই পয়েন্টার অপ্রয়োজনীয় অক্ষর লাফিয়ে যায়। সময় O(n), মেমরি O(1).
#include <ctype.h>
#include <stdio.h>
#include <string.h>
int is_pal(const char *s) {
size_t i = 0, j = strlen(s); /* half-open: compare s[i], s[j-1] */
while (i < j) {
unsigned char a = (unsigned char)s[i];
unsigned char b = (unsigned char)s[j - 1];
if (!isalnum(a)) { i++; continue; }
if (!isalnum(b)) { j--; continue; }
if (tolower(a) != tolower(b)) return 0;
i++;
j--;
}
return 1;
}
int main(void) {
const char *t[] = { "A man, a plan, a canal: Panama", "race a car",
"", ".,!", "No 'x' in Nixon", "ab" };
for (int k = 0; k < 6; k++)
printf("%d \"%s\"\n", is_pal(t[k]), t[k]);
return 0;
}প্রত্যাশিত আউটপুট: ১, ০, ১, ১, ১, ০ — ক্রমে Panama, “race a car”, খালি, “.,!”, Nixon, “ab”. দুটি সূক্ষ্ম বিষয়: j-কে শেষ অক্ষরের পরের ঘর ধরা হয়েছে (half-open range), তাই খালি স্ট্রিংয়ে j - 1 কখনো ঋণাত্মক হয় না। আর isalnum-কে দেওয়ার আগে unsigned char-এ cast — ঋণাত্মক char পাঠানো undefined behavior.
ফলো-আপ: সর্বোচ্চ একটি অক্ষর মুছে palindrome বানানো যায় কি? (প্রথম অমিলে দুটি বিকল্প — বাঁ দিকেরটা বা ডান দিকেরটা বাদ — দুটোই যাচাই করুন, মোট O(n).)
সমস্যা ৩ · প্রথম অপুনরাবৃত্ত অক্ষর ও ডুপ্লিকেট খোঁজা (সহজ)
প্রশ্ন: স্ট্রিংয়ে যে অক্ষর ঠিক একবার আছে, তার মধ্যে প্রথমটির index দিন; না থাকলে -1. একই কৌশলে “কোন অক্ষর বা সংখ্যা একাধিকবার আছে” প্রশ্নেরও উত্তর হয়।
জিজ্ঞেস করে নিন: শুধু ছোট হাতের ইংরেজি অক্ষর, নাকি যেকোনো byte? বড়-ছোট হাত কি আলাদা?
পদ্ধতি ও জটিলতা: প্রতিটি অক্ষরের জন্য আবার পুরো স্ট্রিং গুনলে O(n²)। তার বদলে একটি frequency array — ২৫৬টি ঘরের একটি গোনার অ্যারে — দুবার পার হলেই হয়: সময় O(n), মেমরি O(1) (২৫৬ একটি ধ্রুবক)।
#include <stdio.h>
int first_unique(const char *s) {
int cnt[256] = {0}; /* one counter per byte value */
for (const char *p = s; *p; p++)
cnt[(unsigned char)*p]++;
for (int i = 0; s[i]; i++)
if (cnt[(unsigned char)s[i]] == 1) return i;
return -1;
}
int main(void) {
const char *t[] = { "leetcode", "loveleetcode", "aabb", "", "z" };
for (int k = 0; k < 5; k++)
printf("%-14s -> %d\n", t[k], first_unique(t[k]));
return 0;
}প্রত্যাশিত আউটপুট: leetcode -> 0, loveleetcode -> 2 (‘v’), aabb -> -1, খালি স্ট্রিং -1, z -> 0. গোনার অ্যারে আসলে সবচেয়ে সরল hash table — key নিজেই index. মান যখন ছোট একটি পরিসরে থাকে, তখন এটি সবসময় প্রথম পছন্দ।
ফলো-আপ: ১ থেকে n পর্যন্ত সংখ্যার n+1 আকারের অ্যারেতে একটি সংখ্যা দুবার আছে — O(1) মেমরিতে, অ্যারে না বদলে বের করুন। (উত্তর: অ্যারেকে লিংকড লিস্টের মতো ভেবে Floyd-এর cycle কৌশল — সমস্যা ১০।) আর স্ট্রিম আকারে অক্ষর আসতে থাকলে প্রতিবার “এখন পর্যন্ত প্রথম অপুনরাবৃত্ত” কীভাবে দেবেন?
সমস্যা ৪ · Two sum (মাঝারি)
অংশ ২-এ পুরো সাত ধাপে সমাধান করা হয়েছে। ইন্টারভিউর আগে নিজে খাতায় তিনটি পদ্ধতিই আবার লিখুন, আর প্রতিটির জটিলতা মুখে বলুন। এর আত্মীয় সমস্যা: 3-sum, “যোগফল k এমন উপ-অ্যারে কয়টি” (prefix sum + hash), আর দুই পয়েন্টারের sliding window — যেমন “ধনাত্মক অ্যারেতে যোগফল অন্তত S এমন সবচেয়ে ছোট টানা অংশ”।
সমস্যা ৫ · সাজানো অ্যারে থেকে ডুপ্লিকেট বাদ, আর শূন্যগুলো শেষে পাঠানো (সহজ)
প্রশ্ন: (ক) সাজানো অ্যারে থেকে ডুপ্লিকেট in-place বাদ দিয়ে নতুন দৈর্ঘ্য ফেরত দিন। (খ) অ্যারের সব শূন্য শেষে পাঠান, বাকিদের ক্রম না বদলে।
জিজ্ঞেস করে নিন: অ্যারের শেষাংশে কী থাকল, তা কি গুরুত্বপূর্ণ? ক্রম ধরে রাখা কি বাধ্যতামূলক?
পদ্ধতি ও জটিলতা: একটি “পড়ার” পয়েন্টার আর একটি “লেখার” পয়েন্টার। পড়ার পয়েন্টার সব ঘর দেখে; রাখার মতো মান পেলে লেখার পয়েন্টারের জায়গায় বসায়। সময় O(n), মেমরি O(1).
#include <stdio.h>
int dedup(int *a, int n) { /* a is sorted; returns new length */
if (n == 0) return 0;
int w = 1; /* write position */
for (int r = 1; r < n; r++) /* read position */
if (a[r] != a[w - 1]) a[w++] = a[r];
return w;
}
void move_zeros(int *a, int n) { /* keep order of non-zeros */
int w = 0;
for (int r = 0; r < n; r++)
if (a[r] != 0) a[w++] = a[r];
while (w < n) a[w++] = 0;
}
void show(const int *a, int n) {
for (int i = 0; i < n; i++) printf("%d ", a[i]);
printf("| n=%d\n", n);
}
int main(void) {
int a[] = {1, 1, 2, 3, 3, 3, 7};
int b[] = {0, 1, 0, 3, 12};
show(a, dedup(a, 7));
move_zeros(b, 5);
show(b, 5);
return 0;
}প্রত্যাশিত আউটপুট: 1 2 3 7 | n=4 আর 1 3 12 0 0 | n=5. দুটি সমস্যার কাঠামো হুবহু এক — শুধু “রাখার মতো” শর্তটা আলাদা। এই ছাঁচ চিনে ফেললে “নির্দিষ্ট মান মুছুন”, “সর্বোচ্চ দুবার রাখুন” — সব একই কোডে হয়।
Edge case: n = 0 (dedup-এ আলাদা করে সামলানো), সব এক মান, কোনো শূন্য নেই, সব শূন্য। ফলো-আপ: লেখার সংখ্যা কমাতে হবে (যেমন flash memory-তে)? তখন শুধু দরকার হলে লিখুন।
সমস্যা ৬ · অ্যারে k ঘর ঘোরানো, আর ম্যাট্রিক্স ৯০° ঘোরানো (মাঝারি)
প্রশ্ন: n আকারের অ্যারেকে ডানে k ঘর ঘোরান — [1 2 3 4 5], k = 2 হলে [4 5 1 2 3]. বাড়তি অ্যারে ছাড়া।
জিজ্ঞেস করে নিন: k কি n-এর চেয়ে বড় হতে পারে? ঋণাত্মক? n = 0?
পদ্ধতি ও জটিলতা: একবারে এক ঘর ঘোরানো k বার করলে O(n·k). নতুন অ্যারেতে কপি করলে O(n) মেমরি। সুন্দর কৌশল: পুরো অ্যারে উল্টান, তারপর প্রথম k ঘর, তারপর বাকিটা — সময় O(n), মেমরি O(1).
static void rev(int *a, int i, int j) {
while (i < j) { int t = a[i]; a[i++] = a[j]; a[j--] = t; }
}
void rotate_right(int *a, int n, int k) { /* [1 2 3 4 5], k=2 -> [4 5 1 2 3] */
if (n <= 1) return;
k %= n;
if (k < 0) k += n; /* negative k = rotate left */
rev(a, 0, n - 1); /* 5 4 3 2 1 */
rev(a, 0, k - 1); /* 4 5 3 2 1 */
rev(a, k, n - 1); /* 4 5 1 2 3 */
}k %= n না করলে k = 7, n = 5-এ rev(a, 0, 6) অ্যারের বাইরে লিখত। আর n = 0-এ k %= n নিজেই শূন্য দিয়ে ভাগ — তাই n <= 1 পরীক্ষাটি আগে।
ম্যাট্রিক্স ৯০° ঘড়ির কাঁটার দিকে: n×n matrix-কে আগে transpose করুন (a[i][j] ↔ a[j][i], শুধু j > i ঘরগুলোতে), তারপর প্রতিটি সারি উল্টান। সময় O(n²), মেমরি O(1). উল্টো দিকে ঘোরাতে transpose-এর পর সারির বদলে কলাম উল্টান। রিভিশন: পর্ব ২১। ফলো-আপ: ম্যাট্রিক্স বর্গাকার না হলে in-place করা যায় কি? (সাধারণভাবে না — আকারই বদলে যায়, নতুন অ্যারে লাগে।)
৪ · C-এর প্রিয় প্রশ্ন — লাইব্রেরি ফাংশন নিজে লিখুন
C-ইন্টারভিউর একটি বিশেষ গোত্র: “strlen, strcpy, memcpy, memmove বা atoi নিজে লিখুন।” এখানে অ্যালগরিদম সহজ — পরীক্ষা হয় খুঁটিনাটির: const, size_t, overlap, overflow. রিভিশন: পর্ব ১৮, পর্ব ৪৫, পর্ব ২৬।
সমস্যা ৭ · strlen, strcpy, memcpy আর memmove (মাঝারি)
প্রশ্ন: এই চারটি ফাংশন standard library ছাড়া লিখুন। তারপর ব্যাখ্যা করুন memcpy আর memmove-এর পার্থক্য কী।
জিজ্ঞেস করে নিন: standard-এর মতো একই signature চাই? (void *memmove(void *dst, const void *src, size_t n)) — হ্যাঁ ধরে নিন।
পদ্ধতি: memcpy-তে উৎস আর গন্তব্য একে অপরের ওপর পড়তে (overlap) পারবে না — পড়লে undefined behavior. memmove-কে overlap সামলাতে হয়: গন্তব্য উৎসের আগে থাকলে সামনে থেকে কপি, পরে থাকলে পেছন থেকে। সব ক্ষেত্রে সময় O(n).
char *my_strcpy(char *dst, const char *src) {
char *d = dst;
while ((*d++ = *src++) != '\0')
; /* copies the '\0' too */
return dst;
}
void *my_memcpy(void *dst, const void *src, size_t n) {
unsigned char *d = dst; /* byte by byte through unsigned char */
const unsigned char *s = src;
while (n--) *d++ = *s++;
return dst; /* overlap is undefined for memcpy */
}#include <stdio.h>
#include <stddef.h>
size_t my_strlen(const char *s) {
const char *p = s;
while (*p) p++;
return (size_t)(p - s);
}
void *my_memmove(void *dst, const void *src, size_t n) {
unsigned char *d = dst;
const unsigned char *s = src;
if (d == s || n == 0) return dst;
if (d < s) { /* dst before src: copy forward */
for (size_t i = 0; i < n; i++) d[i] = s[i];
} else { /* dst after src: copy backward */
for (size_t i = n; i > 0; i--) d[i - 1] = s[i - 1];
}
return dst;
}
int main(void) {
char buf[16] = "abcdefgh";
printf("len=%zu\n", my_strlen(buf));
my_memmove(buf + 2, buf, 6); /* overlapping, shift right */
printf("%s\n", buf);
my_memmove(buf, buf + 2, 6); /* overlapping, shift left */
printf("%s\n", buf);
return 0;
}প্রত্যাশিত আউটপুট: len=8, ababcdef, abcdefef. দ্বিতীয় লাইনে খেয়াল করুন: ডানে সরানোর সময় সামনে থেকে কপি করলে ‘a’ আর ‘b’ বারবার নিজের ওপরই কপি হতো, ফল হতো abababab. তাই পেছন থেকে কপি।
ইন্টারভিউয়ার যা খোঁজেন: দৈর্ঘ্যের জন্য size_t, উৎস প্যারামিটারে const, byte ধরে কাজের জন্য unsigned char *, আর ফেরত মান dst. একটি সৎ সতর্কতাও বলুন: আলাদা দুটি object-এর পয়েন্টার < দিয়ে তুলনা করা standard C-তে undefined; আসল লাইব্রেরি প্ল্যাটফর্মের জ্ঞান কাজে লাগিয়ে এটি করে, আর ইন্টারভিউতে এই সংস্করণই প্রত্যাশিত।
ফলো-আপ: বড় কপি দ্রুত করবেন কীভাবে? (একবারে ৮ byte বা SIMD দিয়ে, alignment মিলিয়ে — পর্ব ৪৫।) আর strcpy কেন বিপজ্জনক? (গন্তব্যের আকার জানে না — buffer overflow; snprintf বা আকারসহ সংস্করণ ব্যবহার করুন।)
সমস্যা ৮ · atoi লিখুন — overflow সামলে (মাঝারি)
প্রশ্ন: স্ট্রিং থেকে int বানান: শুরুর ফাঁকা জায়গা বাদ, ঐচ্ছিক +/-, তারপর যতক্ষণ অঙ্ক। মান int-এর সীমা ছাড়ালে INT_MAX বা INT_MIN ফেরত দিন।
জিজ্ঞেস করে নিন: overflow হলে কী চাই — সীমায় আটকানো, নাকি error জানানো? অঙ্ক না পেলে কী? (এখানে ০।) "+-12" বৈধ? (না।)
পদ্ধতি ও জটিলতা: আসল চ্যালেঞ্জ v * 10 + d করার আগেই বোঝা যে এটি overflow করবে কি না — কারণ signed overflow ঘটে গেলে আর ধরা যায় না, সেটি undefined behavior. সময় O(n).
#include <ctype.h>
#include <limits.h>
#include <stdio.h>
int my_atoi(const char *s) { /* clamps to INT_MIN / INT_MAX */
while (isspace((unsigned char)*s)) s++;
int sign = 1;
if (*s == '+' || *s == '-') {
if (*s == '-') sign = -1;
s++;
}
int v = 0;
while (isdigit((unsigned char)*s)) {
int d = *s - '0';
if (v > (INT_MAX - d) / 10) /* v * 10 + d would overflow */
return sign == 1 ? INT_MAX : INT_MIN;
v = v * 10 + d;
s++;
}
return sign * v;
}
int main(void) {
const char *t[] = { "42", " -42", "4193 words", "words 987",
"-91283472332", "2147483648", "+-12", "" };
for (int k = 0; k < 8; k++)
printf("%-14s -> %d\n", t[k], my_atoi(t[k]));
return 0;
}প্রত্যাশিত আউটপুট: 42, -42, 4193, 0, -2147483648, 2147483647, 0, 0. শর্ত v > (INT_MAX - d) / 10 গণিতে v * 10 + d > INT_MAX-এর সমান, কিন্তু হিসাবটা কখনো সীমা ছাড়ায় না। "-2147483648" ঠিক INT_MIN হলেও ধনাত্মক রূপ জমাতে গিয়ে overflow ধরা পড়ে এবং INT_MIN ফেরত আসে — ফল সঠিক।
ফলো-আপ: আসল কোডে কী ব্যবহার করবেন? atoi error জানাতে পারে না; strtol end pointer আর errno দিয়ে জানায় (পর্ব ৩০)। আরও প্রশ্ন: hex (0x1A) সমর্থন করুন, অথবা উল্টো কাজ — itoa লিখুন, যেখানে INT_MIN-এর ধনাত্মক রূপ int-এ ধরে না।
৫ · লিংকড লিস্ট, স্ট্যাক ও হিপ
পয়েন্টার নিয়ে আপনার আরাম কতটা, ইন্টারভিউয়ার সেটা সবচেয়ে ভালো বোঝেন linked list-এর প্রশ্নে। এখানে খাতায় বাক্স আর তীর আঁকা প্রায় বাধ্যতামূলক অভ্যাস। রিভিশন: পর্ব ৩৩, পর্ব ৩৪, পর্ব ৩৬।
সমস্যা ৯ · লিংকড লিস্ট উল্টানো, লুপ দিয়ে (সহজ, কিন্তু সবচেয়ে বেশি জিজ্ঞাসিত)
প্রশ্ন: একমুখী লিংকড লিস্টের head দেওয়া আছে। লিস্টটি উল্টে নতুন head ফেরত দিন।
জিজ্ঞেস করে নিন: নতুন node বানানো চলবে? (না — শুধু তীর ঘোরাতে হবে।) খালি লিস্ট আসতে পারে?
পদ্ধতি ও জটিলতা: তিনটি পয়েন্টার — prev, cur, nxt. প্রতিটি node-এ তীর উল্টো দিকে ঘোরান। সময় O(n), মেমরি O(1). নিচে ধাপে ধাপে দেখুন:
1struct node *reverse(struct node *head) {2 struct node *prev = NULL, *cur = head;3 while (cur) {4 struct node *nxt = cur->next;5 cur->next = prev;6 prev = cur;7 cur = nxt;8 }9 return prev;10}
| prev | |
| cur | |
| nxt |
- শুরু। সবুজ লাইনটি এখনো চলেনি, পরের ধাপে চলবে। লিস্ট এখন 1 → 2 → 3, head আছে 1-এ।
- প্রস্তুতি শেষ।
prev= NULL (উল্টানো অংশ এখনো খালি),cur= 1. এবার লুপের প্রথম ঘুর। - আগে বাকি লিস্টটা ধরে রাখুন।
nxt= 2. এই লাইন না থাকলে পরের লাইনে 1-এর তীর ঘোরানোর সাথে সাথে 2 → 3 অংশটি চিরতরে হারিয়ে যেত — এটাই নতুনদের সবচেয়ে সাধারণ ভুল। - প্রথম ঘুর শেষ। 1-এর তীর এখন NULL-এর দিকে;
prev= 1,cur= 2. বাঁ দিকে উল্টানো অংশ, ডান দিকে বাকি অংশ — দুটো আলাদা টুকরো। - দ্বিতীয় ঘুর শেষ। 2-এর তীর 1-এর দিকে।
prev= 2,cur= 3. প্রতিটি ঘুরে ঠিক একটি node উল্টানো অংশে যোগ হচ্ছে — এটাই এই লুপের loop invariant. - লুপ শেষ।
cur= NULL, তাই লুপ থামল।prev= 3 হলো নতুন head: 3 → 2 → 1. খালি লিস্টে লুপ একবারও চলে না, NULL-ই ফেরত আসে — আলাদা if লাগে না।
#include <stdio.h>
struct node { int val; struct node *next; };
struct node *reverse(struct node *head) {
struct node *prev = NULL, *cur = head;
while (cur) {
struct node *nxt = cur->next; /* save the rest of the list */
cur->next = prev; /* turn the arrow around */
prev = cur;
cur = nxt;
}
return prev; /* new head */
}
void print(const struct node *h) {
for (; h; h = h->next) printf("%d ", h->val);
printf("|\n");
}
int main(void) {
struct node n[5], one = {9, NULL};
for (int i = 0; i < 5; i++) /* build 1 -> 2 -> 3 -> 4 -> 5 */
n[i] = (struct node){ i + 1, (i < 4) ? &n[i + 1] : NULL };
print(reverse(&n[0]));
print(reverse(NULL));
print(reverse(&one));
return 0;
}প্রত্যাশিত আউটপুট: 5 4 3 2 1 |, খালি লিস্টের জন্য শুধু |, আর 9 |. এখানে node-গুলো একটি সাধারণ অ্যারেতে রাখা, তাই free লাগেনি। malloc দিয়ে বানালে শেষে প্রতিটি node ফেরত দেওয়ার কথা নিজে থেকে বলুন — ইন্টারভিউতে এটি ভালো সংকেত।
ফলো-আপ: রিকার্শন দিয়ে লিখুন (সময় O(n), কিন্তু call stack-এ O(n) মেমরি — খুব লম্বা লিস্টে stack overflow, পর্ব ১৫)। লিস্টের m থেকে n অংশটুকু উল্টান। k-টি করে দলে উল্টান। লিস্টটি palindrome কি না O(1) মেমরিতে বলুন (মাঝখান খুঁজে দ্বিতীয় অর্ধেক উল্টে তুলনা)।
সমস্যা ১০ · লিস্টে cycle আছে কি না (মাঝারি)
প্রশ্ন: কোনো node-এর next কি আগের কোনো node-এ ফিরে গেছে? থাকলে সাধারণ traversal কখনো শেষ হবে না।
পদ্ধতি ও জটিলতা: দেখা node-গুলোর ঠিকানা hash set-এ রাখলে O(n) মেমরি। Floyd-এর cycle detection (“কচ্ছপ ও খরগোশ”): একটি পয়েন্টার এক ঘর, অন্যটি দুই ঘর করে চলে। cycle থাকলে দ্রুতটি একসময় ধীরটিকে পেছন থেকে ধরে ফেলবে — বৃত্তাকার ট্র্যাকে দুজন দৌড়বিদের মতো। সময় O(n), মেমরি O(1).
int has_cycle(const struct node *head) {
const struct node *slow = head, *fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next; /* 1 step */
fast = fast->next->next; /* 2 steps */
if (slow == fast) return 1; /* met inside the cycle */
}
return 0; /* fast fell off the end */
}কেন ধরবেই? দুজনেই cycle-এ ঢোকার পর প্রতি ধাপে তাদের দূরত্ব ঠিক ১ করে কমে, তাই লাফিয়ে পার হয়ে যাওয়া অসম্ভব। Edge case: খালি লিস্ট, এক node যার next নিজেই, দুই node-এর cycle. fast->next যাচাই না করলে fast->next->next-এ NULL dereference. ফলো-আপ: cycle কোন node থেকে শুরু? (মিলনের পর একটি পয়েন্টার head-এ ফিরিয়ে দুজনকেই এক ঘর করে চালান — যেখানে মিলবে, সেটাই শুরু।) cycle-এর দৈর্ঘ্য? (মিলনবিন্দু থেকে এক পাক ঘুরে গুনুন।)
সমস্যা ১১ · দুটি সাজানো লিস্ট জোড়া লাগানো (সহজ)
প্রশ্ন: দুটি ছোট-থেকে-বড় সাজানো লিস্ট মিলিয়ে একটি সাজানো লিস্ট বানান — নতুন node না বানিয়ে।
পদ্ধতি ও জটিলতা: merge sort-এর merge ধাপ (পর্ব ৪০)। একটি dummy node রাখলে “প্রথম node কোনটা” — এই আলাদা ক্ষেত্রটি আর লাগে না। সময় O(n + m), মেমরি O(1).
struct node *merge(struct node *a, struct node *b) {
struct node dummy = {0, NULL}; /* dummy head: no special case for first node */
struct node *t = &dummy;
while (a != NULL && b != NULL) {
if (a->val <= b->val) { t->next = a; a = a->next; } /* <= keeps it stable */
else { t->next = b; b = b->next; }
t = t->next;
}
t->next = (a != NULL) ? a : b; /* attach whatever is left */
return dummy.next;
}Edge case: একটি বা দুটি লিস্টই খালি, সমান মান, একটি লিস্ট অন্যটির চেয়ে অনেক লম্বা। ফলো-আপ: k-টি সাজানো লিস্ট জোড়া লাগান — min-heap দিয়ে O(N log k), যেখানে N মোট node (k-way merge, পর্ব ৩৬)।
সমস্যা ১২ · বন্ধনী ঠিকঠাক মেলানো কি না (সহজ)
প্রশ্ন: ()[]{} দিয়ে গঠিত একটি স্ট্রিং বৈধ কি না — প্রতিটি খোলা বন্ধনী একই ধরনের বন্ধনী দিয়ে, সঠিক ক্রমে বন্ধ হতে হবে।
জিজ্ঞেস করে নিন: অন্য অক্ষর থাকতে পারে? সর্বোচ্চ দৈর্ঘ্য কত?
পদ্ধতি ও জটিলতা: সবচেয়ে শেষে খোলা বন্ধনীটিই আগে বন্ধ হতে হবে — এটাই LIFO, অর্থাৎ stack (পর্ব ৩৪)। সময় O(n), মেমরি O(n).
#include <stdio.h>
int valid(const char *s) {
char st[128]; /* stack of open brackets */
int top = 0;
for (; *s; s++) {
char c = *s;
if (c == '(' || c == '[' || c == '{') {
if (top == (int)sizeof st) return 0; /* too deep */
st[top++] = c;
} else if (c == ')' || c == ']' || c == '}') {
char want = (c == ')') ? '(' : (c == ']') ? '[' : '{';
if (top == 0 || st[top - 1] != want) return 0;
top--;
} /* other characters are ignored */
}
return top == 0; /* nothing left open */
}
int main(void) {
const char *t[] = { "()[]{}", "([{}])", "(]", "([)]", "((", ")(", "", "a(b)c" };
for (int k = 0; k < 8; k++)
printf("%-8s -> %s\n", t[k], valid(t[k]) ? "valid" : "invalid");
return 0;
}প্রত্যাশিত আউটপুট: valid, valid, invalid, invalid, invalid, invalid, valid, valid. তিনটি আলাদা ব্যর্থতা খেয়াল করুন: "(]" ধরন অমিল, ")(" খালি stack-এ বন্ধ, "((" শেষে stack খালি নয়। শেষেরটি ভুলে যাওয়াই সবচেয়ে সাধারণ বাগ।
ফলো-আপ: শুধু এক ধরনের বন্ধনী হলে stack লাগে কি? (না — একটি counter যথেষ্ট, মেমরি O(1); কিন্তু counter কখনো ঋণাত্মক হলে সাথে সাথে invalid.) সবচেয়ে কম কয়টি বন্ধনী যোগ করলে বৈধ হবে? সবচেয়ে লম্বা বৈধ অংশ কত বড়?
সমস্যা ১৩ · k-তম বৃহত্তম সংখ্যা (মাঝারি)
প্রশ্ন: n টি সংখ্যার মধ্যে k-তম বৃহত্তমটি বের করুন। যেমন [3 2 1 5 6 4], k = 2 → 5.
জিজ্ঞেস করে নিন: ডুপ্লিকেট থাকলে আলাদা গোনা হবে? সংখ্যাগুলো কি একসাথে আসে, নাকি স্ট্রিমে একটা একটা করে?
পদ্ধতি ও জটিলতা: তিনটি উত্তর, প্রতিটির জায়গা আছে:
- সাজিয়ে নেওয়া: O(n log n) — সবচেয়ে সহজ, প্রথমে এটা বলুন।
- k আকারের min-heap: heap-এ সবসময় এ পর্যন্ত দেখা বৃহত্তম k টি থাকে, আর শীর্ষে থাকে তাদের মধ্যে ছোটটি। সময় O(n log k), মেমরি O(k). স্ট্রিমের জন্য এটাই সেরা (পর্ব ৩৬)।
- Quickselect: quicksort-এর partition, কিন্তু শুধু একদিকে নামা — গড়ে O(n), খারাপ ক্ষেত্রে O(n²)।
static void sift_down(int *h, int k, int i) { /* min-heap of size k */
for (;;) {
int s = i, l = 2 * i + 1, r = l + 1;
if (l < k && h[l] < h[s]) s = l;
if (r < k && h[r] < h[s]) s = r;
if (s == i) return;
int t = h[i]; h[i] = h[s]; h[s] = t;
i = s;
}
}
/* k-th largest of a[0..n-1]; 1 <= k <= n; h has room for k ints */
int kth_largest(const int *a, int n, int k, int *h) {
for (int i = 0; i < k; i++) h[i] = a[i];
for (int i = k / 2 - 1; i >= 0; i--) sift_down(h, k, i); /* heapify */
for (int i = k; i < n; i++)
if (a[i] > h[0]) { h[0] = a[i]; sift_down(h, k, 0); } /* replace smallest */
return h[0]; /* smallest of the k largest */
}Edge case: k = 1 (সর্বোচ্চ), k = n (সর্বনিম্ন), সব মান সমান। ফলো-আপ: সংখ্যা এত বেশি যে মেমরিতে ধরে না? (heap-এ শুধু k টি রাখুন, ফাইল থেকে পড়ে যান।) চলমান median? (দুটি heap — পর্ব ৩৬-এর running median.)
৬ · সার্চিং, বিট ও DP
এই তিন গোত্রে একটি করে “চাবির ধারণা” আছে: binary search-এ সীমানা, বিটে x & (x - 1), DP-তে state. চাবিটা ধরতে পারলে সমস্যার বড় অংশ সমাধান। রিভিশন: পর্ব ৪১, পর্ব ৯, পর্ব ৪২, পর্ব ৪৩।
সমস্যা ১৪ · Binary search-এর রূপভেদ — প্রথম অবস্থান, গণনা (মাঝারি)
প্রশ্ন: সাজানো অ্যারেতে x-এর প্রথম অবস্থান দিন (না থাকলে -1). তারপর x কতবার আছে বলুন।
জিজ্ঞেস করে নিন: ডুপ্লিকেট আছে? অ্যারে খালি হতে পারে? ছোট থেকে বড়, নাকি উল্টো?
পদ্ধতি ও জটিলতা: সাধারণ binary search মান পেলেই থামে — কিন্তু সেটা যেকোনো একটি কপি হতে পারে। তার বদলে lower bound লিখুন: “প্রথম যে ঘরে মান x-এর সমান বা বড়।” এই একটি ফাংশন দিয়েই সব রূপভেদ হয়। সময় O(log n).
#include <stdio.h>
/* first index i with a[i] >= x, or n if there is none */
int lower_bound(const int *a, int n, int x) {
int lo = 0, hi = n; /* half-open range [lo, hi) */
while (lo < hi) {
int mid = lo + (hi - lo) / 2; /* not (lo + hi) / 2: no overflow */
if (a[mid] < x) lo = mid + 1;
else hi = mid;
}
return lo;
}
int first_occ(const int *a, int n, int x) {
int i = lower_bound(a, n, x);
return (i < n && a[i] == x) ? i : -1;
}
int main(void) {
int a[] = {1, 3, 3, 3, 5, 8};
int n = 6, q[] = {3, 4, 1, 8, 0, 9};
for (int k = 0; k < 6; k++)
printf("x=%d first=%d lb=%d\n", q[k], first_occ(a, n, q[k]),
lower_bound(a, n, q[k]));
printf("count(3)=%d\n", lower_bound(a, n, 4) - lower_bound(a, n, 3));
return 0;
}প্রত্যাশিত আউটপুট: x=3 → first=1, lb=1; x=4 → first=-1, lb=4; x=1 → 0, 0; x=8 → 5, 5; x=0 → -1, 0; x=9 → -1, 6; শেষে count(3)=3. [lo, hi) আকারের half-open পরিসর রাখলে লুপের শর্ত (lo < hi) আর আপডেট (hi = mid) নিয়ে দ্বিধা থাকে না, আর “পাওয়া যায়নি” হলে n ফেরত আসে।
বিখ্যাত বাগ: mid = (lo + hi) / 2 — বিশাল অ্যারেতে lo + hi int-এর সীমা ছাড়াতে পারে। এই বাগ বহু বছর ধরে বিখ্যাত লাইব্রেরিতেও লুকিয়ে ছিল। ফলো-আপ: ঘোরানো সাজানো অ্যারেতে খোঁজা; বর্গমূলের পূর্ণসংখ্যা অংশ; “উত্তরের ওপর binary search” (binary search on the answer, পর্ব ৪১)।
সমস্যা ১৫ · Set bit গোনা, আর সংখ্যাটি ২-এর ঘাত কি না (সহজ)
প্রশ্ন: একটি unsigned সংখ্যায় কয়টি বিট ১, আর সংখ্যাটি ২-এর ঘাত কি না বলুন।
জিজ্ঞেস করে নিন: signed নাকি unsigned? কত বিট? শূন্যকে কী ধরা হবে?
পদ্ধতি ও জটিলতা: প্রতিটি বিট সরিয়ে দেখলে ৩২ ধাপ। Brian Kernighan-এর নামে পরিচিত কৌশল: x &= x - 1 সবচেয়ে ডানের ১-কে মুছে দেয়, তাই লুপ চলে ঠিক যতগুলো ১ আছে ততবার। ২-এর ঘাতে ঠিক একটি বিট ১, তাই x & (x - 1) শূন্য হয় (পর্ব ৯)।
#include <stdio.h>
int popcount(unsigned x) {
int c = 0;
while (x) {
x &= x - 1; /* clears the lowest set bit */
c++;
}
return c;
}
int is_pow2(unsigned x) {
return x != 0 && (x & (x - 1)) == 0;
}
int main(void) {
unsigned t[] = {0u, 1u, 6u, 8u, 255u, 1024u, 0xFFFFFFFFu};
for (int k = 0; k < 7; k++)
printf("%10u bits=%2d pow2=%d\n", t[k], popcount(t[k]), is_pow2(t[k]));
return 0;
}প্রত্যাশিত আউটপুট (৩২-বিট unsigned ধরে): 0 → bits 0, pow2 0; 1 → 1, 1; 6 → 2, 0; 8 → 1, 1; 255 → 8, 0; 1024 → 1, 1; 4294967295 → 32, 0. x != 0 শর্তটি না থাকলে শূন্যকে ২-এর ঘাত বলে দিত — সবচেয়ে প্রিয় edge case.
ফলো-আপ: GCC-তে __builtin_popcount, C23-এ stdbit.h-এর stdc_count_ones আছে — এগুলো একটি CPU নির্দেশে হতে পারে (popcount). আরও প্রশ্ন: একটি সংখ্যার বিট উল্টান; অ্যারেতে সবাই দুবার, একজন একবার — তাকে খুঁজুন (সব XOR করুন, O(1) মেমরি)।
সমস্যা ১৬ · সিঁড়ি বেয়ে ওঠা ও কয়েন চেঞ্জ (মাঝারি)
প্রশ্ন: (ক) n ধাপের সিঁড়িতে একবারে ১ বা ২ ধাপ উঠতে পারেন — কত উপায়ে চূড়ায় পৌঁছাবেন? (খ) কিছু নোটের মান দেওয়া আছে, প্রতিটি যতবার খুশি নেওয়া যায় — নির্দিষ্ট টাকা বানাতে সবচেয়ে কম কয়টি নোট লাগবে? সম্ভব না হলে -1.
জিজ্ঞেস করে নিন: n বা টাকার পরিমাণ কত বড়? উত্তর কি int-এ ধরবে? (সিঁড়ির উত্তর Fibonacci-র মতো দ্রুত বাড়ে।)
পদ্ধতি ও জটিলতা: দুটোই dynamic programming (পর্ব ৪২)। সিঁড়ি: ways(n) = ways(n-1) + ways(n-2) — দুটি ভ্যারিয়েবলেই হয়, সময় O(n), মেমরি O(1). কয়েন: dp[x] = x টাকা বানানোর সর্বনিম্ন নোট; প্রতিটি নোট c-এর জন্য dp[x - c] + 1 যাচাই। সময় O(amount × k).
#include <limits.h>
#include <stdio.h>
long long stairs(int n) { /* ways to climb with 1 or 2 steps */
long long a = 1, b = 1; /* ways(0), ways(1) */
for (int i = 2; i <= n; i++) { long long c = a + b; a = b; b = c; }
return b;
}
int min_coins(const int *coin, int k, int amount) { /* amount <= 100 */
int dp[101];
dp[0] = 0;
for (int x = 1; x <= amount; x++) {
dp[x] = INT_MAX; /* INT_MAX = not reachable yet */
for (int j = 0; j < k; j++)
if (coin[j] <= x && dp[x - coin[j]] != INT_MAX && dp[x - coin[j]] + 1 < dp[x])
dp[x] = dp[x - coin[j]] + 1;
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
int main(void) {
printf("stairs: %lld %lld %lld %lld\n", stairs(0), stairs(1), stairs(5), stairs(50));
int c1[] = {1, 5, 10, 20}, c2[] = {2}, c3[] = {1, 3, 4};
printf("coins: %d %d %d %d\n", min_coins(c1, 4, 37), min_coins(c2, 1, 3),
min_coins(c3, 3, 6), min_coins(c1, 4, 0));
return 0;
}প্রত্যাশিত আউটপুট: stairs: 1 1 8 20365011074 আর coins: 5 -1 2 0. ৫০ ধাপের উত্তর ২০ বিলিয়নের বেশি, অথচ ৩২-বিট int-এর সীমা প্রায় ২.১ বিলিয়ন — int-এ রাখলে overflow হতো, তাই long long. ৩৭ টাকা: 20 + 10 + 5 + 1 + 1 = ৫টি নোট।
সবচেয়ে মূল্যবান ফলো-আপ: “সবসময় বড় নোট আগে নিলেই তো হয়?” নোট {1, 3, 4} আর ৬ টাকায় greedy নেয় 4 + 1 + 1 = ৩টি, অথচ 3 + 3 = ২টিই যথেষ্ট — কোড ঠিক এটাই দেখাচ্ছে। একটি counterexample দিয়ে greedy ভুল প্রমাণ করা ইন্টারভিউতে চমৎকার মুহূর্ত (পর্ব ৪৩)। আরও: কয়েন চেঞ্জে কত উপায়ে বানানো যায় (লুপের ক্রম বদলে যায়), আর কোন কোন নোট নেওয়া হলো তা ফেরত দেওয়া (traceback).
৭ · C-নির্দিষ্ট ধারণার প্রশ্ন — সংক্ষিপ্ত, কিন্তু গভীর
C-ভিত্তিক পদে (এমবেডেড, সিস্টেম, firmware, নেটওয়ার্কিং) কোডিং সমস্যার পাশাপাশি এই প্রশ্নগুলো প্রায়ই আসে। উত্তর দেওয়ার ভালো কাঠামো: এক লাইনে সংজ্ঞা → একটি ছোট উদাহরণ → একটি ফাঁদ বা বাস্তব ব্যবহার। প্রতিটি প্রশ্ন আগে নিজে মুখে উত্তর দিন, তারপর খুলুন। রিভিশন: পর্ব ১৪, পর্ব ২০, পর্ব ২২, পর্ব ৩১, পর্ব ৪৫, পর্ব ৪৬।
প্র ০১volatile কী করে, আর কখন দরকার? এটি কি থ্রেড-নিরাপত্তা দেয়? (মাঝারি)
volatile compiler-কে বলে: “এই ভ্যারিয়েবল প্রোগ্রামের অজান্তেই বদলাতে পারে — প্রতিবার পড়া ও লেখা সত্যিই মেমরিতে করতে হবে, register-এ জমিয়ে রাখা বা বাদ দেওয়া চলবে না।” Optimization চালু থাকলে compiler সাধারণত ধরে নেয়, কোড না বদলালে মান বদলায় না। তাই while (!ready) {} লুপে ready একবার পড়েই চিরকাল আটকে থাকতে পারে।
যেখানে দরকার: (১) হার্ডওয়্যার register — memory-mapped I/O, যেখানে মান বদলায় যন্ত্র (পর্ব ৫১); (২) signal handler যে ভ্যারিয়েবল বদলায় — সেখানে volatile sig_atomic_t; (৩) setjmp/longjmp-এর পরে ব্যবহৃত local ভ্যারিয়েবল।
ফাঁদ: volatile thread-এর জন্য নয়। এটি atomicity দেয় না (count++ তখনও পড়া-বদলানো-লেখার তিন ধাপ), আর অন্য মেমরি-অপারেশনের ক্রমও নিশ্চিত করে না। থ্রেডে লাগবে _Atomic বা mutex (পর্ব ৪৯)।
মূল উপলব্ধি: volatile মানে “optimize করে বাদ দিও না”, “একসাথে নিরাপদ” নয়।
প্র ০২static শব্দটির কয়টি অর্থ? (সহজ)
জায়গাভেদে তিনটি আলাদা কাজ — এজন্যই প্রশ্নটি জনপ্রিয়:
- ফাংশনের ভেতরের ভ্যারিয়েবলে: static local — lifetime পুরো প্রোগ্রাম, একবারই initialize হয়, ডাকের পর ডাকে মান থেকে যায়; কিন্তু নাম দেখা যায় শুধু ফাংশনের ভেতরে। উদাহরণ: কতবার ডাকা হলো তার counter.
- ফাইলের স্তরে (ফাংশন বা global ভ্যারিয়েবলে): internal linkage — নামটি শুধু এই
.cফাইলে দেখা যায়, অন্য ফাইলের একই নামের সাথে সংঘাত হয় না। এটি C-তে “private” বানানোর উপায় (পর্ব ১৪, ২৯)। - অ্যারে প্যারামিটারে (C99):
void f(int a[static 10])— caller প্রতিশ্রুতি দেয়, অন্তত ১০টি উপাদান আছে আর পয়েন্টার NULL নয়। compiler এটা থেকে সতর্কবার্তা বা optimization করতে পারে।
ফাঁদ: static local থাকা ফাংশন সাধারণত reentrant নয় — একই সময়ে দুই thread ডাকলে মান মিশে যায় (strtok এর বিখ্যাত উদাহরণ)।
মূল উপলব্ধি: ভেতরে static বদলায় lifetime, বাইরে বদলায় দৃশ্যমানতা।
প্র ০৩const char *p, char *const p আর const char *const p-এর পার্থক্য কী? (সহজ)
ডান থেকে বাঁয়ে পড়ুন (right-left rule, পর্ব ৪৫):
const char *p— “p একটি পয়েন্টার, const char-এর দিকে।”*p = 'x'চলবে না, কিন্তুp++চলবে। স্ট্রিং পড়ার ফাংশনের প্যারামিটার এমনই হয় (strlen(const char *s)).char const *pহুবহু একই।char *const p— “p একটি const পয়েন্টার, char-এর দিকে।”*p = 'x'চলবে, কিন্তু p-কে অন্য কোথাও সরানো যাবে না। একটি নির্দিষ্ট buffer-এ বাঁধা পয়েন্টারের মতো।const char *const p— দুটোই নিষেধ।
ফাঁদ: const মানে “এই পথ দিয়ে বদলাবো না”, “মান কখনো বদলাবে না” নয় — অন্য একটি non-const পয়েন্টার দিয়ে একই মেমরি বদলানো সম্ভব। আর string literal-এর ধরন char[] হলেও সেটি বদলানো undefined; তাই const char *s = "..." লেখাই সঠিক অভ্যাস।
মূল উপলব্ধি: *-এর বাঁয়ে const = ডেটা স্থির; ডানে const = পয়েন্টার স্থির।
প্র ০৪Stack আর heap মেমরির পার্থক্য কী? কোনটা কখন ব্যবহার করবেন? (সহজ)
- Stack: ফাংশনের local ভ্যারিয়েবল থাকে এখানে। ফাংশন ডাকলে stack frame তৈরি, ফিরলে নিজে থেকেই মুছে যায়। বরাদ্দ খুব দ্রুত (শুধু একটি register সরানো)। কিন্তু আকার সীমিত — অনেক Linux সিস্টেমে মূল thread-এর ডিফল্ট কয়েক MB (প্রায়ই 8 MB). বিশাল local অ্যারে বা অসীম রিকার্শনে stack overflow.
- Heap:
malloc/freeদিয়ে নিজে নেওয়া ও ফেরত দেওয়া। আকার অনেক বড়, আর ফাংশন ফিরলেও ডেটা থাকে। কিন্তু বরাদ্দ তুলনামূলক ধীর, ভুলে ফেরত না দিলে memory leak, দুবার দিলে double free.
কখন কোনটা: ছোট, আকার আগে জানা, শুধু ফাংশনের ভেতরে লাগে — stack. আকার চলার সময় জানা যায়, অনেক বড়, বা ফাংশনের পরেও বাঁচতে হবে — heap.
ক্লাসিক ফাঁদ: local অ্যারের ঠিকানা ফেরত দেওয়া — char *f(void) { char buf[16]; return buf; }. ফাংশন ফিরতেই buf-এর জায়গা অবৈধ — dangling pointer. GCC এখানে warning দেয়।
মূল উপলব্ধি: stack-এর lifetime ঠিক করে ফাংশনের কাঠামো, heap-এর lifetime ঠিক করেন আপনি।
প্র ০৫একই পয়েন্টার দুবার free করলে কী হয়? কীভাবে এড়াবেন? (মাঝারি)
Double free হলো undefined behavior. বাস্তবে allocator মুক্ত block-গুলোর হিসাব রাখে block-এর ভেতরেই কিছু তথ্য লিখে। একই block দুবার মুক্ত তালিকায় ঢুকলে পরের দুটি malloc একই ঠিকানা দিতে পারে — দুটি আলাদা object একই মেমরিতে, একে অপরকে নষ্ট করে।
আধুনিক glibc অনেক ক্ষেত্রে এটি ধরে ফেলে প্রোগ্রাম থামিয়ে দেয় (free(): double free detected). কিন্তু সব ক্ষেত্রে নয় — আর আক্রমণকারীরা এই ধরনের ত্রুটি কাজে লাগিয়ে নিজের কোড চালানোর পথ খুঁজেছে, তাই এটি নিরাপত্তা-ত্রুটিও।
এড়ানোর উপায়: (১) মালিকানা স্পষ্ট রাখুন — কে free করবে, এক জায়গায় লেখা থাকুক; (২) free(p); p = NULL; — free(NULL) নিয়ম অনুযায়ী কিছুই করে না; (৩) টেস্টে AddressSanitizer বা Valgrind চালান (পর্ব ৩১)।
মূল উপলব্ধি: মেমরির প্রতিটি block-এর ঠিক একজন মালিক, আর ঠিক একবার free.
প্র ০৬ফাংশনের ভেতরে sizeof(arr) কেন অ্যারের আকার দেয় না? (সহজ)
প্যারামিটার হিসেবে লেখা int arr[] বা int arr[10]-কে compiler আসলে int *arr ধরে নেয় (array decay, পর্ব ১৮)। তাই ফাংশনের ভেতরে sizeof(arr) হলো একটি পয়েন্টারের আকার — ৬৪-বিট Linux-এ 8, অ্যারে যত বড়ই হোক।
void f(int arr[10]) { printf("%zu\n", sizeof arr); } /* 8: pointer */
int main(void) { int a[10]; printf("%zu\n", sizeof a); f(a); } /* 40, then 8 */
GCC এখানে -Wsizeof-array-argument সতর্কবার্তা দেয়। সমাধান: দৈর্ঘ্য আলাদা প্যারামিটারে পাঠান — f(a, sizeof a / sizeof a[0]), আর এই হিসাবটি করুন অ্যারে যেখানে ঘোষিত, সেখানেই।
মূল উপলব্ধি: C-তে অ্যারে ফাংশনে যায় শুধু ঠিকানা হয়ে; দৈর্ঘ্যের দায়িত্ব আপনার।
প্র ০৭Struct padding কী? sizeof কীভাবে কমাবেন? (মাঝারি)
CPU সাধারণত চায় ৪-byte-এর int ৪-এর গুণিতক ঠিকানায় থাকুক (alignment). তাই compiler member-দের মাঝে ফাঁকা byte বসায় — padding — আর শেষেও, যাতে struct-এর অ্যারেতে প্রতিটি উপাদান ঠিকমতো সাজানো থাকে। একটি সাধারণ x86-64 GCC-তে:
struct a { char c; int i; char d; }; /* 1 + 3 pad + 4 + 1 + 3 pad = 12 */
struct b { int i; char c; char d; }; /* 4 + 1 + 1 + 2 pad = 8 */
কমানোর উপায়: বড় member আগে, ছোট পরে সাজান। __attribute__((packed)) padding মুছে দেয়, কিন্তু অসংলগ্ন ঠিকানায় পড়া কিছু CPU-তে ধীর বা এমনকি ত্রুটি — তাই শুধু ফাইল বা নেটওয়ার্কের নির্দিষ্ট ফরম্যাটে। offsetof দিয়ে প্রতিটি member-এর অবস্থান দেখা যায় (পর্ব ২২)।
ফাঁদ: struct সরাসরি fwrite বা socket-এ পাঠালে padding আর endianness দুটোই সাথে যায় — অন্য compiler বা যন্ত্রে ভুল পড়বে।
মূল উপলব্ধি: struct-এর আকার member-দের যোগফল নয়; ক্রমই ঠিক করে কত byte নষ্ট হবে।
প্র ০৮আপনার কম্পিউটার little-endian না big-endian, একটি প্রোগ্রাম দিয়ে বলুন। (সহজ)
Endianness হলো বহু-byte সংখ্যার byte-গুলো মেমরিতে কোন ক্রমে থাকে। Little-endian-এ সবচেয়ে কম গুরুত্বপূর্ণ byte থাকে সবচেয়ে ছোট ঠিকানায়। x86-64 আর সাধারণভাবে ব্যবহৃত ARM ব্যবস্থাগুলো little-endian; নেটওয়ার্কের byte order হলো big-endian (পর্ব ৫০)।
#include <stdio.h>
int main(void) {
unsigned int x = 1;
unsigned char *p = (unsigned char *)&x; /* look at the first byte */
printf("%s-endian\n", *p == 1 ? "little" : "big");
return 0;
}১ সংখ্যাটির byte-গুলো little-endian-এ 01 00 00 00, big-endian-এ 00 00 00 01. unsigned char * দিয়ে যেকোনো object-এর byte পড়া standard অনুযায়ী বৈধ। আরেকটি পরিচিত উপায় হলো union. GCC-তে __BYTE_ORDER__ ম্যাক্রো compile-এর সময়ই উত্তর দেয়।
মূল উপলব্ধি: ফাইল বা নেটওয়ার্কে ডেটা পাঠানোর সময় byte-এর ক্রম নিজে ঠিক করে দিন — htonl বা হাতে byte ধরে লেখা।
প্র ০৯Function pointer কোথায় কাজে লাগে? একটি বাস্তব উদাহরণ দিন। (মাঝারি)
Function pointer একটি ফাংশনের ঠিকানা রাখে, ফলে “কোন কাজটা হবে” সিদ্ধান্তটা চলার সময় নেওয়া যায় (পর্ব ২৮)।
- Callback:
qsort-কে তুলনার ফাংশন দেওয়া — একই sort দিয়ে নাম, বয়স বা বেতন অনুযায়ী সাজানো। - Dispatch table: কমান্ডের নাম থেকে ফাংশন — লম্বা
switch-এর বদলে একটি অ্যারে। CLI টুল আর interpreter-এ খুব প্রচলিত। - State machine ও driver interface: struct-এর ভেতরে
open,read,write-এর পয়েন্টার — Linux kernel-এরstruct file_operationsএর বিখ্যাত উদাহরণ। এভাবেই C-তে polymorphism হয়। - Signal handler ও thread:
signal()আরpthread_create()দুটোই function pointer নেয়।
সিনট্যাক্সের ফাঁদ: int (*cmp)(const void *, const void *) — বন্ধনী না দিলে এটি হয়ে যায় পয়েন্টার ফেরত দেওয়া ফাংশনের ঘোষণা। typedef দিয়ে পড়া সহজ করুন।
মূল উপলব্ধি: function pointer দিয়ে “কী করতে হবে” আর “কখন করতে হবে” আলাদা করা যায় — এটাই callback-ভিত্তিক নকশার মূল।
প্র ১০Undefined behavior কী? একটি উদাহরণ দিন যেখানে compiler আপনার কোড “বদলে” দেয়। (কঠিন)
Undefined behavior মানে C standard সেই কাজের ফলাফলের ব্যাপারে কোনো নিশ্চয়তা দেয় না — প্রোগ্রাম যা খুশি করতে পারে, এমনকি আপাতদৃষ্টিতে “ঠিকঠাক” চলতেও পারে। উদাহরণ: signed int overflow, অ্যারের সীমার বাইরে পড়া, NULL dereference, free-র পরে ব্যবহার, int-এর বিট-সংখ্যার সমান বা বেশি shift, আর i = i++ + 1-এর মতো একই ভ্যারিয়েবল ক্রমহীনভাবে দুবার বদলানো।
int will_overflow(int x) { return x + 1 < x; } /* meant as overflow check */
Signed overflow যেহেতু undefined, compiler ধরে নিতে পারে এটি কখনো ঘটে না — তাহলে x + 1 < x সবসময় মিথ্যা। -O2-এ GCC এই ফাংশনকে প্রায়ই সরাসরি return 0 বানিয়ে দেয়। সঠিক পরীক্ষা: x == INT_MAX, অথবা GCC-র __builtin_add_overflow বা C23-এর ckd_add.
ধরার উপায়: -fsanitize=undefined (UBSan), আর -Wall -Wextra (পর্ব ৪৬)।
মূল উপলব্ধি: “আমার মেশিনে চলছে” undefined behavior-এর ক্ষেত্রে কোনো প্রমাণ নয়; compiler-এর পরের version বা অন্য optimization-এ আচরণ বদলে যেতে পারে।
প্র ১১malloc, calloc আর realloc — পার্থক্য আর প্রচলিত ফাঁদগুলো কী? (মাঝারি)
- malloc(n): n byte দেয়, ভেতরের মান অনির্দিষ্ট। ফেরত মান NULL কি না সবসময় যাচাই করুন।
- calloc(count, size): সব byte শূন্য করে দেয়, আর
count * sizeoverflow করলে NULL ফেরত দেওয়ার কথা —malloc(count * size)-এ এই সুরক্ষা নেই, গুণফল চুপচাপ ছোট হয়ে যেতে পারে। - realloc(p, n): আকার বদলায়; দরকার হলে নতুন জায়গায় সরিয়ে পুরোনো ডেটা কপি করে। সরলে পুরোনো সব পয়েন্টার অবৈধ।
সবচেয়ে প্রচলিত ফাঁদ: p = realloc(p, n); — ব্যর্থ হলে NULL ফেরত আসে, কিন্তু পুরোনো block তখনও বরাদ্দ। p-তে NULL বসে যাওয়ায় সেই block-এর ঠিকানা হারাল — leak. সঠিক রীতি:
int *tmp = realloc(p, new_n * sizeof *p);
if (tmp == NULL) { /* p is still valid: handle error, free(p) later */ }
else p = tmp;
আরও: malloc(0) NULL বা একটি অনন্য পয়েন্টার দিতে পারে — implementation-এর ওপর নির্ভর করে। realloc(p, 0)-এর আচরণ আগে implementation-defined ছিল; C23-এ এটি undefined করা হয়েছে, তাই ব্যবহার করবেন না। আর sizeof *p লিখলে টাইপ বদলালেও হিসাব ঠিক থাকে।
মূল উপলব্ধি: বরাদ্দের প্রতিটি ফাংশন ব্যর্থ হতে পারে; ব্যর্থতার পথটাই ইন্টারভিউয়ার দেখতে চান।
প্র ১২প্রোগ্রাম Segmentation fault দিয়ে থেমে গেল। আপনি কীভাবে খুঁজবেন? (মাঝারি)
Segmentation fault মানে প্রোগ্রাম এমন মেমরিতে হাত দিয়েছে, যেখানে তার অনুমতি নেই — OS তাকে থামিয়ে দিয়েছে। ইন্টারভিউয়ার এখানে একটি পদ্ধতি শুনতে চান, অনুমান নয় (পর্ব ৩১):
- পুনরুৎপাদন: কোন ইনপুটে ঘটে, নিশ্চিত করুন; সম্ভব হলে ইনপুট ছোট করুন (minimal reproducible example).
- Sanitizer:
-g -fsanitize=address,undefinedদিয়ে build করুন — AddressSanitizer সাধারণত ঠিক লাইন আর মেমরির ইতিহাস দেখিয়ে দেয়। - Debugger:
gdb-এ চালিয়ে crash-এর পরbt(backtrace) — কোন ফাংশন, কোন লাইন; তারপর সন্দেহজনক পয়েন্টারprintকরুন। - সাধারণ কারণগুলো মিলিয়ে দেখুন: NULL dereference, অ্যারের সীমার বাইরে, initialize না করা পয়েন্টার, free-র পরে ব্যবহার, string literal-এ লেখা, অসীম রিকার্শনে stack overflow.
- ঠিক করার পর: সেই ইনপুট দিয়ে একটি regression test যোগ করুন।
$ gcc -g -fsanitize=address,undefined -Wall -Wextra bug.c -o bug
$ ./bug # ASan prints the bad line and the stack
$ gcc -g -O0 bug.c -o bug && gdb ./bug
(gdb) run
(gdb) bt # backtrace: which function, which line
(gdb) print p # inspect the suspicious pointer
$ valgrind ./bug # second opinion on memory errors
মূল উপলব্ধি: segfault যেখানে ঘটে, ভুলটা প্রায়ই সেখানে নয় — অনেক আগে কেউ মেমরি নষ্ট করেছে। Sanitizer সেই আগের মুহূর্তটা ধরে।
৮ · কথা বলা, আচরণগত প্রশ্ন আর AI
একই সমাধান জানা দুজন প্রার্থীর একজন নির্বাচিত হন, অন্যজন হন না — প্রায়ই পার্থক্যটা যোগাযোগে। এই দক্ষতাও অনুশীলনে বাড়ে, ঠিক কোডের মতো।
ইন্টারভিউর ঘরে পাঁচটি অভ্যাস
- জোরে ভাবুন (think aloud): “আমি প্রথমে brute force ভাবছি… এটা O(n²), n যদি ১০৫ হয় তাহলে ধীর… তাই hash ভাবছি।” চুপ করে দুই মিনিট ভাবার চেয়ে অর্ধেক-গোছানো ভাবনা বলাও ভালো — ইন্টারভিউয়ার পথ দেখে সাহায্য করতে পারেন।
- প্রশ্ন করুন, ধরে নেবেন না: “ইনপুট কি সাজানো?”, “খালি অ্যারে আসতে পারে?” — এগুলো দুর্বলতা নয়, পেশাদারিত্ব। বাস্তব কাজেও অস্পষ্ট চাহিদা নিয়েই শুরু হয়।
- Hint-কে উপহার ভাবুন: ইন্টারভিউয়ার “যদি অ্যারেটা সাজানো থাকত?” বললে সেটা একটা দরজা। ধন্যবাদ দিয়ে ধরুন, আর বলুন hint থেকে কী বুঝলেন। hint নিয়ে এগোনো সাধারণত hint উপেক্ষা করে আটকে থাকার চেয়ে অনেক ভালো দেখায়।
- ভুল স্বীকার করুন, দ্রুত: dry run-এ নিজের বাগ ধরলে বলুন, “এখানে একটা off-by-one আছে, ঠিক করছি।” নিজের ভুল নিজে ধরা ইন্টারভিউতে খুব ইতিবাচক সংকেত — বাস্তব কাজে এটাই সবচেয়ে দরকারি অভ্যাস।
- না জানলে সৎ থাকুন: “
restrictনিয়ে নিশ্চিত নই, তবে যতটুকু জানি…” — বানিয়ে বলা অভিজ্ঞ ইন্টারভিউয়ার সাথে সাথে ধরে ফেলেন।
আচরণগত প্রশ্নে STAR কাঠামো
“এমন একটা সময়ের কথা বলুন যখন…” — এ ধরনের প্রশ্নে STAR কাঠামোয় উত্তর গোছান: Situation (পরিস্থিতি), Task (আপনার দায়িত্ব), Action (আপনি নিজে কী করলেন), Result (ফল, সম্ভব হলে সংখ্যায়, আর কী শিখলেন)।
একটি নমুনা: “বিশ্ববিদ্যালয়ের একটি দলগত প্রজেক্টে আমরা C-তে একটি লাইব্রেরি-ব্যবস্থাপনা প্রোগ্রাম বানাচ্ছিলাম (S). জমা দেওয়ার দুই দিন আগে প্রোগ্রাম মাঝে মাঝে crash করছিল, আর আমার দায়িত্ব ছিল ফাইল-সংরক্ষণের অংশ (T). আমি AddressSanitizer চালিয়ে দেখলাম, একটি স্ট্রিং কপিতে buffer-এর বাইরে লেখা হচ্ছে। strcpy-এর বদলে snprintf বসালাম, আর পাঁচটি test case লিখলাম (A). এরপর আর crash হয়নি; সেই থেকে আমি প্রতিটি প্রজেক্টে শুরু থেকেই sanitizer চালাই (R).” — ছোট, সত্যি, আর “আমি কী করলাম” স্পষ্ট।
৪–৫টি এমন গল্প আগে থেকে তৈরি রাখুন: একটি কঠিন বাগ, একটি দলগত মতবিরোধ, একটি ব্যর্থতা ও তা থেকে শেখা, নিজে থেকে নেওয়া একটি উদ্যোগ। বানানো গল্প নয় — পরের প্রশ্নেই ফাঁক বেরিয়ে যায়।
AI-এর যুগে ইন্টারভিউ: নিয়ম আর সততা
ইন্টারভিউ চলাকালীন AI-এর সাহায্য অনেক প্রতিষ্ঠানেই নিষিদ্ধ — live coding-এ, online assessment-এ, এমনকি take-home-এও অনেক সময় নিয়ম লেখা থাকে। কিছু প্রতিষ্ঠান আবার AI ব্যবহারের অনুমতি দিয়ে দেখতে চায় আপনি সেটি কীভাবে যাচাই করেন। নিয়ম প্রতিষ্ঠানভেদে আলাদা আর সময়ের সাথে বদলায় — তাই প্রতিটি ধাপের আগে নিয়ম পড়ুন, অস্পষ্ট হলে জিজ্ঞেস করুন। নিয়ম ভেঙে পাওয়া চাকরিতে প্রথম সপ্তাহেই ধরা পড়ে যাবেন, কারণ তখন কাজটা আপনাকেই করতে হবে।
কেন মুখস্থ নয়, বোঝা? ইন্টারভিউয়ার জানেন, জনপ্রিয় সমস্যার সমাধান অনলাইনে আছে। তাই আসল পরীক্ষা হয় ফলো-আপে: “এবার মেমরি O(1) করুন”, “ইনপুট স্ট্রিমে এলে?” মুখস্থ সমাধান এখানেই ভেঙে পড়ে, আর বোঝা সমাধান নতুন শর্তে বাঁকানো যায়। এই পাতার প্রতিটি সমস্যায় ফলো-আপ রাখার কারণ এটাই।
প্রস্তুতির সময় AI হতে পারে চমৎকার mock interview-এর সঙ্গী — যে কখনো ক্লান্ত হয় না, আর রাত দুটোতেও প্রশ্ন করতে রাজি। শর্ত: উত্তর নয়, প্রশ্ন আর মূল্যায়ন চাইবেন।
নিচের prompt দিয়ে একটি পূর্ণ mock interview চালান। খেয়াল করুন, AI-কে স্পষ্ট বলা হয়েছে সমাধান না দিতে, আর শেষে কঠোরভাবে মূল্যায়ন করতে:
AI দিয়ে অনুশীলনের চেকলিস্ট
৯ · চার সপ্তাহের অনুশীলন পরিকল্পনা
প্রতিদিন ১.৫–২ ঘণ্টা ধরে নিচের ছকটি একটি কাঠামো — আপনার হাতে সময় কম-বেশি হলে সপ্তাহগুলো ছোট বা লম্বা করুন। প্রতিটি সপ্তাহে আগে কোর্সের পর্ব রিভিশন, তারপর সেই বিষয়ের সমস্যা। LeetCode, HackerRank, Codeforces আর বাংলাদেশে জনপ্রিয় Toph ও LightOJ-এর মতো প্ল্যাটফর্মে এ ধরনের অনেক সমস্যা পাবেন — বিষয় বা tag ধরে বেছে নিন।
| সপ্তাহ | বিষয় | যে পর্ব রিভিশন করবেন | প্রতিদিনের কাজ |
|---|---|---|---|
| ১ | অ্যারে, স্ট্রিং, two pointer, জটিলতা | ১৬, ১৭, ১৯, ২১, ৩৯ | ২টি সহজ সমস্যা; এই পাতার সমস্যা ১–৬ খাতায়; প্রতিটির জটিলতা লিখে রাখা |
| ২ | পয়েন্টার, মেমরি, C-নির্দিষ্ট প্রশ্ন | ১৮, ২০, ২২, ৩১, ৪৫, ৪৬ | string.h-এর ৫টি ফাংশন নিজে লেখা ও test; অংশ ৭-এর ১২টি প্রশ্ন মুখে উত্তর; পর্ব ৫৪–৫৫-এর প্রশ্ন |
| ৩ | লিংকড লিস্ট, স্ট্যাক, কিউ, ট্রি, হিপ, হ্যাশ | ৩৩, ৩৪, ৩৫, ৩৬, ৩৭, ৩৮ | ২টি মাঝারি সমস্যা; প্রতিটি ডেটা স্ট্রাকচার শূন্য থেকে ২০ মিনিটে লেখা; সপ্তাহে একটি mock interview |
| ৪ | সর্টিং, সার্চিং, বিট, DP, greedy | ৯, ৪০, ৪১, ৪২, ৪৩, ৪৪ | সময় ধরে OA-র মতো সেট (৯০ মিনিটে ৩টি); সপ্তাহে দুটি mock interview; আগের ভুলগুলো আবার সমাধান |
একটি ভুলের খাতা রাখুন। প্রতিটি ভুল সমাধানের পর তিন লাইন লিখুন: কোন ধারণা মিস করেছিলাম, কোন edge case ভুলেছিলাম, পরেরবার কোন সংকেত দেখে চিনব। এক মাস পরে এই খাতাটিই আপনার সবচেয়ে মূল্যবান নোট হবে। পুরোনো সমস্যা কয়েক দিন পর আবার সমাধান করা (upsolving-এর মতো) নতুন সমস্যার চেয়ে প্রায়ই বেশি শেখায়।
ইন্টারভিউর আগের রাতে নতুন কিছু শিখতে যাবেন না। নিজের ক্যাপস্টোন প্রজেক্ট (পর্ব ৫২) আরেকবার দেখে নিন — “আপনার প্রজেক্টে সবচেয়ে কঠিন অংশ কোনটা ছিল?” প্রশ্নটি প্রায় নিশ্চিত, আর এর উত্তরেই আপনি সবচেয়ে আত্মবিশ্বাসী থাকবেন।
১০ · কোর্স সমাপ্তি — ৫৬টি পর্বের পরে আপনি কোথায়
এটি ৮টি মডিউল আর ৫৬টি পর্বের এই কোর্সের শেষ পর্ব। একবার পেছনে তাকান। পর্ব ১-এ প্রশ্ন ছিল, “C কেন?” পর্ব ৩-এ দেখেছিলেন, একটি .c ফাইল কীভাবে প্রোগ্রাম হয়। আজ আপনি একটি লিংকড লিস্টের তীর ঘোরাতে পারেন, memmove কেন পেছন থেকে কপি করে বোঝাতে পারেন, আর একটি অচেনা সমস্যাকে সাত ধাপে ভাঙতে পারেন।
এখন আপনি যা পারেন
- কম্পিউটারের ভেতরে ডেটা কীভাবে থাকে — বিট, byte, পয়েন্টার, stack আর heap — নিজের ভাষায় ব্যাখ্যা করতে।
- অ্যারে, স্ট্রিং, struct, লিংকড লিস্ট, স্ট্যাক, কিউ, ট্রি, হিপ, হ্যাশ টেবিল আর গ্রাফ নিজ হাতে বানাতে।
- যেকোনো সমাধানের সময় ও মেমরি Big-O-তে মাপতে, আর trade-off বুঝে পদ্ধতি বাছতে।
- header, Makefile, test, sanitizer আর debugger দিয়ে একটি বহু-ফাইলের প্রোগ্রাম নির্ভরযোগ্যভাবে দাঁড় করাতে।
- process, thread, socket আর এমবেডেড হার্ডওয়্যারের সাথে C কীভাবে কথা বলে, তা বুঝতে।
- AI-এর লেখা কোড যাচাই করতে — আর দরকারে তার ভুল ধরতে।
অনুশীলন চালিয়ে যাওয়ার উপায়
- প্রতিদিন অল্প: দিনে একটি সমস্যা, পুরো সাত ধাপে — সপ্তাহে একদিন বড় সেশনের চেয়ে বেশি কাজে দেয়।
- কনটেস্টে নিয়মিত: Codeforces বা Toph-এর কনটেস্ট আর পরে upsolving (পর্ব ৪৪)।
- একটি প্রজেক্ট বড় করুন: ক্যাপস্টোনে নতুন একটি feature যোগ করুন, test লিখুন, GitHub-এ রাখুন — portfolio নিজে থেকেই বড় হবে।
- অন্যকে শেখান: বন্ধুকে একটি পয়েন্টারের সমস্যা বোঝাতে গেলেই বুঝবেন নিজের কোথায় ফাঁক আছে।
- মাঝে মাঝে ফিরে আসুন: পর্ব ৫৩–৫৫-এর প্রশ্নব্যাংক দিয়ে রিভিশন, আর কোর্স হোম থেকে যে পর্ব দুর্বল লাগে সেটি আবার।
এরপর কোন পথে
Data Structures & Algorithms
ইন্টারভিউর জন্য আরও গভীর ডেটা স্ট্রাকচার, গ্রাফ অ্যালগরিদম আর DP-র ধরন।
System Design
অভিজ্ঞ পদের ইন্টারভিউর বড় অংশ — cache, load balancer, ডেটাবেস ভাগ করা।
C++ Programming
C-এর ওপর class, template আর STL — অনেক কোডিং ইন্টারভিউতে সবচেয়ে প্রচলিত ভাষা।
Operating Systems
Process, virtual memory, scheduling — সিস্টেম পদের ইন্টারভিউর প্রিয় বিষয়।
বোঝা, যাচাই করা আর ব্যাখ্যা করতে পারা — এগুলোই আপনাকে ইঞ্জিনিয়ার বানায়।
৫৬টি পর্ব শেষ করা সহজ কাজ নয় — আপনার ধৈর্য আর পরিশ্রমকে আন্তরিক অভিনন্দন। প্রথম segfault-এর সামনে যে বিভ্রান্তি ছিল, প্রথমবার পয়েন্টার বুঝতে যে সময় লেগেছিল, সেগুলোই আজ আপনার শক্তি। ইন্টারভিউতে একবার বা দুবার ব্যর্থ হওয়া খুব স্বাভাবিক; প্রতিটি ইন্টারভিউ থেকে একটি শিক্ষা নিয়ে ভুলের খাতায় লিখুন, আর এগিয়ে যান। কোনো পর্বে ভুল পেলে বা প্রশ্ন থাকলে আমাদের জানাবেন। শুভকামনা রইল — ABCL TECH-এর পরের কোর্সে আবার দেখা হবে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমইন্টারভিউতে একটি সমস্যা পেলে ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ❓ প্রশ্ন করে সমস্যা ও constraints পরিষ্কার করা
- 📝 নিজে উদাহরণ ও edge case লেখা
- 🧭 আগে brute force বলা, তারপর উন্নত পদ্ধতি খোঁজা
- ⏱️ বেছে নেওয়া পদ্ধতির সময় ও মেমরি বলা
- ⌨️ ইন্টারভিউয়ারের সম্মতি নিয়ে কোড লেখা
- 🔍 নিজের উদাহরণ দিয়ে dry run করে পরীক্ষা
(সহজ) Two sum-এর brute force সমাধান — প্রতিটি জোড়া (i, j) যাচাই — এর সময়-জটিলতা কত?
- O(n)
- O(n²)
- O(n log n)
- O(1)
(মাঝারি) নিচের লুপটি একটি স্ট্রিং পেছন থেকে ছাপানোর জন্য লেখা: for (size_t i = strlen(s) - 1; i >= 0; i--) putchar(s[i]); — সমস্যা কী?
- শুরু হওয়া উচিত strlen(s) থেকে
- putchar-এর বদলে printf লাগবে
- i কখনো ঋণাত্মক হয় না, তাই লুপ থামে না এবং অ্যারের বাইরে পড়ে
- i-- এর বদলে i++ হওয়া উচিত
(মাঝারি) Binary search-এ mid = (lo + hi) / 2 লেখার ঝুঁকি কী?
- বড় index-এ lo + hi overflow করতে পারে
- ভাগফল দশমিকে আসে, তাই ভুল ঘর পড়ে
- জটিলতা O(n) হয়ে যায়
- ছোট অ্যারেতে লুপ কখনো থামে না
(সহজ) Floyd-এর cycle detection (ধীর ও দ্রুত পয়েন্টার)-এ বাড়তি মেমরি কত লাগে?
- O(n)
- O(log n)
- O(n²)
- O(1)
(মাঝারি) p = realloc(p, 2 * n * sizeof *p); লাইনে লুকানো বাগ কোনটি?
- sizeof *p ভুল, sizeof p হওয়া উচিত
- ব্যর্থ হলে পুরোনো block-এর ঠিকানা হারিয়ে যায় — leak
- পুরোনো ডেটা কপি হয় না, memcpy লাগবে
- পরে পুরোনো p আলাদা করে free করতে হবে
(মাঝারি) n টি সংখ্যার স্ট্রিম থেকে k-তম বৃহত্তম বের করতে k আকারের min-heap ব্যবহার করলে সময় কত?
- O(n)
- O(n log n)
- O(n log k)
- O(n · k)
(কঠিন) নোট {1, 3, 4} দিয়ে ৬ টাকা বানাতে “সবসময় বড় নোট আগে” (greedy) কয়টি নোট নেয়, আর সর্বনিম্ন কত?
- Greedy ৩টি, সর্বনিম্ন ২টি
- দুটোই ২টি
- দুটোই ৩টি
- Greedy ৪টি, সর্বনিম্ন ২টি
(সহজ) Live coding-এ আপনি আটকে গেছেন, আর ইন্টারভিউয়ার বললেন, “অ্যারেটা সাজানো থাকলে কী করতেন?” সবচেয়ে ভালো প্রতিক্রিয়া কোনটি?
- নিজের আগের পথেই চুপচাপ চেষ্টা চালিয়ে যাওয়া
- বলা যে প্রশ্নে তো সাজানোর কথা ছিল না
- সরাসরি পুরো সমাধান জানতে চাওয়া
- ধন্যবাদ দিয়ে জোরে ভাবা — “সাজানো থাকলে দুই পয়েন্টার চালাতে পারি, তাহলে আগে সাজাই…”
(সহজ) int is_pow2(unsigned x) { return (x & (x - 1)) == 0; } — কোন ইনপুটে এটি ভুল উত্তর দেয়?
- 1
- 0
- 1024
- 6
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব শব্দ এখানে একসাথে:
মনে রাখুন
- ইন্টারভিউয়ার দেখেন বোঝা, যোগাযোগ, সঠিকতা, জটিলতা, টেস্টিং ও কোডের মান — শুধু শেষ কোড নয়।
- প্রতিটি সমস্যায় সাত ধাপ: বুঝুন → উদাহরণ → পদ্ধতি → জটিলতা → কোড → পরীক্ষা → উন্নতি.
- আগে brute force, তারপর “কোন কাজ বারবার হচ্ছে?” — hash, দুই পয়েন্টার, heap বা DP দিয়ে সেটি দ্রুত করুন।
- C-তে edge case-ই আসল পরীক্ষা: খালি ইনপুট,
NULL,size_tunderflow, signed overflow, overlap,free. - মুখস্থ নয়, বুঝুন — ফলো-আপ প্রশ্নে বোঝাই কাজে আসে; আর ইন্টারভিউর সময় AI-এর নিয়ম আগে জেনে নিন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — দুর্বল বিষয় খুঁজে আবার পড়ুন।
- পর্ব ৫৩ · আউটপুট কী হবে? — কোড পড়ার অনুশীলনরিভিশনDry run-এর অভ্যাস, যা ইন্টারভিউর “পরীক্ষা” ধাপে সরাসরি কাজে লাগে।
- পর্ব ৫৫ · লিখিত ও মৌখিক (ভাইভা) প্রশ্নআগের পর্বধারণার প্রশ্নে গুছিয়ে উত্তর দেওয়ার অনুশীলন।
- Data Structures & Algorithmsপরের কোর্সইন্টারভিউর জন্য আরও গভীর ডেটা স্ট্রাকচার ও অ্যালগরিদম।
- System Designগভীরে যানবড় সিস্টেমের নকশা — অভিজ্ঞ পদের ইন্টারভিউর মূল অংশ।
- C++ Programmingপাশের ট্র্যাকC থেকে C++ — STL দিয়ে ইন্টারভিউর কোড আরও ছোট ও নিরাপদ।