Data StructuresData Structures
How we store data so we can find it, add it, and remove it fast. This is the heart of CSE admission exams. Data কীভাবে রাখলে দ্রুত খোঁজা, যোগ করা আর মুছে ফেলা যায় — সেটাই data structure। CSE admission exam-এর সবচেয়ে গুরুত্বপূর্ণ অংশ এটা।
1. Arrays and Linked Lists1. Arrays এবং Linked Lists
Array: data in one continuous blockArray: এক টানা memory block-এ data
An array stores elements side by side in memory. All elements have the same type and size. Because the block is continuous, the computer can jump to any index with simple math. This is why array access is \( O(1) \).
Array element গুলোকে memory-তে পাশাপাশি রাখে। সব element-এর type আর size একই। Memory block টানা (contiguous) বলে computer সহজ অঙ্ক করে যেকোনো index-এ সরাসরি যেতে পারে। এজন্যই array access \( O(1) \)।
int array starts at address 2000. Each int is 4 bytes. Where is A[5]?
Address = 2000 + 5 × 4 = 2020.
For a 2D array
A[R][C] stored row-major (like C): address of A[i][j] = base + (i × C + j) × size. So for int A[10][20] at base 1000, A[3][4] is at 1000 + (3×20 + 4)×4 = 1000 + 256 = 1256.
int array শুরু হয়েছে address 2000-এ। প্রতিটা int = 4 bytes। A[5] কোথায়?
Address = 2000 + 5 × 4 = 2020।
2D array
A[R][C] row-major-এ (C language-এর মতো) থাকলে: A[i][j]-এর address = base + (i × C + j) × size। যেমন int A[10][20], base 1000 হলে A[3][4] = 1000 + (3×20 + 4)×4 = 1000 + 256 = 1256।
Array weakness: inserting or deleting in the middle needs shifting. To insert at position 0 of an array with \( n \) items, we shift all \( n \) items right. That is \( O(n) \). Also, a normal array has fixed size.
Array-এর দুর্বলতা: মাঝখানে insert বা delete করতে element shift করতে হয়। \( n \) item-এর array-তে position 0-তে insert করতে হলে সব \( n \) item ডানে সরাতে হয়। এটা \( O(n) \)। আর সাধারণ array-এর size fixed।
Linked list: nodes connected by pointersLinked list: pointer দিয়ে জোড়া node
A linked list stores data in separate nodes. Each node holds the data and a pointer to the next node. Nodes can live anywhere in memory. So the list can grow and shrink easily. But to reach the 100th node, we must walk through 99 nodes first — access is \( O(n) \).
Linked list data রাখে আলাদা আলাদা node-এ। প্রতিটা node-এ থাকে data আর পরের node-এর pointer। Node গুলো memory-র যেকোনো জায়গায় থাকতে পারে। তাই list সহজে বড়-ছোট হতে পারে। কিন্তু 100 নম্বর node-এ যেতে হলে আগের 99টা node পার হতে হয় — access \( O(n) \)।
Three common kinds:
- Singly linked list: each node has one pointer,
next. We can only move forward. - Doubly linked list: each node has
nextandprev. We can move both ways. Deleting a known node is easy, but each node needs extra memory. - Circular linked list: the last node points back to the first node instead of NULL. Useful for round-robin tasks.
তিনটা common ধরন:
- Singly linked list: প্রতিটা node-এ একটাই pointer,
next। শুধু সামনে যাওয়া যায়। - Doubly linked list: প্রতিটা node-এ
nextআরprevদুটোই থাকে। দুই দিকেই যাওয়া যায়। কোনো node জানা থাকলে delete করা সহজ, কিন্তু প্রতি node-এ বাড়তি memory লাগে। - Circular linked list: শেষ node NULL-এর বদলে প্রথম node-কে point করে। Round-robin কাজে কাজে লাগে।
C code: insert and delete in a singly linked listC code: singly linked list-এ insert এবং delete
struct Node {
int data;
struct Node *next;
};
/* Insert at the front: O(1) */
struct Node* insertFront(struct Node *head, int value) {
struct Node *n = (struct Node*) malloc(sizeof(struct Node));
n->data = value;
n->next = head; /* new node points to old head */
return n; /* new node becomes the head */
}
/* Insert after a given node p: O(1) */
void insertAfter(struct Node *p, int value) {
struct Node *n = (struct Node*) malloc(sizeof(struct Node));
n->data = value;
n->next = p->next;
p->next = n;
}
/* Delete the first node with given value: O(n) to find it */
struct Node* deleteValue(struct Node *head, int value) {
if (head == NULL) return NULL;
if (head->data == value) { /* case 1: delete head */
struct Node *t = head->next;
free(head);
return t;
}
struct Node *cur = head;
while (cur->next != NULL && cur->next->data != value)
cur = cur->next;
if (cur->next != NULL) { /* case 2: found in middle/end */
struct Node *t = cur->next;
cur->next = t->next; /* skip over the node */
free(t);
}
return head;
}
insertAfter, if you write p->next = n; before n->next = p->next;, you lose the rest of the list. BUET loves "what is wrong with this code" questions on this.insertAfter-এ যদি আগে p->next = n; লেখো, তারপর n->next = p->next;, তাহলে list-এর বাকি অংশ হারিয়ে যাবে। BUET-এ "এই code-এ ভুল কী" ধরনের প্রশ্ন এখান থেকে আসে।Insert into a Sorted Linked ListSorted Linked List-এ Insert
Very common exam task: the list is already sorted (ascending), and we must insert a new value so the list stays sorted. The trick is to walk with two pointers — prev and curr — and stop as soon as curr's data is not smaller than the new value. Then the new node goes between prev and curr. Two cases need care:
- Head insert: the list is empty, or the new value is smaller than the head → the new node becomes the new head.
- Middle / tail insert: walk until
curris NULL (tail) orcurr->data >= value(middle), then re-link.
Exam-এর খুব common কাজ: list আগে থেকেই sorted (ascending), নতুন একটা value এমনভাবে insert করতে হবে যেন list sorted-ই থাকে। কৌশলটা হলো দুইটা pointer — prev আর curr — দিয়ে হাঁটা, আর যেই curr-এর data নতুন value-র চেয়ে ছোট না হয়, সেখানেই থামা। তখন নতুন node বসবে prev আর curr-এর মাঝে। দুইটা case-এ সাবধান:
- Head insert: list খালি, বা নতুন value head-এর চেয়ে ছোট → নতুন node-ই নতুন head হবে।
- Middle / tail insert:
currNULL হওয়া (tail) বাcurr->data >= valueহওয়া (middle) পর্যন্ত হাঁটো, তারপর re-link করো।
/* insert value into an ascending sorted list; returns the (maybe new) head */
struct Node* sortedInsert(struct Node *head, int value) {
struct Node *n = malloc(sizeof(struct Node));
n->data = value;
n->next = NULL;
/* case 1: empty list, or new value goes before the head */
if (head == NULL || value < head->data) {
n->next = head;
return n; /* n is the new head */
}
/* case 2: prev/curr walk to find the spot (middle or tail) */
struct Node *prev = head, *curr = head->next;
while (curr != NULL && curr->data < value) {
prev = curr;
curr = curr->next;
}
n->next = curr; /* curr may be NULL: tail insert */
prev->next = n; /* link n after prev */
return head; /* head did not change */
}
Step 1: 25 < 10? No → not a head insert.
Step 2:
prev = 10, curr = 20. Is 20 < 25? Yes → move up: prev = 20, curr = 30.
Step 3: Is 30 < 25? No → stop here.
Step 4:
n->next = curr (25 → 30), then prev->next = n (20 → 25).
Result:
10 → 20 → 25 → 30 → 40. Sorted, as promised.
Check the edge cases too: inserting 5 hits case 1 (new head: 5 → 10 → ...); inserting 50 walks until
curr = NULL and lands at the tail (... → 40 → 50). Time: \( O(n) \) — one walk, no extra space.
Step 1: 25 < 10? না → head insert নয়।
Step 2:
prev = 10, curr = 20। 20 < 25? হ্যাঁ → এগোও: prev = 20, curr = 30।
Step 3: 30 < 25? না → এখানেই থামো।
Step 4:
n->next = curr (25 → 30), তারপর prev->next = n (20 → 25)।
Result:
10 → 20 → 25 → 30 → 40। Sorted-ই আছে।
Edge case গুলোও দেখো: 5 insert করলে case 1 লাগে (নতুন head: 5 → 10 → ...); 50 insert করলে
curr = NULL পর্যন্ত হেঁটে tail-এ বসে (... → 40 → 50)। Time: \( O(n) \) — একবার হাঁটা, extra space নেই।
p->next), no array brackets, and always show the head-insert case separately.p->next) দিয়ে লেখো, কোনো array bracket নয়, আর head-insert case টা সবসময় আলাদা করে দেখাও।Array vs linked listArray vs linked list
| Operation / propertyOperation / property | Array | Singly linked listSingly linked list |
|---|---|---|
| Access by indexIndex দিয়ে access | \( O(1) \) | \( O(n) \) |
| Search (unsorted)Search (unsorted) | \( O(n) \) | \( O(n) \) |
| Insert / delete at frontসামনে insert / delete | \( O(n) \) (shift all)(সব shift করতে হয়) | \( O(1) \) |
| Insert / delete in middle (position known)মাঝে insert / delete (position জানা) | \( O(n) \) | \( O(1) \) (after reaching the node)(node-এ পৌঁছানোর পরে) |
| Memory layoutMemory layout | Contiguous, cache friendlyContiguous, cache friendly | Scattered, extra pointer per nodeছড়ানো, প্রতি node-এ বাড়তি pointer |
| SizeSize | Fixed (static array)Fixed (static array) | Grows and shrinks easilyসহজে বাড়ে-কমে |
Cycle Detection — Floyd's Tortoise and HareCycle Detection — Floyd's Tortoise and Hare
Sometimes a linked list has a problem: the last node does not point to NULL. Instead it points back to some earlier node. This makes a cycle (loop). If we walk the list with one pointer, we go round and round forever. Floyd's algorithm finds a cycle using two pointers:
- slow (the tortoise) moves 1 step at a time.
- fast (the hare) moves 2 steps at a time.
- If fast reaches NULL → the list ends normally → no cycle.
- If slow and fast ever land on the same node → there is a cycle.
Time: \( O(n) \). Extra space: \( O(1) \) — only two pointers. That is why exams love this answer.
কখনো কখনো linked list-এ একটা সমস্যা থাকে: শেষ node NULL-কে point করে না। বরং আগের কোনো node-কে point করে। এতে একটা cycle (loop) তৈরি হয়। এক pointer দিয়ে হাঁটলে আমরা চিরকাল ঘুরতেই থাকব। Floyd's algorithm দুইটা pointer দিয়ে cycle খুঁজে বের করে:
- slow (tortoise) একবারে 1 step যায়।
- fast (hare) একবারে 2 step যায়।
- fast যদি NULL-এ পৌঁছায় → list স্বাভাবিকভাবে শেষ → cycle নেই।
- slow আর fast যদি কখনো একই node-এ পড়ে → cycle আছে।
Time: \( O(n) \)। Extra space: \( O(1) \) — মাত্র দুইটা pointer। এজন্যই exam-এ এই উত্তরটা এত প্রিয়।
/* returns 1 if the list has a cycle, 0 if it ends at NULL */
int hasCycle(struct Node *head) {
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; /* they met inside a cycle */
}
return 0; /* fast fell off the end: no cycle */
}
Why it works (simple argument): If there is no cycle, fast reaches NULL and we stop — easy. If there is a cycle, both pointers eventually enter it and go round and round. Inside the cycle, fast gains exactly 1 position on slow every step. So the gap between them shrinks by 1 each step. The gap must reach 0 before slow finishes even one full lap — so they are guaranteed to meet. The loop always ends.
কেন কাজ করে (সহজ যুক্তি): Cycle না থাকলে fast NULL-এ পৌঁছে যায়, আমরা থেমে যাই — সহজ। Cycle থাকলে দুই pointer-ই একসময় cycle-এর ভিতরে ঢুকে ঘুরতে থাকে। Cycle-এর ভিতরে fast প্রতি step-এ slow-এর থেকে ঠিক 1 position এগোয়। তাই তাদের মধ্যের gap প্রতি step-এ 1 করে কমে। slow এক চক্কর শেষ করার আগেই gap 0 হয়ে যায় — মানে তারা মিলবেই। Loop সবসময় শেষ হয়।
Answer approach: Run Floyd's two-pointer walk: each round,
slow = slow->next and fast = fast->next->next. If fast or fast->next becomes NULL → the list terminates → it is a snake. If slow == fast at some point → the two pointers met inside a loop → it is a snail. Time \( O(n) \), extra space \( O(1) \). (A simpler answer — remembering every visited node in a hash table — also works but needs \( O(n) \) extra space. Mention that Floyd is better.)
Answer approach: Floyd's two-pointer walk চালাও: প্রতি round-এ
slow = slow->next আর fast = fast->next->next। fast বা fast->next NULL হয়ে গেলে → list শেষ হয় → এটা snake। কোনো এক সময় slow == fast হলে → pointer দুটো loop-এর ভিতরে মিলে গেছে → এটা snail। Time \( O(n) \), extra space \( O(1) \)। (সহজ আরেকটা উত্তর — প্রতিটা visited node hash table-এ রাখা — সেটাও চলে, কিন্তু \( O(n) \) extra space লাগে। বলে দাও Floyd better।)
2. Stack2. Stack
A stack is a LIFO (Last In, First Out) structure. Think of a pile of plates. You put a new plate on top. You also take a plate from the top. The last plate you put is the first one you take.
Main operations, all \( O(1) \):
push(x)— put x on top.pop()— remove and return the top item.peek()(ortop()) — look at the top item without removing it.isEmpty()— check if the stack has no items.
Pushing into a full array stack causes overflow. Popping an empty stack causes underflow.
Stack হলো LIFO (Last In, First Out) structure। প্লেটের স্তূপের মতো। নতুন প্লেট উপরে রাখো, নেওয়ার সময়ও উপর থেকে নাও। শেষে যেটা রেখেছো, সেটাই আগে বের হয়।
মূল operation, সবগুলোই \( O(1) \):
push(x)— x-কে উপরে রাখা।pop()— উপরের item সরিয়ে ফেরত দেওয়া।peek()(বাtop()) — না সরিয়ে উপরের item দেখা।isEmpty()— stack খালি কি না দেখা।
Full array stack-এ push করলে overflow হয়। খালি stack-এ pop করলে underflow হয়।
Array implementationArray implementation
#define MAX 100
int stack[MAX];
int top = -1; /* empty stack */
void push(int x) {
if (top == MAX - 1) { printf("Overflow\n"); return; }
stack[++top] = x; /* move top up, then store */
}
int pop(void) {
if (top == -1) { printf("Underflow\n"); return -1; }
return stack[top--]; /* return item, then move top down */
}
int peek(void) { return stack[top]; }
Linked list implementationLinked list implementation
Push = insert at the head. Pop = delete the head. Both are \( O(1) \), and there is no fixed size limit.
Push মানে head-এ insert। Pop মানে head delete। দুটোই \( O(1) \), আর size-এর কোনো fixed limit নেই।
struct Node { int data; struct Node *next; };
struct Node *top = NULL;
void push(int x) {
struct Node *n = (struct Node*) malloc(sizeof(struct Node));
n->data = x;
n->next = top;
top = n;
}
int pop(void) {
if (top == NULL) return -1; /* underflow */
int v = top->data;
struct Node *t = top;
top = top->next;
free(t);
return v;
}
Application 1: balanced parenthesesApplication 1: balanced parentheses
To check if brackets like {[()]} are balanced: scan left to right. Push every opening bracket. For every closing bracket, pop and check that it matches. At the end, the stack must be empty.
{[()]}-এর মতো bracket balanced কি না দেখতে: বাম থেকে ডানে scan করো। প্রতিটা opening bracket push করো। প্রতিটা closing bracket-এ pop করে match কি না দেখো। শেষে stack খালি থাকতে হবে।
{[(]}. Push {, push [, push (. Next char is ] — pop gives (, but ( does not match ]. So it is not balanced.
{[(]} check করি। Push {, push [, push (। পরের character ] — pop করলে পাই (, কিন্তু ( আর ] match করে না। তাই এটা balanced নয়।
Application 2: infix to postfix conversionApplication 2: infix থেকে postfix conversion
Infix: A + B (operator between operands). Postfix: A B + (operator after operands). Computers like postfix because no parentheses are needed. Conversion rules (shunting-yard idea):
- Operand → output it directly.
(→ push it.)→ pop and output until(is found; discard both parentheses.- Operator → pop and output all stack operators with higher or equal precedence, then push the new operator. (Precedence:
^>* />+ -.^is right-associative, so for^pop only strictly higher.) - End of input → pop everything to output.
Infix: A + B (operator মাঝে)। Postfix: A B + (operator পরে)। Computer postfix পছন্দ করে কারণ parentheses লাগে না। Conversion-এর নিয়ম:
- Operand → সরাসরি output-এ।
(→ push।)→(পাওয়া পর্যন্ত pop করে output-এ দাও; দুই parentheses-ই বাদ।- Operator → stack-এ থাকা higher বা equal precedence-এর operator গুলো pop করে output-এ দাও, তারপর নতুন operator push করো। (Precedence:
^>* />+ -।^right-associative, তাই^-এর ক্ষেত্রে শুধু strictly higher হলে pop।) - Input শেষ → stack-এর সব pop করে output-এ।
A + B * (C - D) to postfix.
| Symbol | Stack (bottom→top) | Output |
|---|---|---|
| A | (empty) | A |
| + | + | A |
| B | + | A B |
| * | + * | A B ( * has higher precedence than +, so just push ) |
| ( | + * ( | A B |
| C | + * ( | A B C |
| - | + * ( - | A B C |
| D | + * ( - | A B C D |
| ) | + * | A B C D - ( pop until "(" ) |
| end | (empty) | A B C D - * + |
A B C D - * +
A + B * (C - D)-কে postfix-এ convert করি।
| Symbol | Stack (নিচ→উপর) | Output |
|---|---|---|
| A | (খালি) | A |
| + | + | A |
| B | + | A B |
| * | + * | A B ( *-এর precedence + থেকে বেশি, তাই শুধু push ) |
| ( | + * ( | A B |
| C | + * ( | A B C |
| - | + * ( - | A B C |
| D | + * ( - | A B C D |
| ) | + * | A B C D - ( "(" পাওয়া পর্যন্ত pop ) |
| শেষ | (খালি) | A B C D - * + |
A B C D - * +
6 2 3 + - 3 *.
Push 6, 2, 3. See +: pop 3, 2 → 2+3=5, push 5. See -: pop 5, 6 → 6−5=1, push 1. Push 3. See *: pop 3, 1 → 1×3=3. Note: for - and /, the first popped value is the right operand.
6 2 3 + - 3 * evaluate করি।
Push 6, 2, 3। + পেলে: pop 3, 2 → 2+3=5, push 5। - পেলে: pop 5, 6 → 6−5=1, push 1। Push 3। * পেলে: pop 3, 1 → 1×3=3। খেয়াল রাখো: - আর /-এর ক্ষেত্রে প্রথমে pop হওয়া value-টা right operand।
Application 3: function call stackApplication 3: function call stack
When a program calls a function, the computer pushes a "frame" (return address, local variables) on the call stack. When the function returns, its frame is popped. Recursion works because of this. Too-deep recursion fills the stack — that is a stack overflow. Other uses: undo feature, browser back button, DFS.
Program কোনো function call করলে computer একটা "frame" (return address, local variable) call stack-এ push করে। Function return করলে frame pop হয়। Recursion এই ভাবেই কাজ করে। বেশি গভীর recursion হলে stack ভরে যায় — সেটাই stack overflow। আরও ব্যবহার: undo feature, browser-এর back button, DFS।
Application 4: palindrome check with a stack and a queueApplication 4: stack আর queue দিয়ে palindrome check
MADAM) using one stack and one queue.
Idea: Push every character on a stack AND enqueue it in a queue. The stack gives the string back reversed (LIFO); the queue gives it back in order (FIFO). If both outputs match character by character, the string equals its own reverse — a palindrome.
MADAM) — একটা stack আর একটা queue দিয়ে check করার pseudocode লেখো।
Idea: প্রতিটা character stack-এ push করো এবং queue-তে enqueue করো। Stack string-টা উল্টো করে ফেরত দেয় (LIFO); queue ঠিক order-এ ফেরত দেয় (FIFO)। দুটোর output character ধরে ধরে মিলে গেলে string টা তার নিজের reverse-এর সমান — মানে palindrome।
isPalindrome(s):
S = empty stack, Q = empty queue
for each character c in s:
push(S, c) // S will return s reversed
enqueue(Q, c) // Q will return s in order
while S is not empty:
if pop(S) != dequeue(Q):
return FALSE // mismatch: not a palindrome
return TRUE // all matched
MADAM: pop order = M A D A M, dequeue order = M A D A M — all 5 pairs match → palindrome. With BUET: first pair is pop = T vs dequeue = B → mismatch → not a palindrome. Time \( O(n) \), space \( O(n) \).MADAM দিয়ে trace: pop order = M A D A M, dequeue order = M A D A M — 5টা pair-ই মিলে যায় → palindrome। BUET দিয়ে: প্রথম pair-এ pop = T আর dequeue = B → মিলল না → palindrome নয়। Time \( O(n) \), space \( O(n) \)।3. Queue3. Queue
A queue is a FIFO (First In, First Out) structure. Like a line at a ticket counter. New people join at the rear. Service happens at the front. The person who came first leaves first.
enqueue(x)— add x at the rear. \( O(1) \)dequeue()— remove the item at the front. \( O(1) \)front()— look at the front item.
Queue হলো FIFO (First In, First Out) structure। টিকিট কাউন্টারের লাইনের মতো। নতুন মানুষ ঢোকে rear-এ, service হয় front-এ। যে আগে এসেছে, সে আগে বের হয়।
enqueue(x)— rear-এ x যোগ করা। \( O(1) \)dequeue()— front-এর item সরানো। \( O(1) \)front()— front-এর item দেখা।
Circular queue: reusing empty spaceCircular queue: খালি জায়গা আবার ব্যবহার
In a simple array queue, after many dequeues the front part of the array becomes wasted. A circular queue fixes this: when the index reaches the end, it wraps around to 0 using modulo. With array size \( N \):
সাধারণ array queue-তে অনেকবার dequeue-এর পর array-র সামনের অংশ waste হয়ে যায়। Circular queue এটা ঠিক করে: index শেষে পৌঁছালে modulo দিয়ে আবার 0-তে ঘুরে আসে। Array size \( N \) হলে:
#define N 5
int q[N];
int front = 0, count = 0; /* count-based version: simplest */
void enqueue(int x) {
if (count == N) { printf("Full\n"); return; }
q[(front + count) % N] = x;
count++;
}
int dequeue(void) {
if (count == 0) { printf("Empty\n"); return -1; }
int v = q[front];
front = (front + 1) % N;
count--;
return v;
}
front and rear pointers (no count), then "full" and "empty" can look the same. The standard trick: keep one slot always unused.
Empty:
front == rear.
Full:
(rear + 1) % N == front.
So a circular queue of array size N holds at most N − 1 items in this scheme. BUET asks this a lot.
front আর rear pointer রাখলে (count ছাড়া) "full" আর "empty" একই রকম দেখায়। Standard trick: একটা slot সবসময় খালি রাখা।
Empty:
front == rear।
Full:
(rear + 1) % N == front।
তাই এই scheme-এ array size N-এর circular queue-তে সর্বোচ্চ N − 1 item রাখা যায়। BUET-এ এটা প্রায়ই আসে।
(4+1)%5 == 0 == front, so it is full (one slot wasted). Dequeue twice → front = 2. Enqueue 50 → stored at index 4, rear = 0 (wrapped around!). Items in order: 30, 40, 50.
(4+1)%5 == 0 == front, তাই এটা full (একটা slot waste)। দুইবার dequeue → front = 2। Enqueue 50 → index 4-এ বসে, rear = 0 (ঘুরে গেল!)। Item-এর order: 30, 40, 50।
Queue with a singly linked list: O(1) enqueue AND dequeueSingly linked list দিয়ে queue: O(1) enqueue এবং dequeue
Answer: Keep two pointers:
front (first node) and rear (last node). Then enqueue at the rear, dequeue at the front. Both touch only one end directly — no walking needed.
Answer: দুইটা pointer রাখো:
front (প্রথম node) আর rear (শেষ node)। তারপর rear-এ enqueue, front-এ dequeue করো। দুটো operation-ই সরাসরি এক প্রান্তে কাজ করে — কোনো হাঁটাহাঁটি লাগে না।
struct Node { int data; struct Node *next; };
struct Node *front = NULL, *rear = NULL;
void enqueue(int x) { /* O(1): attach at rear */
struct Node *n = (struct Node*) malloc(sizeof(struct Node));
n->data = x; n->next = NULL;
if (rear == NULL) { front = rear = n; return; } /* empty queue */
rear->next = n; /* old last points to new */
rear = n; /* new node is the rear */
}
int dequeue(void) { /* O(1): detach at front */
if (front == NULL) return -1; /* empty */
int v = front->data;
struct Node *t = front;
front = front->next; /* second node becomes front */
if (front == NULL) rear = NULL; /* queue became empty */
free(t);
return v;
}
rear to the node before the last one — and a singly linked list has no prev pointer. The only way to find that node is to walk from the front: \( O(n) \). So the assignment of ends is forced: remove where you can reach the next node (front), add where NULL goes (rear).
rear-কে শেষের আগের node-এ আনতে হবে — আর singly linked list-এ কোনো prev pointer নেই। সেই node খুঁজতে front থেকে হেঁটে যাওয়া ছাড়া উপায় নেই: \( O(n) \)। তাই প্রান্ত বাছাই একটাই: যেখান থেকে পরের node ধরা যায় সেখানে remove (front), যেখানে NULL বসে সেখানে add (rear)।
Deque and priority queueDeque এবং priority queue
- Deque (double-ended queue): insert and delete allowed at both ends. It can act as both a stack and a queue.
- Priority queue: each item has a priority. Dequeue always removes the highest priority item, not the oldest one. Best implementation: a heap (Section 5), giving \( O(\log n) \) insert and delete.
Queue applications: CPU scheduling, printer jobs, BFS in graphs, buffering (keyboard, network packets).
- Deque (double-ended queue): দুই প্রান্তেই insert আর delete করা যায়। এটা stack আর queue দুটোর মতোই কাজ করতে পারে।
- Priority queue: প্রতিটা item-এর একটা priority থাকে। Dequeue সবসময় highest priority item সরায়, পুরনোটা নয়। Best implementation: heap (Section 5), যাতে insert আর delete \( O(\log n) \)।
Queue-এর ব্যবহার: CPU scheduling, printer job, graph-এ BFS, buffering (keyboard, network packet)।
4. Trees and Binary Search Tree (BST)4. Trees এবং Binary Search Tree (BST)
Tree terminologyTree terminology
A tree is a hierarchy of nodes with no cycles. Key words:
- Root: the top node.
- Parent / child: a node directly above / below another.
- Leaf: a node with no children.
- Edge: the link between a parent and a child. A tree with \( n \) nodes has exactly \( n - 1 \) edges.
- Height: the longest path from the root down to a leaf (counted in edges).
- Depth (level): distance of a node from the root. Root is at level 0.
- Subtree: a node plus all nodes under it.
Tree হলো cycle ছাড়া node-এর একটা hierarchy। Key শব্দগুলো:
- Root: সবচেয়ে উপরের node।
- Parent / child: ঠিক উপরের / নিচের node।
- Leaf: যে node-এর কোনো child নেই।
- Edge: parent আর child-এর মধ্যের link। \( n \) node-এর tree-তে edge ঠিক \( n - 1 \)টা।
- Height: root থেকে leaf পর্যন্ত সবচেয়ে লম্বা path (edge-এ গোনা)।
- Depth (level): root থেকে node-এর দূরত্ব। Root থাকে level 0-তে।
- Subtree: একটা node আর তার নিচের সব node।
Binary tree typesBinary tree-র ধরন
A binary tree gives each node at most 2 children (left and right).
- Full binary tree: every node has 0 or 2 children (never exactly 1).
- Complete binary tree: all levels are full, except maybe the last, which is filled from the left. (Heaps use this.)
- Perfect binary tree: all levels are completely full. A perfect tree of height \( h \) has \( 2^{h+1} - 1 \) nodes and \( 2^h \) leaves.
Useful fact: a binary tree with height \( h \) has at most \( 2^h \) nodes at level \( h \). A binary tree with \( n \) nodes has height at least \( \lfloor \log_2 n \rfloor \).
Binary tree-তে প্রতিটা node-এর সর্বোচ্চ 2টা child থাকে (left আর right)।
- Full binary tree: প্রতিটা node-এর child হয় 0টা নয় 2টা (কখনো ঠিক 1টা নয়)।
- Complete binary tree: সব level পূর্ণ, শুধু শেষ level-টা হয়তো বাকি — আর সেটা বাম দিক থেকে ভরা। (Heap এটাই ব্যবহার করে।)
- Perfect binary tree: সব level সম্পূর্ণ ভরা। Height \( h \)-এর perfect tree-তে node \( 2^{h+1} - 1 \)টা, leaf \( 2^h \)টা।
দরকারি fact: level \( h \)-এ সর্বোচ্চ \( 2^h \)টা node থাকতে পারে। \( n \) node-এর binary tree-র height কমপক্ষে \( \lfloor \log_2 n \rfloor \)।
Binary Search Tree (BST)Binary Search Tree (BST)
BST rule: for every node, all keys in the left subtree are smaller, and all keys in the right subtree are larger. This rule lets us search like binary search: at each node, go left or right and throw away half the tree (if balanced).
BST-র নিয়ম: প্রতিটা node-এর left subtree-র সব key তার থেকে ছোট, আর right subtree-র সব key বড়। এই নিয়মের জন্য binary search-এর মতো খোঁজা যায়: প্রতিটা node-এ left বা right-এ গিয়ে (balanced হলে) অর্ধেক tree বাদ দেওয়া যায়।
Tree traversalsTree traversal
- Preorder: Root → Left → Right. (Used to copy a tree.)
- Inorder: Left → Root → Right. (On a BST this gives sorted order!)
- Postorder: Left → Right → Root. (Used to delete a tree.)
- Level order: level by level, top to bottom — done with a queue (BFS).
- Preorder: Root → Left → Right। (Tree copy করতে কাজে লাগে।)
- Inorder: Left → Root → Right। (BST-তে এটা দেয় sorted order!)
- Postorder: Left → Right → Root। (Tree delete করতে কাজে লাগে।)
- Level order: level ধরে ধরে, উপর থেকে নিচ — queue দিয়ে করা হয় (BFS)।
void inorder(struct Node *r) {
if (r == NULL) return;
inorder(r->left);
printf("%d ", r->data); /* move this line first for preorder,
last for postorder */
inorder(r->right);
}
- Preorder:
8 3 1 6 4 7 10 14 - Inorder:
1 3 4 6 7 8 10 14← sorted, as promised - Postorder:
1 4 7 6 3 14 10 8 - Level order:
8 3 10 1 6 14 4 7
- Preorder:
8 3 1 6 4 7 10 14 - Inorder:
1 3 4 6 7 8 10 14← কথামতো sorted - Postorder:
1 4 7 6 3 14 10 8 - Level order:
8 3 10 1 6 14 4 7
Building a tree from traversalsTraversal থেকে tree বানানো
Inorder alone cannot rebuild a tree. But inorder + preorder (or inorder + postorder) can. Method: the first item of preorder is the root. Find it in inorder — everything to its left is the left subtree, everything to its right is the right subtree. Repeat.
শুধু inorder দিয়ে tree বানানো যায় না। কিন্তু inorder + preorder (বা inorder + postorder) দিয়ে যায়। পদ্ধতি: preorder-এর প্রথম item-টাই root। সেটাকে inorder-এ খুঁজে বের করো — তার বামের সব হলো left subtree, ডানের সব right subtree। এভাবে repeat করো।
A B D E C F, Inorder = D B E A C F.
Root = A (first of preorder). In inorder, left of A = {D, B, E}, right of A = {C, F}.
Left part: preorder gives B next → B is root of left subtree. In
D B E, D is left of B, E is right of B.
Right part: preorder gives C → root of right subtree. In
C F, F is right of C.
Final tree: A(root), left = B with children D, E; right = C with right child F.
A B D E C F, Inorder = D B E A C F।
Root = A (preorder-এর প্রথম)। Inorder-এ A-এর বামে = {D, B, E}, ডানে = {C, F}।
Left অংশ: preorder-এ পরে আসে B → left subtree-র root B।
D B E-তে D হলো B-র left, E হলো B-র right।
Right অংশ: preorder-এ C → right subtree-র root।
C F-এ F হলো C-র right child।
Final tree: A(root), left = B (child D, E); right = C (right child F)।
BST insert and searchBST insert এবং search
struct Node* insert(struct Node *r, int key) {
if (r == NULL) { /* empty spot found */
struct Node *n = (struct Node*) malloc(sizeof(struct Node));
n->data = key; n->left = n->right = NULL;
return n;
}
if (key < r->data) r->left = insert(r->left, key);
else if (key > r->data) r->right = insert(r->right, key);
return r;
}
struct Node* search(struct Node *r, int key) {
if (r == NULL || r->data == key) return r;
if (key < r->data) return search(r->left, key);
return search(r->right, key);
}
Insert 5 → tree is empty, 5 becomes root.
Insert 3 → 3 < 5, go left. Left is empty → 3 becomes left child of 5.
Insert 8 → 8 > 5, go right. Right is empty → 8 becomes right child of 5.
Insert 1 → 1 < 5 go left, 1 < 3 go left again → 1 becomes left child of 3.
Inorder now:
1 3 5 8 (sorted ✓).
Insert 5 → tree খালি, 5 হলো root।
Insert 3 → 3 < 5, left-এ যাই। Left খালি → 3 হলো 5-এর left child।
Insert 8 → 8 > 5, right-এ যাই। Right খালি → 8 হলো 5-এর right child।
Insert 1 → 1 < 5 left, 1 < 3 আবার left → 1 হলো 3-এর left child।
এখন inorder:
1 3 5 8 (sorted ✓)।
BST delete: the 3 casesBST delete: 3টা case
- Leaf node: just remove it.
- One child: replace the node with its only child.
- Two children: find the inorder successor (smallest node in the right subtree). Copy its value into the node. Then delete the successor (which has at most one child). The inorder predecessor (largest in left subtree) also works.
- Leaf node: সরাসরি মুছে দাও।
- একটা child: node-এর জায়গায় তার একমাত্র child বসাও।
- দুইটা child: inorder successor খুঁজে বের করো (right subtree-র সবচেয়ে ছোট node)। তার value node-এ copy করো। তারপর successor-কে delete করো (তার সর্বোচ্চ একটা child থাকে)। Inorder predecessor (left subtree-র সবচেয়ে বড়) দিয়েও চলে।
1 4 6 7 8 10 14 — still sorted ✓.
1 4 6 7 8 10 14 — এখনো sorted ✓।
Balanced trees: the AVL ideaBalanced tree: AVL-এর ধারণা
An AVL tree is a BST that keeps itself balanced. Rule: for every node, the heights of the left and right subtrees differ by at most 1 (balance factor ∈ {−1, 0, +1}). After each insert or delete, if a node breaks the rule, the tree fixes itself with rotations:
- LL case (left-left heavy) → one right rotation.
- RR case → one left rotation.
- LR case → left rotation on the child, then right rotation on the node.
- RL case → right rotation on the child, then left rotation on the node.
Result: height stays \( O(\log n) \), so search, insert, delete are all guaranteed \( O(\log n) \). Example: insert 1, 2, 3 into an AVL tree. After 3, node 1 is RR-heavy → left rotation → 2 becomes root with children 1 and 3.
AVL tree হলো এমন BST যেটা নিজেকে balanced রাখে। নিয়ম: প্রতিটা node-এ left আর right subtree-র height-এর পার্থক্য সর্বোচ্চ 1 (balance factor ∈ {−1, 0, +1})। Insert বা delete-এর পর কোনো node নিয়ম ভাঙলে tree নিজেকে ঠিক করে rotation দিয়ে:
- LL case (left-left heavy) → একটা right rotation।
- RR case → একটা left rotation।
- LR case → child-এ left rotation, তারপর node-এ right rotation।
- RL case → child-এ right rotation, তারপর node-এ left rotation।
ফলাফল: height থাকে \( O(\log n) \), তাই search, insert, delete সবই guaranteed \( O(\log n) \)। Example: AVL tree-তে 1, 2, 3 insert করো। 3-এর পরে node 1 হয় RR-heavy → left rotation → 2 হয় root, child 1 আর 3।
5. Heap5. Heap
A heap is a complete binary tree with an ordering rule:
- Max-heap: every parent ≥ its children. The largest value sits at the root.
- Min-heap: every parent ≤ its children. The smallest value sits at the root.
Careful: a heap is NOT a BST. It only compares parent with child — left and right children have no order between them.
Heap হলো একটা complete binary tree, সাথে একটা ordering নিয়ম:
- Max-heap: প্রতিটা parent ≥ তার child। সবচেয়ে বড় value থাকে root-এ।
- Min-heap: প্রতিটা parent ≤ তার child। সবচেয়ে ছোট value থাকে root-এ।
সাবধান: heap কিন্তু BST নয়। এটা শুধু parent-child compare করে — left আর right child-এর মধ্যে কোনো order নেই।
Array representationArray representation
Because the tree is complete, we can store it in an array with no gaps and no pointers. With 0-based index \( i \):
Tree টা complete বলে কোনো gap বা pointer ছাড়াই array-তে রাখা যায়। 0-based index \( i \) হলে:
Insert (sift-up)Insert (sift-up)
Put the new item at the end of the array (next empty leaf). Then sift up: while the item is bigger than its parent (max-heap), swap them. Cost: \( O(\log n) \) because the tree height is \( \log n \).
নতুন item-টা array-র শেষে রাখো (পরের খালি leaf-এ)। তারপর sift up: item টা parent-এর থেকে বড় হলে (max-heap-এ) swap করো, যতক্ষণ দরকার। Cost: \( O(\log n) \), কারণ tree-র height \( \log n \)।
[90, 75, 80, 40, 60, 30, 55].
Step 1: place 85 at index 7 →
[90, 75, 80, 40, 60, 30, 55, 85]. Parent of 7 = ⌊(7−1)/2⌋ = 3, value 40.
Step 2: 85 > 40 → swap →
[90, 75, 80, 85, 60, 30, 55, 40]. Parent of 3 = 1, value 75.
Step 3: 85 > 75 → swap →
[90, 85, 80, 75, 60, 30, 55, 40]. Parent of 1 = 0, value 90.
Step 4: 85 < 90 → stop. Done in 2 swaps.
[90, 75, 80, 40, 60, 30, 55]।
Step 1: 85-কে index 7-এ রাখো →
[90, 75, 80, 40, 60, 30, 55, 85]। 7-এর parent = ⌊(7−1)/2⌋ = 3, value 40।
Step 2: 85 > 40 → swap →
[90, 75, 80, 85, 60, 30, 55, 40]। 3-এর parent = 1, value 75।
Step 3: 85 > 75 → swap →
[90, 85, 80, 75, 60, 30, 55, 40]। 1-এর parent = 0, value 90।
Step 4: 85 < 90 → থামো। 2টা swap-এই শেষ।
Extract-max (sift-down / heapify)Extract-max (sift-down / heapify)
To remove the max (the root): take the root out, move the last array element to the root, shrink the array. Then sift down: swap the root with its larger child until both children are smaller. Cost: \( O(\log n) \).
Max (root) সরাতে: root বের করো, array-র শেষ element-টা root-এ বসাও, array ছোট করো। তারপর sift down: root-কে তার বড় child-এর সাথে swap করতে থাকো, যতক্ষণ না দুই child-ই ছোট হয়। Cost: \( O(\log n) \)।
[90, 85, 80, 75, 60, 30, 55, 40].
Step 1: return 90. Move last element 40 to root →
[40, 85, 80, 75, 60, 30, 55].
Step 2: children of 40 are 85 and 80. Larger = 85 → swap →
[85, 40, 80, 75, 60, 30, 55].
Step 3: children of 40 (index 1) are 75 and 60. Larger = 75 → swap →
[85, 75, 80, 40, 60, 30, 55].
Step 4: index 3 has no children → stop. Heap valid again.
[90, 85, 80, 75, 60, 30, 55, 40] থেকে max extract করি।
Step 1: 90 return করো। শেষ element 40-কে root-এ বসাও →
[40, 85, 80, 75, 60, 30, 55]।
Step 2: 40-এর child 85 আর 80। বড়টা = 85 → swap →
[85, 40, 80, 75, 60, 30, 55]।
Step 3: 40-এর (index 1) child 75 আর 60। বড়টা = 75 → swap →
[85, 75, 80, 40, 60, 30, 55]।
Step 4: index 3-এর কোনো child নেই → থামো। Heap আবার ঠিক।
/* sift-down for a max-heap, 0-based, heap size n */
void heapify(int a[], int n, int i) {
int largest = i, l = 2*i + 1, r = 2*i + 2;
if (l < n && a[l] > a[largest]) largest = l;
if (r < n && a[r] > a[largest]) largest = r;
if (largest != i) {
int t = a[i]; a[i] = a[largest]; a[largest] = t;
heapify(a, n, largest);
}
}
/* build-heap: heapify all internal nodes, bottom-up */
void buildHeap(int a[], int n) {
for (int i = n/2 - 1; i >= 0; i--)
heapify(a, n, i);
}
Build-heap and heapsortBuild-heap এবং heapsort
Build-heap: call sift-down on every internal node from the last one (index \( n/2 - 1 \)) up to the root. Surprising fact: this takes only \( O(n) \) total, not \( O(n \log n) \), because most nodes are near the bottom and sift only a little.
Heapsort idea: build a max-heap, then repeat \( n \) times: swap the root (max) with the last element, shrink the heap by 1, sift down the new root. The array becomes sorted in place. Time: \( O(n \log n) \), extra space: \( O(1) \).
Priority queue use: a heap is the standard priority queue — insert \( O(\log n) \), peek max/min \( O(1) \), extract \( O(\log n) \). Used in Dijkstra, Prim, Huffman coding, and CPU schedulers.
Build-heap: শেষ internal node (index \( n/2 - 1 \)) থেকে root পর্যন্ত প্রতিটাতে sift-down চালাও। মজার fact: এতে মোট লাগে মাত্র \( O(n) \), \( O(n \log n) \) নয় — কারণ বেশিরভাগ node নিচের দিকে থাকে, তারা সামান্যই sift করে।
Heapsort-এর ধারণা: max-heap বানাও, তারপর \( n \) বার repeat করো: root (max)-কে শেষ element-এর সাথে swap করো, heap ১ কমাও, নতুন root-কে sift down করো। Array in-place sorted হয়ে যায়। Time: \( O(n \log n) \), extra space: \( O(1) \)।
Priority queue হিসেবে: heap-ই standard priority queue — insert \( O(\log n) \), max/min দেখা \( O(1) \), extract \( O(\log n) \)। Dijkstra, Prim, Huffman coding, CPU scheduler-এ ব্যবহার হয়।
[10, 20, 15, 30, 40].
n = 5, so start heapify at the last internal node, index \( n/2 - 1 = 1 \), and go down to 0.
Step 1 (i = 1): node 20, children 30 (index 3) and 40 (index 4). Larger child = 40 → swap →
[10, 40, 15, 30, 20].
Step 2 (i = 0): node 10, children 40 and 15. Larger child = 40 → swap →
[40, 10, 15, 30, 20].
Step 3: 10 sank to index 1; its children are 30 and 20. Larger child = 30 → swap →
[40, 30, 15, 10, 20].
Step 4: 10 is at index 3, no children → stop. Final array:
[40, 30, 15, 10, 20].
[10, 20, 15, 30, 40] থেকে max-heap বানাই।
n = 5, তাই শেষ internal node, index \( n/2 - 1 = 1 \) থেকে heapify শুরু করে 0 পর্যন্ত নামো।
Step 1 (i = 1): node 20, child 30 (index 3) আর 40 (index 4)। বড় child = 40 → swap →
[10, 40, 15, 30, 20]।
Step 2 (i = 0): node 10, child 40 আর 15। বড় child = 40 → swap →
[40, 10, 15, 30, 20]।
Step 3: 10 নেমে গেল index 1-এ; তার child 30 আর 20। বড় child = 30 → swap →
[40, 30, 15, 10, 20]।
Step 4: 10 এখন index 3-এ, কোনো child নেই → থামো। Final array:
[40, 30, 15, 10, 20]।
6. Hashing6. Hashing
Hashing stores items so we can find them in \( O(1) \) time on average. A hash function turns a key into an array index: \( h(k) = k \bmod m \) is the most common, where \( m \) is the table size. A good hash function spreads keys evenly and is fast to compute. Picking \( m \) as a prime number helps spread keys better.
Hashing এমনভাবে item রাখে যাতে গড়ে \( O(1) \) time-এ খুঁজে পাওয়া যায়। Hash function একটা key-কে array index বানায়: সবচেয়ে common হলো \( h(k) = k \bmod m \), যেখানে \( m \) হলো table size। ভালো hash function key গুলোকে সমানভাবে ছড়ায় আর দ্রুত হিসাব হয়। \( m \) prime number নিলে key ভালো ছড়ায়।
Collision: two keys, one indexCollision: দুই key, এক index
A collision happens when two different keys hash to the same index. It is unavoidable (pigeonhole principle), so we need a plan. Two main plans:
দুইটা আলাদা key একই index-এ hash হলে collision হয়। এটা এড়ানো অসম্ভব (pigeonhole principle), তাই একটা plan লাগে। মূল দুইটা plan:
Plan 1: ChainingPlan 1: Chaining
Each table slot holds a linked list. Colliding keys just join the list at that slot. Simple, and the table can hold more items than slots. Search cost depends on list length.
Table-এর প্রতিটা slot-এ একটা linked list থাকে। Collision হওয়া key গুলো সেই slot-এর list-এ ঢুকে যায়। সহজ, আর slot-এর চেয়ে বেশি item-ও রাখা যায়। Search-এর খরচ list-এর length-এর উপর নির্ভর করে।
Plan 2: Open addressingPlan 2: Open addressing
All items live inside the table itself. On collision, we probe (try) other slots by a fixed rule until an empty slot is found:
- Linear probing: \( h_i(k) = (h(k) + i) \bmod m \) — try next slot, then next... Problem: primary clustering (long runs of filled slots grow).
- Quadratic probing: \( h_i(k) = (h(k) + i^2) \bmod m \) — jump 1, 4, 9, 16... Less clustering, but keys with the same start follow the same path (secondary clustering).
- Double hashing: \( h_i(k) = (h_1(k) + i \cdot h_2(k)) \bmod m \) — a second hash decides the step size. Best spreading. \( h_2(k) \) must never be 0; a common choice is \( h_2(k) = R - (k \bmod R) \) for a prime \( R \lt m \).
সব item table-এর ভিতরেই থাকে। Collision হলে fixed নিয়মে অন্য slot-এ probe (চেষ্টা) করা হয়, খালি slot না পাওয়া পর্যন্ত:
- Linear probing: \( h_i(k) = (h(k) + i) \bmod m \) — পরের slot, তারপর তার পরেরটা... সমস্যা: primary clustering (ভরা slot-এর লম্বা সারি বাড়তে থাকে)।
- Quadratic probing: \( h_i(k) = (h(k) + i^2) \bmod m \) — লাফ 1, 4, 9, 16... Clustering কম, কিন্তু একই জায়গা থেকে শুরু করা key একই পথে যায় (secondary clustering)।
- Double hashing: \( h_i(k) = (h_1(k) + i \cdot h_2(k)) \bmod m \) — দ্বিতীয় একটা hash ঠিক করে step size। সবচেয়ে ভালো ছড়ায়। \( h_2(k) \) কখনো 0 হওয়া চলবে না; common choice: prime \( R \lt m \)-এর জন্য \( h_2(k) = R - (k \bmod R) \)।
50 mod 7 = 1 → slot 1. ✓
700 mod 7 = 0 → slot 0. ✓
76 mod 7 = 6 → slot 6. ✓
85 mod 7 = 1 → collision with 50! Try slot 2 → empty ✓ (1 probe extra).
92 mod 7 = 1 → collision! Slot 2 full, slot 3 empty ✓ (2 probes extra).
73 mod 7 = 3 → collision with 92! Slot 4 empty ✓.
Final table:
[700, 50, 85, 92, 73, —, 76] (index 0..6). See how slots 1–4 formed a cluster.
50 mod 7 = 1 → slot 1। ✓
700 mod 7 = 0 → slot 0। ✓
76 mod 7 = 6 → slot 6। ✓
85 mod 7 = 1 → 50-এর সাথে collision! Slot 2 চেষ্টা → খালি ✓ (বাড়তি 1 probe)।
92 mod 7 = 1 → collision! Slot 2 ভরা, slot 3 খালি ✓ (বাড়তি 2 probe)।
73 mod 7 = 3 → 92-এর সাথে collision! Slot 4 খালি ✓।
Final table:
[700, 50, 85, 92, 73, —, 76] (index 0..6)। দেখো slot 1–4 মিলে একটা cluster হয়ে গেছে।
Load factor and complexityLoad factor এবং complexity
Load factor \( \alpha = n / m \) — items divided by slots. It measures how full the table is. With chaining, average search cost is about \( 1 + \alpha \) and \( \alpha \) can go above 1. With open addressing, \( \alpha \) must stay below 1, and performance drops sharply as \( \alpha \to 1 \). Practical rule: resize (rehash into a bigger table) when \( \alpha \) crosses about 0.7.
- Average case: search, insert, delete all \( O(1) \).
- Worst case (all keys collide): \( O(n) \).
Load factor \( \alpha = n / m \) — item সংখ্যা ভাগ slot সংখ্যা। Table কতটা ভরা তা বোঝায়। Chaining-এ average search cost প্রায় \( 1 + \alpha \), আর \( \alpha \) 1-এর বেশিও হতে পারে। Open addressing-এ \( \alpha \) অবশ্যই 1-এর নিচে থাকতে হবে, আর \( \alpha \to 1 \) হলে performance দ্রুত খারাপ হয়। কাজের নিয়ম: \( \alpha \) প্রায় 0.7 পার হলে resize করো (বড় table-এ rehash)।
- Average case: search, insert, delete সবই \( O(1) \)।
- Worst case (সব key collide করলে): \( O(n) \)।
7. Graph Representations7. Graph Representations
A graph \( G = (V, E) \) is a set of vertices (nodes) and edges (connections). Edges can be directed (one-way, like Twitter follow) or undirected (two-way, like Facebook friendship). A weighted graph puts a number (cost, distance) on each edge. We store graphs in two main ways.
Graph \( G = (V, E) \) হলো কিছু vertex (node) আর edge (connection)-এর set। Edge হতে পারে directed (একমুখী, যেমন Twitter follow) বা undirected (দ্বিমুখী, যেমন Facebook friendship)। Weighted graph-এ প্রতিটা edge-এ একটা সংখ্যা (cost, distance) থাকে। Graph রাখার মূল দুইটা উপায়।
Adjacency matrixAdjacency matrix
A \( V \times V \) 2D array. adj[u][v] = 1 if there is an edge from u to v, else 0 (for weighted graphs, store the weight instead). Undirected graphs give a symmetric matrix.
একটা \( V \times V \) 2D array। u থেকে v-তে edge থাকলে adj[u][v] = 1, নাহলে 0 (weighted graph-এ 1-এর বদলে weight রাখা হয়)। Undirected graph-এ matrix টা symmetric হয়।
Adjacency listAdjacency list
An array of lists. adj[u] is a list of all neighbors of u. For weighted graphs, each list entry stores (neighbor, weight).
List-এর একটা array। adj[u] হলো u-এর সব neighbor-এর list। Weighted graph-এ list-এর প্রতিটা entry-তে থাকে (neighbor, weight)।
Matrix (rows/cols 0..3):
0 1 2 3
0 [ 0 1 1 0 ]
1 [ 1 0 1 0 ]
2 [ 1 1 0 1 ]
3 [ 0 0 1 0 ]
List: 0 → [1, 2] ; 1 → [0, 2] ; 2 → [0, 1, 3] ; 3 → [2].
Matrix (row/col 0..3):
0 1 2 3
0 [ 0 1 1 0 ]
1 [ 1 0 1 0 ]
2 [ 1 1 0 1 ]
3 [ 0 0 1 0 ]
List: 0 → [1, 2] ; 1 → [0, 2] ; 2 → [0, 1, 3] ; 3 → [2]।
Matrix vs listMatrix vs list
| PropertyProperty | Adjacency matrix | Adjacency list |
|---|---|---|
| MemoryMemory | \( O(V^2) \) | \( O(V + E) \) |
| Check "is there an edge u–v?""u–v edge আছে?" check | \( O(1) \) | \( O(\deg(u)) \) |
| List all neighbors of uu-এর সব neighbor বের করা | \( O(V) \) | \( O(\deg(u)) \) |
| Add an edgeEdge যোগ করা | \( O(1) \) | \( O(1) \) |
| Full traversal (BFS/DFS)পুরো traversal (BFS/DFS) | \( O(V^2) \) | \( O(V + E) \) |
| Best forকখন ভালো | Dense graphs (E close to V²)Dense graph (E প্রায় V²) | Sparse graphs (most real graphs)Sparse graph (বাস্তবের বেশিরভাগ graph) |
Answer: adjacency list. First check density: an undirected graph with 100 vertices can have at most \( \frac{100 \times 99}{2} = 4950 \) edges. We only have about 300, and 300 ≪ 4950 (roughly 6% of the maximum) — the graph is sparse.
Memory comparison:
• Matrix: \( 100 \times 100 = 10000 \) cells — always, even if the graph had zero edges.
• List: about \( V + 2E = 100 + 600 = 700 \) entries (each undirected edge sits in two lists).
700 vs 10000 → the list uses about 14 times less memory. BFS/DFS also improve: \( O(V+E) = 700 \) steps instead of \( O(V^2) = 10000 \). Pick the matrix only when the graph is dense, or when you need many \( O(1) \) "is there an edge u–v?" checks.
Answer: adjacency list। আগে density দেখো: 100 vertex-এর undirected graph-এ সর্বোচ্চ \( \frac{100 \times 99}{2} = 4950 \)টা edge হতে পারে। আমাদের আছে মাত্র 300, আর 300 ≪ 4950 (সর্বোচ্চের প্রায় 6%) — graph টা sparse।
Memory comparison:
• Matrix: \( 100 \times 100 = 10000 \) cell — সবসময়, edge শূন্য হলেও।
• List: প্রায় \( V + 2E = 100 + 600 = 700 \) entry (প্রতিটা undirected edge দুইটা list-এ থাকে)।
700 vs 10000 → list-এ প্রায় 14 গুণ কম memory লাগে। BFS/DFS-ও ভালো হয়: \( O(V^2) = 10000 \) step-এর বদলে \( O(V+E) = 700 \) step। Matrix নেবে শুধু graph dense হলে, বা অনেকবার \( O(1) \)-এ "u–v edge আছে?" check লাগলে।
Traversal (BFS uses a queue, DFS uses a stack/recursion) and shortest-path algorithms are covered in detail in the next chapter (06. Algorithms). Here just remember: BFS/DFS cost \( O(V + E) \) with a list and \( O(V^2) \) with a matrix.
Traversal (BFS-এ queue লাগে, DFS-এ stack/recursion) আর shortest-path algorithm বিস্তারিত আছে পরের chapter-এ (06. Algorithms)। এখানে শুধু মনে রাখো: BFS/DFS-এর cost list-এ \( O(V + E) \), matrix-এ \( O(V^2) \)।
Big-O summary for all structuresসব structure-এর Big-O summary
| StructureStructure | Access | Search | Insert | Delete | NoteNote |
|---|---|---|---|---|---|
| Array | \( O(1) \) | \( O(n) \) | \( O(n) \) | \( O(n) \) | Sorted array: search \( O(\log n) \)Sorted array-তে search \( O(\log n) \) |
| Linked list | \( O(n) \) | \( O(n) \) | \( O(1) \) | \( O(1) \) | Insert/delete at a known positionজানা position-এ insert/delete |
| Stack | \( O(n) \) | \( O(n) \) | \( O(1) \) | \( O(1) \) | push/pop/peek at top onlyশুধু top-এ push/pop/peek |
| Queue | \( O(n) \) | \( O(n) \) | \( O(1) \) | \( O(1) \) | enqueue rear, dequeue frontrear-এ enqueue, front-এ dequeue |
| BST (balanced) | \( O(\log n) \) | \( O(\log n) \) | \( O(\log n) \) | \( O(\log n) \) | Skewed worst case: all \( O(n) \)Skewed হলে worst case সব \( O(n) \) |
| Heap | — | \( O(n) \) | \( O(\log n) \) | \( O(\log n) \) | find max/min \( O(1) \); build-heap \( O(n) \)max/min দেখা \( O(1) \); build-heap \( O(n) \) |
| Hash table | — | \( O(1) \)* | \( O(1) \)* | \( O(1) \)* | *average; worst \( O(n) \)*average; worst \( O(n) \) |
| Graph (adj. list) | — | \( O(V+E) \) | \( O(1) \) | \( O(E) \) | Memory \( O(V+E) \); matrix \( O(V^2) \)Memory \( O(V+E) \); matrix-এ \( O(V^2) \) |
Practice Questions (Admission Style)Practice Questions (Admission Style)
int array (4 bytes per element) starts at address 5000. What is the address of A[7]?
int array (প্রতি element 4 bytes) শুরু হয়েছে address 5000-এ। A[7]-এর address কত?
Show Answerউত্তর দেখুন
Show Answerউত্তর দেখুন
Show Answerউত্তর দেখুন
Show Answerউত্তর দেখুন
Show Answerউত্তর দেখুন
Show Answerউত্তর দেখুন
5 3 2 * + 4 -?
5 3 2 * + 4 --এর মান কত?
Show Answerউত্তর দেখুন
*: 3 × 2 = 6, push. See +: 5 + 6 = 11, push. Push 4. See -: 11 − 4 = 7. (Infix form: 5 + 3×2 − 4 = 7.)* পেলে: 3 × 2 = 6, push। + পেলে: 5 + 6 = 11, push। Push 4। - পেলে: 11 − 4 = 7। (Infix রূপ: 5 + 3×2 − 4 = 7।)Show Answerউত্তর দেখুন
Show Answerউত্তর দেখুন
A * (B + C) - D / E to postfix. Show the stack at each step.A * (B + C) - D / E-কে postfix-এ convert করো। প্রতি step-এ stack দেখাও।Show Answerউত্তর দেখুন
A B C + * D E / -
Trace (symbol → stack → output):
A → [] → A
* → [*] → A
( → [*, (] → A
B → [*, (] → A B
+ → [*, (, +] → A B
C → [*, (, +] → A B C
) → [*] → A B C + (pop until "(")
- → [-] → A B C + * (pop *, same/higher precedence than -; push -)
D → [-] → A B C + * D
/ → [-, /] → A B C + * D (/ is higher than -, just push)
E → [-, /] → A B C + * D E
end → [] → A B C + * D E / -
A B C + * D E / -
Trace (symbol → stack → output):
A → [] → A
* → [*] → A
( → [*, (] → A
B → [*, (] → A B
+ → [*, (, +] → A B
C → [*, (, +] → A B C
) → [*] → A B C + ("(" পর্যন্ত pop)
- → [-] → A B C + * (*-এর precedence -, থেকে বেশি তাই pop; তারপর - push)
D → [-] → A B C + * D
/ → [-, /] → A B C + * D (/-এর precedence - থেকে বেশি, শুধু push)
E → [-, /] → A B C + * D E
শেষ → [] → A B C + * D E / -
Show Answerউত্তর দেখুন
50
/ \
30 70
/ \ / \
20 40 60 80
Preorder (Root-L-R): 50 30 20 40 70 60 80
Postorder (L-R-Root):
20 40 30 60 80 70 50
(Quick check — inorder would be 20 30 40 50 60 70 80, sorted ✓)
50
/ \
30 70
/ \ / \
20 40 60 80
Preorder (Root-L-R): 50 30 20 40 70 60 80
Postorder (L-R-Root):
20 40 30 60 80 70 50
(Quick check — inorder হতো 20 30 40 50 60 70 80, sorted ✓)
Show Answerউত্তর দেখুন
50
/ \
40 70
/ / \
20 60 80
New inorder: 20 40 50 60 70 80 — still sorted, so the BST property holds.
50
/ \
40 70
/ / \
20 60 80
নতুন inorder: 20 40 50 60 70 80 — এখনো sorted, তাই BST property ঠিক আছে।
50 25 12 37 75 62 and inorder 12 25 37 50 62 75. Draw the tree. Is it a BST?50 25 12 37 75 62 আর inorder 12 25 37 50 62 75। Tree টা আঁকো। এটা কি BST?Show Answerউত্তর দেখুন
Left part: preorder continues with 25 → root of left subtree; in {12, 25, 37}, 12 is its left, 37 its right.
Right part: preorder gives 75 → root of right subtree; in {62, 75}, 62 is left of 75.
50
/ \
25 75
/ \ /
12 37 62
Yes, it is a BST — the inorder is sorted (12 25 37 50 62 75), which happens exactly when the tree is a BST.
Left অংশ: preorder-এ পরে 25 → left subtree-র root; {12, 25, 37}-এ 12 তার left, 37 তার right।
Right অংশ: preorder-এ 75 → right subtree-র root; {62, 75}-এ 62 হলো 75-এর left।
50
/ \
25 75
/ \ /
12 37 62
হ্যাঁ, এটা BST — inorder টা sorted (12 25 37 50 62 75), আর ঠিক তখনই tree টা BST হয়।
Show Answerউত্তর দেখুন
(i) Linear probing — probe (h+i) mod 10:
89 → 9 ✓. 18 → 8 ✓. 49 → 9 full → 0 ✓. 58 → 8 full → 9 full → 0 full → 1 ✓. 69 → 9 full → 0 full → 1 full → 2 ✓.
Table: index 0 = 49, 1 = 58, 2 = 69, 8 = 18, 9 = 89. (Big cluster around 8–2!)
(ii) Quadratic probing — probe (h+i²) mod 10:
89 → 9 ✓. 18 → 8 ✓. 49 → 9 full → 9+1=10→0 ✓. 58 → 8 full → 8+1=9 full → 8+4=12→2 ✓. 69 → 9 full → 0 full → 9+4=13→3 ✓.
Table: index 0 = 49, 2 = 58, 3 = 69, 8 = 18, 9 = 89. Spread is better — clustering is reduced.
(i) Linear probing — probe (h+i) mod 10:
89 → 9 ✓। 18 → 8 ✓। 49 → 9 ভরা → 0 ✓। 58 → 8 ভরা → 9 ভরা → 0 ভরা → 1 ✓। 69 → 9 ভরা → 0 ভরা → 1 ভরা → 2 ✓।
Table: index 0 = 49, 1 = 58, 2 = 69, 8 = 18, 9 = 89। (8–2 ঘিরে বড় cluster!)
(ii) Quadratic probing — probe (h+i²) mod 10:
89 → 9 ✓। 18 → 8 ✓। 49 → 9 ভরা → 9+1=10→0 ✓। 58 → 8 ভরা → 8+1=9 ভরা → 8+4=12→2 ✓। 69 → 9 ভরা → 0 ভরা → 9+4=13→3 ✓।
Table: index 0 = 49, 2 = 58, 3 = 69, 8 = 18, 9 = 89। ছড়ানো ভালো — clustering কমেছে।
[4, 10, 3, 5, 1] using build-heap (bottom-up), showing each heapify step. (ii) Then perform one extract-max and show the array. (iii) Why does build-heap take \( O(n) \) and not \( O(n \log n) \)?[4, 10, 3, 5, 1] থেকে build-heap (bottom-up) দিয়ে max-heap বানাও, প্রতিটা heapify step দেখাও। (ii) তারপর একবার extract-max করে array দেখাও। (iii) Build-heap কেন \( O(n \log n) \) নয়, \( O(n) \)?Show Answerউত্তর দেখুন
(i) n = 5, last internal node = ⌊5/2⌋ − 1 = 1.
heapify(1): node 10, children 5 (index 3) and 1 (index 4). 10 is already the largest → no change. Array: [4, 10, 3, 5, 1].
heapify(0): node 4, children 10 and 3. Largest = 10 → swap → [10, 4, 3, 5, 1]. Now node 4 (index 1) has children 5 and 1. Largest = 5 → swap → [10, 5, 3, 4, 1]. This is the max-heap.
(ii) Extract-max: return 10, move last element 1 to root → [1, 5, 3, 4]. Sift down: children of 1 are 5, 3 → swap with 5 → [5, 1, 3, 4]; children of 1 (index 1) is 4 → swap → [5, 4, 3, 1].
(iii) In build-heap, about n/2 nodes are leaves and do zero work; n/4 nodes sift at most 1 level; n/8 at most 2 levels; and so on. The total is \( \sum_{h} \frac{n}{2^{h+1}} \cdot O(h) = O(n) \), because the series \( \sum h/2^h \) converges to a constant. Only the root can sift the full \( \log n \) levels — most nodes do almost nothing.
(i) n = 5, শেষ internal node = ⌊5/2⌋ − 1 = 1।
heapify(1): node 10, child 5 (index 3) আর 1 (index 4)। 10-ই সবচেয়ে বড় → বদল নেই। Array: [4, 10, 3, 5, 1]।
heapify(0): node 4, child 10 আর 3। বড়টা = 10 → swap → [10, 4, 3, 5, 1]। এখন node 4 (index 1)-এর child 5 আর 1। বড়টা = 5 → swap → [10, 5, 3, 4, 1]। এটাই max-heap।
(ii) Extract-max: 10 return করো, শেষ element 1-কে root-এ বসাও → [1, 5, 3, 4]। Sift down: 1-এর child 5, 3 → 5-এর সাথে swap → [5, 1, 3, 4]; 1-এর (index 1) child 4 → swap → [5, 4, 3, 1]।
(iii) Build-heap-এ প্রায় n/2 node হলো leaf, তাদের কোনো কাজ নেই; n/4 node সর্বোচ্চ 1 level sift করে; n/8 node সর্বোচ্চ 2 level; এভাবে চলে। মোট \( \sum_{h} \frac{n}{2^{h+1}} \cdot O(h) = O(n) \), কারণ \( \sum h/2^h \) series টা একটা constant-এ converge করে। শুধু root-ই পুরো \( \log n \) level sift করতে পারে — বেশিরভাগ node প্রায় কিছুই করে না।
Show Answerউত্তর দেখুন
slow = head; fast = head;
while (fast != NULL and fast->next != NULL):
slow = slow->next // 1 step
fast = fast->next->next // 2 steps
if (slow == fast): report "snail" // cycle found
report "snake" // fast hit NULL
If fast (or fast->next) becomes NULL, the walk reached the end of the list, so it is a snake. If slow == fast, the pointers met inside a loop, so it is a snail.
Why it terminates: Snake case — fast moves forward 2 nodes per round, so it reaches NULL in at most n/2 rounds. Snail case — once both pointers are inside the loop, fast gains exactly 1 position on slow each round, so their gap (at most the loop length) shrinks to 0 within one lap of slow — they must meet. Either way the loop ends after \( O(n) \) rounds, with \( O(1) \) space (just two pointers).
slow = head; fast = head;
while (fast != NULL and fast->next != NULL):
slow = slow->next // 1 step
fast = fast->next->next // 2 steps
if (slow == fast): report "snail" // cycle পাওয়া গেছে
report "snake" // fast NULL-এ পৌঁছেছে
fast (বা fast->next) NULL হয়ে গেলে হাঁটা list-এর শেষে পৌঁছেছে, তাই এটা snake। আর slow == fast হলে pointer দুটো loop-এর ভিতরে মিলেছে, তাই এটা snail।
Terminate করে কেন: Snake case — fast প্রতি round-এ 2 node এগোয়, তাই সর্বোচ্চ n/2 round-এ NULL-এ পৌঁছায়। Snail case — দুই pointer loop-এ ঢোকার পর fast প্রতি round-এ slow-এর থেকে ঠিক 1 position এগোয়, তাই তাদের gap (সর্বোচ্চ loop-এর length) slow-এর এক চক্করের মধ্যেই 0 হয়ে যায় — মিলতেই হবে। দুই ক্ষেত্রেই loop \( O(n) \) round-এ শেষ, space \( O(1) \) (শুধু দুইটা pointer)।
Show Answerউত্তর দেখুন
(i) Keep two pointers,
front and rear. Enqueue at the rear, dequeue at the front.
(ii)
void enqueue(int x) {
struct Node *n = malloc(sizeof(struct Node));
n->data = x; n->next = NULL;
if (rear == NULL) { front = rear = n; return; }
rear->next = n; /* old last links to new node */
rear = n;
}
int dequeue(void) {
if (front == NULL) return -1; /* empty */
int v = front->data;
struct Node *t = front;
front = front->next;
if (front == NULL) rear = NULL; /* last item removed */
free(t);
return v;
}
Both functions do a fixed number of pointer changes — no loop — so both are \( O(1) \).
(iii) Dequeue at the rear must move
rear back to the second-last node. A singly linked list has no prev pointer, so the only way to find the second-last node is to walk from front — that is \( O(n) \). Enqueue at front would be \( O(1) \), but dequeue would be \( O(n) \). So the working assignment is forced: add at the rear, remove at the front. (Don't forget the two edge cases: enqueue into an empty queue sets both pointers; dequeue of the last item resets rear to NULL.)
(i) দুইটা pointer রাখো,
front আর rear। Rear-এ enqueue, front-এ dequeue।
(ii)
void enqueue(int x) {
struct Node *n = malloc(sizeof(struct Node));
n->data = x; n->next = NULL;
if (rear == NULL) { front = rear = n; return; }
rear->next = n; /* পুরনো শেষ node নতুনটাকে link করে */
rear = n;
}
int dequeue(void) {
if (front == NULL) return -1; /* খালি */
int v = front->data;
struct Node *t = front;
front = front->next;
if (front == NULL) rear = NULL; /* শেষ item সরানো হলো */
free(t);
return v;
}
দুই function-ই নির্দিষ্ট কয়েকটা pointer বদলায় — কোনো loop নেই — তাই দুটোই \( O(1) \)।
(iii) Rear-এ dequeue করতে হলে
rear-কে দ্বিতীয়-শেষ node-এ পিছিয়ে আনতে হয়। Singly linked list-এ prev pointer নেই, তাই দ্বিতীয়-শেষ node খুঁজতে front থেকে হেঁটে যাওয়া ছাড়া উপায় নেই — সেটা \( O(n) \)। Front-এ enqueue \( O(1) \) হতো, কিন্তু dequeue হয়ে যেত \( O(n) \)। তাই কাজের নিয়ম একটাই: rear-এ add, front-এ remove। (দুইটা edge case ভুলো না: খালি queue-তে enqueue করলে দুই pointer-ই set হয়; শেষ item dequeue করলে rear আবার NULL হয়।)
sortedInsert that inserts a new value so the list stays sorted. Use pointer notation only (no array brackets). Handle every case: empty list, insert at the head, in the middle, and at the tail. State the time complexity.sortedInsert লেখো যা নতুন একটা value insert করলেও list sorted থাকে। শুধু pointer notation ব্যবহার করো (কোনো array bracket নয়)। সব case সামলাও: খালি list, head-এ insert, মাঝে insert, tail-এ insert। Time complexity-ও লেখো।Show Answerউত্তর দেখুন
struct Node* sortedInsert(struct Node *head, int value) {
struct Node *n = malloc(sizeof(struct Node));
n->data = value;
n->next = NULL;
/* empty list, or value goes before the head → new head */
if (head == NULL || value < head->data) {
n->next = head;
return n;
}
/* prev/curr walk: stop where curr->data >= value */
struct Node *prev = head, *curr = head->next;
while (curr != NULL && curr->data < value) {
prev = curr;
curr = curr->next;
}
n->next = curr; /* curr == NULL means tail insert */
prev->next = n;
return head;
}
Why it covers every case: (1) Empty list: head == NULL → the new node becomes the head. (2) Head insert: value < head->data → same branch, new node points to the old head. (3) Middle: the walk stops at the first curr with data ≥ value, and the node is linked between prev and curr. (4) Tail: the walk runs off the end, curr == NULL, so n->next = NULL and prev (the old tail) links to n. The pointer order matters: set n->next = curr before prev->next = n, or you lose the rest of the list. Time: \( O(n) \) — one walk down the list; extra space \( O(1) \).
struct Node* sortedInsert(struct Node *head, int value) {
struct Node *n = malloc(sizeof(struct Node));
n->data = value;
n->next = NULL;
/* খালি list, বা value head-এর আগে বসবে → নতুন head */
if (head == NULL || value < head->data) {
n->next = head;
return n;
}
/* prev/curr walk: curr->data >= value হলেই থামো */
struct Node *prev = head, *curr = head->next;
while (curr != NULL && curr->data < value) {
prev = curr;
curr = curr->next;
}
n->next = curr; /* curr == NULL মানে tail insert */
prev->next = n;
return head;
}
সব case কীভাবে cover হলো: (1) খালি list: head == NULL → নতুন node-ই head। (2) Head insert: value < head->data → একই branch, নতুন node পুরনো head-কে point করে। (3) মাঝে: walk থামে প্রথম যে curr-এর data ≥ value সেখানে, আর node টা prev ও curr-এর মাঝে link হয়। (4) Tail: walk শেষ পর্যন্ত চলে যায়, curr == NULL, তাই n->next = NULL আর prev (পুরনো tail) n-কে link করে। Pointer-এর order গুরুত্বপূর্ণ: prev->next = n-এর আগে n->next = curr লেখো, নাহলে list-এর বাকি অংশ হারিয়ে যাবে। Time: \( O(n) \) — list ধরে একবার হাঁটা; extra space \( O(1) \)।