LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

ยฉ 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

๐Ÿ› ๏ธComputing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
๐Ÿ“ŠMath & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
๐Ÿ“šPreparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
๐ŸงฎML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
๐Ÿ“ฆProduction ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
๐ŸงชCore LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
๐ŸงฐApplied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
๐ŸŽ“Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
๐Ÿง Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
๐ŸงฌAdvanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
๐Ÿค–Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
โšกInference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
๐Ÿ—๏ธSystem Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
๐ŸŽคAI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
๐Ÿ”ฌProject Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnComputing FoundationsData Structures for AI
โš™๏ธEasyMLOps & Deployment

Data Structures for AI

Choose Python lists, hash-based collections, deques, heaps, trees, graphs, tries, and caches by the operations an AI system must serve.

21 min read
Learning path
Step 7 of 196 in the full curriculum
MPS & Metal for ML on MacSQL and Data Modeling

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

Search three model-serving notes for token hit. Document 2 contains both words, document 0 contains one, and document 1 contains neither:

doc idtext
0token cache eviction
1gradient checkpointing memory
2token hit cache

A loop over a Python list can find those matches. You used lists and dictionaries in Python for AI Engineering: lists keep items in order, while dictionaries retrieve values by key. Here the choice changes how much work a search repeats. A retriever finds documents relevant to a query; ours will rank them by the number of distinct query words they contain.

With three notes, scanning every word is cheap. With many notes and repeated queries, we'd rather build a lookup once and reuse it. The same records will help us compare lists, indexes, heaps, and caches without changing the search problem.

Let the operation choose the shape

Before naming a structure, ask which operation must stay cheap and predictable:

  • "Which item is at position 2?" points to a sequence.
  • "Which documents contain token?" points to a key-value mapping.
  • "Have I already seen document 2?" points to a set.
  • "Which waiting job is oldest?" points to a queue.
  • "Which retained result is weakest?" points to a heap.
  • "Which notes share a parent topic, link to each other, or begin with this prefix?" points to a tree, graph, or trie.

These aren't interchangeable labels. Each structure preserves a different promise about position, identity, order, or connection. The useful choice follows from the operation you need to repeat.[1]Reference 1Introduction to Algorithms (3rd ed.)https://mitpress.mit.edu/9780262033848/introduction-to-algorithms/

Lists and array-shaped sequences

A Python list is an ordered, mutable sequence. CPython stores it as a resizable array of object references, so an integer index reaches a known position directly. Our strings fit a list because they're ordinary Python objects.[2]Reference 2Python Design and History FAQhttps://docs.python.org/3/faq/design.html

That shape differs from array.array, which packs a restricted basic value type, and from a NumPy array or tensor, which usually holds one numeric type in a rectangular shape and supports vectorized math. The distinction matters here because the corpus needs flexible Python objects, not a rectangular block of one numeric type.

Start with three operations and predict what each one has to move:

operationbeforeafterwhat had to move
read docs[2][doc 0, doc 1, doc 2]doc 2nothing
append doc 3[doc 0, doc 1, doc 2][doc 0, doc 1, doc 2, doc 3]usually no existing item
insert doc 3 at front[doc 0, doc 1, doc 2][doc 3, doc 0, doc 1, doc 2]every existing reference shifts right

An append occasionally needs a larger backing array and copies existing references. Spread across many appends, that occasional resize still gives constant work per append on average. The complexity table calls this amortized constant time.

That last row explains why a list is a poor first-in, first-out queue when code repeatedly uses pop(0). The output is correct, but removing the first item still moves every later reference. When arrival order becomes the operation to protect, a deque fits better. We'll use it later.

The list is still the right starting point here because document order is stable and a complete scan is easy to verify by hand.

Now run the query against the list. The match counts look like this:

doc idcontains tokencontains hittotal matches
0101
1000
2112

Document 2 should rank above document 0 because it matches both terms. Document 1 contributed nothing, yet the scan still opened it. That unused read is the cost we want the next structure to remove.

To skip irrelevant documents, we'll need extra information prepared before the query arrives. That saves repeated scanning, at the cost of storing and updating an index.

Building an inverted index

The query starts with a term such as token, so let the lookup start there instead of at document 0. The system should jump from the term to matching documents.

An inverted index answers that direction of lookup. It reverses the document-to-terms view into a term-to-documents map. The key changes from a document ID to the thing the query already has: a term.

viewkeyvalue
forward scandocument IDterms in one document
inverted indextermdocument IDs containing that term

