এই পর্বে যা শিখবেন
- “নিয়ম” (interface) আর “ভেতরের গঠন” (implementation) আলাদা করে ভাবা — একই stack দুইভাবে বানানো
- Stack দিয়ে বন্ধনী মেলানো, postfix হিসাব আর undo/redo — ধাপে ধাপে মেমরির ছবিসহ
- Circular buffer কীভাবে
%দিয়ে ঘুরে আসে, আর ভরা ও খালি queue কীভাবে আলাদা করবেন - Deque, priority queue আর OS, keyboard, নেটওয়ার্কে queue-এর আসল ব্যবহার
- AI-এর লেখা stack/queue কোড যাচাইয়ের একটি চেকলিস্ট
১ · নিয়ম আর গঠন — abstract data type কী
পর্ব ৩৩-এ আপনি linked list বানিয়েছেন — pointer দিয়ে জোড়া ঘরের শিকল। আজ আমরা দুটি নতুন কাঠামো বানাব, যারা আকারে খুব সরল, কিন্তু কম্পিউটারের প্রায় প্রতিটি স্তরে কাজ করে: stack আর queue.
শুরুতেই একটি গুরুত্বপূর্ণ কথা। Stack বা queue কোনো নির্দিষ্ট কোড নয়, এটি একটি নিয়মের তালিকা। “Stack” বললে আমরা বুঝি — ওপরে রাখা যায়, ওপর থেকে তোলা যায়, ওপরেরটা দেখা যায়, আর খালি কি না জানা যায়। ব্যস। এই নিয়মগুলো কীভাবে মানা হবে — array দিয়ে, নাকি linked list দিয়ে — সেটি সম্পূর্ণ আলাদা প্রশ্ন।
যে কাঠামোকে শুধু তার নিয়ম দিয়ে চেনা হয়, তাকে বলে abstract data type (সংক্ষেপে ADT). পর্ব ২৯-এ আপনি একই ভাবনা দেখেছিলেন: header ফাইলে থাকে interface — বাইরের সবার জন্য প্রতিশ্রুতি, আর .c ফাইলে থাকে implementation — প্রতিশ্রুতি রাখার আসল কৌশল।
একটি ADT-র দুই স্তর
Users depend on the rules, never on the insides
- ব্যবহারকারীর কোড
stack_push(s, 5)শুধু নিয়ম জানে - Interface (নিয়ম)
push · pop · peek · is_emptystack.h — চুক্তি - Implementation
array অথবা linked liststack.c — বদলানো যায়
bKash অ্যাপে “Send Money” চাপলে আপনি জানেন কী হবে — টাকা যাবে, ব্যালান্স কমবে, একটি মেসেজ আসবে। এটাই নিয়ম। ভেতরে কোন সার্ভারে, কোন ডেটাবেসে, কোন ভাষায় সেটি হচ্ছে — আপনি জানেন না, জানার দরকারও নেই। কোম্পানি ভেতরের সব বদলে ফেললেও আপনার অভ্যাস বদলাতে হয় না। ADT ঠিক এই চুক্তি।
C-তে একটি stack-এর interface দেখতে এমন হতে পারে। পর্ব ২৯-এর opaque pointer খেয়াল করুন — ব্যবহারকারী struct-এর ভেতরে কী আছে দেখতেই পান না:
/* stack.h - the rules. Users see only this file. */
typedef struct Stack Stack; /* opaque: fields are hidden */
Stack *stack_new(int capacity);
int stack_push(Stack *s, int x); /* returns 0 if full (overflow) */
int stack_pop(Stack *s, int *out); /* returns 0 if empty (underflow) */
int stack_peek(const Stack *s, int *out);
int stack_is_empty(const Stack *s);
void stack_free(Stack *s);খেয়াল করুন, stack_pop মানটি সরাসরি ফেরত দেয় না — ফেরত দেয় সফল হলো কি না (১ বা ০), আর মানটি লিখে দেয় out pointer-এ। কেন? কারণ stack খালি থাকলে “কোনো মান নেই” — এই খবরটি আলাদা করে জানানো দরকার। -1 ফেরত দিয়ে “খালি” বোঝালে, কেউ সত্যিই -1 রাখলে আর আলাদা করা যাবে না। পর্ব ৩০-এর error handling এখানেই কাজে লাগছে।
২ · Stack — শেষে যে এল, সে-ই আগে যায়
বিয়েবাড়ির বুফে টেবিলের কথা ভাবুন। পরিষ্কার থালাগুলো একটির ওপর আরেকটি সাজানো। ধোয়া থালা এলে রাখা হয় ওপরে। অতিথি থালা নেন ওপর থেকে। মাঝখান থেকে টেনে কেউ নেন না — স্তূপ ভেঙে পড়বে! ফলে যে থালাটি সবশেষে রাখা হয়েছে, সেটিই সবার আগে কারও হাতে যায়।
এই নিয়মের নাম LIFO — Last In, First Out, অর্থাৎ “শেষে এলে আগে যাবে”। আপনি প্রতিদিন এটি ব্যবহার করেন:
🔙 ব্রাউজারের Back বোতাম
প্রতিটি নতুন পাতায় গেলে আগের পাতাটি একটি stack-এ ওঠে। Back চাপলে ওপরেরটি — অর্থাৎ ঠিক আগের পাতাটি — বের হয়।
সবশেষে দেখা পাতা আগে↩️ Undo (Ctrl+Z)
Word বা VS Code-এ প্রতিটি পরিবর্তন stack-এ জমে। Undo চাপলে সবচেয়ে সাম্প্রতিক পরিবর্তনটি আগে বাতিল হয়।
শেষ কাজ আগে বাতিল📞 ফাংশন ডাকা
পর্ব ১৩-এর call stack মনে আছে? যে ফাংশন সবশেষে ডাকা হয়েছে, সেটিই সবার আগে শেষ হয়ে ফেরে।
ভেতরের ফাংশন আগে শেষচারটি কাজ — এর বেশি কিছু নয়
| কাজ | যা করে | যে ভুল আটকাতে হবে |
|---|---|---|
| push(x) | ওপরে x রাখে | জায়গা ভরা থাকলে overflow — array-র বাইরে লেখা |
| pop() | ওপরেরটি তুলে ফেরত দেয় | খালি থাকলে underflow — আবর্জনা মান পড়া |
| peek() | ওপরেরটি শুধু দেখে, তোলে না | খালি থাকলে underflow |
| is_empty() | খালি কি না জানায় | — |
চারটি কাজই একটিমাত্র ধাপে হয় — stack-এ এক লাখ জিনিস থাকুক বা দুটি, সময় একই। কারণ আমরা কখনো মাঝখানে হাত দিই না, শুধু ওপরে। পর্ব ৩৯-এ এর নাম শিখবেন O(1).
মেমরিতে একটি array-stack
সবচেয়ে সহজ উপায়: একটি array আর একটি পূর্ণসংখ্যা top. এখানে আমরা top-এর অর্থ ঠিক করলাম — “stack-এ কয়টি জিনিস আছে”, যা একই সাথে “পরের খালি ঘরের index”. তিনটি push-এর পর মেমরি এমন দেখায়:
index : 0 1 2 3 4
data : [ 10 ] [ 20 ] [ 30 ] [ ? ] [ ? ]
^
top = 3 (৩টি আছে; পরের push যাবে ঘর ৩-এ)
নির্দিষ্ট মাপের জায়গা, পাশাপাশি ঘর — দ্রুত, আর CPU cache-এর জন্য চমৎকার। ভরে গেলে push ব্যর্থ হয় (অথবা পর্ব ২০-এর মতো realloc দিয়ে বড় করা যায়)।
struct Stack { int *data; int top; int cap; }; /* top = number of items */
int stack_push(Stack *s, int x) {
if (s->top == s->cap) return 0; /* overflow: refuse, never write */
s->data[s->top++] = x; /* write, then move top up */
return 1;
}
int stack_pop(Stack *s, int *out) {
if (s->top == 0) return 0; /* underflow: nothing to give */
*out = s->data[--s->top]; /* move top down, then read */
return 1;
}পর্ব ৩৩-এর linked list-এর মাথাকেই stack-এর “ওপর” ধরা হয়। প্রতিটি push-এ একটি নতুন ঘর malloc হয়, তাই ভরে যাওয়ার সীমা শুধু মেমরি। দাম: প্রতিটি ঘরে বাড়তি একটি pointer, আর বারবার malloc/free.
struct Node { int value; struct Node *next; };
struct Stack { struct Node *head; }; /* top of stack = head of list */
int stack_push(Stack *s, int x) {
struct Node *n = malloc(sizeof *n);
if (n == NULL) return 0; /* out of memory */
n->value = x; n->next = s->head; s->head = n;
return 1;
}
int stack_pop(Stack *s, int *out) {
struct Node *n = s->head;
if (n == NULL) return 0; /* underflow */
*out = n->value; s->head = n->next; free(n);
return 1;
}দুটি সংস্করণের stack.h হুবহু এক। যে প্রোগ্রাম stack ব্যবহার করছে, সে জানতেই পারে না ভেতরে কোনটি চলছে। এটাই ADT-র শক্তি।
top-এর অর্থ একবার ঠিক করে সবখানে মেনে চলুন। অনেকে top মানে ধরেন “ওপরের জিনিসের index” — তখন খালি stack-এ top = -1, আর push হয় data[++top] = x. দুটি নিয়মই ঠিক, কিন্তু এক কোডে দুটি মিশে গেলে এক ঘর এদিক-ওদিক হয় — একটি ক্লাসিক off-by-one বাগ। AI-এর কোডে এটি প্রায়ই দেখা যায়।
৩ · বন্ধনী মেলানো — stack-এর প্রথম আসল কাজ
আপনি VS Code-এ একটি { লিখে ভুলে গেলেন বন্ধ করতে। Compiler কীভাবে টের পায়? অথবা a[f(x)] লেখার সময় editor কীভাবে বোঝে কোন ] কোন [-এর জোড়া? উত্তর: একটি stack.
নিয়মটি সহজ। বাঁ থেকে ডানে একটি একটি অক্ষর পড়ুন:
- খোলা বন্ধনী (
( [ {) পেলে — push. - বন্ধ বন্ধনী (
) ] }) পেলে — stack খালি হলে ভুল; না হলে pop করে মিলিয়ে দেখুন জোড়া ঠিক কি না। - শেষে stack খালি থাকলে সব ঠিক; কিছু রয়ে গেলে কোনো বন্ধনী খোলা থেকে গেছে।
কেন stack? কারণ বন্ধনী একটির ভেতরে আরেকটি বসে। যে বন্ধনী সবশেষে খোলা হয়েছে, সেটিকেই সবার আগে বন্ধ হতে হবে — ঠিক LIFO. নিচে { [ ( ) ] ( } পরীক্ষা করা হচ্ছে। “পরে” চাপুন, আর ডান দিকে stack-এর অবস্থা দেখুন:
1for (; *s; s++) {2 char c = *s;3 if (c == '(' || c == '[' || c == '{') {4 if (top == 64) return 0; /* overflow */5 st[top++] = c; /* push */6 } else if (c == ')' || c == ']' || c == '}') {7 char want = ...; /* matching opener */8 if (top == 0) return 0; /* underflow */9 if (st[--top] != want) return 0;10 }11}12return top == 0;
| c | |
| want | |
| top | |
| stack (নিচ → ওপর) |
- শুরু। Stack খালি,
top= ০. এখনো কোনো অক্ষর পড়া হয়নি। লক্ষ্য: প্রতিটি বন্ধ বন্ধনী তার ঠিক জোড়ার খোলা বন্ধনীটি পায় কি না। {পড়লাম — খোলা, তাই push. লাইন ৪-এ আগে দেখে নেওয়া হলো জায়গা আছে কি না (overflow যাচাই), তারপর লাইন ৫-এ ঘর ০-তে{রাখা হলো।topহলো ১.[— আবার push. এখন stack-এ দুটি: নিচে{, ওপরে[. মনে রাখুন, ওপরেরটিই “সবশেষে খোলা”।(— push. তিনটি খোলা বন্ধনী একটির ভেতরে আরেকটি। এখন যে বন্ধ বন্ধনীই আসুক, তাকে আগে(-এর সাথে মিলতে হবে।)— বন্ধ বন্ধনী। Stack খালি নয় (underflow নেই), তাই pop: ওপর থেকে উঠল(. আমাদের দরকার ছিল(— মিলে গেছে।topকমে ২.]— আবার মিল। Pop করে পাওয়া গেল[, যা]-এর ঠিক জোড়া। এখন stack-এ শুধু{. এ পর্যন্ত সব ঠিক।(— নতুন একটি খোলা বন্ধনী, push. Stack:{ (. খেয়াল করুন, এই(-টি কখনো বন্ধ হয়নি — এটিই ভুলের উৎস।}— ভুল ধরা পড়ল! Pop করে পাওয়া গেল(, কিন্তু}-এর জোড়া হওয়ার কথা{. মিলল না, তাই ফাংশন সাথে সাথে ০ ফেরত দিল। Compiler-এর পরিচিত “expected ')' before '}'” ধরনের মেসেজের পেছনে এই একই যুক্তি —}-এর আগে একটি)দরকার ছিল।
এবার আসল কোডটি চালিয়ে দেখুন। একই ফাংশন পাঁচটি ভিন্ন লেখা পরীক্ষা করছে — শেষের ")(" খেয়াল করুন, এখানে প্রথম অক্ষরেই underflow:
#include <stdio.h>
int balanced(const char *s) {
char st[64]; int top = 0; /* the stack and its height */
for (; *s; s++) {
char c = *s;
if (c == '(' || c == '[' || c == '{') {
if (top == 64) return 0; /* overflow */
st[top++] = c; /* push */
} else if (c == ')' || c == ']' || c == '}') {
char want = c == ')' ? '(' : c == ']' ? '[' : '{';
if (top == 0) return 0; /* underflow: nothing to close */
if (st[--top] != want) return 0; /* pop and compare */
}
}
return top == 0; /* leftovers = unclosed */
}
int main(void) {
const char *t[] = { "{[()]()}", "{[()](}", "(()", "a[i] = f(x);", ")(" };
for (int i = 0; i < 5; i++) printf("%-13s -> %s\n", t[i], balanced(t[i]) ? "OK" : "ERROR");
return 0;
}ফাংশনের ভেতরের st array আর top মিলেই একটি stack. বন্ধনী ছাড়া অন্য অক্ষর (a, =, ;) সরাসরি বাদ যায় — তাই a[i] = f(x); ঠিকঠাক “OK” পায়। (() শেষ পর্যন্ত কোনো ভুল জোড়া পায় না, কিন্তু শেষে stack-এ একটি ( রয়ে যায় — তাই শেষ লাইনের top == 0 যাচাইটি না থাকলে এটি ভুল করে “OK” হতো। আর )(-এ প্রথম অক্ষরেই stack খালি; underflow যাচাই না থাকলে st[-1] পড়া হতো — array-র বাইরের মেমরি।
৪ · Postfix — ক্যালকুলেটর ও compiler যেভাবে হিসাব করে
স্কুলে আমরা লিখি (5 + 3) * 2. অপারেটর বসে দুটি সংখ্যার মাঝখানে — একে বলে infix notation. মানুষের চোখে সহজ, কিন্তু কম্পিউটারের জন্য ঝামেলার: বন্ধনী আছে, আর “গুণ আগে, যোগ পরে” — এমন অগ্রাধিকারের নিয়ম (পর্ব ০৭-এর precedence) মানতে হয়।
একই হিসাব অন্যভাবে লেখা যায়: 5 3 + 2 *. এখানে অপারেটর বসে সংখ্যাগুলোর পরে — postfix notation. পড়ার নিয়ম: “দুটি সংখ্যা পেলে রেখে দাও; অপারেটর পেলে শেষ দুটি সংখ্যা নিয়ে কাজ করো।” বন্ধনীর দরকারই পড়ে না, অগ্রাধিকারের নিয়মও লাগে না। আর “শেষ দুটি সংখ্যা” রাখার জন্য সবচেয়ে মানানসই জায়গা — stack.
| এখন পড়ছি | |
| কাজ | |
| stack (নিচ → ওপর) |
- শুরু। Stack খালি। আমরা বাঁ থেকে ডানে একটি একটি টোকেন পড়ব। নিয়ম দুটি: সংখ্যা হলে push; অপারেটর হলে দুটি pop, হিসাব, তারপর ফল push.
5— সংখ্যা, push. এখনো জানি না এটি দিয়ে কী হবে, তাই রেখে দিলাম।3— সংখ্যা, push. Stack-এ এখন ৫ আর তার ওপরে ৩.+— অপারেটর! দুটি pop: প্রথমে উঠল ৩ (ডান পাশের সংখ্যা), তারপর ৫ (বাঁ পাশের)। ৫ + ৩ = ৮, আর ৮-কে আবার push করা হলো। ক্রম খেয়াল রাখুন — বিয়োগ বা ভাগে প্রথম pop হওয়া মানটি ডান পাশে বসে:a - b-তে আগে ওঠেb.2— push. Stack: ৮, ২. আগের যোগের ফলটি এখন একটি সাধারণ সংখ্যার মতোই অপেক্ষা করছে।*— অপারেটর। Pop ২, pop ৮. ৮ × ২ = ১৬, push. কোনো বন্ধনী ছাড়াই “আগে যোগ, পরে গুণ” ঠিক হয়ে গেল — কারণ ক্রমটিই লেখার মধ্যে আছে।- সব টোকেন শেষ। Stack-এ ঠিক একটি মান — ১৬, এটাই উত্তর। যদি শেষে একাধিক মান থাকত, বা কোনো অপারেটরের সময় দুটি মান না পাওয়া যেত (underflow), তাহলে রাশিটিই ভুলভাবে লেখা।
১৯২০-এর দশকে পোলিশ যুক্তিবিদ Jan Łukasiewicz যুক্তিবিদ্যার সূত্র বন্ধনী ছাড়া লেখার জন্য অপারেটরকে সামনে বসানোর একটি পদ্ধতি চালু করেন — একে আজও বলা হয় “Polish notation”. পরে কম্পিউটার বিজ্ঞানীরা এর উল্টো রূপ, অর্থাৎ অপারেটর পেছনে বসানো পদ্ধতি ব্যবহার শুরু করেন, যার নাম “Reverse Polish Notation” (RPN). ১৯৭০-এর দশকে Hewlett-Packard (HP)-এর বিখ্যাত বৈজ্ঞানিক ক্যালকুলেটরগুলোতে কোনো = বোতাম ছিল না — ব্যবহারকারী সরাসরি RPN-এ লিখতেন, আর ভেতরে একটি ছোট stack হিসাব করত।
আজও এই ভাবনা আপনার চারপাশে কাজ করছে। Compiler প্রায়ই (5 + 3) * 2-কে ভেতরে postfix-এর মতো ক্রমে সাজায়। Java-র JVM, Python-এর CPython আর ব্রাউজারের WebAssembly — তিনটিই stack machine: এদের নির্দেশগুলো একটি stack থেকে মান তোলে, হিসাব করে, ফল আবার stack-এ রাখে। Infix থেকে postfix বানানোর বিখ্যাত পদ্ধতিটিও (Dijkstra-র “shunting-yard” অ্যালগরিদম) একটি stack দিয়েই কাজ করে।
৫ · Stack সবখানে — call stack, undo/redo আর গোলকধাঁধা
পর্ব ১৩-এ দেখেছিলেন, প্রতিটি ফাংশন ডাকলে মেমরিতে একটি stack frame ওপরে যোগ হয়, আর ফাংশন শেষ হলে সেটি সরে যায়। পর্ব ১৫-এর recursion-এ সেই frame-গুলোই একটির ওপর আরেকটি জমছিল। এটি আজকের stack-ই — শুধু CPU নিজে এটি চালায়, push/pop লাগে না, আর জায়গা ফুরিয়ে গেলে হয় stack overflow.
নামের মিল দেখে বিভ্রান্ত হবেন না। পর্ব ২০-এর মেমরির “stack” হলো মেমরির একটি এলাকা; আজকের stack হলো ডেটা রাখার একটি নিয়ম। মেমরির এলাকাটির নামই এসেছে নিয়মটি থেকে, কারণ সেখানে frame-গুলো LIFO নিয়মে আসে-যায়। আর আপনি নিজে যে stack বানাবেন, সেটি মেমরির যেকোনো জায়গায় থাকতে পারে — heap-এও।
Undo আর Redo — দুটি stack-এর খেলা
একটি editor-এ Undo আর Redo দুটোই কীভাবে কাজ করে? উত্তর: দুটি stack. প্রতিটি নতুন কাজ যায় undo stack-এ। Undo চাপলে undo stack থেকে pop হয়ে কাজটি যায় redo stack-এ। Redo চাপলে উল্টো পথে ফেরে। নিচে দেখুন — বিশেষ করে শেষ ধাপটি, যেখানে অনেকেই অবাক হন:
| পর্দায় লেখা | |
| undo stack (নিচ → ওপর) | |
| redo stack (নিচ → ওপর) |
- শুরু। পর্দা ফাঁকা, দুটি stack-ই খালি। প্রতিটি “কাজ” আমরা একটি অক্ষর টাইপ করা হিসেবে ধরছি।
- A টাইপ করলেন। কাজটি undo stack-এ push হলো।
- B টাইপ। Undo stack-এ আরেকটি push. ওপরে এখন B — অর্থাৎ Undo চাপলে আগে B বাতিল হবে।
- C টাইপ। তিনটি কাজ জমা। পর্দায় ABC.
- Ctrl+Z (Undo). Undo stack থেকে pop — উঠল C. লেখা থেকে C মুছল, আর C push হলো redo stack-এ, যাতে চাইলে ফিরিয়ে আনা যায়।
- আবার Undo. এবার B উঠে redo stack-এ গেল — C-এর ওপরে। পর্দায় শুধু A.
- Ctrl+Y (Redo). Redo stack থেকে pop — ওপরে ছিল B, সেটিই ফিরল। B আবার undo stack-এ গেল। পর্দায় AB.
- নতুন কিছু টাইপ করলেন — D. D গেল undo stack-এ, আর redo stack পুরো খালি হয়ে গেল! কারণ নতুন কাজের পর পুরোনো “ভবিষ্যৎ” (C) আর অর্থবহ নয়। ব্রাউজারেও একই নিয়ম: কয়েকবার Back চেপে নতুন লিংকে গেলে Forward বোতাম নিভে যায়।
আরেকটি বড় ব্যবহার সামনে আসছে। গোলকধাঁধায় পথ খোঁজার সময় আমরা একটি পথ ধরে যতদূর পারি যাই, আটকে গেলে ঠিক আগের মোড়ে ফিরে অন্য পথ ধরি। “ঠিক আগের মোড়” মনে রাখার জিনিসটি একটি stack. পর্ব ৩৮-এ এর নাম শিখবেন DFS (depth-first search), আর দেখবেন এর ভাই BFS চলে queue দিয়ে।
৬ · Queue — যে আগে এসেছে, সে আগে সেবা পাবে
ব্যাংকে ঢুকে আপনি মেশিন থেকে একটি টোকেন নিলেন — ৪৭ নম্বর। কাউন্টারে ডাকা হচ্ছে ৪২. আপনার আগে যাঁরা এসেছেন, তাঁরা আগে সেবা পাবেন; আপনার পরে যাঁরা আসবেন, তাঁরা পাবেন আপনার পরে। কেউ লাইন ভাঙলে সবাই রেগে যান — কারণ এই নিয়মটাই ন্যায্য।
এই নিয়মের নাম FIFO — First In, First Out, “আগে এলে আগে যাবে”। Queue-এর দুটি মূল কাজ:
➕ enqueue(x)
Enqueue — লাইনের পেছনে (tail) নতুন মান যোগ। ভরা থাকলে ব্যর্থ।
নতুন টোকেন নেওয়া➖ dequeue()
Dequeue — লাইনের সামনের (head) মান বের করা। খালি থাকলে underflow.
কাউন্টারে ডাক পড়া🖨️ প্রিন্টার queue
অফিসে পাঁচজন একসাথে Print চাপলেন। প্রিন্টার একটিই — কাজগুলো লাইনে দাঁড়ায়, যেটি আগে পৌঁছেছে সেটি আগে ছাপা হয়।
আগে পাঠানো ফাইল আগেসাধারণ array-তে queue বানালে সমস্যা কোথায়
প্রথম ভাবনা: একটি array নিই, পেছনে যোগ করি, সামনে থেকে বের করি। কিন্তু সামনে থেকে বের করার পর ঘর ০ ফাঁকা। এখন দুটি পথ — দুটিই খারাপ:
| পথ | কী হয় | সমস্যা |
|---|---|---|
| প্রতিবার সবাইকে এক ঘর সামনে সরানো | লাইনের সবাই এক পা এগোলেন | ১০ লাখ জিনিস থাকলে প্রতি dequeue-তে ১০ লাখ কপি — খুব ধীর |
| কাউকে না সরিয়ে শুধু “সামনে” কোথায়, সেটি এগিয়ে নেওয়া | দ্রুত — শুধু একটি সংখ্যা বাড়ে | সামনের ঘরগুলো ফাঁকা পড়ে থাকে, আর শেষে array “ভরা” মনে হয়, যদিও অর্ধেক খালি |
দ্বিতীয় পথটি প্রায় ঠিক। শুধু একটি কৌশল যোগ করতে হবে: শেষ ঘরের পরে আবার প্রথম ঘরে ফিরে আসা। পরের অংশে সেটিই দেখব।
৭ · Circular buffer — % দিয়ে ঘুরে আসা
দেয়ালঘড়ির কাঁটার কথা ভাবুন। ১২-এর পর ১৩ আসে না, আবার ১ আসে — কাঁটা ঘুরে শুরুতে ফেরে। Array-কেও যদি এভাবে ভাবি — শেষ ঘরের পরে আবার ঘর ০ — তাহলে সামনের ফাঁকা ঘরগুলো আবার কাজে লাগে। একে বলে circular buffer বা ring buffer.
ঘুরে আসার কাজটি করে পর্ব ০৭-এর modulus অপারেটর %: tail = (tail + 1) % CAP. ৫ ঘরের buffer-এ ঘর ৪-এর পর (4 + 1) % 5 = ০ — একদম শুরুতে ফিরে এলাম। এটি পর্ব ০৫-এর wraparound-এর মতোই, শুধু এবার আমরা ইচ্ছা করে করছি।
আমাদের queue-তে তিনটি সংখ্যা থাকবে: head (পরের dequeue কোন ঘর থেকে), tail (পরের enqueue কোন ঘরে), আর count (কয়টি আছে)। নিচের ডায়াগ্রামে খেয়াল রাখুন tail কখন ৪ থেকে ০-তে লাফ দেয়, আর শেষে কেন count ছাড়া চলত না:
1int enqueue(Queue *q, int x) {2 if (q->count == CAP) return 0; /* full */3 q->data[q->tail] = x;4 q->tail = (q->tail + 1) % CAP;5 q->count++; return 1;6}7int dequeue(Queue *q, int *out) {8 if (q->count == 0) return 0; /* empty */9 *out = q->data[q->head];10 q->head = (q->head + 1) % CAP;11 q->count--; return 1;12}
| data[0] | |
| data[1] | |
| data[2] | |
| data[3] | |
| data[4] | |
| head | |
| tail | |
| count |
- খালি queue.
head=tail= ০,count= ০. “-” মানে ঘরটি এখন queue-এর অংশ নয়। - enqueue 10. লাইন ২: ভরা নয়। লাইন ৩:
data[tail]অর্থাৎ ঘর ০-তে ১০. লাইন ৪:tailএক ঘর এগিয়ে ১.count১. - enqueue 20. ঘর ১-এ ২০,
tailহলো ২. - enqueue 30. ঘর ২-এ ৩০,
tail৩. লাইনে এখন তিনজন: ১০ সবার সামনে, ৩০ সবার পেছনে। - dequeue. লাইন ৮: খালি নয়। লাইন ৯: সামনের মান, অর্থাৎ
data[head]= ১০ বের হলো — যে আগে এসেছিল।headএগিয়ে ১. ঘর ০-তে পুরোনো ১০ আসলে মেমরিতে রয়ে গেছে, কিন্তু এখন সেটি queue-এর বাইরে — তাই “-”। - আবার dequeue. বের হলো ২০,
headহলো ২. এখন সামনের দুটি ঘর ফাঁকা — সাধারণ array হলে এগুলো চিরকাল নষ্ট হতো। - enqueue 40. ঘর ৩-এ ৪০,
tail৪. এখনtailarray-র একদম শেষ ঘরের দিকে নির্দেশ করছে। - enqueue 50 — ঘুরে আসা! ঘর ৪-এ ৫০. তারপর
tail = (4 + 1) % 5= ০. Array শেষ, তাইtailশুরুতে ফিরে এল — ঠিক ঘড়ির কাঁটার মতো।%না থাকলেtailহতো ৫, আর পরের লেখা যেত array-র বাইরে। - enqueue 60. ৬০ বসল ঘর ০-তে — যে ঘর ১০ ছেড়ে গিয়েছিল। এখন queue-এর ক্রম ঘর ২ → ৩ → ৪ → ০: ৩০, ৪০, ৫০, ৬০. Array-তে ক্রম ভাঙা দেখালেও queue-এর ক্রম ঠিকই আছে।
- enqueue 70 — queue ভরে গেল। ঘর ১-এ ৭০,
tailহলো ২,count৫. এখন খেয়াল করুন:head=tail= ২. একদম শুরুতেওhead=tailছিল — তখন queue খালি ছিল! শুধুheadআরtailদেখে ভরা আর খালি আলাদা করা যায় না। সেজন্যইcountরাখা। - enqueue 80 — প্রত্যাখ্যান। লাইন ২:
count == CAP, তাই কিছু না লিখেই ০ ফেরত। এই যাচাই না থাকলে ৮০ বসত ঘর ২-এ — সবচেয়ে পুরোনো ৩০ চুপচাপ মুছে যেত, আর কেউ টেরও পেত না।
এবার একই queue আসল কোডে চালিয়ে দেখুন। কাজের ক্রম ঠিক উপরের ডায়াগ্রামের মতো, শেষে বাকি সবাইকে dequeue করা হচ্ছে:
#include <stdio.h>
#define CAP 5
typedef struct { int data[CAP]; int head, tail, count; } Queue;
int enqueue(Queue *q, int x) {
if (q->count == CAP) return 0; /* full: refuse */
q->data[q->tail] = x;
q->tail = (q->tail + 1) % CAP; /* wrap around */
q->count++; return 1;
}
int dequeue(Queue *q, int *out) {
if (q->count == 0) return 0; /* empty: nothing to give */
*out = q->data[q->head];
q->head = (q->head + 1) % CAP;
q->count--; return 1;
}
int main(void) {
Queue q = { .count = 0 }; int x, v;
for (v = 10; v <= 30; v += 10) enqueue(&q, v);
dequeue(&q, &x); dequeue(&q, &x); /* 10 and 20 leave */
for (v = 40; v <= 80; v += 10) printf("enqueue %d: %s\n", v, enqueue(&q, v) ? "ok" : "FULL");
while (dequeue(&q, &x)) printf("dequeue -> %d\n", x);
return 0;
}প্রথম চারটি enqueue “ok”, আর ৮০ পায় “FULL” — কারণ তখন ৫টি ঘরই ভরা। শেষের লুপে মানগুলো বের হয় ৩০, ৪০, ৫০, ৬০, ৭০ ক্রমে — ঠিক যে ক্রমে ঢুকেছিল, যদিও মেমরিতে ৬০ আর ৭০ বসে আছে array-র শুরুতে। Queue q = { .count = 0 } লেখায় বাকি সব field-ও শূন্য হয়ে যায় (পর্ব ২২-এর designated initializer). dequeue মান ফেরত না দিয়ে সফলতা ফেরত দেয় — তাই while (dequeue(...)) লুপটি queue খালি হলেই নিজে থেকে থামে।
ভরা আর খালি — তিনটি প্রচলিত সমাধান
| কৌশল | খালি কখন | ভরা কখন | দাম |
|---|---|---|---|
আলাদা count রাখা (আমাদের কোড) | count == 0 | count == CAP | একটি বাড়তি ভ্যারিয়েবল |
| একটি ঘর সবসময় ফাঁকা রাখা | head == tail | (tail + 1) % CAP == head | ৫ ঘরের buffer-এ রাখা যায় মাত্র ৪টি |
অবিরাম বাড়তে থাকা index, ঘর বের করতে % CAP | head == tail | tail - head == CAP | CAP সাধারণত ২-এর ঘাত রাখা হয়; সংখ্যা উপচে পড়লেও ঠিক থাকতে unsigned লাগে |
ঋণাত্মক ভাগশেষের ফাঁদ: পেছনের দিকে এক ঘর যেতে চাইলে (যেমন deque-এর সামনে যোগ করতে) অনেকে লেখেন (head - 1) % CAP. কিন্তু head = ০ হলে C-তে -1 % 5-এর ফল -1, ৪ নয় — পর্ব ০৭-এ দেখেছিলেন, C-তে ভাগশেষের চিহ্ন ভাজ্যের মতো হয়। ফলে data[-1] — array-র বাইরে। নিরাপদ রূপ: (head - 1 + CAP) % CAP.
৮ · Deque, priority queue আর বাস্তবের সিস্টেম
দুই মুখ খোলা — deque
Deque (উচ্চারণ “ডেক”, double-ended queue) — সামনে-পেছনে দুই দিকেই যোগ ও বের করা যায়। শুধু এক মাথা ব্যবহার করলে এটি stack, দুই মাথা ব্যবহার করলে queue. Circular buffer দিয়েই সহজে বানানো যায় — শুধু head-কে পেছনে সরানোর সময় উপরের ঋণাত্মক ভাগশেষের সতর্কতা মানতে হবে। Python-এর collections.deque আর C++-এর std::deque এই কাঠামো। নামটি খেয়াল করুন: deque একটি কাঠামো, আর dequeue একটি কাজ — দুটি আলাদা।
আগে এলে নয়, জরুরি হলে আগে — priority queue
ঢাকা মেডিকেলের জরুরি বিভাগে কেউ টোকেনের ক্রমে সেবা পান না। হার্ট-অ্যাটাকের রোগী পরে এলেও আগে যান, হাত কেটে যাওয়া রোগী অপেক্ষা করেন। এটি priority queue — প্রতিটি মানের সাথে একটি “গুরুত্ব” থাকে, আর সবচেয়ে গুরুত্বপূর্ণটি আগে বের হয়।
সাধারণ array-তে এটি করলে প্রতিবার পুরো array খুঁজতে হয়। দ্রুত করার জন্য ব্যবহার হয় binary heap নামের একটি tree-কাঠামো (নাম এক হলেও পর্ব ২০-এর মেমরির heap এলাকার সাথে এর কোনো সম্পর্ক নেই)। পর্ব ৩৫-এ tree শেখার পর পর্ব ৩৬-এ (হিপ ও প্রায়োরিটি কিউ) এটি নিজ হাতে বানাবেন। দুটি জায়গার মধ্যে সবচেয়ে ছোট পথ খোঁজার Dijkstra অ্যালগরিদম থেকে শুরু করে OS-এর কাজ বাছাই — সবখানে priority queue.
আপনার কম্পিউটারের ভেতরে queue
OS-এর run queue
এই মুহূর্তে আপনার কম্পিউটারে শতাধিক প্রোগ্রাম ও thread CPU চাইছে। OS তাদের একটি run queue-এ রাখে, আর scheduler বেছে বেছে প্রতিটিকে কয়েক মিলিসেকেন্ড করে CPU দেয়। Linux-এর আধুনিক scheduler এটিকে সাধারণ FIFO নয়, বরং ন্যায্যতার হিসাবে সাজানো একটি কাঠামো হিসেবে রাখে।
Operating Systems কোর্সে বিস্তারিতKeyboard buffer
কম্পিউটার ব্যস্ত থাকলেও আপনার টাইপ হারায় না — প্রতিটি চাপ একটি ছোট circular buffer-এ জমে, প্রোগ্রাম সময় পেলে ক্রম মেনে পড়ে নেয়। পুরোনো PC-তে এই buffer ভরে গেলে কম্পিউটার “বিপ” শব্দ করত।
driver আর প্রোগ্রামের মাঝেনেটওয়ার্ক প্যাকেট
Wi-Fi কার্ড বা রাউটারে ডেটার টুকরো (packet) একের পর এক আসে। এগুলো জমে ring buffer-এ, আর OS সেখান থেকে তুলে নেয়। রাউটারের queue ভরে গেলে নতুন packet ফেলে দেওয়া হয় — তখনই ভিডিও কল আটকে যায়।
Computer Networks কোর্সে বিস্তারিতসার্ভিসের মধ্যে message queue
বড় অ্যাপে অর্ডার নেওয়ার সার্ভিস আর SMS পাঠানোর সার্ভিস সরাসরি কথা বলে না। প্রথমটি একটি message queue-তে কাজ রেখে যায়, দ্বিতীয়টি নিজের গতিতে তুলে নেয়। একটি ধীর হলে অন্যটি আটকে যায় না।
RabbitMQ, Kafka ইত্যাদিQueue: আগে এলে আগে যায় — ন্যায্যভাবে অপেক্ষা করায়।
সব queue-এর একটি সাধারণ বিপদ আছে: যদি জিনিস ঢোকে বের হওয়ার চেয়ে দ্রুত, তাহলে queue বাড়তেই থাকে। শেষ পর্যন্ত মেমরি শেষ, অথবা অপেক্ষার সময় অসহ্য। ভালো সিস্টেম তখন উৎসকে বলে “একটু থামো” — একে বলে backpressure. ভাবনার প্রশ্ন ০৫-এ এটি নিয়ে গভীরে যাব।
৯ · AI-এর যুগে stack ও queue
“C-তে একটা stack লিখে দাও” — যেকোনো AI দশ সেকেন্ডে কোড দেবে, আর বেশিরভাগ সময় সাধারণ ইনপুটে সেটি চলবেও। সমস্যা লুকিয়ে থাকে edge case-এ: খালি stack-এ pop, ভরা queue-তে enqueue, ঠিক শেষ ঘরে ঘুরে আসা। এগুলো সাধারণ পরীক্ষায় ধরা পড়ে না, পড়ে আসল ব্যবহারে।
AI সাধারণত যেসব ভুল করে
| AI-এর ভুল | যা ঘটে | কীভাবে ধরবেন |
|---|---|---|
| push-এ ভরা কি না যাচাই নেই | Array-র বাইরে লেখা — buffer overflow, পাশের ডেটা নষ্ট | CAP-এর চেয়ে একটি বেশি push করে দেখুন |
| pop/dequeue-এ খালি কি না যাচাই নেই | data[-1] বা পুরোনো আবর্জনা মান ফেরত | খালি অবস্থায় একবার pop করুন |
খালি বোঝাতে -1 ফেরত | কেউ সত্যিই -1 রাখলে সেটিকে “খালি” ভাবা হয় | ফেরত মানের অর্থ জিজ্ঞেস করুন |
top-এর দুই অর্থ মিশে যাওয়া | এক ঘর আগে/পরে লেখা-পড়া (off-by-one) | ১টি push আর ১টি pop হাতে trace করুন |
Circular buffer-এ শুধু head == tail দিয়ে ভরা ও খালি দুটোই বোঝানো | ভরা queue-কে খালি ভাবা — সব ডেটা “হারিয়ে” যায় | ঠিক CAP সংখ্যক enqueue করে is_empty দেখুন |
% CAP ভুলে যাওয়া, বা (i - 1) % CAP | Index CAP বা -1 হয়ে array-র বাইরে | শেষ ঘর পেরিয়ে আরও কয়েকটি enqueue/dequeue করুন |
AI-কে কোড লিখতে বলার সময় নিয়মগুলো আগেই বলে দিন — কী হলে কী ফেরত দেবে, আর কোন পরীক্ষাগুলো চাই। তাহলে সে edge case নিয়ে ভাবতে বাধ্য হয়:
AI-এর লেখা stack/queue কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১CPU নিজেই কেন একটি hardware stack রাখে? Queue বা অন্য কোনো কাঠামো কেন নয়?
কারণ ফাংশন ডাকার ধরনটাই LIFO. main ডাকল a-কে, a ডাকল b-কে। এখন b শেষ হলে ফিরতে হবে a-তে, main-এ নয়। অর্থাৎ যে “ফেরার ঠিকানা” সবশেষে মনে রাখা হয়েছে, সেটিই সবার আগে লাগবে। Queue হলে b শেষ করে সোজা main-এ ফিরে যেত — পুরো প্রোগ্রামের যুক্তি ভেঙে পড়ত।
এত ঘন ঘন কাজটি হয় যে CPU-নির্মাতারা এটি সরাসরি হার্ডওয়্যারে বসিয়েছেন:
- একটি বিশেষ register: x86-64-এ
RSP, ARM-এSP— stack-এর “ওপর” কোথায়, সেই ঠিকানা সবসময় এখানে থাকে। আমাদেরtopভ্যারিয়েবলেরই হার্ডওয়্যার রূপ। - বিশেষ নির্দেশ: x86-এ
PUSH,POP, আরCALLনিজেই return address stack-এ রাখে;RETসেটি তুলে সেখানে লাফ দেয়। ARM একটু আলাদা — ফেরার ঠিকানা আগে একটি register-এ (link register) যায়, আর ফাংশন নিজে আরেকটি ফাংশন ডাকলে তখন সেটি stack-এ রাখে। - Interrupt: চলার মাঝে keyboard বা timer-এর সংকেত এলে CPU বর্তমান অবস্থা stack-এ রেখে সংকেতটি সামলায়, তারপর তুলে আবার আগের কাজে ফেরে। Interrupt-এর ভেতরে আরেকটি interrupt এলেও একই নিয়ম — আবার LIFO.
দামটা কোথায়? ফেরার ঠিকানা আর local array পাশাপাশি একই stack-এ থাকে। তাই কোনো array-র সীমা পেরিয়ে লিখলে (buffer overflow) আক্রমণকারী ফেরার ঠিকানাটিই বদলে দিতে পারে — CPU তখন তার পছন্দের কোডে “ফেরে”। দশকের পর দশক এটি সবচেয়ে প্রচলিত আক্রমণের একটি ছিল। পর্ব ১৯-এর stack canary, আর আধুনিক কিছু CPU-তে ফেরার ঠিকানার আলাদা লুকানো কপি (shadow stack) রাখা — এসব সুরক্ষা এই সমস্যার জবাব।
মূল উপলব্ধি: যে সমস্যার স্বভাব LIFO, তার জন্য stack শুধু সুবিধাজনক নয় — একমাত্র স্বাভাবিক সমাধান। আর কোনো কাঠামো যত মৌলিক হয়, সেটি তত নিচের স্তরে, শেষ পর্যন্ত হার্ডওয়্যারে নেমে আসে। সাথে নেমে আসে তার দুর্বলতাও।
প্র ০২Driver, অডিও, নেটওয়ার্ক — সবখানে কেন ring buffer? Linked list queue কেন নয়?
হার্ডওয়্যারের কাছাকাছি কোডের তিনটি কঠিন শর্ত আছে, আর ring buffer তিনটিই মেটায়।
- মেমরি আগে থেকেই নির্দিষ্ট: একটি network card প্রতি সেকেন্ডে লাখো packet আনতে পারে। প্রতিটির জন্য
mallocডাকা মানে ধীর হওয়া, আর খারাপ মুহূর্তে মেমরি না পাওয়ার ঝুঁকি। Ring buffer একবার জায়গা নেয়, তারপর একই ঘর বারবার ব্যবহার করে। Interrupt-এর ভেতরে, যেখানে অপেক্ষা করা নিষেধ, এটি খুব জরুরি। - প্রতিটি কাজ একই, ছোট সময়ে: enqueue আর dequeue — দুটিই কয়েকটি নির্দেশ, কোনো খোঁজাখুঁজি বা সরানো নেই। অডিওতে এটি জীবন-মরণ প্রশ্ন: sound card প্রতি সেকেন্ডে হাজার হাজার নমুনা চায়। Buffer সময়মতো না ভরলে (underrun) স্পিকারে “ক্লিক” বা ভাঙা শব্দ শোনা যায়।
- লেখক আর পাঠক আলাদা: সাধারণত একপাশে হার্ডওয়্যার বা driver লেখে (
tailসরায়), অন্যপাশে প্রোগ্রাম পড়ে (headসরায়)। প্রত্যেকে শুধু নিজের index বদলায়। এক লেখক, এক পাঠক হলে সাবধানে লিখলে lock ছাড়াই নিরাপদে চালানো যায় — যদিও সেখানে CPU-র মেমরি-ক্রমের সূক্ষ্ম নিয়ম মানতে হয়।
বাস্তব উদাহরণ: network card আর OS-এর মাঝে packet-এর ঠিকানা রাখা হয় “descriptor ring”-এ; Linux kernel-এ kfifo নামে একটি সাধারণ ring buffer আছে; আর Linux-এর আধুনিক I/O ব্যবস্থা io_uring-এর নামেই “ring” — প্রোগ্রাম আর kernel দুটি ring buffer দিয়ে কাজ আদান-প্রদান করে। এসব জায়গায় ক্ষমতা প্রায়ই ২-এর ঘাত রাখা হয়, যাতে ধীর %-এর বদলে দ্রুত bit mask (i & (CAP - 1)) ব্যবহার করা যায় — পর্ব ০৯-এর bitwise অপারেটর।
আরেক রূপ: “ভরে গেলে সবচেয়ে পুরোনোটি মুছে লেখো”। গাড়ির dashcam বা সার্ভারের সাম্প্রতিক log এভাবে চলে — সবসময় শেষ কয়েক ঘণ্টা বা শেষ কয়েক হাজার লাইন থাকে।
মূল উপলব্ধি: নির্দিষ্ট মাপ কখনো কখনো দুর্বলতা নয়, শক্তি। যে সিস্টেমে সময় ও মেমরির নিশ্চয়তা দরকার, সেখানে “সীমা আগে থেকেই জানা” কাঠামো সবচেয়ে নির্ভরযোগ্য।
প্র ০৩Postfix-এ বন্ধনী লাগে না কেন? আর মানুষ infix-এ লিখলে কম্পিউটার কীভাবে সেটিকে postfix-এ নেয়?
Infix-এ 5 + 3 * 2 দেখে আপনাকে বাইরের একটি নিয়ম মনে করতে হয় — “গুণ আগে”। নিয়ম না জানলে দুটি অর্থ সম্ভব: ১৬ বা ১১. বন্ধনী আর অগ্রাধিকারের নিয়ম এই দ্ব্যর্থতা দূর করার জন্যই। Postfix-এ ক্রমটিই নির্দেশ: 5 3 2 * + মানে ১১, আর 5 3 + 2 * মানে ১৬. প্রতিটি অপারেটর ঠিক তার আগের দুটি মান (stack-এর ওপরের দুটি) নেয় — দ্ব্যর্থতার জায়গাই নেই।
এই চিন্তার শুরু ১৯২০-এর দশকে পোলিশ যুক্তিবিদ Jan Łukasiewicz-এর হাতে। তিনি যুক্তিবিদ্যার সূত্র বন্ধনী ছাড়া লেখার জন্য অপারেটরকে সামনে বসানোর (prefix) পদ্ধতি তৈরি করেন, যাকে আজ “Polish notation” বলা হয়। পরে কম্পিউটারে উল্টো রূপটি — Reverse Polish Notation — বেশি কাজের প্রমাণিত হলো, কারণ বাঁ থেকে ডানে পড়ে একটি stack দিয়েই হিসাব হয়ে যায়। HP-র বৈজ্ঞানিক ক্যালকুলেটরে, আর Forth ও PostScript (প্রিন্টারের ভাষা) — এসব ভাষায় RPN সরাসরি ব্যবহার হয়।
Infix থেকে postfix: Edsger Dijkstra-র “shunting-yard” অ্যালগরিদম — নামটি রেলের শান্টিং ইয়ার্ড থেকে, যেখানে বগিগুলো পাশের লাইনে সরিয়ে রেখে ক্রম বদলানো হয়। সংখ্যা পেলে সরাসরি output-এ যায়; অপারেটর যায় একটি stack-এ অপেক্ষা করতে; নতুন অপারেটরের চেয়ে বেশি বা সমান অগ্রাধিকারের অপারেটর stack-এ থাকলে আগে সেগুলো output-এ নামে। খোলা বন্ধনী stack-এ ওঠে, আর বন্ধ বন্ধনী এলে মিলিয়ে তোলা পর্যন্ত সব নামে। এখানে একটি নয়, দুই কাজে stack — অপারেটর অপেক্ষা করানো আর বন্ধনী মেলানো।
আধুনিক compiler সাধারণত সরাসরি postfix লেখে না; সে রাশিটিকে একটি tree (expression tree) বানায়। কিন্তু সেই tree-কে নির্দিষ্ট ক্রমে পড়লে ঠিক postfix পাওয়া যায় — আর JVM বা CPython-এর bytecode দেখতে হুবহু postfix-এর মতো: “মান তোলো, মান তোলো, যোগ করো”।
মূল উপলব্ধি: মানুষের জন্য সুবিধাজনক রূপ আর যন্ত্রের জন্য সুবিধাজনক রূপ আলাদা হতে পারে। ভালো সিস্টেম দুটোর মাঝে একটি নির্ভরযোগ্য অনুবাদক বসায় — এখানে সেই অনুবাদকের হৃদয় একটি stack.
প্র ০৪FIFO ন্যায্য, priority জরুরি — দুটির মধ্যে টানাপোড়েন কীভাবে মেটানো হয়?
শুধু FIFO হলে জরুরি কাজকেও অকারণে অপেক্ষা করতে হয় — ব্যাংকে টোকেনের লাইনে একজন বয়স্ক অসুস্থ মানুষকে এক ঘণ্টা দাঁড় করিয়ে রাখা ন্যায্য দেখায়, কিন্তু মানবিক নয়। আবার শুধু priority হলে নতুন নতুন জরুরি কাজ আসতে থাকলে কম গুরুত্বের কাজ কখনোই সুযোগ পায় না। এই অবস্থার নাম starvation (অনাহার)।
বাস্তব সিস্টেম তাই দুটি মিশিয়ে চলে:
- আলাদা লাইন: ব্যাংকে প্রবীণ বা প্রতিবন্ধী নাগরিকদের জন্য আলাদা কাউন্টার। প্রতিটি লাইনের ভেতরে FIFO, কিন্তু লাইনগুলোর মধ্যে অগ্রাধিকার। OS আর রাউটার এভাবে কয়েকটি queue রাখে।
- Aging (অপেক্ষায় গুরুত্ব বাড়ানো): যে কাজ যত বেশিক্ষণ অপেক্ষা করছে, তার গুরুত্ব ধীরে ধীরে তত বাড়ে। একসময় সে-ও সামনে চলে আসে — starvation অসম্ভব হয়।
- ন্যায্য ভাগ: Linux-এর scheduler-এর মতো ব্যবস্থা প্রতিটি কাজ এ পর্যন্ত কতটুকু CPU পেয়েছে তার হিসাব রাখে, আর সবচেয়ে কম পাওয়া কাজটিকে আগে সুযোগ দেয়।
একটি বিখ্যাত ঘটনা: ১৯৯৭ সালে মঙ্গল গ্রহে নামা NASA-র Mars Pathfinder বারবার নিজে থেকে restart হচ্ছিল। কারণ ছিল priority inversion: কম গুরুত্বের একটি কাজ একটি সম্পদ আটকে রেখেছিল, বেশি গুরুত্বের কাজ সেটির অপেক্ষায় ছিল, আর মাঝারি গুরুত্বের কাজগুলো কম গুরুত্বেরটিকে চলতেই দিচ্ছিল না। পৃথিবী থেকে একটি সেটিং বদলে (priority inheritance চালু করে) সমস্যাটি মেটানো হয় — অপেক্ষায় রাখা কাজটি সাময়িকভাবে বেশি গুরুত্ব পায়।
মূল উপলব্ধি: “কে আগে যাবে” প্রশ্নটি শুধু প্রযুক্তিগত নয়, নীতিগতও। ন্যায্যতা, জরুরিতা আর কেউ যেন চিরকাল বঞ্চিত না থাকে — ভালো scheduler তিনটির ভারসাম্য রাখে।
প্র ০৫যদি queue-তে জিনিস ঢোকে বের হওয়ার চেয়ে দ্রুত — তখন কী হয়, আর ভালো সিস্টেম কী করে?
সরল অঙ্ক: প্রতি সেকেন্ডে ১০০টি অনুরোধ আসছে, সার্ভার সামলাতে পারে ৮০টি। প্রতি সেকেন্ডে ২০টি করে জমছে। এক ঘণ্টায় ৭২,০০০ — queue অসীমের দিকে। Queue কেবল সাময়িক চাপ সামলাতে পারে; গড় আগমন যদি গড় সেবার চেয়ে বেশি হয়, কোনো মাপের queue-ই যথেষ্ট নয়।
আরেকটি সূক্ষ্ম দিক: queue যত লম্বা, অপেক্ষার সময় তত বেশি। Queueing theory-র একটি সরল সূত্র (Little's law) বলে — গড়ে লাইনে থাকা জিনিসের সংখ্যা = আগমনের হার × গড় অপেক্ষার সময়। অর্থাৎ বড় queue মানে বেশি দেরি। রাউটারে খুব বড় buffer রাখলে packet হারায় না ঠিকই, কিন্তু ভিডিও কল আর অনলাইন গেমে দেরি অসহ্য হয়ে যায় — এই সমস্যার নামই “bufferbloat”.
তাই ভালো সিস্টেম queue-কে সীমিত রাখে, আর ভরে এলে একটি নীতি মানে:
- উৎসকে থামানো (backpressure): লেখককে অপেক্ষা করানো বা “ধীরে পাঠাও” বলা। TCP-তে প্রাপক জানিয়ে দেয় তার কাছে আর কতটুকু জায়গা আছে, প্রেরক তার বেশি পাঠায় না।
- স্পষ্টভাবে প্রত্যাখ্যান: “এখন ব্যস্ত, পরে চেষ্টা করুন” — যেমন ওয়েবসাইটের HTTP 503 বা 429. ঈদের আগে অনলাইন টিকিটের সাইট ধীর হয়ে গেলে এই কৌশলই প্রায়ই চোখে পড়ে।
- ফেলে দেওয়া: নতুনটি ফেলা (আমাদের
FULL), অথবা পুরোনোটি মুছে নতুনটি রাখা — লাইভ ভিডিওতে পুরোনো ফ্রেমের চেয়ে নতুন ফ্রেম বেশি দামি।
সবচেয়ে খারাপ নীতি হলো কোনো নীতি না থাকা — অসীম queue, যা নীরবে বাড়তে বাড়তে একদিন পুরো সার্ভারের মেমরি খেয়ে ফেলে, আর তখন সবাই একসাথে সেবা হারায়।
মূল উপলব্ধি: Queue সমস্যার সমাধান নয়, সমস্যা সামলানোর সময় কেনার উপায়। ভরা অবস্থায় কী হবে — সেই সিদ্ধান্ত নকশার শুরুতেই নিতে হয়। আমাদের ছোট enqueue-এর return 0 লাইনটি আসলে এই বড় সিদ্ধান্তেরই ক্ষুদ্র রূপ।
অনুশীলন
খাতা-কলম নিন। এগুলোর বেশিরভাগে কোড লিখতে হবে না — হাতে stack আর queue চালানোই আসল কাজ।
-
বন্ধনী শিকারি: হাতে stack চালিয়ে বলুন
( [ ] { ( ) } ) ]কোন অক্ষরে ব্যর্থ হয় এবং কেন। তারপরbrackets.c-এরmain-এ লেখাটি যোগ করে মিলিয়ে নিন।প্রথম আটটি অক্ষর নিখুঁতভাবে মেলে:
(push,[push,]মেলে,{push,(push,)মেলে,}মেলে,)মেলে — এখানে stack খালি। নবম অক্ষর]এলে pop করার মতো কিছুই নেই — underflow. ফাংশন ০ ফেরত দেয়, ফল “ERROR”. মজার ব্যাপার: underflow যাচাইয়ের লাইনটি মুছে দিলেst[--top]পড়তst[-1]— array-র বাইরের মেমরি, যার মান অনির্দিষ্ট। কোনো দিন হয়তো ঠিকঠাক “ERROR” দিত, কোনো দিন ভুল উত্তর বা crash. -
Postfix ক্যালকুলেটর হোন: (ক)
2 3 4 * + 5 --এর মান হাতে stack দিয়ে বের করুন। (খ)(7 - 2) * 3-কে postfix-এ লিখুন।(ক) push ২, push ৩, push ৪ → stack: ২ ৩ ৪.
*: pop ৪, pop ৩, push ১২ → ২ ১২.+: pop ১২, pop ২, push ১৪ → ১৪. push ৫ → ১৪ ৫.-: প্রথম pop ৫ (ডান পাশ), তারপর ১৪ (বাঁ পাশ) → ১৪ − ৫ = ৯. ক্রম উল্টালে ৫ − ১৪ = −৯ আসত — বিয়োগ আর ভাগে pop-এর ক্রম খুব গুরুত্বপূর্ণ।(খ)
7 2 - 3 *. প্রথমে ৭ আর ২ থেকে বিয়োগ, তারপর সেই ফল আর ৩-এর গুণ। বন্ধনী লাগল না, কারণ ক্রমই বলে দিচ্ছে কোনটি আগে। -
Ring buffer-এর ছবি: একটি circular queue-তে
CAP= ৪, এখনhead= ৩,tail= ১,count= ২. কোন কোন ঘরে ডেটা আছে, কোন ক্রমে? এরপর দুটি enqueue করলেtailআরcountকত হবে, আর তখন queue-এর অবস্থা কী?সামনে
head= ৩, তাই প্রথম মান ঘর ৩-এ; পরেরটি(3 + 1) % 4= ঘর ০-তে। অর্থাৎ ডেটা আছে ঘর ৩ আর ০-তে, এই ক্রমে। দুটি enqueue: প্রথমটি ঘর ১-এ,tail২; দ্বিতীয়টি ঘর ২-এ,tail৩.countহলো ৪ =CAP— queue ভরা। আর দেখুন, এখনhead=tail= ৩ — খালি queue-এর মতোই দেখাচ্ছে!countনা থাকলে এই অবস্থাকে খালি ভেবে ভুল হতো। -
কোড বদলান — এক ঘর ফাঁকা রাখার নকশা:
ring_queue.cথেকেcountসরিয়ে দিন। ভরা ও খালি চেনার জন্য “একটি ঘর সবসময় ফাঁকা” নিয়ম ব্যবহার করুন। প্রোগ্রাম চালালে আউটপুট কীভাবে বদলায়, আর কেন?বদলাতে হবে মাত্র দুটি শর্ত, আর
count++/count--মুছে দিতে হবে:enqueue:if ((q->tail + 1) % CAP == q->head) return 0;— পরের ঘরটিheadহলে ভরা ধরা হয়।dequeue:if (q->head == q->tail) return 0;— দুটি সমান মানে খালি, এখন আর কোনো দ্ব্যর্থতা নেই।
চালালে দেখবেন ৪০, ৫০, ৬০ “ok”, কিন্তু ৭০ আর ৮০ দুটিই “FULL”. কারণ এখন ৫ ঘরের buffer-এ সর্বোচ্চ ৪টি রাখা যায় — একটি ঘর ইচ্ছা করে ফাঁকা রাখা হচ্ছে ভরা আর খালি আলাদা করার জন্য। শেষে বের হবে ৩০, ৪০, ৫০, ৬০. এটাই trade-off: একটি ভ্যারিয়েবল কম, বদলে একটি ঘর কম।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমCircular buffer-এ নিরাপদ enqueue-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🛑
count == CAPকি না দেখা — ভরা হলে ০ ফেরত - ✍️
data[tail]-এ নতুন মান লেখা - 🔁
tail = (tail + 1) % CAP— এক ঘর এগোনো, দরকারে ঘুরে আসা - ➕
countএক বাড়ানো - ✅ ১ ফেরত দিয়ে সফলতা জানানো
একটি খালি stack-এ পরপর push 1, push 2, push 3, তারপর একবার pop করা হলো। কোন মান বের হবে?
- 1
- 3
- 2
- কিছুই না, underflow হবে
CAP = 5 এমন একটি circular queue-তে tail = 4. একটি enqueue-এর পর tail কত হবে?
- 5
- 3
- 1
- 0
একটি circular queue-তে কোনো count রাখা হয়নি, শুধু head আর tail. দেখা গেল head == tail. এর অর্থ কী?
- নিশ্চিতভাবে খালি
- নিশ্চিতভাবে ভরা
- খালি অথবা ভরা — শুধু এ দুটি দেখে বলা সম্ভব নয়
- কোডে নিশ্চয়ই বাগ আছে
Postfix রাশি 4 2 - 3 *-এর মান কত?
- 6
- -6
- -2
- 24
নিচের কোনটি সবচেয়ে স্বাভাবিকভাবে queue দিয়ে চলে?
- Editor-এ Undo
- ব্রাউজারের Back বোতাম
- একটি প্রিন্টারে অনেকের পাঠানো ফাইল ছাপা
- কোডে বন্ধনী মেলানো
AI-এর লেখা stack-এ int pop(void) { return data[--top]; }. এর সবচেয়ে বড় সমস্যা কী, আর কীভাবে ঠিক করবেন?
- --top-এর বদলে top-- লিখতে হবে
- খালি stack-এ underflow — আগে top == 0 যাচাই করে সফলতা/ব্যর্থতা আলাদাভাবে ফেরত দিতে হবে
- ফাংশনের নাম stack_pop রাখতে হবে
- খালি হলে -1 ফেরত দিলেই যথেষ্ট
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Stack ও queue হলো abstract data type — নিয়ম আগে; ভেতরে array না linked list, সেটি আলাদা সিদ্ধান্ত।
- Stack = LIFO: push, pop, peek. বন্ধনী মেলানো, postfix হিসাব, undo/redo আর call stack — সবই এই নিয়মে।
- Queue = FIFO: enqueue পেছনে, dequeue সামনে। প্রিন্টার, OS-এর run queue, keyboard, নেটওয়ার্ক — সবখানে।
- Circular buffer:
(i + 1) % CAPদিয়ে ঘুরে আসা; শুধুhead == tailদিয়ে ভরা-খালি আলাদা হয় না —countবা এক ঘর ফাঁকা রাখুন। - প্রতিটি লেখার আগে overflow, প্রতিটি পড়ার আগে underflow যাচাই; ব্যর্থতা জানান আলাদাভাবে — AI-এর কোডে এগুলোই আগে খুঁজুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৫ · ট্রি ও BSTপরবর্তী পর্বডেটাকে ডালে ডালে সাজানো — দ্রুত খোঁজা, যোগ করা আর মুছে ফেলা।
- C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Data Structures & Algorithmsগভীরে যানHeap দিয়ে priority queue, monotonic stack, deque-এর কৌশল আর অ্যালগরিদমের গভীর বিশ্লেষণ।
- Operating Systemsপাশের ট্র্যাকScheduler-এর run queue, interrupt আর call stack — OS কীভাবে কাজ ভাগ করে দেয়।
- Computer Networksপাশের ট্র্যাকরাউটারের packet queue, TCP flow control আর ভিড়ের সময় নেটওয়ার্ক কীভাবে টিকে থাকে।