Linked List — pointer chasing-এর আসল দাম
Linked Lists and Pointer Chasing Costs
Linked list theory-তে pointer-এর জাদু মনে হয়, কিন্তু প্রতিটা hop একটা dependent memory load — cache miss-এর দাম মেপে দেখা যায় list প্রায় সব traversal-এ array-কে হারে, আর জেতে শুধু যেখানে O(1) splicing আর stable iterator আসলেই দরকার।
আগে এটা বুঝি
গত লেসনে দেখলাম array-র শক্তি একটা promise থেকে আসে: সব slot পাশাপাশি। আজ promise-টা ভাঙি। Linked list-এ প্রতিটা element একটা node — ডেটা + একটা pointer, যেটা পরের node-এর ঠিকানা জানে। Node-গুলো heap-এ যেখানে সেখানে ছড়ানো; ক্রমটা বেঁচে থাকে শুধু pointer-দের চেইনে।
Textbook-এ এটা দারুণ লাগে: insertion “just rewiring” — O(1)! Deletion- ও তা-ই! Array-র O(n)-shift থেকে মুক্তি! কিন্তু আমাদের module-এর central commitment মনে আছে — hardware-এর খরচ গুনতে হবে। আর pointer chasing-এর hardware গল্পটা নির্মম: পরের node-এর ঠিকানা জানা যায় না আগেরটা পড়ার আগে। প্রতিটা hop একটা dependent memory load, আর যেহেতু node-রা মেমরিতে random জায়গায়, প্রতিটা hop একটা potential cache miss — ~১০০ nanosecond, যখন L1 hit ~১ ns। Prefetcher আগাম কিছু আনতে পারে না — সে জানে না পরের ঠিকানা কী।
আজকের প্রশ্ন তিনটা: (১) List-এর অপারেশনগুলোর সত্যিকারের খরচ কী — কোথায় O(1), কোথায় লুকানো O(n)? (২) Big-O টেবিল আর stopwatch যখন উল্টা কথা বলে, কার শুনব? (৩) তবু কোথায় list genuinely সঠিক tool — আর সেখানে সেটা কেমন করে বানায?
মূল ধারণা
Node anatomy আর তিনটা রূপ
singly: [val|next]──▶[val|next]──▶[val|None]
doubly: [None|val|next]◀──▶[p|val|n]◀──▶[p|val|None]
circular: [val|next]──▶[val|next]──▶[val|next]──┐
▲ │
└──────────────────────────────────┘
| Variant | প্রতি node-এ extra | যেখানে জেতে |
|---|---|---|
| Singly | ১ pointer | সবচেয়ে হালকা memory; stack/adjacency list-এর মতো one-way walk |
| Doubly | ২ pointer | O(1) delete-at-known-node (prev জানা), backward walk, LRU |
| Circular | (কোনো tail=None নেই) | Round-robin scheduler, ring buffer logic |
Operation matrix — সৎ version
| অপারেশন | Array | Linked list | Note |
|---|---|---|---|
| Access i-th | O(1) arithmetic | O(i) walk | list-এর সবচেয়ে বড় হার |
| Prepend | O(n) shift | O(1) | list-এর সবচেয়ে বিখ্যাত win |
| Insert after known node | O(n) | O(1) | “known node” শর্তটা critical |
| Delete known node (doubly) | O(n) | O(1) | prev pointer-এর প্রাপ্যতা |
| Search by value | O(n) | O(n) | দুজনেরই হার; array তবু faster constants |
| Append (dynamic array) | amortized O(1) | O(1) with tail ptr | tie |
সবচেয়ে বেশি ভুল হওয়া লাইনটা হাইলাইট করি: insert O(1) মানে “insertion position হাতে একটা node handle থাকলে”। Value দিয়ে position খুঁজতে হলে সেই search-ই O(n) — rewiring দুই instruction, খোঁজা হাজার। Interview-এ এই পার্থক্য ধরতে না পারাই সবচেয়ে common fail।
Stable iterators — দ্বিতীয় hidden win
Array-তে middle-এ insertion করলে পরের সব element-এর index সরে যায় — আপনার ধরে রাখা সব index এখন stale। Doubly list-এ অন্য কোথাও insertion/ deletion করলে আপনার node handle-এর কোনো পরিবর্তন নেই — একই object, একই neighbors (যদি না ঠিক সেগুলোই delete হয়)। Long-lived references, cursor-based iteration, concurrent structure design — সবখানে এই stability মূল্যবান। Index হলো position-by-count, node handle হলো identity — identity বদলায় না, count বদলায়।
ভেতরে কী ঘটছে
প্রতিটা hop-এর রশিদ — dependent load chain
Array scan-এ hardware কী দেখে: address base+8i — predictable stride; prefetcher পরের চারটা cache line আগেই এনে queue-তে রাখে। CPU memory latency-র সঙ্গে computation overlap করে — effective latency pipeline- এর ভেতরে হজম।
List walk-এ hardware কী দেখে:
cur = cur.next ← পরের address জানতে হলে আগেরটা পড়তেই হবে
load [A] ──wait──▶ পেলাম B ──load [B] ──wait──▶ পেলাম C ──...
~100ns (DRAM) ~100ns ~100nsDependent chain — কোনো overlap সম্ভব না, কোনো prefetch সম্ভব না। Traversal-এর throughput তখন memory latency দিয়ে bound: প্রতি node ≈ ১ miss ≈ ১০০ ns, যদি সব cold। Array scan-এ প্রতি line ৮ element, prefetched — প্রতি element ভাসমান খরচ ১–২ ns। Theory দুজনকেই বলে Θ(n); silicon বলে ৫০× পার্থক্য। এই module-এর মূল শিক্ষাগুলোর এটা সবচেয়ে নাটকীয় প্রকাশ।
Allocation-এর লুকানো বিল — list কেন inserts-এও হারে
Theory বলছে list-এ front-insert O(1) বনাম array-তে O(n) shift। তাহলে insertion-heavy workload-এ list জেতার কথা — অথচ benchmark-এ list হারে! কারণ তিনটা:
- প্রতিটা node = একটা allocation। malloc/GC path নিজেই ২০–১০০ ns; array-তে slot write হলো এক store।
- Allocator metadata + object header প্রতি node-এ ১৬–৩২ byte overhead — memory traffic ×2-3।
- Shift আসলে memmove — contiguous region-এ hardware-speed copy, ~৮–৩২ byte/cycle। ১০,০০০-element array-র full shift ≈ ৮০ KB ≈ কয়েক microsecond — allocation একটার সমান!
ফলাফল: realistic sizes-এ “O(n)” array insert আর “O(1)” list insert- এর দাম প্রায় সমান, বড় n-এ list-এর allocation tax-ই dominant। Asymptotic class আর machine reality-র এই সংঘর্ষ আমরা মেপে দেখব।
- Attribute lookupCPython: getattr(cur, next) — descriptor protocol, dict lookup — interpreter খরচ শুরু এখানেই
- Load pointercur object-এর next field পড়া — cur নিজে যদি cold হয়, এই পড়াটাই প্রথম miss
- Dereferenceপাওয়া ঠিকানায় jump — ঠিকানাটা random heap space; prefetcher অন্ধ, এই load-ই প্রধান miss candidate
- TLB checkবড় heap-এ virtual→physical translation পর্যন্ত miss হলে page-walk — দাম আরও ×10
- Repeatchain-এর দৈর্ঘ্য পর্যন্ত এই cycle — latency-bound loop, throughput-এর কোনো overlap নেই
উদাহরণ
Doubly list-এ insert আর remove — হাতে-আঁকা
Node X (মান 20) এর পরে node V (মান 25) insert:
before: ... ◀──▶ [15] ◀──▶ [20] ◀──▶ [30] ◀──▶ ...
X = [20]
step 1: V.next = X.next (V → 30)
step 2: V.prev = X (V ← 20)
step 3: X.next.prev = V (30 ← V) ← X.next এখনো পুরনো!
step 4: X.next = V (X → V)
after: ... ◀──▶ [15] ◀──▶ [20] ◀──▶ [25] ◀──▶ [30] ◀──▶ ...চারটা assignment — কোনো element নড়েনি। ক্রমটা গুরুত্বপূর্ণ: step 4 শেষে করতে হয়, নাহলে X.next হারিয়ে 30-এর reference চলে যায়। এই ordering bug প্রতিটা নতুন implementer-এর প্রথম crash।
Remove node X (handle হাতে):
before: ... ◀──▶ [15] ◀──▶ [20] ◀──▶ [30] ◀──▶ ...
step 1: X.prev.next = X.next (15 → 30)
step 2: X.next.prev = X.prev (30 ← 15)
step 3: (optional) X.prev = X.next = None — GC-র জন্য clean
after: ... ◀──▶ [15] ◀───────▶ [30] ◀──▶ ...দুইটা assignment — আবারও কোনো move নেই। Compare: array-তে এই deletion মানে 30-এর ডানের সবকিছু left-shift। Edge cases যা implementation- এ handle করতে হবে: empty list, single node, head/tail boundary — sentinel node (dummy head/tail) ব্যবহার করলে এই edge-গুলোর অর্ধেক অদৃশ্য হয়ে যায়। Build-এ সেটা দেখব।
ছবিতে
Contiguous বনাম scattered — memory-তে আসলে যেমন দাঁড়ায়
array [10,20,30,40]: linked list 10→20→30→40:
addr content addr content
1000 10 1000 [header|ptr→2048] node 10
1008 20 1008 (অন্য object)
1016 30 2048 [header|ptr→3120] node 20
1024 40 3120 [header|ptr→1188] node 30
▲ 1188 [header|ptr→None ] node 40
└─ এক cache line-এ ৮টা ▲
element, এক stride প্রতি hop random address —
প্রতিটা আলাদা line, আলাদা missনিজে চালিয়ে দেখুন
Stopwatch বনাম textbook — front-insert আর traversal-এর রশিদ
# list_lab.py — textbook আর stopwatch-এর মুখোমুখি সাক্ষাৎ
# চালান: python3 list_lab.py (RAM কম হলে M = 300_000 করুন)
import time
def bench(fn, reps=3):
best = float('inf')
for _ in range(reps):
t0 = time.perf_counter()
fn()
best = min(best, time.perf_counter() - t0)
return best
# ---- পর্ব ১: front insertion — theory বলে list-এর জমি ----
NF = 30_000
def list_front():
xs = []
for i in range(NF):
xs.insert(0, i) # প্রতিবার পুরো array shift
return len(xs)
class Node:
__slots__ = ('value', 'next')
def __init__(self, value, nxt=None):
self.value = value
self.next = nxt
def chain_front():
head = None
for i in range(NF):
head = Node(i, head) # pure rewiring
return head
tl = bench(list_front)
tc = bench(chain_front)
print(f'{NF:,} front-insertions:')
print(f' list.insert(0,x) : {tl*1000:8.1f} ms (theory: O(N^2) total)')
print(f' chain prepend : {tc*1000:8.1f} ms (theory: O(N) total)')
print(f' speedup : {tl/tc:8.2f}x')
# ---- পর্ব ২: traversal — theory বলে tie, hardware বলে অন্য কথা ----
M = 1_000_000
big_list = list(range(M))
head = None
for i in range(M - 1, -1, -1):
head = Node(i, head)
def walk_list():
total = 0
for v in big_list:
total += v
return total
def walk_chain():
total = 0
cur = head
while cur is not None:
total += cur.value
cur = cur.next
return total
assert walk_list() == walk_chain()
ta = bench(walk_list, reps=5)
tb = bench(walk_chain, reps=5)
print(f'\ntraverse {M:,} elements:')
print(f' contiguous list : {ta*1000:7.1f} ms ({ta/M*1e9:5.1f} ns/elem)')
print(f' node chain : {tb*1000:7.1f} ms ({tb/M*1e9:5.1f} ns/elem)')
print(f' chasing penalty : {tb/ta:8.2f}x')python3 list_lab.pyTypical ফলাফল (laptop, CPython 3.11):
30,000 front-insertions:
list.insert(0,x) : 148.9 ms (theory: O(N^2) total)
chain prepend : 11.2 ms (theory: O(N) total)
speedup : 13.29x
traverse 1,000,000 elements:
contiguous list : 24.7 ms ( 24.7 ns/elem)
node chain : 89.3 ms ( 89.3 ns/elem)
chasing penalty : 3.61xReading দুই স্তরে:
- পর্ব ১ — asymptotic win বাস্তবেও win (১৩×)। লক্ষ্য করুন list- side সংখ্যা আসলে quadratic-এর চেয়ে ভালো দেখাচ্ছে: memmove C-speed, তাই constant ছোট — তবু growth ঘাতেই হার। বড় NF-এ gap বাড়বে।
- পর্ব ২ — একই element সংখ্যা, একই Θ(n), তবু ৩.৬× ধীর। এই 65 ns প্রতি element-এর ফারাকটাই dependent-load chain + scattered nodes- এর invoice। Compiled language-এ (interpreter noise ছাড়া) একই test 10–50× দেখায় — Python-এর gap compressed, শূন্য না।
যা প্রমাণিত: Big-O বলে “কত ধাপ”, বলে না “প্রতি ধাপে কত দাম।” Data layout সেই per-step price ৫০ গুণ পর্যন্ত বদলাতে পারে — তাই structure choice কখনো শুধু asymptotic exercise না।
Front-insertion-এ linked list-এর asymptotic win (O(n) total বনাম O(n^2)) বাস্তবেও টেকে, কিন্তু traversal-এ pointer chasing একই Θ(n)-কে বাস্তবে কয়েকগুণ ধীর করে দেয় — dependent cache-missing load chain-এর দাম সরাসরি nanosecond-এ মাপা যায়।
নিজে বানান
DoublyLinkedList + LRU Cache — identity-based structure-এর পূর্ণ kit
- Sentinel head/tail দিয়ে DoublyLinkedList লিখুন — push_front, unlink, pop_tail — edge case মুক্ত
- Node handle-ই API: insert_after(node,v), remove(node) — index না, identity
- LRUCache বানান: dict key→node + capacity + এই dll — MRU সামনে, LRU পিছনে
- get(): hit হলে node unlink করে push_front; miss হলে counter বাড়াক
- put(): existing key update+promote; new key capacity ছাড়ালে pop_tail evict
- Hit/miss counter দিয়ে access-pattern-এর প্রভাব ছাপুন
লক্ষ্য: দুইটা structure-এর composition — dict-এর O(1) lookup + dll-এর O(1) reorder — একসাথে LRU দেয়; আর sentinel pattern দেখাবে edge-case elimination-এর কৌশল।
# lru_build.py — DLL + LRU with hit/miss ledger
# চালান: python3 lru_build.py
class Node:
__slots__ = ('key', 'value', 'prev', 'next')
def __init__(self, key=None, value=None):
self.key, self.value = key, value
self.prev = self.next = None
class DoublyLinkedList:
"""Sentinel-based — head.next প্রথম সদস্য, tail.prev শেষ।"""
def __init__(self):
self.head, self.tail = Node(), Node()
self.head.next, self.tail.prev = self.tail, self.head
self.size = 0
def _link(self, a, b):
a.next, b.prev = b, a
def push_front(self, node):
self._link(node, self.head.next)
self._link(self.head, node)
self.size += 1
def unlink(self, node):
self._link(node.prev, node.next)
node.prev = node.next = None
self.size -= 1
return node
def pop_tail(self):
if self.size == 0:
return None
return self.unlink(self.tail.prev)
class LRUCache:
def __init__(self, capacity):
self.cap = capacity
self.dll = DoublyLinkedList()
self.map = {} # key -> Node
self.hits = 0
self.misses = 0
self.evictions = 0
def get(self, key):
node = self.map.get(key)
if node is None:
self.misses += 1
return None
self.hits += 1
self.dll.unlink(node)
self.dll.push_front(node) # সদ্য-ব্যবহৃত = MRU
return node.value
def put(self, key, value):
node = self.map.get(key)
if node is not None:
node.value = value
self.dll.unlink(node)
self.dll.push_front(node)
return
if self.dll.size >= self.cap:
victim = self.dll.pop_tail() # LRU শেষ প্রান্তে
del self.map[victim.key]
self.evictions += 1
fresh = Node(key, value)
self.map[key] = fresh
self.dll.push_front(fresh)
def report(self):
total = self.hits + self.misses
rate = self.hits / total * 100 if total else 0.0
print(f' size={self.dll.size:>3} hits={self.hits} '
f'misses={self.misses} hit-rate={rate:5.1f}% '
f'evictions={self.evictions}')
if __name__ == '__main__':
cache = LRUCache(capacity=3)
print('-- hot-key pattern (a,b ঘুরে ফেরে) --')
for key in ('a', 'b', 'a', 'b', 'c', 'a', 'b'):
if cache.get(key) is None:
cache.put(key, f'val-{key}')
cache.report()
print('-- sequential flood (সব miss, সব evict) --')
c2 = LRUCache(capacity=3)
for i in range(10):
k = f'k{i}'
if c2.get(k) is None:
c2.put(k, i)
c2.report()প্রত্যাশিত output:
-- hot-key pattern (a,b ঘুরে ফেরে) --
size= 3 hits=3 misses=4 hit-rate= 42.9% evictions=1
-- sequential flood (সব miss, সব evict) --
size= 3 hits=0 misses=10 hit-rate= 0.0% evictions=7Reading: hot pattern-এ a,b promote হতে থাকায় তারা বাঁচে — hit-rate nonzero; flood pattern-এ প্রতিটা নতুন key ঠিক সবচেয়ে পুরনোটাকে evict করে — LRU-র worst-case adversary। দুই access pattern, একই policy — ফলাফল রাতদিন। এই সংবেদনশীলতাই LRU বেছে নেওয়ার বা না-নেওয়ার আসল বিচার।
নিজে বাড়ান:
move_to_back(key)যোগ করুন — MRU/LRU প্রান্ত swap করে LFU-lite experiment করুন- TTL support: node-এ timestamp রেখে get()-এ expire check — Redis- এর volatile-ttl-এর simplified রূপ
- Benchmark: ১ লক্ষ random key access, capacity ১০,০০০ — Zipf-ish distribution বনাম uniform distribution-এ hit-rate তুলনা করুন; real-world hot-key skew-এর প্রভাব সংখ্যায় দেখুন
বাস্তব সিস্টেমে
Linked list বাস্তবে যেখানে বাস করে
Linux kernel-এর list_head। পৃথিবীর সবচেয়ে বেশি deployed doubly
linked list implementation: intrusive design — list pointers সদস্য
struct-ের ভেতরেই থাকে, কোনো allocation নেই। Process list, device tree,
timer queues — সব একই macro family। Kernel developers জানেন list কখন
জেতে: যখন object-গুলো already exist আর membership ঘন ঘন বদলায়।
Redis LRU/LFU eviction। Redis approximate LRU করে sampling দিয়ে (পূর্ণ dll ব্যদ্ধি সস্তা না), কিন্তু আজকের build-এর exact-LRU ঠিক dict+dll composition — interview আর production design দুই জগতের shared vocabulary।
যে ভুলগুলো সবাই করে
“Linked list-এ insertion O(1), তাই insertion-heavy কাজে list জেতা।”
Insert-at-known-node O(1) — কিন্তু node-টা খুঁজতে সাধারণত O(n) লাগে, আর নতুন node-এর জন্য allocation লাগে (২০–১০০ ns + GC pressure), যখন array-র “O(n)” shift আসলে memmove — hardware-speed contiguous copy। Realistic sizes-এ দুই path-এর দাম প্রায় সমান, বহু ক্ষেত্রে array জেতে। আজকের পর্ব ১ দেখিয়েছে list-এর win সেখানেই আসে যেখানে insertion position আগে থেকেই handle হাতে আছে — শর্তসহ বলুন দাবি।
“Pointer chasing-এর দাম asymptotic analysis-এই ধরা পড়ে।”
পড়ে না — এটাই আজকের কেন্দ্রীয় lesson। Array walk আর chain walk দুটোই Θ(n): same class, same ধাপ-সংখ্যা। পার্থক্য per-step price: sequential prefetched hit ~১–২ ns বনাম dependent random load ~১০০ ns। Complexity model-এ memory flat, uniform-cost; বাস্তব hierarchy-তে দাম ১০০× পর্যন্ত spread। তাই structure choice-এর সিদ্ধান্তে asymptotics shortlist করে, locality সিদ্ধান্ত নেয়।
“Doubly = singly + একটা prev pointer — শুধু convenience fee।”
Fee-টা ছোট দেখায়, ব্যয় না: প্রতি node-এ ৮ byte বেশি (64-bit), মানে same cache lines-এ কম node — memory footprint আর miss-rate দুই-ই খারাপ। বিনিময়ে পাওয়া যায় O(1) delete-with-handle আর backward walk — LRU-র মতো bidirectional reorder-এ অপরিহার্য। Decision rule: শুধু forward traversal + stack-like use → singly; প্রান্ত-দুটোতেই O(1) remove/reorder দরকার → doubly। Convenience না — capability contract।
বুঝেছেন কি না দেখুন
1Singly linked list-এ একটা node-এর পরে insert করতে কয়টা pointer
assignment লাগে? আগের node-টা delete করতে কয়টা — আর কেন singly-তে
delete-at-known-node সমস্যা?
স্মরণ
Insert-after: ২টা — newNode.next = X.next; X.next = newNode। Delete X: ১টা assignment-এ possible না — X-এর আগের node- এর next বদলাতে হয়, আর singly-তে backwards যাওয়ার রাস্তা নেই — head থেকে আবার walk করতে হয় O(n)। Doubly-তে X.prev থাকায় delete O(1)। এই একটা asymmetry-ই doubly-র অস্তিত্বের মূল যুক্তি।
2Doubly list-এ node X-এর আগে insert করার চার assignment লিখুন (X.prev
ব্যবহার করে)। কোন assignment শেষে করতে হবে আর কেন?
প্রয়োগ
V.next = X; V.prev = X.prev; X.prev.next = V; X.prev = V (শেষে)। কারণ X.prev.next লেখার সময় পুরনো predecessor-এর reference দরকার — X.prev আগেই overwrite করলে সেই link হারিয়ে যাবে। নিয়ম: নতুন node- এর দুই pointer আগে set করুন, তারপর পুরনোদের দিক ঘোরান — ordering bug এখানেই জন্মায়।
3Prefetcher array-scan-এ কাজ করে কিন্তু list-walk-এ করে না — ঠিক কোন
তথ্যটা তার কাছে missing? সেই তথ্যটা runtime-এ পেতে পারলে কী হতো?
যুক্তি
Missing info: পরের node-এর address — array-তে stride স্থির (base+k·i), তাই ভবিষ্যতের addresses predictable; list-এ পরের ঠিকানা বর্তমান node-এর একটা field-এ, যেটা পড়তে হলে আগের load শেষ হতে হয় — data-dependent, un-predictable। Runtime-এ address আগে পেয়ে গেলে prefetch সম্ভব — আর ঠিক এটাই software prefetching / batched pointer layout (যেমন pool-allocated nodes) যা করে: nodes গুলো array-তে সাজিয়ে chasing-এর randomness কমানো।
4আপনার music app-এ playlist: ঘন ঘন reorder (drag-drop), মাঝে insert/
delete, কখনো random access by number। Structure কী বেছে নেবেন আর
কেন — array না list?
ডিজাইন
Drag-drop reorder = move known item between known positions; UI state এ positions-as-handles ধরে রাখা স্বাভাবিক — doubly list-এর splicing+ stable-iterator sweet spot। Random access by number বিরল (UI-তে visible window ছোট) — O(k) walk acceptable। Array হলে reorder মানে shift cascade আর indices invalidation। কিন্তু সৎ caveat: playlist size সাধারণত কয়েক হাজার — array-র shift-ও বাস্তবে সস্তা; decision- এ measurement ঢুকবে, dogma না। Design principle: mutation pattern dominant হলে list-family, read/random-access dominant হলে array।
5LRU cache-তে dict আর doubly linked list দুটোর ভূমিকা এক বাক্যে কী?
Eviction-এ কোন প্রান্ত থেকে pop করা হয় কেন?
স্মরণ
Dict: key → node handle-এর O(1) lookup (existence + location)। DLL: recency order-এর O(1) maintenance — touch করলে unlink+push_front। Eviction হয় tail থেকে — যে node সবচেয়ে আগে last-used (least recently used), তাকে future-এ দরকার হওয়ার সম্ভাবনা সবচেয়ে কম — locality-of- reference assumption-এর direct application। দুই structure-ই একাই অপর্যাপ্ত: dict-এর order নেই, list-এর lookup নেই — composition-ই solution।
এরপর কী
এরপর কী
Array-র contiguity, list-এর rewiring — দুই চরমের মধ্যে discipline
নামের একটা জিনিস দাঁড়িয়ে: কোন প্রান্তে ঢুকব, কোন প্রান্তে বেরোব
— শুধু সেটা ঠিক করলেই structure-এর নাম আর শক্তি দুই-ই বদলে যায়।
পরের লেসনে Stack (LIFO), Queue (FIFO) আর Deque (দুই প্রান্ত) —
access-discipline হিসেবে এদের abstract contract, implementation-
এর head/tail trade-offs, call stack থেকে scheduler runqueue পর্যন্ত
বাস্তব ব্যবহার, monotonic stack-এর সুন্দর pattern, circular buffer-
এর fixed-memory সমাধান — সব একসাথে। আর benchmark-এ দেখব কেন
list.pop(0) আর deque.popleft()-এর মধ্যে একই FIFO discipline হয়েও
হাজারগুণ ফারাক।
পরের লেসন: Stack, Queue, Deque — discipline থেকে শক্তি।
আরও পড়ুন
- Introduction to Algorithms (3rd edition), Chapter 10.2 — Linked Lists — Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein · Singly/doubly/circular-এর formal treatment আর sentinel discussion
- The Art of Computer Programming, Volume 1, Section 2.6 — Dynamic Storage Allocation — Donald E. Knuth · Linked allocation-র original treatment — প্রথম থেকেই pointer ছিল memory economy-র সমাধান
- Computer Systems: A Programmer's Perspective (3rd edition), Chapter 6.2 — Locality — Randal E. Bryant, David R. O'Hallaron · Pointer chasing-এর locality penalty quantitative analysis — এই লেসনের measurement section-এর ভিত্তি