Python calls its built-in hash map a dict. A hash function narrows down where a key could be stored; equality checks distinguish keys that collide. Keys must be hashable: their hash stays stable, and keys that compare equal must have equal hashes. Strings and integers work as keys; a mutable list doesn't.[3]Reference 3Built-in Typeshttps://docs.python.org/3/library/stdtypes.html

Python dictionaries preserve insertion order, but that order isn't what makes lookup fast. For this index, the important behavior is that a term leads to its posting collection, regardless of where that key was inserted.

The index maps each term to its posting list. A posting records a matching document ID; larger indexes can also store term positions or frequency. In this first version, a Python set[int] is enough because our score asks only whether each query term appears in each document.[4]Reference 4Introduction to Information Retrieval.https://nlp.stanford.edu/IR-book/

Build the index by hand

Split each document into terms and store the matching document IDs. For token, predict which rows should be reachable before checking the table:

termdocument IDs
token{0, 2}
cache{0, 2}
eviction{0}
gradient{1}
checkpointing{1}
memory{1}
hit{2}

Now the query can enter through the index. It asks for two posting lists instead of reopening all three documents:

query termposting list
token{0, 2}
hit{2}

Turn those posting lists into match counts by adding one for each query term that contains a document:

doc idmatches from tokenmatches from hitfinal score
0101
2112

Document 2 wins because it matches both terms. Document 0 still matters because it matches one term. The index removed document 1 from scoring entirely, so lookup and scoring now have a visible boundary.

Query-time comparison for token hit. A linear list scan sequentially checks every document in memory, reading doc 1 for zero matches. An inverted index jumps from query terms directly to posting sets {doc 0, doc 2} and {doc 2}, bypassing doc 1 completely.
The query-time paths differ. A linear scan rereads every document in memory; an existing inverted index visits only matching posting lists, bypassing non-matching documents entirely.

Why a set lives inside the dictionary

The dictionary solves exact term lookup. The set solves uniqueness and membership. Keeping those jobs separate makes the index easier to reason about.

Suppose document 2 were token token hit. The posting list for token should still contain document 2 once, not twice. That's why the value is a set of document IDs instead of a list of document IDs.

Keep the two operations separate:

  • Dictionary asks: "given this term, which collection is its value?"
  • Set asks: "is this document ID already in that collection?"

Python sets don't promise a useful iteration order, so examples sort them before printing. Production lexical indexes commonly keep ordered, compressed posting lists and richer statistics. The Python set is a teaching choice: it makes membership and duplicate prevention visible before storage layout and fast posting intersections enter the design.[4]Reference 4Introduction to Information Retrieval.https://nlp.stanford.edu/IR-book/

A repeated term is the smallest proof. In the demo below, document 2 says token twice, but membership in the token posting set still contributes one document ID.

posting-list-membership.py
1from collections import defaultdict 2 3docs_with_repeat = [ 4 "token cache eviction", 5 "gradient checkpointing memory", 6 "token token hit", 7] 8postings: dict[str, set[int]] = defaultdict(set) 9 10for doc_id, text in enumerate(docs_with_repeat): 11 for term in text.split(): 12 postings[term].add(doc_id) 13 14print("token postings", sorted(postings["token"])) 15print("document 2 counted once", len(postings["token"]) == 2)
Output
1token postings [0, 2] 2document 2 counted once True

Why does the inverted index store document IDs in a set instead of a list in this binary-match scorer?

Answer

A set keeps each document ID unique. Repeating token inside one document must not add multiple posting entries or inflate its binary match score.

Keep only the winners

The index has narrowed the query to scored candidates. Now the system needs the best few results, not a fully sorted report of the corpus. That distinction determines the next structure.

A heap maintains a small group of winners while candidates arrive. If you need two results out of thousands, it can avoid fully sorting candidates you won't return. With only two matches in our current corpus, a sort is perfectly reasonable; add one more candidate to expose the selection decision.

For the three notes, the scored candidates are:

doc idscore
22
01

A binary heap stores a partial order rather than a fully sorted list. The heapq operations used here maintain a min-heap: the smallest key is always at index 0. The remaining list isn't globally sorted, which is fine because the root is the only position we need to inspect for eviction.[5]Reference 5heapq: Heap queue algorithmhttps://docs.python.org/3/library/heapq.html[1]Reference 1Introduction to Algorithms (3rd ed.)https://mitpress.mit.edu/9780262033848/introduction-to-algorithms/

That root guarantee fits top-k retrieval. Keep only k winners, with the weakest winner at the root. You often want the best 5, 10, or 20 candidates rather than a complete ranking of every candidate. The root tells you exactly which retained result can leave when a stronger candidate appears.

Suppose doc 3, hit latency token, arrives with score 2. Keep only two results. Doc 0 should leave because its score is 1, while docs 2 and 3 each score 2. Which tied result should come first?

