এই পর্বে যা শিখবেন
- Queue ADT-র তিনটি মূল অপারেশন আর FIFO নিয়ম — বাস্তব সারির সাথে মিলিয়ে
- Circular buffer-এ head, tail, modulo আর full/empty চেনা — হাতে trace করে
- Linked list আর দুটি stack দিয়ে queue বানানো, আর amortized O(1)-এর যুক্তি
std::queue,std::dequeআরcollections.dequeঠিকভাবে ব্যবহার — কোন অপারেশনের দাম কত- BFS, scheduling, প্রিন্টার আর producer–consumer-এ queue; আর monotonic deque দিয়ে sliding window maximum O(n)-এ
১ · Queue কী — আগে এলে আগে সেবা
সকাল দশটায় মতিঝিলের একটি ব্যাংকে টাকা জমা দিতে গেছেন। কাউন্টারের সামনে লম্বা সারি। নিয়মটা সবাই জানেন: নতুন কেউ এলে দাঁড়ান সারির শেষে, আর ক্যাশিয়ার সেবা দেন সারির একেবারে সামনের জনকে। কেউ মাঝখানে ঢুকতে পারেন না, আর পেছনের জন আগে সেবা পান না। যিনি আগে এসেছেন, তিনি আগে যাবেন। কমলাপুরের টিকিট কাউন্টার, মেট্রোরেলের কার্ড রিচার্জের লাইন, পরীক্ষার হলে খাতা জমার সারি — সবখানে একই নিয়ম।
কম্পিউটারে এই নিয়মে চলা ডেটা স্ট্রাকচারের নাম queue (কিউ)। এর নিয়মের নাম FIFO — First In, First Out: যে আগে ঢোকে, সে আগে বের হয়। আগের পর্বে দেখা stack ঠিক উল্টো — সেখানে LIFO, শেষে ঢোকা থালাটিই আগে ওঠে।
➕ Enqueue — পেছনে দাঁড়ানো
নতুন উপাদান যোগ হয় সারির পেছনে (rear বা back). একে বলে enqueue.
O(1)➖ Dequeue — সামনে থেকে বিদায়
সবচেয়ে পুরোনো উপাদানটি বের হয় সামনে (front) থেকে। একে বলে dequeue.
O(1)👀 Front — কে আছেন সামনে
বের না করে শুধু সামনের উপাদানটি দেখা (অনেক বইয়ে peek). সাথে থাকে isEmpty আর size.
লক্ষ করুন, এখানে আমরা বলিনি queue মেমরিতে কীভাবে রাখা হবে — শুধু বলেছি কোন অপারেশন আছে আর কী নিয়মে চলে। এটিই abstract data type (ADT): একটি চুক্তি। চুক্তিটি পূরণ করা যায় অনেকভাবে — সাধারণ অ্যারে, circular buffer, linked list, এমনকি দুটি stack দিয়ে। এই পর্বের বড় অংশ জুড়ে আমরা এই বাস্তবায়নগুলো তুলনা করব, কারণ প্রতিটির খরচ আলাদা।
| অপারেশন | কাজ | C++ (std::queue) | Python (deque) |
|---|---|---|---|
| enqueue(x) | x-কে পেছনে যোগ | q.push(x) | q.append(x) |
| dequeue() | সামনেরটি সরানো | q.pop() (কিছু ফেরত দেয় না) | q.popleft() (মান ফেরত দেয়) |
| front() | সামনেরটি দেখা | q.front() | q[0] |
| isEmpty() | খালি কি না | q.empty() | not q |
| size() | কতজন আছে | q.size() | len(q) |
খালি queue থেকে dequeue করা যায় না। একে বলে underflow. C++-এ খালি std::queue-এ front() বা pop() ডাকা undefined behavior — প্রোগ্রাম crash করতে পারে, বা কোনো আবর্জনা মান দিয়ে নিঃশব্দে ভুল উত্তর দিতে পারে। Python-এ খালি deque-এ popleft() দিলে IndexError আসে। তাই সবসময় আগে empty() যাচাই করুন। উল্টো দিকে, নির্দিষ্ট মাপের queue ভর্তি থাকলে নতুন উপাদান ঢোকানোর চেষ্টা হলো overflow.
২ · সরল অ্যারে-queue — কেন জায়গা নষ্ট হয়
প্রথম চেষ্টা হিসেবে সবচেয়ে সোজা পথ: ৬ ঘরের একটি অ্যারে, আর দুটি index — front (সামনের উপাদান কোথায়) আর rear (পরের উপাদান কোথায় বসবে). enqueue মানে a[rear] = x; rear++, dequeue মানে front++. দুটিই O(1) — শুনতে চমৎকার। কিন্তু কয়েকটি অপারেশন চালিয়ে দেখুন কী ঘটে:
| সারিতে আছে | |
| front | |
| rear | |
| খালি কিন্তু অব্যবহারযোগ্য ঘর | |
| শেষ ঘটনা |
- শুরু। ৬ ঘরের খালি অ্যারে। front = rear = 0 — দুটি সমান মানে সারি খালি।
- তিনজন এলেন। enqueue 1, 2, 3 — প্রত্যেকে
a[rear]-এ বসলেন, rear বেড়ে হলো 3. প্রতিটি কাজ O(1). - দুজন সেবা পেলেন। dequeue দুবার — 1 আর 2 বেরিয়ে গেলেন, front এগিয়ে 2. খেয়াল করুন, ঘর ০ আর ১ এখন খালি, কিন্তু front আর কখনো পেছনে ফিরবে না।
- আরও তিনজন। enqueue 4, 5, 6 — rear পৌঁছাল 6, অর্থাৎ অ্যারের একদম বাইরে। সারিতে মাত্র ৪ জন।
- “ভর্তি” — অথচ জায়গা আছে! 7 ঢোকাতে গেলে rear == 6 == capacity, তাই কোড বলবে queue ভর্তি। অথচ সামনের ২টি ঘর খালি পড়ে আছে। অপারেশন চলতে থাকলে প্রতিটি ঘর একবার ব্যবহার হয়ে চিরতরে নষ্ট হবে — ব্যাংকের লাইন যেন প্রতিবার এক পা করে দরজার দিকে সরে শেষে রাস্তায় নেমে যাচ্ছে।
- একটি “সমাধান” — সবাইকে বাঁয়ে সরানো। 3, 4, 5, 6-কে ঘর ০–৩-এ কপি করলে আবার জায়গা হয়। কিন্তু এতে লাগল ৪টি কপি — n জন থাকলে O(n). প্রতি dequeue-তে এভাবে সরালে n টি dequeue-তে মোট Θ(n²) কাজ। আমাদের চাই এমন উপায়, যাতে কাউকে সরাতে না হয় আর কোনো ঘরও নষ্ট না হয়।
ব্যাংকে কিন্তু মানুষ সরে না — ক্যাশিয়ারই এক কাউন্টার থেকে পরের দিকে তাকান। আমাদের দরকার ঠিক এই চিন্তা: উপাদানগুলো যেখানে আছে সেখানেই থাকুক, শুধু “সামনে কে” আর “পেছনে কোথায়” — এই দুটি চিহ্ন ঘুরে ঘুরে এগোক। অ্যারের শেষে পৌঁছালে চিহ্ন আবার শুরুতে ফিরে যাক, যেন অ্যারের দুই প্রান্ত জোড়া দিয়ে একটি আংটি বানানো হয়েছে।
৩ · Circular buffer — শেষ ঘরের পরে আবার প্রথম ঘর
এই আংটির মতো অ্যারের নাম circular buffer বা ring buffer. কৌশল একটিই — index বাড়ানোর সময় modulo নেওয়া:
cap = 4 হলে index চলে 0 → 1 → 2 → 3 → 0 → 1 … — শেষ ঘরের পরে আবার প্রথম ঘর। একে বলে wrap-around. আমরা রাখব তিনটি সংখ্যা: head (সামনের উপাদানের ঘর), tail (পরের উপাদান যে ঘরে বসবে) আর size (কতজন আছে). নিয়মগুলো:
Pseudocode (0-based index, cap = array length)
ENQUEUE(x):
if size == cap: report FULL // overflow
buf[tail] = x
tail = (tail + 1) % cap // wrap around
size = size + 1
DEQUEUE():
if size == 0: report EMPTY // underflow
x = buf[head]
head = (head + 1) % cap
size = size - 1
return x
// invariant: tail == (head + size) % cap,
// the queue is buf[head], buf[head+1], ... (size items, all mod cap)এবার cap = 4-এর একটি buffer-এ নিচের ক্রমটি ধাপে ধাপে দেখুন — বিশেষ করে ধাপ ৬-এ tail কীভাবে শেষ থেকে শুরুতে ফেরে, আর ধাপ ৩ ও ৯-এ head আর tail একই ঘরে থাকলেও অর্থ কীভাবে সম্পূর্ণ আলাদা। এই ক্রমটিই নিচের প্রোগ্রাম চালায়।
| শেষ অপারেশন | |
| head | |
| tail | |
| size | |
| সারি (front → back) |
- প্রথম enqueue. 10 বসল
buf[tail]= ঘর ০-এ, tail হলো (0 + 1) % 4 = 1. head = 0, size = 1. - আরও দুটি। 20 ঘর ১-এ, 30 ঘর ২-এ; tail এখন 3, size = 3. এ পর্যন্ত সাধারণ অ্যারের মতোই।
- প্রথম wrap-around. 40 বসল ঘর ৩-এ, আর tail = (3 + 1) % 4 = 0 — অ্যারের শেষ পেরিয়ে আবার শুরুতে। এখন head = tail = 0, size = 4: buffer ভর্তি।
- ভর্তি, তাই প্রত্যাখ্যান। 50 ঢোকানোর চেষ্টা — size == cap, তাই FULL. কিছুই বদলাল না। খেয়াল করুন: শুধু head আর tail দেখে বলা যেত না এটি ভর্তি না খালি — দুটিই 0. সিদ্ধান্ত দিল
size. - দুজন বিদায়। dequeue দুবার — 10 আর 20 বের হলো, head এগিয়ে 2. ঘর ০ আর ১ এখন খালি — এবং সরল অ্যারের মতো নষ্ট নয়, কারণ tail ঠিক সেখানেই (0) দাঁড়িয়ে।
- খালি ঘরের পুনর্ব্যবহার। 50 বসল ঘর ০-এ — অ্যারের শুরুতে, অথচ সারির একদম পেছনে। tail = 1. মেমরিতে ক্রম 50, _, 30, 40, কিন্তু সারির ক্রম 30 → 40 → 50. head থেকে পড়া শুরু করলেই ঠিক ক্রম পাওয়া যায়।
- আবার ভর্তি। 60 বসল ঘর ১-এ; tail = 2 = head, size = 4. কাউকে এক ঘরও সরাতে হয়নি।
- head-এর wrap-around. dequeue 30 (ঘর ২), তারপর 40 (ঘর ৩) — head হলো (3 + 1) % 4 = 0. head-ও এবার আংটি ঘুরে শুরুতে এলো। বাকি 50, 60.
- খালি। dequeue 50, 60 — head = 2 = tail, size = 0. ধাপ ৭-এও head = tail = 2 ছিল, তখন buffer ভর্তি; এখন খালি। একই ছবি, উল্টো অর্থ — এজন্যই আলাদা
size(বা অন্য কোনো কৌশল) লাগে।
Full আর empty আলাদা করার তিনটি উপায়
| কৌশল | খালি | ভর্তি | দাম |
|---|---|---|---|
আলাদা size রাখা (এই পর্বের কোড) | size == 0 | size == cap | একটি বাড়তি ভ্যারিয়েবল |
| একটি ঘর সবসময় খালি রাখা | head == tail | (tail + 1) % cap == head | cap ঘরে সর্বোচ্চ cap − 1 টি উপাদান |
| কখনো না-কমা counter (cap = 2-এর ঘাত) | in == out | in - out == cap | ঘর = in & (cap - 1); overflow-এর হিসাব সাবধানে |
সময়ের হিসাব সহজ: প্রতিটি অপারেশনে কয়েকটি তুলনা, একটি লেখা বা পড়া আর একটি modulo — কোনো লুপ নেই। তাই enqueue, dequeue, front তিনটিই worst case O(1), amortized নয়, সত্যিকারের প্রতিবার। জায়গা Θ(cap), আগে থেকে নির্দিষ্ট। এবার পুরো class চালিয়ে দেখুন — আউটপুটের প্রতিটি লাইন ওপরের ডায়াগ্রামের একটি অবস্থা:
#include <cstdio>
#include <initializer_list>
#include <string>
#include <vector>
using namespace std;
class CircularQueue {
vector<int> buf;
int head = 0, tail = 0, sz = 0; // head: front, tail: next free slot
int cap() const { return (int)buf.size(); }
public:
explicit CircularQueue(int c) : buf(c) {}
int size() const { return sz; }
bool enqueue(int x) {
if (sz == cap()) return false; // full: size == capacity
buf[tail] = x; tail = (tail + 1) % cap(); // wrap around
++sz; return true;
}
// dequeue: the caller must check size() > 0 first
int dequeue() { int x = buf[head]; head = (head + 1) % cap(); --sz; return x; }
void show(const string& op) const {
printf("%-12s [", op.c_str());
for (int i = 0; i < cap(); ++i)
if ((i - head + cap()) % cap() < sz) printf(i ? " %2d" : "%2d", buf[i]);
else printf(i ? " ." : " .");
printf("] head=%d tail=%d size=%d\n", head, tail, sz);
}
};
int main() {
CircularQueue q(4);
auto add = [&](int x) { q.show((q.enqueue(x) ? "enqueue " : "FULL ") + to_string(x)); };
for (int x : {10, 20, 30, 40, 50}) add(x);
for (int t = 0; t < 2; ++t) q.show("dequeue " + to_string(q.dequeue()));
for (int x : {50, 60}) add(x);
while (q.size() > 0) q.show("dequeue " + to_string(q.dequeue()));
}আউটপুট:
enqueue 10 [10 . . .] head=0 tail=1 size=1
enqueue 20 [10 20 . .] head=0 tail=2 size=2
enqueue 30 [10 20 30 .] head=0 tail=3 size=3
enqueue 40 [10 20 30 40] head=0 tail=0 size=4
FULL 50 [10 20 30 40] head=0 tail=0 size=4
dequeue 10 [ . 20 30 40] head=1 tail=0 size=3
dequeue 20 [ . . 30 40] head=2 tail=0 size=2
enqueue 50 [50 . 30 40] head=2 tail=1 size=3
enqueue 60 [50 60 30 40] head=2 tail=2 size=4
dequeue 30 [50 60 . 40] head=3 tail=2 size=3
dequeue 40 [50 60 . .] head=0 tail=2 size=2
dequeue 50 [ . 60 . .] head=1 tail=2 size=1
dequeue 60 [ . . . .] head=2 tail=2 size=0
লাইন “enqueue 40”-এ tail = 0 (wrap-around), “enqueue 50”-এ ঘর ০-এ 50 বসেছে, আর “dequeue 40”-এ head = 0. head = tail দুবার এসেছে — একবার size = 4 (ভর্তি), একবার size = 0 (খালি). show()-তে ঘর i ব্যবহৃত কি না যাচাই হয় (i - head + cap()) % cap() < sz দিয়ে — অর্থাৎ head থেকে ঘড়ির কাঁটার দিকে i কত দূরে. এখানে + cap() জরুরি: C++-এ % ঋণাত্মক সংখ্যায় ঋণাত্মক ফল দেয় (যেমন -2 % 4 == -2), যা Python-এর আচরণ থেকে আলাদা।
class CircularQueue:
def __init__(self, cap):
self.buf = [None] * cap
self.head = self.tail = self.size = 0 # head: front, tail: next free slot
def enqueue(self, x):
if self.size == len(self.buf): # full: size == capacity
return False
self.buf[self.tail] = x
self.tail = (self.tail + 1) % len(self.buf) # wrap around
self.size += 1
return True
def dequeue(self): # caller checks size > 0
x = self.buf[self.head]
self.head = (self.head + 1) % len(self.buf)
self.size -= 1
return x
def show(self, op):
cap = len(self.buf)
cells = [str(self.buf[i]) if (i - self.head) % cap < self.size else "."
for i in range(cap)]
print(f"{op:<12} [{' '.join(f'{c:>2}' for c in cells)}] "
f"head={self.head} tail={self.tail} size={self.size}")
q = CircularQueue(4)
for x in [10, 20, 30, 40, 50]:
q.show(f"enqueue {x}" if q.enqueue(x) else f"FULL {x}")
for _ in range(2):
q.show(f"dequeue {q.dequeue()}")
for x in [50, 60]:
q.show(f"enqueue {x}" if q.enqueue(x) else f"FULL {x}")
while q.size > 0:
q.show(f"dequeue {q.dequeue()}")আউটপুট C++-এর সাথে হুবহু এক:
enqueue 10 [10 . . .] head=0 tail=1 size=1
enqueue 20 [10 20 . .] head=0 tail=2 size=2
enqueue 30 [10 20 30 .] head=0 tail=3 size=3
enqueue 40 [10 20 30 40] head=0 tail=0 size=4
FULL 50 [10 20 30 40] head=0 tail=0 size=4
dequeue 10 [ . 20 30 40] head=1 tail=0 size=3
dequeue 20 [ . . 30 40] head=2 tail=0 size=2
enqueue 50 [50 . 30 40] head=2 tail=1 size=3
enqueue 60 [50 60 30 40] head=2 tail=2 size=4
dequeue 30 [50 60 . 40] head=3 tail=2 size=3
dequeue 40 [50 60 . .] head=0 tail=2 size=2
dequeue 50 [ . 60 . .] head=1 tail=2 size=1
dequeue 60 [ . . . .] head=2 tail=2 size=0
Python-এ (i - self.head) % cap সবসময় 0 থেকে cap − 1-এর মধ্যে থাকে, কারণ Python-এর %-এর ফলের চিহ্ন ভাজকের মতো — তাই এখানে + cap লাগেনি। বাস্তবে Python-এ নিজে ring buffer লেখার দরকার কমই পড়ে: collections.deque(maxlen=4) নিজেই নির্দিষ্ট মাপের buffer, ভর্তি অবস্থায় নতুন উপাদান এলে উল্টো প্রান্ত থেকে পুরোনোটি ফেলে দেয় (অংশ ৬)। তবে এই আচরণ “প্রত্যাখ্যান” নয়, “পুরোনোটি মুছে ফেলা” — কোনটি চান, সেটি আগে ঠিক করুন।
৪ · Linked list দিয়ে queue — সীমাহীন সারি
Circular buffer-এর একটিই সীমাবদ্ধতা: cap আগে থেকে ঠিক করতে হয়। কতজন আসবেন জানা না থাকলে কী করবেন? একটি পথ হলো ভর্তি হলে দ্বিগুণ মাপের নতুন অ্যারে বানিয়ে উপাদানগুলো head থেকে ক্রমানুসারে কপি করা — ঠিক পর্ব ১০-এর dynamic array-এর মতো, amortized O(1). আরেকটি পথ linked list (পর্ব ১২): প্রতিটি উপাদান একটি node, যে পরের node-এর ঠিকানা রাখে। দুটি pointer রাখি — head (সামনের node, এখান থেকে বের হয়) আর tail (শেষ node, এর পরে নতুন node জোড়া লাগে).
struct Node { int val; Node* next = nullptr; };
struct ListQueue {
Node* head = nullptr; // front: remove here
Node* tail = nullptr; // back: insert after this
void enqueue(int x) {
Node* n = new Node{x};
if (tail) tail->next = n; else head = n; // empty queue: n is also the front
tail = n;
}
int dequeue() { // caller checks head != nullptr
Node* n = head;
int x = n->val;
head = n->next;
if (!head) tail = nullptr; // queue became empty: fix tail too!
delete n;
return x;
}
};দুটি অপারেশনই worst case O(1) — শুধু কয়েকটি pointer বদলায়। কিন্তু দুটি সূক্ষ্ম প্রশ্ন আছে, যা পরীক্ষা আর ইন্টারভিউতে প্রায়ই আসে:
- কেন head-এ বের, tail-এ ঢোকানো — উল্টোটা কেন নয়? Singly linked list-এ শেষ node সরাতে হলে তার আগের node-টি জানা দরকার (তার
nextnull করতে হবে) — সেটি পেতে শুরু থেকে হাঁটতে হয়, O(n). শুরুর node সরানো কিন্তু O(1). তাই সরানোর কাজ head-এ, যোগের কাজ tail-এ। - শেষ উপাদান বের হলে tail-ও ঠিক করতে হয়।
if (!head) tail = nullptr;লাইনটি বাদ দিলে tail মুছে ফেলা node-কে নির্দেশ করে থাকবে (dangling pointer), আর পরের enqueue সেই মৃত node-এ লিখবে — নিঃশব্দে memory নষ্ট। এই ভুলটি AI-এর লেখা কোডেও দেখা যায়।
| বিষয় | Circular buffer | Linked list |
|---|---|---|
| enqueue / dequeue | O(1) worst case (নির্দিষ্ট cap); বাড়ানো গেলে amortized O(1) | O(1) worst case |
| আকার | নির্দিষ্ট, বা দ্বিগুণ করে বাড়াতে হয় | যত খুশি (memory থাকা পর্যন্ত) |
| প্রতি উপাদানে বাড়তি জায়গা | নেই (খালি ঘর ছাড়া) | একটি pointer (64-bit-এ ৮ বাইট) + allocation-এর খরচ |
| Cache | উপাদান পাশাপাশি — খুব দ্রুত | node ছড়ানো — প্রায়ই cache miss |
| i-তম উপাদান দেখা | O(1): buf[(head + i) % cap] | O(i) |
বাস্তবে অ্যারে-ভিত্তিক queue প্রায় সবসময় দ্রুত চলে, কারণ আধুনিক CPU পাশাপাশি থাকা ডেটা খুব দ্রুত পড়ে (পর্ব ১০)। Linked list queue-এর আসল সুবিধা আসে যখন প্রতিটি উপাদান নিজেই বড় বা আগে থেকে তৈরি node — যেমন operating system-এর ভেতরে প্রসেসের তালিকা, যেখানে node-টি প্রসেসের নিজের কাঠামোর ভেতরেই থাকে।
৫ · দুটি stack দিয়ে queue — amortized O(1)
ইন্টারভিউর একটি অতি প্রিয় প্রশ্ন: “শুধু দুটি stack ব্যবহার করে একটি queue বানান।” প্রথমে অসম্ভব মনে হয় — stack উল্টো ক্রমে বের করে, queue সোজা ক্রমে। কিন্তু উল্টোকে আবার উল্টালে সোজা হয়! একটি থালার স্তূপ আরেক টেবিলে একটি একটি করে সরালে নিচের থালাটি ওপরে চলে আসে।
instack: enqueue মানে শুধুin.push(x).outstack: dequeue মানেout.pop()— তবেoutখালি থাকলে আগেin-এর সব উপাদান একে একে pop করেout-এ push করুন। তাতে সবচেয়ে পুরোনো উপাদানটিout-এর ওপরে চলে আসে।
মূল শর্ত: স্থানান্তর হবে শুধু তখন, যখন out খালি। out-এ কিছু থাকা অবস্থায় in থেকে ঢাললে নতুন উপাদান পুরোনোদের ওপরে বসে ক্রম ভেঙে যাবে। নিচে ৪টি enqueue আর ৪টি dequeue দেখুন। “মোট stack-অপারেশন” সারিতে নজর রাখুন:
| শেষ অপারেশন | |
| in | |
| out | |
| ফেরত মান | |
| শেষ হওয়া queue-অপারেশন | |
| মোট stack-অপারেশন (push + pop) |
- enqueue 1. সরাসরি
in-এ push — একটি stack-অপারেশন। - enqueue 2, 3. আরও দুটি push.
in-এর ওপরে 3, নিচে 1 — queue-এর সামনের জন (1) এখন stack-এর একদম তলায়। মোট খরচ ৩. - প্রথম dequeue — ঢালা।
outখালি, তাইinথেকে 3, 2, 1 একে একে pop করেout-এ push: ৩টি pop + ৩টি push = ৬টি অপারেশন। ক্রম উল্টে গেল — এখন 1 আছেout-এর ওপরে। মোট ৯. - dequeue শেষ।
out.pop()দিল 1 — ঠিক যে আগে এসেছিল। এই একটি dequeue-তে খরচ হলো ৭টি অপারেশন: একটি queue-অপারেশন সত্যিই ব্যয়বহুল হতে পারে। - enqueue 4.
out-এ কিছু থাকলেও নতুন উপাদান সবসময়in-এ যায়। 4 অপেক্ষা করবে, যতক্ষণ না 2 আর 3 বের হয়। - dequeue → 2.
outখালি নয়, তাই ঢালার দরকার নেই — সরাসরি pop. একটিমাত্র অপারেশন। - dequeue → 3. আবার সস্তা: একটি pop.
outএখন খালি। - dequeue → 4.
outখালি, তাই 4-কে ঢালা (১ pop + ১ push), তারপর pop. শেষে ৮টি queue-অপারেশনে মোট ১৬টি stack-অপারেশন — গড়ে ঠিক ২টি। প্রতিটি উপাদান ঠিক ৪বার ছোঁয়া হয়েছে: in-এ push, in থেকে pop, out-এ push, out থেকে pop.
Pseudocode (queue from two stacks)
ENQUEUE(x): in.push(x)
DEQUEUE():
if out is empty:
while in is not empty: // pour: reverses the order
out.push(in.pop())
if out is empty: report EMPTY
return out.pop()Invariant: out-এর প্রতিটি উপাদান in-এর প্রতিটি উপাদানের চেয়ে পুরোনো, আর out-এর ওপরের উপাদানটিই সবচেয়ে পুরোনো। শুরুতে দুটিই খালি, তাই সত্য। enqueue নতুন (সবচেয়ে কম পুরোনো) উপাদান in-এ দেয় — সত্য থাকে। ঢালা হয় কেবল out খালি থাকলে, আর ঢালার ফলে in-এর তলার (সবচেয়ে পুরোনো) উপাদান out-এর ওপরে আসে — সত্য থাকে। তাই out.pop() সবসময় সবচেয়ে পুরোনোটি দেয়: FIFO ঠিক।
Amortized খরচ: একটি উপাদান জীবনে সর্বোচ্চ ৪টি stack-অপারেশন পায় (in-এ push, in থেকে pop, out-এ push, out থেকে pop) — কারণ out থেকে কখনো in-এ ফেরে না। তাই যেকোনো m টি queue-অপারেশনের ক্রমে মোট খরচ ≤ 4m, অর্থাৎ প্রতি অপারেশনে amortized O(1). তবে একটি একক dequeue-র worst case Θ(n) — যখন n টি উপাদান একসাথে ঢালতে হয়। জায়গা Θ(n).
এই “বেশিরভাগ সস্তা, মাঝে মাঝে দামি, কিন্তু গড়ে সস্তা” হিসাবটি এখানে আমরা সরল গণনা দিয়ে করলাম, যাকে বলে aggregate method. পর্ব ৬০-এ একই সমস্যা accounting আর potential method দিয়েও প্রমাণ করব — প্রতিটি enqueue-কে ৪ টাকা দিয়ে ভবিষ্যতের খরচের জন্য জমা রাখার মতো করে। সতর্কতা: amortized মানে “গড়”, তবে এলোমেলো ইনপুটের গড় নয় — এটি যেকোনো ক্রমের জন্য নিশ্চিত গড়। কিন্তু যে সিস্টেমে প্রতিটি অপারেশন সময়মতো শেষ হতেই হবে (যেমন অডিও বা নিয়ন্ত্রণ যন্ত্র), সেখানে মাঝে মাঝের Θ(n) ঝাঁকুনিও বিপদ।
৬ · Deque — দুই মাথা খোলা সারি, আর লাইব্রেরির queue
বাসের দুই দরজার কথা ভাবুন — সামনে দিয়েও ওঠা-নামা যায়, পেছন দিয়েও। deque (উচ্চারণ “ডেক”, double-ended queue) ঠিক এমন: দুই প্রান্তেই ঢোকানো ও বের করা O(1). তাই একই deque দিয়ে queue (পেছনে ঢোকানো, সামনে বের করা) আর stack (একই প্রান্তে ঢোকানো-বের করা) দুটিই বানানো যায়। Circular buffer-এ deque বানানোও সহজ: সামনে ঢোকাতে head = (head - 1 + cap) % cap, পেছন থেকে বের করতে tail = (tail - 1 + cap) % cap.
| কাজ | C++ std::deque | Python collections.deque | সময় |
|---|---|---|---|
| পেছনে ঢোকানো | push_back(x) | append(x) | O(1) |
| সামনে ঢোকানো | push_front(x) | appendleft(x) | O(1) |
| পেছন থেকে বের | pop_back() | pop() | O(1) |
| সামনে থেকে বের | pop_front() | popleft() | O(1) |
| দুই প্রান্ত দেখা | front(), back() | d[0], d[-1] | O(1) |
| মাঝের i-তম উপাদান | d[i] | d[i] | C++: O(1); Python: O(n) (প্রান্ত থেকে দূরত্ব অনুযায়ী) |
| মাঝে ঢোকানো/মোছা | insert, erase | insert, del d[i] | O(n) |
C++: std::queue একটি adapter, ভেতরে std::deque
std::queue নিজে কোনো ডেটা রাখে না। এটি একটি container adapter — ভেতরে ডিফল্টভাবে একটি std::deque রাখে, আর বাইরে শুধু push, pop, front, back, empty, size খোলা রাখে। এর ওপর লুপ চালানো বা q[i] লেখা যায় না — ইচ্ছাকৃতভাবেই, যাতে কেউ FIFO নিয়ম ভাঙতে না পারে। আর মনে রাখুন, q.pop() কিছু ফেরত দেয় না; মান চাইলে আগে q.front() পড়ুন।
std::deque ভেতরে একটি লম্বা অ্যারে নয়, আবার linked list-ও নয়। এটি অনেকগুলো নির্দিষ্ট মাপের ছোট ব্লক (libstdc++-এ প্রতিটি ৫১২ বাইটের মতো), আর ব্লকগুলোর ঠিকানার একটি তালিকা (map) রাখে। সামনে জায়গা ফুরোলে সামনে নতুন ব্লক, পেছনে ফুরোলে পেছনে — পুরোনো উপাদান কখনো সরাতে হয় না। তাই d[i] O(1) (কোন ব্লক, ব্লকের কোন ঘর — দুটি ভাগ-হিসাব), আর প্রান্তে ঢোকানো O(1). বিনিময়ে প্রতিটি index-এ একটি বাড়তি ধাপ লাগে, তাই সাধারণ ঘোরাঘুরিতে এটি std::vector-এর চেয়ে কিছুটা ধীর।
Python: list.pop(0) কেন ফাঁদ
Python-এর list ভেতরে একটি dynamic array (পর্ব ১০). শেষে append আর pop() amortized O(1), কিন্তু pop(0) প্রথম ঘর খালি করে বাকি সব উপাদানকে এক ঘর বাঁয়ে সরায় — ঠিক অংশ ২-এর ধাপ ৬. n উপাদানের list-এ খরচ Θ(n). পুরো list এভাবে খালি করলে মোট প্রায় n²/2 টি সরানো: n = 10⁵ হলে প্রায় ৫০০ কোটি পয়েন্টার-কপি। BFS-এ queue.pop(0) লিখে কনটেস্টে Time Limit Exceeded পাওয়া খুব পরিচিত অভিজ্ঞতা। একই কারণে list.insert(0, x)-ও Θ(n).
from collections import deque
q = deque()
q.append(1); q.append(2); q.append(3) # enqueue: O(1)
print(q.popleft()) # dequeue: O(1) -> 1
q.appendleft(0) # deque: push at the front, O(1)
print(list(q)) # [0, 2, 3]
last3 = deque(maxlen=3) # fixed-size ring: old items fall off
for x in range(1, 6):
last3.append(x)
print(list(last3)) # [3, 4, 5]collections.deque ভেতরে ছোট ছোট ব্লকের একটি doubly linked list, তাই দুই প্রান্তেই O(1). maxlen দিলে এটি নির্দিষ্ট মাপের ring buffer — “শেষ ১০০টি লগ লাইন” বা “শেষ ৫ মিনিটের রিডিং” রাখার সহজ উপায়। তবে মাঝখানের d[i] O(n), তাই ঘন ঘন index দরকার হলে list-ই ভালো। একাধিক thread-এর মধ্যে কাজ ভাগাভাগির জন্য আছে queue.Queue — অংশ ৭ দেখুন।
আরও গভীরে: cap যদি 2-এর ঘাত হয় (যেমন 1024), তাহলে i % cap-এর বদলে i & (cap - 1) লেখা যায় — ভাগের চেয়ে bitwise AND অনেক সস্তা। Linux kernel-এর kfifo এই কারণেই 2-এর ঘাতের মাপ চায়, আর head/tail-কে কখনো না কমা counter হিসেবে রাখে, যাতে full/empty আলাদা করতে বাড়তি ঘর না লাগে। একজন producer আর একজন consumer হলে এমন ring buffer কোনো lock ছাড়াই নিরাপদে বানানো যায় — head শুধু একজন লেখে, tail শুধু অন্যজন — তবে তার জন্য memory ordering ঠিক রাখতে হয় (C++-এ std::atomic-এর acquire/release).
৭ · Queue কোথায় কাজ করে — BFS থেকে প্রিন্টার
“অপেক্ষা করো, পালা এলে কাজ হবে” — এই ধারণা কম্পিউটারের প্রায় সব স্তরে আছে। কয়েকটি গুরুত্বপূর্ণ উদাহরণ:
🗺️ BFS — স্তরে স্তরে খোঁজা
BFS-এ শুরুর node-কে queue-তে রেখে বারবার সামনেরটি বের করি আর তার নতুন প্রতিবেশীদের পেছনে ঢোকাই। FIFO নিশ্চিত করে যে দূরত্ব ১-এর সবাই দূরত্ব ২-এর আগে দেখা হবে — তাই ওজনহীন graph-এ সবচেয়ে ছোট পথ মেলে। পর্ব ৪০-এ বিস্তারিত।
O(V + E)⏱️ Task scheduling
Operating system round-robin পদ্ধতিতে প্রস্তুত প্রসেসগুলো একটি queue-তে রাখে: সামনের জন একটি ছোট সময় চলে, কাজ শেষ না হলে আবার পেছনে গিয়ে দাঁড়ায়। কেউ চিরকাল বঞ্চিত হয় না।
পালা করে ন্যায্যতা🖨️ Printer queue
অফিসের দশজন একসাথে প্রিন্ট দিলে প্রিন্টার একটিই কাজ করতে পারে। কাজগুলো একটি queue-তে জমা থাকে (spooling), আর আগে পাঠানো ফাইল আগে ছাপা হয় — কেউ নিজের কম্পিউটারে বসে অপেক্ষা করেন না।
ধীর যন্ত্রের সামনে buffer🏭 Producer–consumer buffer
গার্মেন্টস কারখানায় সেলাই লাইনের এক টেবিল শার্ট বানায়, পরের টেবিল বোতাম লাগায় — মাঝে একটি ঝুড়ি। ঝুড়ি ভর্তি হলে প্রথমজন থামেন, খালি হলে দ্বিতীয়জন। সফটওয়্যারে এটিই producer–consumer: কিবোর্ডের buffer, ভিডিও স্ট্রিমিং, ওয়েব সার্ভারের অনুরোধের সারি, bKash-এর মতো সিস্টেমে লেনদেনের message queue.
গতির পার্থক্য সামলানোBFS-এর মূল লুপটি দেখতে কেমন, একটি ছোট আভাস (পুরো ব্যাখ্যা পর্ব ৪০-এ):
// dist[v] = number of edges from s to v (-1 = not reached yet)
queue<int> q;
dist[s] = 0; q.push(s);
while (!q.empty()) {
int u = q.front(); q.pop(); // oldest discovered node first (FIFO)
for (int v : adj[u])
if (dist[v] == -1) { dist[v] = dist[u] + 1; q.push(v); }
}এখানে queue-এর বদলে stack দিলে খোঁজার ক্রম বদলে যায় — এক দিকে গভীরে চলে যায় (DFS-এর মতো, পর্ব ৪১), আর dist আর সবচেয়ে ছোট পথ থাকে না। একটি ডেটা স্ট্রাকচার বদলালেই অ্যালগরিদমের অর্থ বদলে যায় — এটিই DSA-র সৌন্দর্য।
Priority queue — সারিতে যখন “জরুরি” আগে
হাসপাতালের জরুরি বিভাগে কিন্তু FIFO চলে না: হৃদরোগী পরে এলেও আগে দেখা হয়। এমন সারির নাম priority queue — প্রতিটি উপাদানের একটি অগ্রাধিকার থাকে, আর সবসময় সবচেয়ে বেশি অগ্রাধিকারেরটি আগে বের হয়। নাম “queue” হলেও ভেতরের কাঠামো সম্পূর্ণ আলাদা: সাধারণত একটি binary heap, যেখানে ঢোকানো আর বের করা O(log n). C++-এ std::priority_queue, Python-এ heapq. পর্ব ২৩-এ heap তৈরি থেকে শুরু করে পুরোটা দেখব; পর্ব ৪৩-এ Dijkstra-র অ্যালগরিদম এর ওপরেই দাঁড়িয়ে।
৮ · Monotonic deque — sliding window maximum O(n)-এ
এবার deque-এর সবচেয়ে চমকপ্রদ ব্যবহার। সমস্যা: n টি সংখ্যার একটি অ্যারে আর একটি সংখ্যা k. দৈর্ঘ্য k-এর প্রতিটি পরপর অংশের (sliding window) সর্বোচ্চ মান বের করুন। যেমন গত ৭ দিনের প্রতিটি ৭-দিনের জানালায় সর্বোচ্চ তাপমাত্রা, বা শেয়ারবাজারে প্রতিটি ৩০ মিনিটের সর্বোচ্চ দাম।
সরল সমাধান: প্রতিটি জানালায় k টি সংখ্যা দেখা — মোট O(n·k). n = 10⁶ আর k = 10⁵ হলে প্রায় 10¹¹ তুলনা, কয়েক মিনিটের কাজ। আমরা চাই O(n), অর্থাৎ k যত বড়ই হোক।
ধরুন জানালায় একটি পুরোনো সংখ্যা 3, আর তার পরে এলো 6. এখন থেকে 3 আর কখনো কোনো জানালার সর্বোচ্চ হতে পারবে না — কারণ 3 যতদিন জানালায় থাকবে, 6-ও থাকবে (6 পরে এসেছে, তাই পরে বেরোবে), আর 6 বড়। তাই 3-কে এখনই ফেলে দেওয়া যায়। যারা টিকে থাকে, তারা সামনে থেকে পেছনে কমতে থাকা ক্রমে সাজানো — একে বলে monotonic deque.
অ্যালগরিদম, প্রতিটি নতুন index i-এর জন্য তিনটি কাজ (deque-এ মান নয়, index রাখি — যাতে বোঝা যায় কে জানালা থেকে বেরিয়ে গেছে):
- মেয়াদ শেষ: সামনের index যদি ≤ i − k হয়, সেটি জানালার বাইরে —
pop_front. - দুর্বলদের বিদায়: পেছনের index-এর মান যতক্ষণ ≤ a[i],
pop_back— এরা আর কখনো সর্বোচ্চ হবে না। - যোগ: i-কে পেছনে
push_back. জানালা পূর্ণ হলে (i ≥ k − 1) উত্তর = a[সামনের index].
a = [5, 3, 4, 1, 2, 6, 2, 1, 0], k = 3-এর ওপর ধাপে ধাপে দেখুন। ওপরের সারি index, “win” লাইনের = চিহ্ন বর্তমান জানালা, “dq” লাইনের * চিহ্ন deque-এ থাকা index:
| i, a[i] | |
| সামনে থেকে মেয়াদোত্তীর্ণ | |
| পেছন থেকে বাদ | |
| deque (index) | |
| deque (মান) | |
| জানালার সর্বোচ্চ | |
| মোট pop |
- i = 0, a = 5. deque খালি, তাই কাউকে সরানোর নেই; index 0 ঢুকল। জানালায় এখনো ৩টি সংখ্যা হয়নি, তাই কোনো উত্তর নেই।
- i = 1, a = 3. পেছনের মান 5 > 3, তাই 5 থাকে। index 1 পেছনে ঢুকল। deque-এর মান [5, 3] — কমতে থাকা ক্রম ঠিক আছে।
- i = 2, a = 4 — প্রথম উত্তর। পেছনের 3 ≤ 4, তাই index 1 বাদ (3 আর কখনো সর্বোচ্চ হবে না, কারণ 4 তার পরে এসেছে আর বড়)। 5 > 4, থামি। deque [0, 2], মান [5, 4]. জানালা [5, 3, 4] পূর্ণ — সর্বোচ্চ = সামনের মান 5.
- i = 3, a = 1 — মেয়াদ শেষ। নতুন জানালা index 1–3. সামনের index 0 ≤ 3 − 3 = 0, তাই 5 জানালার বাইরে —
pop_front. তারপর 4 > 1, তাই 1 শুধু পেছনে বসে। deque [2, 3], সর্বোচ্চ 4. লক্ষ করুন, 5 সবচেয়ে বড় হলেও বয়সের কারণে বিদায় নিল — তাই index রাখা জরুরি। - i = 4, a = 2. সামনের index 2 এখনো জানালায় (2 > 1). পেছনের 1 ≤ 2, বাদ; 4 > 2, থামি। deque [2, 4], মান [4, 2]. সর্বোচ্চ 4.
- i = 5, a = 6 — দুই দিক থেকেই সরানো। সামনের index 2 ≤ 5 − 3 = 2, তাই 4 মেয়াদোত্তীর্ণ। তারপর পেছনের 2 ≤ 6, বাদ। deque-এ শুধু [5]. সর্বোচ্চ 6. একটি ধাপে ২টি pop হলো — কিন্তু প্রতিটি index জীবনে একবারই বের হয়।
- i = 6, a = 2. 6 > 2, তাই 2 পেছনে বসে। deque [5, 6], মান [6, 2]. সর্বোচ্চ 6. যদি 6 বেরিয়ে যায়, তখন 2-ই হবে পরের দাবিদার — deque তাকে সেজন্যই রাখছে।
- i = 7, a = 1. 2 > 1, শুধু পেছনে যোগ। deque [5, 6, 7], মান [6, 2, 1] — কঠোরভাবে কমছে। সর্বোচ্চ 6.
- i = 8, a = 0. সামনের index 5 ≤ 8 − 3 = 5, তাই 6 বেরিয়ে গেল। পরের দাবিদার 2 এখন সামনে। 1 > 0, তাই 0 পেছনে বসে। deque [6, 7, 8], সর্বোচ্চ 2. উত্তর: [5, 4, 4, 6, 6, 6, 2]. ৯টি index ঢুকেছে, মোট ৬টি pop — কোনো index দুবার ঢোকেনি বা দুবার বেরোয়নি।
শুদ্ধতা। প্রতিটি ধাপের শেষে দুটি invariant সত্য: (ক) deque-এ শুধু বর্তমান জানালার index, বাঁ থেকে ডানে বাড়তে থাকা ক্রমে, আর তাদের মান কঠোরভাবে কমতে থাকা ক্রমে; (খ) জানালার যে index deque-এ নেই, তাকে বাদ দিয়েছে তার পরের কোনো index j, যার মান অন্তত সমান — আর j এখনো জানালায় (j পরে এসেছে, তাই পরে বেরোবে)। (খ) থেকে, জানালার সর্বোচ্চ মানের অন্তত একটি index deque-এ আছে; (ক) থেকে, deque-এর সবচেয়ে বড় মান সামনে। তাই সামনের মানই জানালার সর্বোচ্চ।
খরচ। একটি ধাপে while লুপ অনেকবার ঘুরতে পারে (ধাপ ৬-এ দুবার সরানো হলো), তাই এক ধাপের worst case O(k). কিন্তু পুরো রানে প্রতিটি index ঠিক একবার push হয় আর সর্বোচ্চ একবার pop — মোট deque-অপারেশন ≤ 2n. তাই মোট সময় Θ(n), k-এর ওপর নির্ভর করে না। জায়গা O(k) (deque-এ কখনো k-এর বেশি index থাকে না), আর উত্তরের জন্য n − k + 1. এটি অংশ ৫-এর মতোই amortized যুক্তি।
#include <deque>
#include <iostream>
#include <vector>
using namespace std;
template <class C> void put(const C& c) { // prints like a Python list
cout << '[';
for (bool first = true; auto x : c) { cout << (first ? "" : ", ") << x; first = false; }
cout << ']';
}
void window_max(const vector<int>& a, int k) {
deque<int> dq; // indices; their values decrease front to back
vector<int> res;
int pops = 0;
for (int i = 0; i < (int)a.size(); ++i) {
if (!dq.empty() && dq.front() <= i - k) { dq.pop_front(); ++pops; } // slid out
while (!dq.empty() && a[dq.back()] <= a[i]) { dq.pop_back(); ++pops; }
dq.push_back(i);
if (i >= k - 1) {
res.push_back(a[dq.front()]);
cout << "i=" << i << " window=";
put(vector<int>(a.begin() + i - k + 1, a.begin() + i + 1));
cout << " deque(idx)="; put(dq);
cout << " max=" << a[dq.front()] << '\n';
}
}
cout << "answer: "; put(res); cout << '\n';
cout << "pushes=" << a.size() << " pops=" << pops
<< " (each index enters and leaves at most once)\n";
}
int main() { window_max({5, 3, 4, 1, 2, 6, 2, 1, 0}, 3); }আউটপুট:
i=2 window=[5, 3, 4] deque(idx)=[0, 2] max=5
i=3 window=[3, 4, 1] deque(idx)=[2, 3] max=4
i=4 window=[4, 1, 2] deque(idx)=[2, 4] max=4
i=5 window=[1, 2, 6] deque(idx)=[5] max=6
i=6 window=[2, 6, 2] deque(idx)=[5, 6] max=6
i=7 window=[6, 2, 1] deque(idx)=[5, 6, 7] max=6
i=8 window=[2, 1, 0] deque(idx)=[6, 7, 8] max=2
answer: [5, 4, 4, 6, 6, 6, 2]
pushes=9 pops=6 (each index enters and leaves at most once)
প্রতিটি লাইনের deque(idx) ওপরের ডায়াগ্রামের সাথে মিলিয়ে দেখুন। শেষ লাইন amortized যুক্তির প্রমাণ: ৯টি push, ৬টি pop — মোট 15 ≤ 2n = 18. পেছনের শর্তে <= থাকায় সমান মান এলে পুরোনোটি বাদ যায় (নতুনটি বেশিদিন টিকবে); < লিখলেও উত্তর ঠিক থাকত, শুধু deque একটু বড় হতো। put() শুধু Python-এর মতো করে list ছাপানোর সাহায্যকারী — এতে C++20-এর init-statement সহ range-for ব্যবহার হয়েছে।
from collections import deque
def window_max(a, k):
dq = deque() # indices; their values decrease front to back
res, pops = [], 0
for i, x in enumerate(a):
if dq and dq[0] <= i - k: # front index slid out of the window
dq.popleft(); pops += 1
while dq and a[dq[-1]] <= x: # smaller ones can never be a max again
dq.pop(); pops += 1
dq.append(i)
if i >= k - 1:
res.append(a[dq[0]])
print(f"i={i} window={a[i-k+1:i+1]} deque(idx)={list(dq)} max={a[dq[0]]}")
print(f"answer: {res}")
print(f"pushes={len(a)} pops={pops} (each index enters and leaves at most once)")
window_max([5, 3, 4, 1, 2, 6, 2, 1, 0], 3)আউটপুট C++-এর সাথে হুবহু এক:
i=2 window=[5, 3, 4] deque(idx)=[0, 2] max=5
i=3 window=[3, 4, 1] deque(idx)=[2, 3] max=4
i=4 window=[4, 1, 2] deque(idx)=[2, 4] max=4
i=5 window=[1, 2, 6] deque(idx)=[5] max=6
i=6 window=[2, 6, 2] deque(idx)=[5, 6] max=6
i=7 window=[6, 2, 1] deque(idx)=[5, 6, 7] max=6
i=8 window=[2, 1, 0] deque(idx)=[6, 7, 8] max=2
answer: [5, 4, 4, 6, 6, 6, 2]
pushes=9 pops=6 (each index enters and leaves at most once)
Python-এ dq[-1] হলো পেছনের, dq[0] সামনের উপাদান — দুটিই deque-এ O(1). এখানে list দিয়ে pop(0) করলে প্রতিটি মেয়াদোত্তীর্ণ সরানো O(k) হয়ে পুরো সুবিধা নষ্ট হতো। সর্বনিম্ন চাইলে শুধু তুলনা উল্টে দিন (a[dq[-1]] >= x) — deque তখন বাড়তে থাকা ক্রমে থাকবে।
এই ধাঁচটি অনেক রূপে আসে: sliding window minimum, “প্রতিটি দিনের পরে প্রথম বেশি তাপমাত্রার দিন” (monotonic stack), আর dynamic programming-এ “গত k ঘরের মধ্যে সেরা মান” দরকার হলে প্রতিটি ঘর O(1)-এ। পর্ব ১৬-এ two pointer আর sliding window-এর সাধারণ কৌশল দেখবেন; সেখানেও এই deque বারবার ফিরে আসবে।
৯ · কোন queue কখন — এক নজরে তুলনা
| বাস্তবায়ন | enqueue | dequeue | জায়গা | কখন বেছে নেবেন |
|---|---|---|---|---|
| সরল অ্যারে (front++) | O(1) | O(1), কিন্তু ঘর নষ্ট | মোট enqueue-র সমান | মোট উপাদান আগে থেকে জানা ও কম হলে (যেমন কনটেস্টে BFS-এর জন্য n মাপের অ্যারে) |
সরিয়ে-নেওয়া অ্যারে / list.pop(0) | O(1) | Θ(n) | Θ(n) | প্রায় কখনো না |
| Circular buffer | O(1) worst | O(1) worst | Θ(cap) | সর্বোচ্চ আকার জানা; audio, network, embedded |
| Linked list | O(1) worst | O(1) worst | Θ(n) + pointer | সীমাহীন আকার, node আগে থেকেই তৈরি |
| দুটি stack | O(1) | amortized O(1), worst Θ(n) | Θ(n) | শুধু stack পাওয়া যায় (functional ভাষা, ইন্টারভিউ) |
std::queue / collections.deque | O(1) | O(1) | Θ(n) | প্রতিদিনের কাজে — প্রথম পছন্দ |
পরীক্ষা ও ইন্টারভিউতে:
- Circular queue-তে full আর empty কীভাবে আলাদা করবেন? — আলাদা
sizeরাখুন, অথবা একটি ঘর খালি রেখে(rear + 1) % cap == frontহলে ভর্তি ধরুন। - দুটি stack দিয়ে queue — dequeue-র complexity? — amortized O(1) (প্রতিটি উপাদান সর্বোচ্চ ৪টি stack-অপারেশন), একক dequeue-র worst case O(n).
- Python-এ queue-এর জন্য list কেন নয়? —
pop(0)বাকি সব উপাদান সরায়, Θ(n);collections.deque.popleft()O(1). - Sliding window maximum O(n)-এ কীভাবে? — index-এর monotonic deque: মেয়াদোত্তীর্ণ সামনে থেকে, ছোটগুলো পেছন থেকে বাদ; প্রতিটি index একবার ঢোকে, একবার বের হয়।
- BFS-এ queue-এর বদলে stack দিলে কী হয়? — খোঁজার ক্রম আর স্তরভিত্তিক থাকে না, তাই ওজনহীন গ্রাফে সবচেয়ে ছোট দূরত্বের নিশ্চয়তা হারায়।
১০ · AI-এর যুগে queue আর deque — ছোট ভুল, বড় ফল
“একটি circular queue লিখে দাও” বা “sliding window maximum সমাধান করো” — AI কয়েক সেকেন্ডে পরিপাটি কোড দেয়, আর ছোট উদাহরণে তা প্রায়ই চলে। কিন্তু queue-এর ভুলগুলো থাকে কিনারায়: ঠিক ভর্তি হওয়ার মুহূর্তে, শেষ উপাদান বের হওয়ার মুহূর্তে, wrap-around-এর ঘরে। আর complexity-র দাবিতে — বিশেষ করে Python-এ।
Queue-তে AI সাধারণত যেসব ভুল করে
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
Circular queue-তে শুধু head == tail দিয়ে full ও empty দুটিই যাচাই — ভর্তি queue-কে খালি ভাবে | ঠিক cap টি enqueue করে তারপর isEmpty() ডাকুন |
C++-এ (head - 1) % cap — head = 0 হলে ঋণাত্মক index | push_front-এ head = 0 অবস্থায় চালান; + cap আছে কি না দেখুন |
Python-এ list.pop(0) দিয়ে queue, অথচ “O(1)” দাবি | n = 10⁵-এ সময় মাপুন, n দ্বিগুণ করলে সময় চারগুণ হলে Θ(n²) |
| Linked list queue-তে শেষ node বের হলে tail null না করা | enqueue → dequeue → enqueue → front ক্রমে পরীক্ষা করুন |
| দুটি stack-এ প্রতি dequeue-তে out থেকে in-এ ফেরত ঢালা — প্রতিটি অপারেশন O(n) | অপারেশন গুনতে counter বসান; ২n-এর বেশি হলে নকশা ভুল |
| Monotonic deque-এ index-এর বদলে মান রাখা — সমান মান থাকলে ভুল উপাদান মেয়াদোত্তীর্ণ হয় | [2, 2, 1, 1] ধরনের পুনরাবৃত্ত মানে, k = 2-এ brute force-এর সাথে মেলান |
AI-কে সমাধান লিখতে না বলে বরং আপনার বোঝাপড়া পরীক্ষা করাতে ব্যবহার করুন। নিচের prompt-টি আপনাকেই হাতে trace করতে বাধ্য করে:
Queue-কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১ব্যাংকে একটি লম্বা সারি ভালো, নাকি প্রতিটি কাউন্টারের সামনে আলাদা সারি? এর পেছনে কোন গণিত আছে?
সারি নিয়ে গণিতের জন্ম টেলিফোন থেকে। বিশ শতকের শুরুতে কোপেনহেগেন টেলিফোন কোম্পানির ডেনিশ প্রকৌশলী Agner Krarup Erlang প্রশ্ন করেন: কতগুলো লাইন রাখলে গ্রাহককে বেশি অপেক্ষা করতে হবে না, আবার অযথা লাইনও পড়ে থাকবে না? ১৯০৯ সালে তিনি এ নিয়ে প্রথম প্রবন্ধ প্রকাশ করেন, আর সেখান থেকে জন্ম নেয় queueing theory. টেলিফোন ট্রাফিকের একক আজও তাঁর নামে “erlang”. পরে ১৯৫৩ সালে David Kendall সারির ধরন লেখার একটি সংক্ষিপ্ত চিহ্ন-পদ্ধতি দেন (যেমন M/M/1 — এলোমেলো আগমন, এলোমেলো সেবা-সময়, একজন সেবাদাতা), যা এখনো ব্যবহৃত হয়।
১৯৬১ সালে John Little একটি আশ্চর্য সরল সম্পর্ক প্রমাণ করেন, যা Little's law নামে পরিচিত: L = λW. অর্থাৎ গড়ে সারিতে যত মানুষ (L) = প্রতি মিনিটে গড়ে যতজন আসেন (λ) × একজনের গড় অপেক্ষার সময় (W). ব্যাংকে প্রতি মিনিটে ২ জন এলে আর প্রত্যেকে গড়ে ১৫ মিনিট থাকলে, ভেতরে গড়ে ৩০ জন। সূত্রটি প্রায় যেকোনো স্থিতিশীল ব্যবস্থায় খাটে — সার্ভারের অনুরোধ থেকে কারখানার কাজ পর্যন্ত।
এবার আসল প্রশ্ন। চারটি কাউন্টারের সামনে চারটি আলাদা সারি থাকলে এক সারিতে ধীর একজন গ্রাহক (ধরুন অনেক কাগজপত্র নিয়ে এসেছেন) পুরো সারিকে আটকে দেন, অথচ পাশের কাউন্টার হয়তো খালি। একটি সাধারণ সারি থেকে যে কাউন্টার খালি হয় সেখানে পাঠালে (যেমন অনেক ব্যাংকে টোকেন ব্যবস্থা) কোনো কাউন্টার অলস বসে থাকে না যখন কেউ অপেক্ষায় আছেন। Queueing theory দেখায় যে একই মোট সেবা-ক্ষমতায় এই “এক সারি, অনেক সেবাদাতা” ব্যবস্থায় গড় অপেক্ষা সাধারণত কম, আর অপেক্ষার অনিশ্চয়তাও কম। সুপারশপে অবশ্য আলাদা সারিই বেশি দেখা যায়, কারণ এক লম্বা সারি দেখতে ভয়ের, আর জায়গাও লাগে বেশি — মানুষের মনস্তত্ত্বও নকশার অংশ।
সফটওয়্যারেও একই সিদ্ধান্ত নিতে হয়: একাধিক worker thread-এর জন্য একটি সাধারণ কাজের queue, নাকি প্রত্যেকের নিজের queue? নিজের queue-তে lock নিয়ে কাড়াকাড়ি কম, কিন্তু কেউ ফাঁকা বসে থাকতে পারে — তাই অনেক ব্যবস্থায় ফাঁকা worker অন্যের queue থেকে কাজ “চুরি” করে (work stealing).
মূল উপলব্ধি: Queue শুধু একটি ডেটা স্ট্রাকচার নয়, অপেক্ষার একটি গাণিতিক মডেল। কতগুলো সারি, কতজন সেবাদাতা, কোন নিয়মে পরের জন — এই নকশা-সিদ্ধান্তগুলোই ঠিক করে ব্যবস্থাটি কতটা দ্রুত ও ন্যায্য।
প্র ০২Python-এর list এত জনপ্রিয়, তবু queue-র জন্য আলাদা deque কেন লাগল? আর thread-এর জন্য queue.Queue কেন?
CPython-এ list হলো object-এর পয়েন্টারের একটি dynamic array. শেষে যোগ করা বা শেষ থেকে সরানো সস্তা, কারণ বাকি কাউকে নড়তে হয় না। কিন্তু সামনে থেকে সরাতে হলে বাকি সব পয়েন্টারকে এক ঘর সরাতে হয়। এই সরানো C-তে memmove দিয়ে খুব দ্রুত হয়, তাই কয়েক হাজার উপাদানে টের পাওয়া যায় না — আর এটিই ফাঁদ। ছোট পরীক্ষায় সব ঠিক লাগে, কিন্তু কয়েক লাখ উপাদানের BFS-এ সময় দ্বিঘাত হারে বাড়ে।
এই সমস্যার সমাধান হিসেবে Python 2.4-এ (২০০৪ সালে) collections module-এ deque যোগ হয়। এর নকশা লক্ষ করার মতো: এটি ছোট ছোট নির্দিষ্ট মাপের ব্লকের (CPython-এ প্রতি ব্লকে ৬৪টি ঘর) একটি doubly linked list. একক উপাদানের linked list হলে প্রতিটি উপাদানের জন্য দুটি পয়েন্টারের বাড়তি জায়গা আর আলাদা memory allocation লাগত; ব্লক করায় সেই খরচ ৬৪ ভাগে ভাগ হয়ে যায়, আর একটি ব্লকের ভেতরের উপাদানগুলো পাশাপাশি থাকায় cache-এরও সুবিধা পাওয়া যায়। বিনিময়ে মাঝখানের d[i] O(n) — প্রান্ত থেকে ব্লক ধরে ধরে হাঁটতে হয়। C++-এর std::deque-ও ব্লক ব্যবহার করে, কিন্তু ব্লকের ঠিকানার একটি অ্যারে (map) রাখে বলে সেখানে d[i] O(1). একই নাম, ভেতরে ভিন্ন trade-off.
Python-এর documentation বলে যে deque-এর দুই প্রান্তের append আর pop thread-safe. তবু একাধিক thread-এর producer–consumer কাজের জন্য আছে আলাদা queue.Queue. কারণ সেখানে শুধু নিরাপদ যোগ-বিয়োগ যথেষ্ট নয় — consumer-কে queue খালি থাকলে অপেক্ষা করতে হয় (get() blocking), producer-কে নির্দিষ্ট মাপের queue ভর্তি থাকলে অপেক্ষা করতে হয় (maxsize), আর কাজ শেষের হিসাব রাখতে task_done() ও join() লাগে। ভেতরে এটি একটি deque-কে lock আর condition variable দিয়ে ঘিরে রাখে। asyncio-র জন্য আবার আছে asyncio.Queue.
অর্থাৎ Python-এ “queue” বলতে অন্তত তিনটি জিনিস: একক thread-এর দ্রুত সারি (deque), thread-এর মধ্যে সমন্বয়ের সারি (queue.Queue), আর অগ্রাধিকারের সারি (heapq, বা queue.PriorityQueue).
মূল উপলব্ধি: একটি ADT-র পেছনে বাস্তবায়নের বিকল্প অনেক, আর প্রতিটি আলাদা প্রশ্নের উত্তর — কোন অপারেশন সস্তা হবে, কোনটি দামি, আর একসাথে কতজন ব্যবহার করবে। লাইব্রেরির ডকুমেন্টেশনে প্রতিটি অপারেশনের complexity দেখে নেওয়ার অভ্যাস এই কারণেই জরুরি।
প্র ০৩Ring buffer কি শুধু পাঠ্যবইয়ের অনুশীলন, নাকি আধুনিক কম্পিউটারের গভীরে এটি সবখানে?
আপনার কম্পিউটার যখন ইন্টারনেট থেকে একটি প্যাকেট পায়, network card সেটি সরাসরি memory-তে রাখে একটি ring-এর পরের ঘরে — যাকে বলে receive ring বা descriptor ring. Card হলো producer, operating system-এর driver হলো consumer; দুজন head আর tail দিয়ে নিজেদের অবস্থান জানায়। পাঠানোর দিকেও একই রকম transmit ring. Ring ভর্তি হয়ে গেলে নতুন প্যাকেট ফেলে দিতে হয় — উচ্চ গতির সার্ভারে “ring-এর মাপ কত” তাই একটি পরিচিত tuning প্রশ্ন।
শব্দও ring buffer দিয়ে চলে। ভিডিও কল বা গান বাজানোর সময় প্রোগ্রাম অডিও নমুনা একটি buffer-এ লেখে, আর sound card নির্দিষ্ট তালে সেখান থেকে পড়ে। প্রোগ্রাম কোনো কারণে পিছিয়ে পড়লে buffer খালি হয়ে যায় (underrun) — আপনি শোনেন “কট” শব্দ বা বিরতি। Buffer বড় রাখলে এই ঝুঁকি কমে, কিন্তু দেরি (latency) বাড়ে; তাই ভিডিও কলে ছোট buffer, গান শোনায় বড় buffer — একই কাঠামো, ভিন্ন trade-off.
Linux kernel-এ kfifo নামে একটি সাধারণ ring buffer আছে, যার মাপ 2-এর ঘাত হতে হয়, যাতে modulo-র বদলে bitmask দিয়ে index বের করা যায়। ২০১৯ সালে Linux 5.1-এ Jens Axboe-এর তৈরি io_uring আসে — এতে প্রোগ্রাম আর kernel দুটি ring buffer ভাগাভাগি করে: একটিতে প্রোগ্রাম কাজের অনুরোধ রাখে (submission queue), অন্যটিতে kernel ফলাফল রাখে (completion queue). প্রতিটি অনুরোধে আলাদা system call-এর খরচ এড়ানোই এর মূল লক্ষ্য। আর্থিক খাতে লন্ডনের ট্রেডিং প্রতিষ্ঠান LMAX তাদের Disruptor লাইব্রেরি open source করে, যার কেন্দ্রেও একটি ring buffer — উদ্দেশ্য ছিল thread-এর মধ্যে বার্তা পাঠানোয় lock-এর খরচ এড়িয়ে খুব কম দেরিতে লেনদেন সামলানো।
কেন সর্বত্র ring buffer? চারটি কারণ: (১) memory একবারই বরাদ্দ হয়, চলার সময় কোনো allocation নেই — real-time কাজে এটি জরুরি; (২) উপাদান পাশাপাশি, তাই cache-বান্ধব; (৩) একজন producer আর একজন consumer হলে lock ছাড়াই নিরাপদে চালানো যায়, কারণ প্রত্যেকে কেবল নিজের index লেখে; (৪) নির্দিষ্ট মাপ নিজেই একটি নিরাপত্তা — কোনো উৎস অসীম গতিতে ডেটা পাঠালেও memory ফুরিয়ে যায় না, বড়জোর পুরোনো বা নতুন ডেটা ফেলা হয়।
মূল উপলব্ধি: এই পর্বের চার ঘরের ছোট ring আর একটি network card-এর হাজার ঘরের ring একই গণিতে চলে: head, tail, modulo আর full/empty-র সিদ্ধান্ত। ছোট উদাহরণে নিখুঁতভাবে বোঝা মানেই বড় সিস্টেম বোঝার প্রথম ধাপ।
প্র ০৪“Amortized O(1)” কি “O(1)”-এর মতোই ভালো? কোথায় এই পার্থক্য সত্যিই গুরুত্বপূর্ণ?
Amortized analysis-কে একটি আনুষ্ঠানিক পদ্ধতি হিসেবে প্রতিষ্ঠা করেন Robert Tarjan, ১৯৮৫ সালের “Amortized Computational Complexity” প্রবন্ধে। ধারণাটি হলো: একটি একক অপারেশনের খরচ নয়, যেকোনো ক্রমের অপারেশনের মোট খরচকে ভাগ করে দেখা। দুটি stack-এর queue-তে m টি অপারেশনে মোট খরচ ≤ 4m — এটি গ্যারান্টি, কোনো সম্ভাবনা বা “সাধারণত” নয়। তাই বেশিরভাগ ক্ষেত্রে amortized O(1) বাস্তবে O(1)-এর মতোই ভালো, যেমন vector::push_back.
কিন্তু তিনটি জায়গায় পার্থক্যটি কামড় দেয়। প্রথমত, real-time ব্যবস্থা। গাড়ির ব্রেক নিয়ন্ত্রণ, পেসমেকার বা লাইভ অডিওতে প্রতিটি কাজ একটি নির্দিষ্ট সময়ের মধ্যে শেষ হতে হবে। গড় ভালো হলেও একবার Θ(n)-এর ঝাঁকুনি এলে সেই মুহূর্তের সময়সীমা পেরিয়ে যেতে পারে। সেখানে worst-case O(1)-এর circular buffer পছন্দ করা হয়।
দ্বিতীয়ত, persistent বা functional ব্যবহার। Haskell বা OCaml-এর মতো ভাষায় ডেটা স্ট্রাকচার সাধারণত বদলানো হয় না — প্রতিটি অপারেশন নতুন সংস্করণ দেয়, আর পুরোনো সংস্করণও ব্যবহার করা যায়। ধরুন একটি queue-এর এমন সংস্করণ আছে, যার পরের dequeue-তে বড় একটি ঢালা লাগবে। কেউ যদি সেই একই পুরোনো সংস্করণে বারবার dequeue করেন, প্রতিবার পুরো ঢালার খরচ দিতে হয় — জমানো “টাকা” একবারই খরচ করা যায়, বহুবার নয়। Chris Okasaki তাঁর বিখ্যাত বই Purely Functional Data Structures-এ (১৯৯৮) এই সমস্যা বিশ্লেষণ করেন, আর lazy evaluation দিয়ে এমন queue নকশা দেখান, যাতে persistent ব্যবহারেও amortized সীমা টেকে। তারও আগে ১৯৮১ সালে Hood আর Melville দেখান, ঢালার কাজটি ছোট ছোট টুকরোয় আগেভাগে করে রাখলে কয়েকটি list দিয়েও worst-case O(1) queue সম্ভব — জটিল, কিন্তু সম্ভব।
তৃতীয়ত, শত্রুভাবাপন্ন ইনপুট। Amortized সীমা প্রতিটি ক্রমেই টেকে, তাই আক্রমণকারী এটি ভাঙতে পারে না — কিন্তু তিনি এমন সময়ে দামি অপারেশনটি ঘটাতে পারেন, যখন সিস্টেম সবচেয়ে ব্যস্ত। অনলাইন সেবায় তাই শুধু গড় নয়, সবচেয়ে ধীর ১% অনুরোধের সময়ও (tail latency) মাপা হয়।
মূল উপলব্ধি: Amortized O(1) একটি শক্ত গ্যারান্টি, কিন্তু মোট খরচের — একক অপারেশনের নয়। “প্রতিটি কাজ সময়মতো” দরকার হলে worst case দেখুন; “পুরো কাজ দ্রুত” দরকার হলে amortized যথেষ্ট। পর্ব ৬০-এ এই বিশ্লেষণের তিনটি পদ্ধতিই প্রমাণসহ দেখবেন।
প্র ০৫Operating system কি সত্যিই একটি সাধারণ queue দিয়ে ঠিক করে কোন প্রোগ্রাম কখন চলবে? প্রিন্টারের queue কোথা থেকে এলো?
শুরুটা সত্যিই সাধারণ queue দিয়ে। Time-sharing ব্যবস্থার শুরুর দিকে, ১৯৬০-এর দশকে, অনেক ব্যবহারকারী একটি কম্পিউটার ভাগাভাগি করতেন। সবচেয়ে সরল ন্যায্য নিয়ম হলো round-robin: প্রস্তুত প্রসেসগুলো একটি FIFO queue-তে, সামনের জন একটি নির্দিষ্ট সময় (time slice বা quantum, সাধারণত কয়েক মিলিসেকেন্ড) চলে, শেষ না হলে পেছনে ফিরে যায়। n টি প্রসেস থাকলে প্রত্যেকে প্রতি n টি slice-এ একবার সুযোগ পায় — কেউ চিরকাল বঞ্চিত (starvation) হয় না। Slice খুব ছোট হলে প্রসেস বদলানোর (context switch) খরচে সময় নষ্ট হয়, খুব বড় হলে প্রোগ্রাম ধীরে সাড়া দেয় — আবার সেই trade-off.
কিন্তু সব কাজ সমান জরুরি নয়। আপনি টাইপ করছেন — কিবোর্ডের সাড়া এখনই চাই; পেছনে একটি বড় ফাইল কপি হচ্ছে — সেটি একটু পরে হলেও চলে। তাই বাস্তব scheduler একাধিক অগ্রাধিকারের queue রাখে, বা সরাসরি priority queue-এর মতো কাঠামো ব্যবহার করে। Linux 2.6.23-এ (২০০৭) আসা Completely Fair Scheduler (CFS) প্রস্তুত প্রসেসগুলোকে একটি red-black tree-তে রাখত, যাতে সবচেয়ে কম CPU সময় পাওয়া প্রসেসটি দ্রুত বের করা যায় — অর্থাৎ queue-এর ধারণা থেকে priority queue-তে উত্তরণ (red-black tree পর্ব ২৯-এ, heap পর্ব ২৩-এ)। ২০২৩ সালে Linux 6.6-এ এর জায়গায় EEVDF নামের নতুন scheduler আসে, তবে মূল প্রশ্ন একই থাকে: “পরের পালা কার?” Priority-র একটি বিপদ হলো নিম্ন অগ্রাধিকারের কাজ অনন্তকাল অপেক্ষায় থাকতে পারে; সমাধান হিসেবে অপেক্ষা যত বাড়ে, অগ্রাধিকারও তত বাড়ানো হয় (aging).
প্রিন্টারের queue-এর ইতিহাস আরও পুরোনো চাহিদা থেকে। কম্পিউটার যত দ্রুত, প্রিন্টার আর কার্ড-রিডার তত ধীর। প্রোগ্রামকে প্রিন্টারের জন্য বসিয়ে রাখা মানে দামি CPU-র অপচয়। তাই আউটপুট আগে দ্রুত ডিস্কে জমা রাখা হতো, আর আলাদা একটি প্রোগ্রাম সেগুলো একে একে প্রিন্টারে পাঠাত — এটিই spooling. শব্দটির প্রচলিত ব্যাখ্যা “Simultaneous Peripheral Operations On-Line”, যদিও সুতার “spool”-এ পেঁচিয়ে রাখার ছবিটিও এর সাথে মেলে। আজও আপনার কম্পিউটারের “print queue” জানালাটি সেই একই ধারণা: একটি FIFO, যেখান থেকে চাইলে কোনো কাজ বাতিলও করা যায়।
মূল উপলব্ধি: সাধারণ FIFO queue ন্যায্যতা দেয়, কিন্তু অগ্রাধিকার বোঝে না; priority queue অগ্রাধিকার বোঝে, কিন্তু বঞ্চনার ঝুঁকি আনে। বাস্তব সিস্টেম দুটির মিশ্রণ — আর কোন কাঠামো বেছে নেওয়া হবে, তা নির্ভর করে “ন্যায্য” বলতে আমরা কী চাই তার ওপর।
অনুশীলন
খাতা-কলম নিন। প্রথম তিনটি হাতে trace করার, শেষটিতে কোড লিখতে হবে।
-
হাতে trace করুন: cap = 5-এর একটি circular queue (আলাদা
sizeসহ, শুরুতে head = tail = size = 0)-এ এই অপারেশনগুলো চালান: enqueue 1, 2, 3, 4; dequeue তিনবার; enqueue 5, 6, 7. শেষে buf-এর পাঁচটি ঘরে কী আছে (কোন ঘর খালি), head, tail, size কত? আর কতটি enqueue আর সফল হবে?enqueue 1–4: ঘর ০–৩-এ 1, 2, 3, 4; head = 0, tail = 4, size = 4. তিনটি dequeue বের করে 1, 2, 3 — head = 3, size = 1. enqueue 5 বসে ঘর ৪-এ, tail = (4 + 1) % 5 = 0 (wrap-around). enqueue 6 ঘর ০-এ, tail = 1; enqueue 7 ঘর ১-এ, tail = 2.
শেষ অবস্থা: buf = [6, 7, খালি, 4, 5], head = 3, tail = 2, size = 4. সারির ক্রম head থেকে: 4 → 5 → 6 → 7. মাত্র আর একটি enqueue সফল হবে (ঘর ২-এ), তারপর size = 5 = cap, আর head = tail = 3. “একটি ঘর খালি রাখা” কৌশল হলে এখনই queue ভর্তি ধরা হতো, কারণ (tail + 1) % 5 = 3 = head.
-
খরচ গুনুন: দুটি stack দিয়ে বানানো queue-তে এই ক্রম চালান: enqueue a, enqueue b, dequeue, enqueue c, enqueue d, dequeue, dequeue, dequeue. প্রতিটি dequeue কী ফেরত দেয়, প্রতিটি ধাপের পরে in আর out-এ কী থাকে, আর মোট কতটি stack-অপারেশন (push + pop) লাগে?
enqueue a, b → in = [a, b] (২টি অপারেশন). প্রথম dequeue: out খালি, তাই ঢালা — b, a pop করে out-এ push (৪টি), out = [b, a], ওপরে a; pop করে a (১টি). মোট এ পর্যন্ত ৭. enqueue c, d → in = [c, d] (মোট ৯). দ্বিতীয় dequeue: out = [b] খালি নয় — pop করে b (মোট ১০).
তৃতীয় dequeue: out খালি — d, c ঢালা (৪টি), out = [d, c], pop করে c (মোট ১৫). চতুর্থ: pop করে d (মোট ১৬). ক্রম a, b, c, d — FIFO ঠিক। ৮টি queue-অপারেশনে ১৬টি stack-অপারেশন, ঠিক ৪টি উপাদান × ৪. লক্ষ করুন, দ্বিতীয় dequeue-র সময় in-এ c, d থাকলেও ঢালা হয়নি — out খালি ছিল না বলে। ঢাললে c, d উঠে বসত b-এর ওপরে, আর পরের dequeue ভুল করে c দিত।
-
Sliding window minimum trace করুন: a = [4, 2, 12, 3, 8, 7, 1], k = 3. Monotonic deque-এ (সামনে থেকে পেছনে বাড়তে থাকা মান) প্রতিটি i-এর পরে deque-এ কোন index থাকে, আর প্রতিটি জানালার সর্বনিম্ন কত?
নিয়ম: সামনের index ≤ i − k হলে বাদ; পেছনের মান ≥ a[i] যতক্ষণ, বাদ; তারপর i যোগ।
i = 0 (4): [0]. i = 1 (2): 4 ≥ 2, বাদ → [1]. i = 2 (12): [1, 2] → সর্বনিম্ন a[1] = 2. i = 3 (3): সামনের 1 > 0, থাকে; 12 ≥ 3, বাদ → [1, 3] → 2. i = 4 (8): সামনের 1 ≤ 1, মেয়াদোত্তীর্ণ → [3]; 3 < 8 → [3, 4] → 3. i = 5 (7): 8 ≥ 7, বাদ → [3, 5] → 3. i = 6 (1): সামনের 3 ≤ 3, মেয়াদোত্তীর্ণ → [5]; 7 ≥ 1, বাদ → [6] → 1.
উত্তর: [2, 2, 3, 3, 1]. যাচাই: জানালাগুলো [4, 2, 12], [2, 12, 3], [12, 3, 8], [3, 8, 7], [8, 7, 1] — সর্বনিম্ন মেলে। মোট ৭টি push আর ৬টি pop, ২n = ১৪-এর মধ্যে।
-
কোড লিখুন: নির্দিষ্ট মাপের একটি circular deque class লিখুন (C++ বা Python), যাতে
push_front,push_back,pop_front,pop_back,front,backসবই O(1). ভর্তি অবস্থায় push করলেfalseফেরত দেবে। কোন লাইনে ঋণাত্মক index-এর বিপদ আছে?মূল চিন্তা: অংশ ৩-এর মতো head, size রাখুন (tail = (head + size) % cap হিসাব করে নেওয়া যায়). সামনে ঢোকাতে head এক ঘর পেছনে সরে — সেখানেই ঋণাত্মক index-এর বিপদ।
class CircularDeque { vector<int> buf; int head = 0, sz = 0; int cap() const { return (int)buf.size(); } public: explicit CircularDeque(int c) : buf(c) {} bool push_front(int x) { if (sz == cap()) return false; head = (head - 1 + cap()) % cap(); // "+ cap()" keeps it non-negative buf[head] = x; ++sz; return true; } bool push_back(int x) { if (sz == cap()) return false; buf[(head + sz) % cap()] = x; ++sz; return true; } void pop_front() { head = (head + 1) % cap(); --sz; } // caller checks sz > 0 void pop_back() { --sz; } // tail moves back by itself int front() const { return buf[head]; } int back() const { return buf[(head + sz - 1) % cap()]; } };সব অপারেশন কয়েকটি গণনা মাত্র, তাই worst case O(1), জায়গা Θ(cap).
head - 1লাইনে+ cap()না দিলে head = 0 অবস্থায় C++-এ ফল −1 — অ্যারের বাইরে লেখা, undefined behavior. Python-এ(head - 1) % capনিজেই ঠিক থাকে, তবে পরিষ্কারভাবে লেখার অভ্যাস রাখুন। পরীক্ষা করুন: cap = 3-এ push_front 1, push_front 2, push_back 3 → সামনে থেকে ক্রম 2, 1, 3; আর চতুর্থ pushfalse.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমSliding window maximum-এ নতুন index i এলে monotonic deque-এ কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- সামনের index যদি i − k বা তার কম হয়, সেটি সামনে থেকে বাদ দেওয়া
- পেছনের index-এর মান a[i]-এর সমান বা ছোট হলে, বারবার পেছন থেকে বাদ দেওয়া
- i-কে deque-এর পেছনে যোগ করা
- জানালা পূর্ণ হলে (i ≥ k − 1) সামনের index-এর মানকে ওই জানালার সর্বোচ্চ হিসেবে লেখা
একটি খালি queue-তে ক্রমে enqueue 1, enqueue 2, enqueue 3, dequeue, enqueue 4, dequeue করা হলো। এখন front-এ কোন মান?
- 1
- 2
- 3
- 4
cap = 8-এর একটি circular buffer-এ head = 6 আর size = 5. পরের enqueue কোন index-এ বসবে (tail = (head + size) % cap)?
- 11
- 3
- 5
- 2
একটি circular buffer-এ শুধু head আর tail রাখা হয়, আলাদা size নেই; full আর empty আলাদা করতে “একটি ঘর সবসময় খালি” নিয়ম মানা হয়। 8 ঘরের অ্যারেতে সর্বোচ্চ কতটি উপাদান রাখা যাবে?
- 8
- 4
- 6
- 7
দুটি stack (in আর out) দিয়ে queue বানানো হলো: enqueue মানে in-এ push; dequeue-তে out খালি থাকলে in-এর সব উপাদান out-এ ঢেলে তারপর out থেকে pop. n উপাদানের queue-তে dequeue-র খরচ সম্পর্কে কোনটি সঠিক?
- Amortized O(1), কিন্তু একটি একক dequeue-র worst case O(n)
- প্রতিটি dequeue worst case O(1)
- প্রতিটি dequeue গড়ে O(n)
- Amortized O(log n)
Python-এ n উপাদানের একটি সারি থেকে সামনের উপাদান বের করার কোন উপায় O(1)?
- list.pop(0)
- collections.deque.popleft()
- del a[0] (a একটি list)
- a = a[1:] (a একটি list)
n দৈর্ঘ্যের অ্যারেতে দৈর্ঘ্য k-এর প্রতিটি sliding window-এর সর্বোচ্চ মান index-এর monotonic deque দিয়ে বের করলে মোট সময় কত?
- O(n·k)
- O(n log k)
- O(n)
- O(k)
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Queue = FIFO: পেছনে enqueue, সামনে dequeue, front দেখা — তিনটিই O(1) হওয়া উচিত; খালি queue-তে dequeue নয়।
- Circular buffer:
(i + 1) % cap; head == tail মানে খালি অথবা ভর্তি —sizeরাখুন বা একটি ঘর ফাঁকা রাখুন। - Linked list queue: head-এ বের, tail-এ যোগ; শেষ node বের হলে tail-ও null.
- দুটি stack: out খালি হলেই কেবল ঢালা; প্রতিটি উপাদান ≤ ৪ অপারেশন → amortized O(1), একক worst Θ(n).
- Python-এ queue মানে
deque, কখনোlist.pop(0)নয়; monotonic deque-এ index রেখে sliding window maximum Θ(n)-এ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৫ · C++ STL আর Python-এর কালেকশন — তৈরি ডেটা স্ট্রাকচারপরবর্তী পর্বvector, deque, set, map, heapq, dict — কোনটির দাম কত।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- C++ প্রোগ্রামিং · Sequence containerভাষার দিকstd::deque, std::list আর container adapter-এর ব্যবহারিক দিক।
- Java প্রোগ্রামিং · Queue ও PriorityQueueপাশের ট্র্যাকJava-র ArrayDeque, Queue interface আর PriorityQueue.
- Python প্রোগ্রামিং · Listপাশের ট্র্যাকPython list-এর ভেতরের চেহারা — কেন শেষে যোগ সস্তা, সামনে দামি।