By the end of the previous module we had covered the course's full catalog of structures: TaskFlow already has a board (arrays and lists), undo (stacks), notifications (queues and heaps), indexes (hash tables), hierarchies (trees) and dependencies (graphs). Now comes the step that separates someone who "knows structures" from someone who "designs with structures": learning to choose. In this lesson we will build a decision method in the form of questions, gather the whole course into one big comparison table, and reason out loud through several interview-style case studies. The perfect structure doesn't exist; what exists is the right structure for your dominant operations.
Contents
- The method: choose by operations, not by data
- The seven key questions
- Decision tree
- The course's big table
- Worked case studies
- Typical choice mistakes and their symptoms
- Combining structures: the pattern TaskFlow uses
The method: choose by operations, not by data
The most common junior-developer mistake is to ask "which structure fits this data?". The right question is a different one:
Which operations will I perform most often, and what cost can I afford for each one?
The same data (TaskFlow's tasks, which are dicts with id, title, priority, status...) has lived throughout the course inside lists, stacks, queues, heaps, hash tables, trees and graphs. The data never changed; the dominant operation did:
- To look up by id: hash table (
dict), O(1) average. - To undo the last action: stack, O(1) at the end.
- To serve the highest-priority one: heap, O(log n).
- To ask for "priority between 1 and 3": BST/AVL, O(log n + k).
- To order by dependencies: graph + topological order, O(V + E).
The procedure, always the same:
- List the problem's operations (insert, search, delete, traverse, minimum, range...).
- Estimate how frequent each one is (millions of lookups and few insertions, or the other way around?).
- Look up the costs (the big table below) and pick the structure that makes the frequent operations cheap, accepting that the rare ones get more expensive.
- If you're torn between two, measure with
timeit(module 1): real data has the final word.
The seven key questions
Ask yourself these questions in order. The first one you answer with an emphatic "yes" usually points to the structure.
| # | Question | If the answer is yes... | Example in TaskFlow |
|---|---|---|---|
| 1 | Do I access by exact key (id, name, email)? | Hash table: dict / set |
tasks_by_id[42] |
| 2 | Do I need to maintain a total order and run range queries ("between X and Y", "the one after...")? | BST/AVL (or bisect over a stable sorted list) |
"tasks with priority between 1 and 3" |
| 3 | Do I only work at the ends? LIFO or FIFO? | LIFO → stack; FIFO → queue; both ends → deque |
undo (LIFO), notifications (FIFO) |
| 4 | Do I only care about the highest-priority item at any moment, not the full order? | Heap (heapq) |
UrgentInbox |
| 5 | Does the data have a hierarchy (parent-children, containment)? | General tree | projects → tasks → subtasks |
| 6 | Are there many-to-many relationships (dependencies, networks, paths)? | Graph | the DAG of dependencies between tasks |
| 7 | None of the above: just a sequence with access by position or full traversals? | list (dynamic array); linked list if there are many mid-list insertions/deletions with a reference to the node |
the board, on-screen listings |
Two important nuances:
- The questions are not mutually exclusive: a real problem usually answers "yes" to several. That's where the section on combining structures comes in.
- Questions 2 and 1 are often confused. A hash answers "does key 42 exist?" in O(1), but it cannot answer "which keys lie between 1 and 3?" without scanning everything — that was the hash's limit we hit at the end of module 5, and the door through which trees came in.
Decision tree
The same method, as a diagram. Read it top to bottom and stop at the first leaf that fits:
flowchart TD
A[Which operation dominates?] --> B{Lookup by<br/>exact key?}
B -- Yes --> B1{Do I also need<br/>ranges or order?}
B1 -- No --> H[Hash table: dict / set<br/>O 1 average]
B1 -- Yes --> T[BST / AVL<br/>O log n]
B -- No --> C{Do I only work<br/>at the ends?}
C -- LIFO --> P[Stack<br/>O 1]
C -- FIFO --> Q[Queue / deque<br/>O 1]
C -- Both ends --> D[deque<br/>O 1 at both]
C -- No --> E{Only the highest-<br/>priority item?}
E -- Yes --> M[Heap heapq<br/>O log n]
E -- No --> F{Parent-children<br/>hierarchy?}
F -- Yes --> AR[General tree<br/>traversals O n]
F -- No --> G{Many-to-many<br/>relationships?}
G -- Yes --> GR[Graph<br/>BFS/DFS O V+E]
G -- No --> L{Many mid-list<br/>insertions with a node<br/>in hand?}
L -- Yes --> LE[Linked list<br/>O 1 with a reference]
L -- No --> LI[Python list<br/>O 1 access by index]
This tree is a compass, not a law: when the data volume is small (dozens of elements), almost any structure will do and the simplest one wins — which is almost always list or dict.
The course's big table
Every structure we have built or used, with its key operations, when it shines and when it's a trap.
| Structure | Key operations (Big O) | When to use it | When NOT to | Example in TaskFlow |
|---|---|---|---|---|
Array / list |
access by index O(1); append/pop at the end O(1) amortized; insert/pop(0) O(n); search O(n) | Sequences with access by position, traversals, moderate size | Many insertions/deletions at the front or middle; frequent searches by value | On-screen task listing |
| Singly linked list | insert/delete at the head O(1); access by position O(n) | Frequent insertions/deletions at the head; building stacks and queues on top | You need access by index or backward traversal | The first board (LinkedList, module 2) |
| Doubly linked list | insert/delete with a reference to the node O(1); navigate both ways O(1) per step | Forward/backward navigation; deleting a node you have already located | When list or deque covers the case with less code |
The navigable TaskHistory |
| Circular list | advance "to the next" indefinitely O(1) | Rotating turns, round-robin | Almost any other case | TaskDispatcher |
| Stack | push/pop/peek O(1) | The last thing in is the first thing that matters: undo, backtracking, parsing | You need access to the bottom or to middle positions | UndoRedoManager (two stacks) |
| Queue | enqueue/dequeue O(1) | Fair arrival order: turns, messaging, BFS | The serving order is not the arrival order (→ priority) | NotificationQueue |
| Circular queue (ring buffer) | enqueue/dequeue O(1), fixed memory | Bounded-size buffers: the last N events | The size is not bounded | EventLog |
| Priority queue / heap | insert O(log n); extract minimum O(log n); peek minimum O(1) | "Always give me the most urgent one"; top-k; schedulers | You need to look up arbitrary elements or frequently traverse in full order | UrgentInbox with heapq |
deque |
append/pop at both ends O(1); access in the middle O(n) | Queues, bounded histories (maxlen), sliding windows |
Frequent access by index in the middle | BoundedHistory, ProductivityWindow |
Hash table / dict / set |
insert/search/delete O(1) average; no order or ranges | Access by exact key, counting, grouping, memoizing, deduplicating | Ranges, order, "the next one greater than..."; non-hashable keys | tasks_by_id, the tag→ids index |
| BST (unbalanced) | insert/search O(log n) average, O(n) worst (degenerate); range O(log n + k) | Prototypes, randomly arriving data | Data that arrives sorted (it degenerates into a list) | The first version of range search |
| AVL | insert/search/delete O(log n) guaranteed; inorder traversal O(n) sorted | Order + ranges + predictable performance | You only look up by exact key (a hash is simpler and faster) | "Priority between 1 and 3" in production |
| B / B+ tree | search/range O(log n) with wide nodes, optimized for disk | On-disk indexes, databases | In-memory structures (AVL/hash suffice) | The SQLite index where TaskFlow would persist |
| Graph | BFS/DFS O(V+E); Dijkstra O((V+E) log V); topological order O(V+E) | Dependencies, networks, paths, reachability | The data is a plain sequence or a strict hierarchy | The dependency DAG, the scheduler |
Reading tip: don't memorize the table; memorize the rows that surprise you. That insert(0) on a list is O(n) and that a dict cannot answer range queries are the two facts in this table that prevent the most real-world mistakes.
Worked case studies
Let's reason through four scenarios the way they would come up in a technical interview. Notice that the reasoning always follows the method: operations → frequencies → costs.
Case 1: "Design the autocomplete feature"
When the user types "meet" into TaskFlow's search box, the titles starting with that prefix must appear.
- Dominant operation: search by prefix, not by exact key. Question 1 (hash) fails: a
dictonly finds complete keys. - Question 2 (order) does fit: in a collection sorted alphabetically, all the titles starting with "meet" are contiguous. With a sorted
listandbisectwe locate the first candidate in O(log n) and walk forward while the prefix matches. - An alternative using what we know about hashing: a
dictprefix→list of ids, precomputed (for each title we store all its prefixes). O(1) queries, in exchange for a lot of memory. The structure specialized in this is the trie, which will make an appearance in the resources lesson.
import bisect
def autocomplete(sorted_titles, prefix, limit=10):
"""sorted_titles: list sorted alphabetically."""
i = bisect.bisect_left(sorted_titles, prefix) # O(log n)
results = []
while i < len(sorted_titles) and sorted_titles[i].startswith(prefix):
results.append(sorted_titles[i])
if len(results) == limit:
break
i += 1
return resultsbisect_left performs a binary search (the same idea as the BST, over an array): it finds where the prefix would start. After that we just advance while there's a match: O(log n + k) with k results.
Case 2: "The 10 most recent events"
TaskFlow must show the last 10 activity events, discarding the old ones.
- Operations: add at one end, discard at the other, fixed size. Question 3: ends, FIFO with a bound.
- This is exactly a ring buffer, and in Python it's already built:
deque(maxlen=10). Appending is O(1) and the oldest item is evicted automatically.
from collections import deque
events = deque(maxlen=10)
events.append({"type": "create", "task_id": 7}) # O(1); if there are 10, evicts the oldestChoosing a list with pop(0) here would work... at O(n) per eviction: the symptom would be an app that slows down as the activity log grows.
Case 3: "Detect whether the project has circular dependencies"
Before scheduling, TaskFlow must warn if A depends on B, B on C, and C on A.
- "Depends on" is a many-to-many relationship: question 6, graph. A cycle in the dependency graph makes a topological order impossible.
- The module 7 solution: DFS with three colors (white/gray/black). Finding an edge to a gray node (still on the recursion stack) gives the cycle away. Cost O(V + E). Equivalent alternative: if Kahn's algorithm fails to process every vertex, there is a cycle.
- The typical mistake is trying to solve this with lists and nested loops "following dependency chains": it ends in O(n²) or in infinite loops. When you see cross-cutting relationships, model the graph explicitly.
Case 4: "Look up by id" versus "list sorted by priority"
TaskFlow needs (a) to open a task by its id instantly and (b) to show the backlog sorted by priority.
- These are two different dominant operations and no single structure wins at both:
| Need | dict by id |
list sorted by priority |
AVL by priority |
|---|---|---|---|
| Look up by id | O(1) | O(n) | O(n) (the key is the priority, not the id!) |
| List by priority | O(n log n) (sort every time) | O(n) (already sorted) | O(n) inorder |
| Insert | O(1) | O(n) (making room) | O(log n) |
- The mature answer: both. A
dictid→task as the main store, and a sorted structure (a heap if you only serve the most urgent; an AVL or list+bisectif you list ranges) that holds references. That leads us straight into the last section.
Typical choice mistakes and their symptoms
Every bad choice has a recognizable performance signature. Learn to read it:
| Mistake | Symptom | Diagnosis | Fix |
|---|---|---|---|
Searching by value in a list inside a loop |
Everything is fine with 100 elements and crawls with 100,000 | x in lst is O(n) → the loop is O(n²) |
set or dict: x in s is O(1) |
insert(0) / pop(0) on a list |
Enqueuing gets slow as the queue grows | It shifts every element: O(n) | deque (module 4) |
| Sorting the whole list "to get the minimum" | sort() on every iteration: O(n log n) per operation |
You only need the extreme, not the total order | heapq: O(log n) |
| BST fed data that arrives already sorted | "Logarithmic" performance that behaves linearly | The tree degenerated into a list | AVL, or shuffle / use bisect |
dict when you need ranges |
Code full of for k in d: if a <= k <= b |
Hashing destroys order on purpose | AVL / sorted list + bisect |
| A graph "simulated" with lists of lists and cross lookups | Fragile nested loops, cycles that hang the program | Many-to-many relationships left unmodeled | Graph with an adjacency list + BFS/DFS |
| Optimizing without measuring | Days lost on an exotic structure for 50 elements | Tiny n: the constants dominate | timeit first; simple by default |
Combining structures: the pattern TaskFlow uses
Real applications almost never use one structure: they use several, coordinated, each one paying for the operation it does best. TaskFlow is the example we have built over eight modules:
import heapq
class TaskFlowCore:
"""Skeleton of how TaskFlow combines structures (minimal version)."""
def __init__(self):
self.tasks = {} # dict id -> task : lookup by id O(1)
self.by_tag = {} # dict tag -> set of ids : inverted index O(1)
self.urgent = [] # heap (priority, counter, id) : most urgent O(log n)
self._counter = 0 # stable tie-breaker in the heap
def create(self, task):
self.tasks[task["id"]] = task # O(1)
for tag in task.get("tags", []):
self.by_tag.setdefault(tag, set()).add(task["id"]) # O(1)
self._counter += 1
heapq.heappush(self.urgent, (task["priority"], self._counter, task["id"])) # O(log n)
def most_urgent(self):
while self.urgent:
priority, _, id_ = self.urgent[0]
task = self.tasks.get(id_)
# Stale entry: the task was deleted or changed priority
if task is None or task["priority"] != priority:
heapq.heappop(self.urgent) # discard it and keep going
continue
return task
return NonePoints of this pattern worth understanding well:
- The
dictis the canonical store: the single source of truth. The other structures hold only ids (cheap references), never copies of the task. - The heap uses the
(priority, counter, id)tuple from module 4: the counter breaks ties between equal priorities while preserving arrival order, and it prevents comparing dicts. - Lazy deletion: removing from the middle of a heap is awkward, so we don't delete; on lookup, we discard entries whose task no longer exists or has changed. It's a standard trick in real schedulers.
- The price of combining is consistency: every write touches several structures. Centralize modifications in methods (
create,delete...) so that no structure falls out of sync.
Exercises
Exercise 1
For each TaskFlow need, choose a structure and justify it with its Big O: (a) check in O(1) whether an email is already registered; (b) show the tasks with hours between 2 and 5; (c) process the user's "undo" actions; (d) hand out tasks among 3 people in rotating turns; (e) a moving average of hours worked over the last 7 days.
Exercise 2
This code finds the unassigned urgent tasks. Identify the two structure-choice problems and rewrite it:
def urgent_unassigned(tasks, assigned): # tasks: list of dicts; assigned: list of ids
results = []
for t in tasks:
if t["priority"] == 1 and t["id"] not in assigned:
results.append(t)
results.sort(key=lambda t: t["hours"])
return results[:3]Exercise 3
Design (design only: structures and the cost of each operation, no code) TaskFlow's "review mode": it stores the last 50 visited tasks without duplicates; if you visit one that was already there, it moves up to the most recent position; checking whether a task is in the history must be O(1). Hint: it's the same trade-off as an LRU cache.
Solutions
Exercise 1. (a) set — membership O(1); (b) AVL keyed by hours (or a sorted list + bisect if writes are rare) — range O(log n + k); (c) stack — pure LIFO, O(1); (d) circular list (TaskDispatcher) — endless "next" in O(1); (e) deque(maxlen=7) with an incremental sum — O(1) per day, like module 4's ProductivityWindow.
Exercise 2. Problem 1: t["id"] not in assigned over a list is O(n), which makes the loop O(n·m); convert it once into a set → O(1) per check. Problem 2: sorting everything just to keep 3 is O(k log k); for a small top-k use heapq.nsmallest, O(k log 3):
import heapq
def urgent_unassigned(tasks, assigned):
assigned_ids = set(assigned) # O(m), once
candidates = [t for t in tasks
if t["priority"] == 1 and t["id"] not in assigned_ids] # O(n)
return heapq.nsmallest(3, candidates, key=lambda t: t["hours"]) # O(n log 3)Exercise 3. Combine two coordinated structures: a doubly linked list holding the visit order (most recent at the head) and a dict id→node. Visiting a new task: create a node at the head + an entry in the dict, O(1); if it exceeds 50, remove the node at the tail and its entry from the dict, O(1). Visiting an existing one: the dict locates its node in O(1) and, since the list is doubly linked, it unlinks itself and moves to the head in O(1) — exactly the operation a list cannot do cheaply. Membership check: id in dict, O(1). Neither structure achieves this alone; together, everything is O(1).
Conclusion
You now have the judgment the whole course was aiming for: operations first, then the structure, and when in doubt, measure. The seven questions and the decision tree lead you to a candidate; the big table gives you its costs; the performance symptoms warn you when you got it wrong; and the combination pattern (a dict store + auxiliary indexes and heaps) shows you how real applications do it, TaskFlow included. In the next lesson we'll make the reverse journey: we'll walk back through everything we built, module by module, to consolidate the complete map before the final projects.
Data Structures Course
Module 1: Introduction to Data Structures
- What Are Data Structures?
- The Importance of Data Structures in Programming
- Types of Data Structures
- Algorithmic Complexity and Big O Notation
- Arrays and Memory: the Foundation of Data Structures
Module 2: Lists
Module 3: Stacks
- Introduction to Stacks
- Basic Stack Operations
- Stack Implementation
- Stack Applications
- Stack Exercises
Module 4: Queues
- Introduction to Queues
- Basic Queue Operations
- Circular Queues
- Priority Queues
- Double-Ended Queues (Deques)
- Queue Exercises
Module 5: Hash Tables and Dictionaries
- Introduction to Hash Tables
- Hash Functions and Collision Resolution
- Dictionaries and Sets in Practice
- Hash Table Exercises
Module 6: Trees
- Introduction to Trees
- Binary Trees
- Tree Traversals
- Binary Search Trees
- AVL Trees
- B-Trees
- Heaps
- Tree Exercises
Module 7: Graphs
- Introduction to Graphs
- Graph Representation
- Graph Search Algorithms
- Shortest Path Algorithms
- Minimum Spanning Trees
- Graph Applications
- Graph Exercises