Use keys (score, -doc_id). Python compares tuples from left to right, so score wins first. When scores tie, doc 2 has key (2, -2), which is larger than doc 3's (2, -3). The lower document ID wins the tie. Inside a min-heap, however, the smaller key goes at the root so it's ready for eviction:

A top-two min-heap retriever pipeline. Candidate doc 3 arrives with score 2, key (2, -3) and outscores root doc 0 key (1, 0). Doc 0 is evicted, doc 3 enters the heap at the root because (-3) is smaller than doc 2 (-2), and the retriever yields descending top-2 results: doc 2 first, then doc 3.
The root is the next result to evict, not rank 1. Candidate doc 3 beats root doc 0 and stays at the root because (-3) is smaller than doc 2's (-2). The final pipeline extracts winners in descending score order.

Real rankers need an explicit tie-break rule. The heap demo and retriever lab both prefer the lower document ID when scores tie, which keeps output deterministic.

bounded-top-k-heap.py
1import heapq 2 3candidate_scores = [(0, 1), (2, 2), (3, 2)] 4k = 2 5top_k: list[tuple[int, int]] = [] 6 7for doc_id, score in candidate_scores: 8 # Same key rule as search(): higher score wins; lower doc_id wins ties. 9 candidate = (score, -doc_id) 10 if len(top_k) < k: 11 heapq.heappush(top_k, candidate) 12 elif candidate > top_k[0]: 13 evicted = heapq.heapreplace(top_k, candidate) 14 print("evicted", (evicted[0], -evicted[1])) 15 16print("top k", [(score, -doc_key) for score, doc_key in sorted(top_k, reverse=True)])
Output
1evicted (1, 0) 2top k [(2, 2), (2, 3)]

The lab later calls heapq.nlargest, which returns results in descending key order. It's suited to selecting a small k from many candidates.[5]Reference 5heapq: Heap queue algorithmhttps://docs.python.org/3/library/heapq.html CPython can use a full sort when k is at least the number of candidates, so the tiny lab isn't a demonstration of a speedup. The explicit heap above shows what bounded selection does.

You need the best 10 results from a long stream of scored documents. What does a capped min-heap avoid doing?

Answer

It avoids fully sorting every candidate. The heap retains only 10 winners and exposes the weakest winner for replacement, while an explicit tie rule keeps output deterministic.

Building a tiny retriever

Return to the original three notes. First implement the scan, then build an index and accumulate scores from postings. This is the retrieval part of RAG (retrieval-augmented generation): a language model could later read the returned documents. There's no language model in this lab.

Put this in data_structures_demo.py. The tokenizer lowercases and splits on whitespace. Production tokenizers also handle punctuation and language-specific rules, but keeping normalization tiny makes the structure visible. Returning a set means this example scores binary term membership: each distinct query term contributes at most once.

data_structures_demo.py
1from collections import defaultdict 2import heapq 3 4docs = [ 5 "token cache eviction", 6 "gradient checkpointing memory", 7 "token hit cache", 8] 9 10def tokenize(text: str) -> set[str]: 11 return set(text.lower().split()) 12 13def slow_search(query: str) -> list[tuple[int, int]]: 14 terms = tokenize(query) 15 matches: list[tuple[int, int]] = [] 16 for doc_id, text in enumerate(docs): 17 words = tokenize(text) 18 score = sum(term in words for term in terms) 19 if score: 20 matches.append((doc_id, score)) 21 return matches 22 23def build_index(documents: list[str]) -> dict[str, set[int]]: 24 postings: dict[str, set[int]] = defaultdict(set) 25 for doc_id, text in enumerate(documents): 26 for term in tokenize(text): 27 postings[term].add(doc_id) 28 return postings 29 30index = build_index(docs) 31 32def search(query: str, k: int = 2) -> list[int]: 33 if k < 1 or not query.strip(): 34 return [] 35 36 scores: dict[int, int] = defaultdict(int) 37 for term in tokenize(query): 38 for doc_id in index.get(term, set()): 39 scores[doc_id] += 1 40 41 best = heapq.nlargest( 42 k, 43 scores.items(), 44 key=lambda item: (item[1], -item[0]), 45 ) 46 return [doc_id for doc_id, _score in best] 47 48print("slow scan", slow_search("token hit")) 49print("posting list", sorted(index["token"])) 50print("search", search("token hit"))
Output
1slow scan [(0, 1), (2, 2)] 2posting list [0, 2] 3search [2, 0]

