Foundationপ্রথম নীতি থেকে
LEVEL 6লেসন ৪/২৪মাঝারি৪০ মিনিট

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 আসলেই দরকার।

এই লেসন শেষে আপনি পারবেন

  • Node + pointer model-এ singly, doubly আর circular list-এর অপারেশনগুলো হাতে-আঁকা diagram-এ trace করে O-cost টেবিল বানাতে পারবেন — বিশেষত insert-at-known-node বনাম find-এর পার্থক্য ধরে
  • Pointer chasing কেন prefetcher-কে অন্ধ করে — প্রতিটা hop একটা dependent load, একটা potential cache miss — এই hardware story বলতে পারবেন
  • Big-O টেবিল যেখানে linked list-কে winner দেখায়, measurement সেখানেই array-কে দেখায় — এই বৈপরীত্যের কারণ (allocation, locality, constant factor) বিশ্লেষণ করতে পারবেন
  • Linked list genuinely যেখানে জেতে — O(1) splicing, stable iterators, hashmap+dll-ভিত্তিক LRU — সেই ক্ষেত্রগুলো চিনতে ও justify করতে পারবেন
  • সম্পূর্ণ doubly linked list আর LRU cache নিজের হাতে implement করে hit/miss accounting দিয়ে verify করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসনে দেখলাম 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২ pointerO(1) delete-at-known-node (prev জানা), backward walk, LRU
Circular(কোনো tail=None নেই)Round-robin scheduler, ring buffer logic

Operation matrix — সৎ version

অপারেশনArrayLinked listNote
Access i-thO(1) arithmeticO(i) walklist-এর সবচেয়ে বড় হার
PrependO(n) shiftO(1)list-এর সবচেয়ে বিখ্যাত win
Insert after known nodeO(n)O(1)“known node” শর্তটা critical
Delete known node (doubly)O(n)O(1)prev pointer-এর প্রাপ্যতা
Search by valueO(n)O(n)দুজনেরই হার; array তবু faster constants
Append (dynamic array)amortized O(1)O(1) with tail ptrtie

সবচেয়ে বেশি ভুল হওয়া লাইনটা হাইলাইট করি: 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                 ~100ns

Dependent 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-র এই সংঘর্ষ আমরা মেপে দেখব।

cur = cur.next — একটা hop-এর পূর্ণ বিল
  1. Attribute lookupCPython: getattr(cur, next) — descriptor protocol, dict lookup — interpreter খরচ শুরু এখানেই
  2. Load pointercur object-এর next field পড়া — cur নিজে যদি cold হয়, এই পড়াটাই প্রথম miss
  3. Dereferenceপাওয়া ঠিকানায় jump — ঠিকানাটা random heap space; prefetcher অন্ধ, এই load-ই প্রধান miss candidate
  4. TLB checkবড় heap-এ virtual→physical translation পর্যন্ত miss হলে page-walk — দাম আরও ×10
  5. 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

নিজে চালিয়ে দেখুন

EXPERIMENT

Stopwatch বনাম textbook — front-insert আর traversal-এর রশিদ

Linux/macOS, Python 3.8+, ~200 MB free RAM· ১৫ মিনিট
# 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.py

Typical ফলাফল (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.61x

Reading দুই স্তরে:

  • পর্ব ১ — 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-এ মাপা যায়।

নিজে বানান

BUILD IT

DoublyLinkedList + LRU Cache — identity-based structure-এর পূর্ণ kit

Python · ●●○○○
  1. Sentinel head/tail দিয়ে DoublyLinkedList লিখুন — push_front, unlink, pop_tail — edge case মুক্ত
  2. Node handle-ই API: insert_after(node,v), remove(node) — index না, identity
  3. LRUCache বানান: dict key→node + capacity + এই dll — MRU সামনে, LRU পিছনে
  4. get(): hit হলে node unlink করে push_front; miss হলে counter বাড়াক
  5. put(): existing key update+promote; new key capacity ছাড়ালে pop_tail evict
  6. 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=7

Reading: hot pattern-এ a,b promote হতে থাকায় তারা বাঁচে — hit-rate nonzero; flood pattern-এ প্রতিটা নতুন key ঠিক সবচেয়ে পুরনোটাকে evict করে — LRU-র worst-case adversary। দুই access pattern, একই policy — ফলাফল রাতদিন। এই সংবেদনশীলতাই LRU বেছে নেওয়ার বা না-নেওয়ার আসল বিচার।

নিজে বাড়ান:

  1. move_to_back(key) যোগ করুন — MRU/LRU প্রান্ত swap করে LFU-lite experiment করুন
  2. TTL support: node-এ timestamp রেখে get()-এ expire check — Redis- এর volatile-ttl-এর simplified রূপ
  3. 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।

বুঝেছেন কি না দেখুন

1

Singly 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-র অস্তিত্বের মূল যুক্তি।

2

Doubly 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 এখানেই জন্মায়।

3

Prefetcher 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।

5

LRU 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-এর ভিত্তি