Stack, Queue, Deque — discipline থেকে শক্তি
Stacks, Queues and Deques
Stack, Queue আর Deque নতুন storage না — এরা access-discipline: কোন প্রান্তে ঢোকা-বেরোনা ঠিক করলেই recursion, scheduling, BFS/DFS আর undo-র পুরো machinery abstract level-এ ওঠে।
আগে এটা বুঝি
আজকের তিনটা structure নতুন memory layout শেখায় না — গত দুই লেসনের
array আর linked node দিয়েই সবগুলো বানানো যায়। নতুন জিনিসটা contract:
কোন প্রান্তে ঢোকা যাবে, কোন প্রান্তে বেরোনো যাবে। এই restriction-ই
এদের শক্তি — যখন আপনি কোডে stack বা queue নাম দেখেন, আপনি ওই
মুহূর্তেই পুরো behavior জানেন, implementation না দেখেই।
পৃথিবীটা এদের দিয়ে ভরা: প্রতিটা function call আপনার হয়ে call stack- এ push হচ্ছে (operating-systems module-এর stack-and-heap লেসন মনে আছে?), প্রতিটা Ctrl+Z undo stack pop করছে, BFS queue দিয়ে graph ঘুরছে, OS scheduler runqueue দিয়ে process বেছে নিচ্ছে। Discipline শেখা মানে এসব system-এর shared grammar শেখা।
আজকের প্রশ্ন তিনটা: (১) LIFO/FIFO contract ঠিক কী promise করে, আর কী করে না? (২) Array-র কোন প্রান্ত দিয়ে ঢোকানো-বেরোনো করলে implementation সস্তা হয় — আর deque সেই trade-off কীভাবে ভাঙে? (৩) একটা plain stack দিয়ে “পরের বড় element” জাতীয় মনে-হয়-O(n²) সমস্যা কীভাবে linear হয়ে যায়?
মূল ধারণা
তিনটা contract
| Structure | Insert | Remove | Inspect | Contract |
|---|---|---|---|---|
| Stack | push (top) | pop (top) | peek/top | LIFO — last in, first out |
| Queue | enqueue (back) | dequeue (front) | front | FIFO — first in, first out |
| Deque | add front/back | remove front/back | both ends | দুই প্রান্তেই full access |
খেয়াল করুন কী নেই contract-এ: middle insertion, search by value, random index। এগুলো deliberately বাদ — interface ছোট মানে invariant শক্ত, reasoning সহজ। Stack-এ কোনো element “k-th from bottom” চাওয়া contract-violation; ওই দাবিটা করতে চাইলে আপনি আসলে অন্য structure চাইছেন।
Implementation — প্রান্ত-নির্বাচনের গণিত
Array দিয়ে stack: top-কে tail রাখলে push/pop দুই-ই slot write — O(1), কোনো shift নেই। Perfect fit।
Queue-টা tricky: দুইটা প্রান্ত দরকার (front-এ বেরোনো, back-এ ঢোকা)। Array-র head (index 0) দিয়ে dequeue করলে প্রতিবার সব shift — O(n)! তাই options:
option A: two-index sliding window option B: circular buffer
[ f . . . . b ] [ b . f . . . . . ]
dequeue -> f++ dequeue -> f++
enqueue -> b++ enqueue -> b++ (wrap!)
সমস্যা: window ডানে সরতে থাকে, head/tail modulo capacity —
বাঁ দিকের জায়গা মরে যায় সব slot recycle হয় ✓Python list.pop(0) option-A-র সবচেয়ে খারাপ রূপ (shift included);
collections.deque হলো block-linked structure — দুই প্রান্তেই O(1),
গত লেসনে দেখা hybrid। Circular buffer fixed-capacity world-এর
উত্তর — build-এ বানাব।
কোথায় কে বাস করে
- Call stack — function call = frame push, return = pop। Recursion আসলে stack discipline-এর syntax sugar; stack overflow মানে এই structure-টাই capacity-র বাইরে।
- Undo — প্রতিটা edit push; Ctrl+Z pop। Redo হলো দ্বিতীয় stack — undo-pop করা item সেখানে push।
- BFS worklist — queue: আবিষ্কৃত-order-এ process, shortest-path guarantee (unweighted)।
- Scheduler runqueue — round-robin মানে circular queue: চলা process back-এ re-enqueue।
- Browser history — back stack + forward stack, navigation-এ দুই stack-এর মধ্যে transfer।
ভেতরে কী ঘটছে
Deque-এর ভেতরে — blocked compromise
CPython-এর collections.deque: ৬৪-slot contiguous block-গুলোর একটা
doubly-linked chain। Layout:
deque [a,b,c,...,z]
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ blk0: 64 ptr │◀──▶│ blk1: 64 ptr │◀──▶│ blk2: 64 ptr │
└──────────────┘ └──────────────┘ └──────────────┘
▲ ▲
leftmost rightmostappendleft মানে blk0-এ ফাঁকা slot থাকলে এক write — নতুন block allocate শুধু ভরা হলে (amortized again!)। popleft symmetric। Indexing i = block-chain walk + offset — ধীর কিন্তু সম্ভব। Design lesson: pure linked (per-node overhead) আর pure array (end-op shift) — দুই চরমের খরচ block-size parameter দিয়ে tune করা যায়। Block size ১ হলে linked list, অসীম হলে single array — deque হলো dial-এর মাঝের ঘর।
Monotonic stack — pattern-টা নিজে
সমস্যা: array-র প্রতিটা element-এর ডানে প্রথম বড় element কোনটা?
Naive: প্রতি element-এর জন্য ডানে scan — O(n²)। Monotonic stack: elements-কে stack-এ রাখা হয় strictly decreasing অবস্থায়। নতুন element x এলে stack-এর top থেকে সব ছোট element pop করে দাও — প্রতিটা pop হওয়া element-এর answer হলো x। তারপর x push।
xs = [3, 1, 4, 1, 5]
x=3: stack [3]
x=1: 1 ছোট 3-এর, push → [3,1]
x=4: pop 1 (answer 4), pop 3 (answer 4), push → [4]
x=1: push → [4,1]
x=5: pop 1 (ans 5), pop 4 (ans 5), push → [5]
answers: 3→4, 1→4, 4→5, 1→5, 5→Noneকেন linear? প্রতিটা element ঠিক একবার push + ঠিক একবার pop — গত লেসনের multipop-stack aggregate argument হুবহু: total ops ≤ 2n। Amortized thinking আবার ফিরে এল — এবার algorithm design-এর tool হয়ে।
- Method dispatchC-implemented method — Python-level loop নেই, interpreter tax একটাই
- Block checkleftmost block-এ element আছে কি না — lenfield decrement, কোনো shift নেই
- Slot readblock-এর current-leftmost pointer read + advance — contiguous block, cache-friendly
- Block retireblock খালি হলে chain থেকে unlink — free list-এ; এটাই মাঝে মাঝের দামি মুহূর্ত
উদাহরণ
Balanced parentheses — stack-এর ছোট্ট কিন্তু পূর্ণ গল্প
{[()()]} valid? ([)] invalid? Rule: প্রতিটা opener-এর matching closer
ঠিক উল্টো order-এ আসে — LIFO-র হাতের লেখা definition!
def balanced(s):
pairs = {')': '(', ']': '[', '}': '{'}
openers = set(pairs.values())
stack = []
for ch in s:
if ch in openers:
stack.append(ch)
elif ch in pairs:
if not stack or stack.pop() != pairs[ch]:
return False
return not stack
# balanced('{[()()]}') -> True
# balanced('([)]') -> False (pop '(' পায় ']' এর জন্য '[' চেয়েছিল)Trace ([)]: push (, push [, ] আসে — top ( ≠ [ → fail।
Stack discipline mismatch-কে ধরে ফেলে ঠিক যেখানে হয়। Compiler,
linter, JSON parser — সবার ভেতরে এই ছোট function-এর বংশধর।
ছবিতে
তিন discipline, এক ছবি
STACK (LIFO): QUEUE (FIFO): DEQUE:
push ↓ enq ↓ ↑ deq ↓↑ ↓↑
┌─────────┐ ┌─────────────┐ ┌────────────┐
│ ▓ new │ │ ▓ oldest │ │ ▓ ▓ │
│ ▒ │ │ ░ │ │ ▒ ▒ │
│ ░ old │ │ ░ │ │ ░ ░ │
└─────────┘ └─────────────┘ └────────────┘
same end opposite ends both endscapacity=8 circular buffer, কয়েক step পর:
idx: 0 1 2 3 4 5 6 7
[ C ][ D ][ E ][ _ ][ _ ][ X ][ Y ][ Z ]
▲tail ▲head
dequeue -> X (head: 5→6)
enqueue F -> idx3 (tail: 3→4) ← wraparound! 7-এর পরে 0-তে না,
এখানে ফাঁকা 3-এ
full condition: (tail+1) mod cap == head
empty condition: tail == headনিজে চালিয়ে দেখুন
popleft বনাম pop(0) — FIFO discipline-এর দুই দাম
# fifo_lab.py — same contract, different invoices
# চালান: python3 fifo_lab.py
import time
from collections import deque
N = 100_000
def bench(fn):
t0 = time.perf_counter()
fn()
return time.perf_counter() - t0
def drain_list():
q = list(range(N))
while q:
q.pop(0) # প্রতিবার বাকিদের shift — O(size)
def drain_deque():
q = deque(range(N))
while q:
q.popleft() # প্রান্ত-পয়েন্টার advance — O(1)
t_list = bench(drain_list)
t_dq = bench(drain_deque)
print(f'draining {N:,} elements FIFO-style:')
print(f' list.pop(0) : {t_list*1000:9.1f} ms')
print(f' dq.popleft() : {t_dq*1000:9.1f} ms')
print(f' ratio : {t_list/t_dq:9.1f}x')
# scaling proof: doubling-এ deque flat, list quadratic-ish
def drain_list_at(n):
q = list(range(n))
while q:
q.pop(0)
def drain_deque_at(n):
q = deque(range(n))
while q:
q.popleft()
print('\ndoubling check:')
for n in (25_000, 50_000, 100_000):
td = bench(lambda: drain_deque_at(n))
tl = bench(lambda: drain_list_at(n))
print(f' n={n:>6,}: list {tl*1000:8.1f} ms | deque {td*1000:6.1f} ms '
f'| ratio {tl/max(td, 1e-9):6.0f}x')python3 fifo_lab.pyTypical ফলাফল:
draining 100,000 elements FIFO-style:
list.pop(0) : 812.4 ms
dq.popleft() : 4.1 ms
ratio : 198.1x
doubling check:
n= 25,000: list 51.2 ms | deque 1.0 ms | ratio 51x
n= 50,000: list 203.8 ms | deque 2.0 ms | ratio 102x
n=100,000: list 812.4 ms | deque 4.1 ms | ratio 198xReading: list side প্রতি doubling-এ ×4 — quadratic signature (n বার pop, প্রতি pop গড়ে n/2 shift); deque side ×2 — linear total, per-op constant। Ratio-ও প্রতি doubling-এ ×2 — গত লেসনের ratio-of-ratios method আবার কাজে। যা প্রমাণিত: “queue ব্যবহার করেছি” বলাই যথেষ্ট না — implementation-টা আপনার algorithm-এর complexity class নিজেই বদলে দিতে পারে।
একই FIFO discipline-এর দুই implementation-এ per-operation cost রাতদিন: deque.popleft constant থাকে n বাড়লেও, list.pop(0) linearly বাড়ে — implementation choice discipline-এর সঙ্গে যুক্ত না হলে asymptotic class-ই বদলে যায়।
নিজে বানান
Queue + Stack + CircularBuffer + monotonic solver — চার-এক kit
- Array-backed Queue লিখুন head-index sliding approach-এ — dequeue-এ shift নেই, শুধু head++
- Wasted-space দেখুন — head reset/compaction কখন লাগে তা মাপুন
- CircularBuffer লিখুন fixed capacity, modulo arithmetic, full/empty conditions আলাদা করে
- MonotonicStack-based next_greater(xs) লিখুন — প্রতিটা element push+pop exactly once
- তিনটা structure-কে একই workload দিয়ে cross-benchmark করুন
লক্ষ্য: discipline-গুলো নিজের হাতে concrete করা, আর monotonic pattern- এর amortized claim runtime counter দিয়ে verify করা।
# discipline_kit.py — Queue, CircularBuffer, monotonic stack
# চালান: python3 discipline_kit.py
class ArrayQueue:
"""Sliding-head queue — dequeue O(1), কিন্তু space ফেলে যায়।"""
def __init__(self):
self.buf = []
self.head = 0
def enqueue(self, v):
self.buf.append(v)
def dequeue(self):
if self.head >= len(self.buf):
raise IndexError('empty queue')
v = self.buf[self.head]
self.head += 1
return v
def __len__(self):
return len(self.buf) - self.head
class CircularBuffer:
"""Fixed-capacity queue — zero allocation after init."""
def __init__(self, capacity):
self.cap = capacity
self.buf = [None] * capacity
self.head = 0
self.count = 0
def enqueue(self, v):
if self.count == self.cap:
raise IndexError('buffer full')
self.buf[(self.head + self.count) % self.cap] = v
self.count += 1
def dequeue(self):
if self.count == 0:
raise IndexError('buffer empty')
v = self.buf[self.head]
self.head = (self.head + 1) % self.cap
self.count -= 1
return v
def __len__(self):
return self.count
def next_greater(xs):
"""Monotonic stack — returns answers[i] = first greater to the right."""
answers = [None] * len(xs)
stack = [] # indices; values strictly decreasing
pushes = pops = 0
for i, x in enumerate(xs):
while stack and x > xs[stack[-1]]:
answers[stack.pop()] = x
pops += 1
stack.append(i)
pushes += 1
print(f' internal ledger: pushes={pushes} pops={pops} '
f'total={pushes + pops} (bound: 2n = {2 * len(xs)})')
return answers
if __name__ == '__main__':
print('next-greater-element:')
xs = [3, 1, 4, 1, 5, 9, 2, 6]
print(f' input : {xs}')
print(f' answer: {next_greater(xs)}')
import random, time
big = [random.randint(0, 10**6) for _ in range(200_000)]
t0 = time.perf_counter()
next_greater(big)
dt = time.perf_counter() - t0
print(f' 200k elements: {dt*1000:.1f} ms — linear confirmed')
print('circular buffer wraparound:')
cb = CircularBuffer(capacity=4)
for v in (10, 20, 30, 40):
cb.enqueue(v)
print(' dequeued:', cb.dequeue(), cb.dequeue())
cb.enqueue(50); cb.enqueue(60)
drained = []
while len(cb):
drained.append(cb.dequeue())
print(' after wrap:', drained, '(order preserved)')প্রত্যাশিত output:
next-greater-element:
input : [3, 1, 4, 1, 5, 9, 2, 6]
answer: [4, 4, 5, 5, 9, None, 6, None]
internal ledger: pushes=8 pops=6 total=14 (bound: 2n = 16)
200k elements: 118.3 ms — linear confirmed
circular buffer wraparound:
dequeued: 10 20
after wrap: [30, 40, 50, 60] (order preserved)Ledger line-টাই আসল প্রমাণ: total operations কখনো 2n পার করে না — amortized O(1) per element, measurement আর theory একই সংখ্যায় মিলছে।
নিজে বাড়ান:
- ArrayQueue-তে compaction যোগ করুন — head অর্ধেক buffer পার হলে slice-shift; এটা amortized analysis-এর আরেক বাস্তব কেস
- CircularBuffer-এ overwrite-on-full mode যোগ করুন (telemetry ring buffer semantics) — পুরনো data silently ফেলে দেওয়া
- Sliding-window maximum বানান — deque দিয়ে (indices decreasing monotonic রেখে) — monotonic pattern-এর sibling
বাস্তব সিস্টেমে
Discipline-গুলো যেখানে পৃথিবী চালায়
Call stack আর exception unwinding। প্রতিটা language runtime-এর function-call machinery stack-disciplined — OS module-এর stack-and-heap লেসনের সরাসরি ধারাবাহিকতা। Exception ছোঁড়া মানে frames pop হয়ে যাওয়া until handler — LIFO-ই cleanup ordering নির্ধারণ করে (finally blocks, destructors)।
Producer-consumer queues। Message broker (Kafka partition log, RabbitMQ queue), thread-pool task queue, Go channels — FIFO discipline fairness-এর সবচেয়ে সস্তা approximation। Scheduler starvation-এর বিরুদ্ধে round-robin = circular queue + time slice।
BFS আর level-order। Graph shortest path (unweighted), web crawler frontier, tree serialization — queue discipline-ই “nearest first” guarantee দেয়। Worklist abstraction ছাড়া graph algorithms-এর অর্ধেক লিখা কঠিন।
Undo/redo আর command patterns। Editor, design tools, database transaction logs — প্রতিটা reversible operation system-এ দুই stack- এর নাচ। Command object + stack = time travel।
Rate limiters আর sliding windows। Fixed-memory circular buffer timestamp-দের — “শেষ এক মিনিটে কত request” — O(1) update, O(window) query না করেই। Ring buffer-এর সবচেয়ে practical modern use।
যে ভুলগুলো সবাই করে
“Stack আর queue আলাদা data structure — নতুন layout শিখতে হবে।”
নতুন layout কিছুই না — দুটোই array বা linked nodes-এর উপর thin contract। নতুনটা হলো discipline: কোন প্রান্ত খোলা। এজন্যই একই underlying buffer উপর আলাদা API wrapper আলাদা structure “হয়ে” যায়। Interface সরু হওয়াটা দুর্বলতা না — এটাই property: implementation swap করা যায় (array → deque → linked) caller-এর অজান্তে, যতক্ষণ contract মানা হয়।
“Python list-ই universal — queue দরকার হলে append+pop(0) চলবে।”
Contract মিলে, invoice মেলে না। pop(0) প্রতি কলে সব shift — গোটা algorithm-এর complexity class বদলে যায় (আজকের experiment-এ ~২০০×)। Production rule: FIFO লাগলে deque — standard library-র এই একটা habit সবচেয়ে সস্তা performance win। একই কারণে threading.Queue ভেতরে deque-ই ব্যবহার করে।
“Monotonic stack একটা advanced competitive-programming trick — production-এ লাগে না।”
Trick না — এটা amortized argument-এর সরাসরি ফল, আর production ভরা: stock span analysis, temperature/histogram processing, compiler register-allocation heuristics, UI layout (largest rectangle in histogram — rendering dirty-region computation), rate-limit windowing। Pattern- টা চেনার skill: “প্রতিটা element-এর জন্য nearest greater/smaller” — এই phrase শুনলেই monotonic stack মনে করুন।
“Circular buffer-এর full আর empty condition একই হয়ে যায় — তাই design flawed।”
head == tail সত্যিই দুই অর্থে পড়া যায় — classic ambiguity। তিনটা standard resolution আছে, সবই production-proven: (১) এক slot সবসময় খালি রাখা (full: (tail+1)%cap == head); (২) count field রাখা (আমাদের build এটা করে); (৩) monotonic timestamp/sequence number রাখা। Flawed design না — documented convention-এর প্রশ্ন; kernel kfifo count-free variant ব্যবহার করে power-of-two mask দিয়ে।
বুঝেছেন কি না দেখুন
1DFS-এ stack আর BFS-এ queue — কোড প্রায় এক, ফল রাতদিন। ঠিক কোন একটা
সিদ্ধান্তের পার্থক্য এটা তৈরি করে?
স্মরণ
Worklist-এর remove প্রান্ত: DFS same-end (stack top) — সদ্য-আবিষ্কৃত path-এর গভীরে আগে ডোবে; BFS opposite-end (queue front) — আবিষ্কারের ক্রমেই process হয়, মানে distance-order। Push logic identical; extraction side-এর choice-ই exploration semantics নির্ধারণ করে। Discipline-এর শক্তির সবচেয়ে ছোট উদাহরণ।
2Input stream: a b c d — stack-এ push করে মাঝে মাঝে pop করে কোন কোন
permutation output হতে পারে? d c b a আর a b c d দুটোই কি possible?
প্রয়োগ
d c b a possible: চারটা push, তারপর চারটা pop। a b c d-ও possible: push a, pop a, push b, pop b… প্রতিটার পরেই pop। যে permutations অসম্ভব: যেখানে বড় element pop হয়ে গেছে কিন্তু তার চেয়ে ছোট কোনো আগের element এখনো stack-এ আটকে উল্টো order চাইছে — classic example: c আগে, তারপর a, তারপর b (a,b উল্টে আটকে থাকে)। Stack-sortable permutations-এর এই সেট Catalan number দিয়ে গোনা যায়।
3ArrayQueue-তে (sliding head) ১০ লক্ষ enqueue, তারপর ৫ লক্ষ dequeue,
তারপর আবার ১০ লক্ষ enqueue — memory-তে কী ঘটছে? কোন খরচ কোথায় লুকিয়ে?
যুক্তি
Dequeue গুলো শুধু head++ — কিন্তু প্রথম ৫ লক্ষ slot মৃত জঞ্জাল হয়ে থেকে যায়, buffer-এর logical size বাড়তেই থাকে। দ্বিতীয় enqueue phase-এ append পিছনেই হয় — জঞ্জাল কখনো reclaim হয় না। Peak memory ≈ সব-mortal-ই সব-enqueues-এর যোগফল, live items মাত্র ১০ লক্ষ। Fix: compaction (periodic slice) — কিন্তু সেটা O(live) pause; অথবা circular buffer — জঞ্জাল-ই recycle। Space leak এখানে discipline violation না, implementation-এর হিসাবের ভুল।
4Telemetry system: sensor প্রতি ms-এ reading পাঠায়, analyzer মাঝে
মাঝে “শেষ ৫ সেকেন্ডের stats” চায়। Fixed RAM budget। কোন structure,
capacity কত, কোন conditions?
ডিজাইন
Circular buffer, capacity ৫,০০০ (৫ সেকেন্ড × ১০০০/ms)। Sensor enqueue; buffer full হলে overwrite mode — পুরনোটা নিজে থেকেই বেরিয়ে যায় (head advance), কোনো error না — telemetry-তে stale data-র মূল্য শূন্য। Analyzer query = head থেকে count পর্যন্ত sequential walk — ৫,০০০ slot, cache-friendly, microsecond-scale। Memory: ঠিক ৫,০০০ × record size, lifetime-এ একটাও allocation না — deterministic budget। Producer consumer speed mismatch হলে overwrite policy document করুন — silent data loss-এর চেয়ে explicit policy।
5Monotonic stack-এ প্রতিটা element ঠিক কতবার push আর কতবার pop হয়?
Total operations-এর bound কত, আর এটা কোন প্রমাণ-কৌশলের পুনর্ব্যবহার?
স্মরণ
Push ঠিক একবার (প্রতি element), pop-ও ঠিক একবার (একবার popped হলে আর ফেরে না)। Total ≤ 2n → প্রতি element amortized O(1)। কৌশল: গত-গত লেসনের multipop-stack aggregate method হুবহু — দামি-মনে- হওয়া inner while loop-এর মোট কাজ আসলে মোট push-দ্বারা bounded। Double-counting এড়ানোই পুরো প্রমাণ।
এরপর কী
এরপর কী
এতক্ষণের structures সব identity-বান্ধব: array-র index, node-এর position — ক্রমই সব। পরের লেসনে ক্রমকে পুরোপুরি ছেঁড়া দেব: hash table — key থেকে সরাসরি bucket-এ লাফ, O(1) average lookup-এর জাদু। কিন্তু pigeonhole principle বলে collision অনিবার্য — তাহলে O(1) কীভাবে সম্ভব? Chaining বনাম open addressing, load factor, resizing, clustering — আর শেষে adversarial keys দিয়ে hash DoS attack, যেখানে “average case O(1)”- এর distribution assumption প্রথমবার প্রকাশ্যে ভাঙব। আগের পাঁচ লেসনের সব হাতিয়ার — amortized analysis, locality accounting, adversarial thinking — এক লেসনে মিলব।
পরের লেসন: Hash Table — O(1)-এর জাদু আর তার দাম।
আরও পড়ুন
- Introduction to Algorithms (3rd edition), Chapter 10.1 — Stacks and Queues — Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein · Array-ভিত্তিক stack/queue implementation-এর canonical treatment
- The Art of Computer Programming, Volume 1, Section 2.2 — Linear Lists — Donald E. Knuth · Stack/queue/deque-এর classical taxonomy — pushdown list terminology-র উৎস
- Algorithms (4th edition), Section 1.3 — Bags, Queues, and Stacks — Robert Sedgewick, Kevin Wayne · API design আর generic implementation-এর পাঠ্যপুস্তক মডেল