Read the two outputs as two different contracts. slow_search returns score tuples in corpus order, so (2, 2) appears after (0, 1). Those tuples expose the score trace without ranking it. search uses the posting lists, applies top-k selection, and returns [2, 0].

defaultdict(set) creates an empty set when a missing key is accessed with brackets during indexing. During search, index.get(term, set()) returns an empty set for an unknown term without inserting it into the index. A query for attention therefore returns []. Repeated query words are removed by tokenize, so token token hit scores exactly like token hit.

Map each operation to one structure

JobStructureWhy it fits
keep the original corpus orderlistthe documents start as an ordered sequence
jump from term to matching docsdictionary of setsthe query starts with a term
count how many terms matcheddictionary of integerseach document needs a score
keep the best few resultsheapthe system wants top-k rather than a full sorted list

The heap key uses score first and negative document ID second. Higher scores win; lower IDs win ties. That small rule makes repeated runs predictable, so a later debugging trace can distinguish a ranking bug from a tie-break choice.

When order and reuse become the problem

The retriever chose structures by lookup and ranking. Ingestion asks a different question: which job arrived first? Repeated queries ask another: can this result be reused without becoming stale?

Process arrivals in order

New documents arrive and wait to be indexed. Later systems may also split them into chunks and encode each chunk as an embedding, a numerical representation used for similarity search. For one class of equal-priority jobs, arrival order is a clean baseline. The operation isn't "find by key" or "keep the best"; it removes the oldest item.

A queue is the behavior: first in, first out (FIFO). Python's collections.deque is the container used here. Its name means double-ended queue, and it supports appends and pops at either end in approximately constant time: more waiting jobs don't require shifting all existing references. Middle indexing becomes slower, so it isn't a replacement for a list when random position access is the main job.[6]Reference 6collections: Container datatypeshttps://docs.python.org/3/library/collections.html

Why does list.pop(0) hurt? A standard Python list stores item references in a single contiguous array. Calling pop(0) removes the head element, which forces CPython to shift every subsequent reference left by one slot. If 50,000 document ingestion jobs or token chunks sit in a queue, draining them item by item incurs quadratic O(n2)O(n^2)O(n2) total work.

Under the hood, collections.deque avoids shifting by allocating a doubly linked chain of fixed-size blocks (each holding 64 element pointers). In streaming token processors, GPU scheduling pipelines, and LLM inference engines like vLLM[7]Reference 7vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttentionhttps://github.com/vllm-project/vllm, this FIFO contract is frequently implemented as a circular ring buffer: a fixed-size array where head and tail pointers advance modulo buffer capacity ((tail + 1) % capacity). Appends and pops update integer offsets in O(1)O(1)O(1) time without moving existing elements in memory.

Trace the state from left (oldest) to right (newest). Predict the next job after each operation:

operationdeque statenext job
start[doc-3, doc-4]doc-3
append("doc-5")[doc-3, doc-4, doc-5]doc-3
popleft()[doc-4, doc-5]doc-4
queues-for-background-work.py
1from collections import deque 2 3embedding_jobs = deque(["doc-3", "doc-4", "doc-5"]) 4next_job = embedding_jobs.popleft() 5 6print("next job", next_job) 7print("remaining", list(embedding_jobs))
Output
1next job doc-3 2remaining ['doc-4', 'doc-5']

The rule is simple:

  • append new work on one side
  • pop the oldest work from the other side

That's FIFO behavior. If you accidentally process newest work first, old jobs can wait indefinitely while new jobs keep arriving. The queue's order is part of the system's correctness, not just an implementation detail.

An in-memory deque shows ordering, not durability. If a worker writes an embedding and crashes before recording completion, a broker may retry the job. The downstream write needs a durable idempotency key so the retry can't create a duplicate vector. The next chapter puts that guarantee in a database transaction and a uniqueness constraint.

The failure case makes the ordering bug visible. New jobs belong at the tail; putting them at the front lets recent uploads jump ahead of old work. Run it before trusting the queue:

queue-order-failure.py
1from collections import deque 2 3fifo_jobs = deque(["oldest-doc"]) 4fifo_jobs.extend(["new-doc-a", "new-doc-b"]) 5 6newest_first_jobs = deque(["oldest-doc"]) 7newest_first_jobs.appendleft("new-doc-a") 8newest_first_jobs.appendleft("new-doc-b") 9 10print("FIFO runs", fifo_jobs.popleft()) 11print("wrong order runs", newest_first_jobs.popleft())
Output
1FIFO runs oldest-doc 2wrong order runs new-doc-b

Reuse results without serving stale answers

If users ask the same question repeatedly, recomputing retrieval results per request wastes work. A cache can remove that repeated computation, but only if its hit is still valid.

A retrieval cache stores prior ranked document IDs so the system can reuse them. A cache isn't one primitive structure. It combines a mapping from key to value with policies for freshness and eviction. The mapping finds a candidate result; policies decide whether keeping that result stays safe.

caches-for-repeated-queries.py
1cache: dict[tuple[int, str, int], list[int]] = {} 2corpus_version = 1 3query = "token hit" 4k = 2 5 6cache_key = (corpus_version, query, k) 7if cache_key not in cache: 8 cache[cache_key] = search(query, k) 9 10print("cached", cache[cache_key])
Output
1cached [2, 0]

Make freshness part of the key

cache key partwhy it exists
queryseparates one repeated request from another
corpus_versionforces refresh after the document collection changes
kseparates a request for one result from a request for two

If document 2 changes to hit cache, its score falls to 1. It now ties with doc 0, so the correct ranking becomes [0, 2], not the cached [2, 0]. Rebuild the affected index and advance corpus_version before serving queries against the new corpus. Changing the version alone can't repair an old index.

Every input that can change the result belongs in the key or in an invalidation rule. Our lab fixes the scoring rule and has no user-specific documents. A shared service must also separate permission scopes and invalidate results when scoring or normalization changes. Two people sending identical text aren't necessarily allowed to retrieve the same notes.

The demo dictionary can grow without limit. A long-running service also needs a bound such as a maximum entry count or byte budget, plus an eviction policy. Least recently used (LRU) discards the entry that hasn't been accessed for the longest time. With two cache slots, the order changes even on a hit:

requestleast recent to most recentaction
token[token]compute and store
hit[token, hit]compute and store
token again[hit, token]reuse and move to most recent
cache[token, cache]evict hit, then store cache

Versioning prevents stale hits. Eviction bounds storage, even when all entries are still valid. A plain insertion-ordered dictionary doesn't implement LRU by itself: reading a key doesn't move it to the end. OrderedDict.move_to_end() and popitem(last=False) provide the operations needed for this recency order.[6]Reference 6collections: Container datatypeshttps://docs.python.org/3/library/collections.html

Keep k=2 fixed for this failure example. A text-only key finds yesterday's ranking after an update; a versioned key misses and forces recomputation from the updated index.

cache-invalidation-failure.py
1query = "token hit" 2old_version = 1 3new_version = 2 4 5text_only_cache = {query: [2, 0]} 6versioned_cache = {(old_version, query, 2): [2, 0]} 7 8print("text-only stale hit", query in text_only_cache) 9print("versioned stale hit", (new_version, query, 2) in versioned_cache)
Output
1text-only stale hit True 2versioned stale hit False

Why should a retrieval-cache key include corpus_version as well as the query text?

Answer

The same query can have a different correct ranking after documents change. A new version forces a cache miss instead of serving a fast but stale result.

Use paths, links, and prefixes

Lists, dictionaries, sets, deques, and heaps covered position, identity, arrival order, and priority. What if the query asks for a path instead? Three other shapes become useful when the question is about hierarchy, a relationship, or a shared prefix.[1]Reference 1Introduction to Algorithms (3rd ed.)https://mitpress.mit.edu/9780262033848/introduction-to-algorithms/

A tree gives each node one parent path

A rooted tree has a chosen root, parent-child edges, and no cycles. Every node except the root has exactly one parent. In a topic taxonomy, each note follows one path from the root:

note-topic-tree.txt
1notes 2โ”œโ”€โ”€ serving 3โ”‚ โ”œโ”€โ”€ doc 0: token cache eviction 4โ”‚ โ””โ”€โ”€ doc 2: token hit cache 5โ””โ”€โ”€ training 6 โ””โ”€โ”€ doc 1: gradient checkpointing memory

That shape makes subtree questions natural: "show every note under serving." A binary search tree solves a different problem: it orders keys so smaller keys lie in a node's left subtree and larger keys in its right subtree. Each comparison chooses a branch. A balanced tree needs only a logarithmic number of such choices; a badly skewed tree becomes a chain. A topic hierarchy isn't automatically a search tree.

A heap is also drawn as a tree, but it isn't a binary search tree. A min-heap guarantees only that each parent is no larger than its children. It doesn't support arbitrary key search the way a search tree does. Its job is still priority eviction.

A graph allows many-to-many links

Suppose doc 2 belongs under both serving and caching, or doc 0 links directly to doc 2. One-parent taxonomy no longer captures the relationship cleanly. A graph stores vertices (nodes) and edges (links); edges may be directed or undirected, and graphs may contain cycles. Choose it when the links themselves are the data you need to traverse.

Python code commonly represents a sparse graph as a dictionary of neighbor sets:

note-links.py
1links = { 2 0: {2}, 3 1: set(), 4 2: {0}, 5} 6 7print("neighbors of doc 0", sorted(links[0]))
Output
1neighbors of doc 0 [2]

The printout [2] makes the directed edge from doc 0 to doc 2 visible. The reverse edge from doc 2 to doc 0 is why traversal must track visited nodes. A neighbor lookup shows one step; it doesn't yet prove that a full traversal will terminate.

If a traversal can encounter 0 -> 2 -> 0, it needs a visited set. Without one, the search can loop forever. Graph cost depends on representation: an adjacency list stores outgoing neighbors compactly for sparse links, while a matrix makes every possible pair explicit. Representation follows the density and operation of the links.

A trie follows one symbol per edge

A trie, also called a prefix tree, organizes strings by shared prefixes. If the product asks "what completions start with to?", shared characters should be traversed once. Allowed terms token, tool, and top share the path t -> o, then branch:

term-prefix-trie.txt
1(root) 2โ””โ”€โ”€ t 3 โ””โ”€โ”€ o 4 โ”œโ”€โ”€ k -> e -> n token [end] 5 โ”œโ”€โ”€ o -> l tool [end] 6 โ””โ”€โ”€ p top [end]

Looking up prefix to follows two edges from the empty root before enumerating matching suffixes. An end marker distinguishes a complete term from a prefix: if to were also an allowed term, its node would need an end marker even though it has children. Reaching the prefix depends on prefix length, not on how many unrelated words exist. Returning completions still costs time to traverse and produce those matches. Tries can use substantial memory for nodes and child links, so compressed variants merge unbranched paths.

Prefix trees answer "what strings start with this prefix?" Graphs model dependencies and knowledge links. Trees model module, configuration, and taxonomy hierarchies. The shape still follows the operation, even when several shapes can represent the same records.

Compare the work each operation does

Big-O describes how an upper bound on work grows with input size. O(1) stays bounded as the collection grows; O(n) grows linearly. O(log n) grows slowly: doubling the input adds only a constant number of steps for a balanced-tree lookup. These labels don't predict wall-clock time. The later complexity lesson develops the notation more carefully.

In this table, n is items in a structure, m is scored candidates, k is retained winners, V is graph vertices, E is graph edges, and p is prefix length. The bounds assume bounded-cost key comparisons and hashes; reading a long string key has its own cost.[1]Reference 1Introduction to Algorithms (3rd ed.)https://mitpress.mit.edu/9780262033848/introduction-to-algorithms/

Use the table to compare operations, not to crown one structure fastest. A list index is cheap when the position is known. A list scan is cheap to write but grows with every item. The right row depends on the question that dominates your workload.

structure and operationtypical costcaveat
Python list indexO(1)position must already be known
Python list appendamortized O(1)occasional resize copies references
Python list scan or front insert/deleteO(n)scan checks items; front mutation shifts references
Python dict lookup / set membershipexpected O(1)worst case is O(n); keys need stable hashes
deque append / popleft()O(1)middle access is O(n)
heap root peekO(1)only root is guaranteed best or weakest
heap push or popO(log n)heap list isn't globally sorted
bounded top-k over m candidatesO(m log k) time, O(k) extra space for 2 <= k <= mk=1 takes a linear scan; full sorting may be simpler when k is close to m
balanced search-tree lookupO(log n)an unbalanced tree may degrade to O(n)
breadth-first or depth-first graph traversalO(V + E) with adjacency liststraversal still needs a visited set when cycles are possible
trie prefix descentO(p) before outputassumes constant or expected-constant child-edge lookup; completions add output cost

The retriever has more than one cost. Expected dictionary lookup reaches each posting set quickly, but it still scans returned postings. A common term may appear in almost every document. If m candidates receive scores, bounded selection adds O(m log k) work for 2 <= k <= m. The scores dictionary also holds up to m entries, so the lab's total query memory isn't just the heap's O(k). Constant-time key lookup never makes the whole query constant-time.

Diagnose the wrong shape

The wrong structure usually isn't a vocabulary problem. The first working loop feels good enough, so it stays. Diagnose the operation that got slow, duplicated, stale, or impossible to finish:

SymptomLikely causeFix
query gets slower as documents growthe code still scans the whole list per querybuild an index keyed by the query field
same document appears more than oncea list was used where uniqueness mattersstore posting lists as sets
FIFO worker slows as queue growscode repeatedly calls list.pop(0)use deque.append() with deque.popleft()
heap output looks partly out of ordercode assumes every heap position is sortedtrust the root invariant or explicitly sort final winners
old jobs starvework is being popped in the wrong orderuse a FIFO queue and test the order explicitly
cached result looks right until documents changecache has no version or invalidation ruletie the cache key to corpus updates
cache memory keeps growingdictionary has no size or byte boundadd an eviction budget and measure it
graph traversal never finishesa cycle revisits the same nodesrecord visited nodes in a set
prefix lookup is fast but returning results is slowtrie walk is cheap, but completion set is largecap output or rank completions separately
ranking feels mysteriousscores are hidden inside loopsprint one query's per-document score table

The fastest way to debug is still the same: trace one tiny example by hand. Make the operation and its promise visible before changing the container.

Test the promises, not just the ranking

The final ranking can look right while an intermediate promise is broken. These tests check the structures as well as the result:

protect-the-promises-with-tests.py
1from collections import deque 2 3def test_token_lookup_has_two_docs(): 4 assert index["token"] == {0, 2} 5 6def test_empty_query_returns_empty_list(): 7 assert search("") == [] 8 9def test_indexed_search_matches_ranked_scan(): 10 for query in ["token hit", "token", "Token HIT", "token token hit", "attention", ""]: 11 ranked = sorted(slow_search(query), key=lambda item: (-item[1], item[0])) 12 for k in [0, 1, 2, 5]: 13 assert search(query, k) == [doc_id for doc_id, _score in ranked[:k]] 14 15def test_search_normalizes_case(): 16 assert search("Token HIT") == [2, 0] 17 18def test_queue_is_fifo(): 19 jobs = deque(["doc-3", "doc-4"]) 20 assert jobs.popleft() == "doc-3" 21 22def test_graph_cycle_is_bounded_by_visited_set(): 23 links = {0: {2}, 2: {0}} 24 visited: set[int] = set() 25 pending = deque([0]) 26 while pending: 27 doc_id = pending.popleft() 28 if doc_id in visited: 29 continue 30 visited.add(doc_id) 31 pending.extend(links.get(doc_id, set())) 32 assert visited == {0, 2} 33 34test_token_lookup_has_two_docs() 35test_empty_query_returns_empty_list() 36test_indexed_search_matches_ranked_scan() 37test_search_normalizes_case() 38test_queue_is_fifo() 39test_graph_cycle_is_bounded_by_visited_set() 40print("six structural tests passed")
Output
1six structural tests passed

Each test checks one promise:

  • the index maps terms to the right documents
  • empty input is predictable
  • indexed ranking matches the scan, including ties, unknown terms, repeated terms, and different result limits
  • query normalization is consistent
  • queue order stays first-in, first-out
  • graph traversal terminates across a cycle

Predict the next operation on paper

Pause before looking at the solution sketches. Predict the next operation, then name the structure that makes it cheap or safe:

  1. Add a fourth document with docs.append("hit latency token"), then rebuild with index = build_index(docs).
  2. Predict sorted(index["token"]).
  3. Predict search("token hit").
  4. Run search("attention") and explain why it shouldn't crash.
  5. Change document 2 so it no longer contains token, then explain what must happen to the cache.
  6. Choose a structure for each new requirement: autocomplete the prefix to; represent notes with several cross-links; process oldest upload first.
  7. Explain why a min-heap isn't a sorted list and why a heap isn't a binary search tree.

Solution sketches

Use these to check your reasoning, not to skip the exercise.

Practice itemWhat a good answer should show
Add hit latency tokentoken and hit both gain the new document ID.
Predict sorted(index["token"])[0, 2, 3] after the new document is added.
Predict search("token hit")[2, 3]. Documents 2 and 3 tie with score 2, so the lower document ID wins the deterministic tie-break.
Query attentionindex.get(term, set()) returns an empty set, so the function returns [] instead of crashing.
Change document 2Rebuild the affected posting lists and advance the corpus version used in cache keys. Remove obsolete entries through cleanup or eviction.
Prefix, cross-links, oldest uploadUse a trie, graph, and deque, respectively. Each requirement asks for a different path or order.
Heap distinctionsOnly heap root satisfies the global minimum rule. A binary search tree instead orders left and right subtrees to support arbitrary key search.

If your answer feels fuzzy, go back to the score table. Good data-structure reasoning should look inspectable on paper before it looks clever in code. Use the table to predict what work each operation forces before code hides it.

Hand the promises to durable storage

The inverted index, deque, and versioned cache you just built live in one process. Close the process and they're gone. Share them across two workers and you also need identity, uniqueness, and freshness that survive a restart. The in-memory structures made those promises concrete; durability makes them shared.

That's why data structures come before databases. First pick the shape that makes a lookup cheap and inspectable. Then put that promise behind durable tables, joins, transactions, and vector search.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.An ingestion worker repeatedly adds a new document at the right and removes the oldest document from the left. Which Python container best matches that operation?

Correct answer: A deque using append() and popleft()

A deque supports efficient updates at both ends and makes FIFO intent explicit. list.pop(0) returns the correct item but shifts every later reference, a set doesn't preserve FIFO order, and a heap chooses by priority rather than arrival time.

2.Document 5 contains token token hit, and document 6 contains token cache. The inverted index uses a dictionary whose values are sets of document IDs. What should postings["token"] contain?

Correct answer: {5, 6}, preserving one membership for each matching document

The dictionary maps the term to its posting collection, and the set keeps each matching document ID once. Repeating token inside document 5 doesn't duplicate that document's membership.

3.A capped top-2 min-heap retains score keys (1, 0) for doc 0 and (2, -2) for doc 2. Candidate doc 3 arrives with key (2, -3). What should happen?

Correct answer: Replace doc 0 because the root is the weakest retained key

Candidate (2, -3) is stronger than root (1, 0), so doc 0 leaves. Of the two tied winners, doc 3's key is smaller than doc 2's. Doc 3 becomes the new heap root even though doc 2 comes first in the descending result list.

4.A note taxonomy gives every note one parent topic. Later, one note must belong to both serving and caching, and notes may link back to each other. Which shape now fits better?

Correct answer: A graph, because nodes can have multiple links and traversal can represent cycles

A tree gives each non-root node one parent path and forbids cycles. Many-to-many membership and cross-links are graph relationships, so graph traversal must also guard against revisiting nodes.

5.An allowed-term trie contains token, tool, and top. A lookup follows the prefix to. What work remains after reaching that prefix node?

Correct answer: Enumerate or rank the matching suffix branches for all three terms

Descending to to depends on the two-symbol prefix length. Returning completions still requires visiting output branches or ranking a bounded subset, so prefix descent alone isn't the entire query cost.

6.A retrieval cache includes (corpus_version, query, k) in its key but stores every result forever. Which two separate questions are answered by versioning and eviction?

Correct answer: Versioning checks freshness; eviction enforces a memory or entry budget

A corpus version prevents a fast but stale hit after documents change. A separate size or byte bound prevents valid entries from growing until the process runs out of memory.

7.A graph has links 0 -> 2, 2 -> 3, and 3 -> 0. What lets breadth-first traversal visit every reachable node once instead of cycling?

Correct answer: A visited set checked before expanding each node

The set records which vertices have already been processed. When the traversal reaches node 0 again through node 3, it skips expansion instead of adding the same cycle repeatedly. Tracking only the immediate parent doesn't catch a longer cycle.

8.A retriever uses expected O(1) dictionary lookup for each query term. Why can a common term still make the full query slow?

Correct answer: The posting set can still contain nearly every document, and those candidates must be scored

Expected constant-time key lookup reaches the posting set; it doesn't process that set for free. A common term can produce a corpus-sized posting set, followed by candidate scoring and top-k work.

9.Which statement about Python collection semantics is correct for deterministic retrieval output?

Correct answer: dict preserves insertion order; sets don't guarantee iteration order, and get() doesn't insert a missing key.

Python dictionaries iterate in insertion order. Set order shouldn't define ranked output, so this lesson sorts sets before printing. dict.get(key, default) returns the default for a miss without raising KeyError.

9 questions remaining.

Next Step
Continue to SQL and Data Modeling

You can now choose in-memory structures for position, identity, priority, relationships, prefixes, ordered work, and safe reuse. The next chapter takes the same lookup, uniqueness, and freshness promises and stores them in durable tables, constraints, and transactions that more than one process can share.

PreviousMPS & Metal for ML on Mac
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Introduction to Algorithms (3rd ed.)

Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. ยท 2009

https://mitpress.mit.edu/9780262033848/introduction-to-algorithms/

Python Design and History FAQ

Python Software Foundation ยท 2026

https://docs.python.org/3/faq/design.html

Built-in Types

Python Software Foundation ยท 2026

https://docs.python.org/3/library/stdtypes.html

Introduction to Information Retrieval.

Manning, C. D., Raghavan, P., Schutze, H. ยท 2008 ยท Cambridge University Press

https://nlp.stanford.edu/IR-book/

heapq: Heap queue algorithm

Python Software Foundation ยท 2026

https://docs.python.org/3/library/heapq.html

collections: Container datatypes

Python Software Foundation ยท 2026

https://docs.python.org/3/library/collections.html

vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention

vLLM Team ยท 2024

https://github.com/vllm-project/vllm

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.