Everything you've learned in this course fits in one sentence: choose your data well and the code writes itself. This last lesson invites you to prove it to yourself with three integrative projects of increasing difficulty. They are not single-structure exercises: each one forces you to combine several, to justify every choice with its Big O, and to verify with timeit that the promises hold. You'll find detailed briefs, per-structure requirements, a suggested architecture, incremental milestones and "done" criteria — but no finished code: this time, the code is yours.
Contents
- How to approach the projects
- Project 1: Full TaskFlow in the console
- Project 2: Sprint planner
- Project 3: Task search engine
- How to self-assess with
timeit - Course wrap-up
How to approach the projects
- Recommended order: 1 → 2 → 3. Project 1 integrates pieces you've already built; project 2 requires designing an algorithm on top of them; project 3 asks you to design structure and algorithm at the same time.
- Before coding each piece, write one line: "dominant operation → chosen structure → cost". It's the 08-01 method turned into a habit.
- Reuse your classes from the earlier modules (the 08-02 cheat-sheet table is your inventory). Rewrite from scratch only what the project asks you to improve.
- Small milestones: each project comes with incremental milestones; don't move to the next one until the previous one works with test data. A half-finished project that works teaches more than a complete one that won't start.
- General "done" criterion: it works on the test cases, every structure is justified in writing, and the
timeitmeasurements (section 5) confirm the promised costs.
Project 1: Full TaskFlow in the console
Brief
Integrate the pieces built throughout the course into a single coherent console application: a menu loop that lets you manage tasks and projects using, underneath, the right structures for each operation. This is an integration project: the difficulty isn't in any one piece, but in making them all share a single task store without falling out of sync.
Requirements by structure
| Functional requirement | Required structure | Origin |
|---|---|---|
Create, delete and look up tasks by id in O(1) |
dict id→task (canonical store) |
Module 5 |
| Board with columns (pending/in progress/done) and moves between them | LinkedList (or deque) per column |
Module 2 |
| Undo/redo the latest operations | UndoRedoManager (two stacks) |
Module 3 |
| "Next urgent task" in O(log n) | UrgentInbox (heapq with (priority, counter, id)) |
Modules 4 and 6 |
| Search by tag in O(1) average | Inverted index tag→set of ids |
Module 5 |
Projects with subtasks and aggregated cost (budget) |
TreeNode (general tree, postorder) |
Module 6 |
| Dependencies between tasks and a valid "work order" | Graph + topological_order, with has_cycle as validation |
Module 7 |
| Log of the last 20 session events | deque(maxlen=20) or CircularQueue |
Module 4 |
Suggested solution guide
A suggested three-layer architecture, so the structures don't get tangled with the interface:
flowchart TD
UI[interface.py<br/>menu loop, input/print] --> N[core.py<br/>TaskFlow class: create, move,<br/>undo, urgent, search...]
N --> E[structures.py<br/>your classes from modules 2-7]
structures.py: copy your course classes here as they are (or import them from wherever you keep them).core.py: aTaskFlowclass that encapsulates the coordination. Start from theTaskFlowCoreskeleton of 08-01 and extend it. Golden rule: the interface never touches a structure directly; every modification goes through a core method, which updates all affected structures (dict, index, heap, graph...) in the same call.- For undo, store invertible actions on the stack: for example
("create", id)is undone by deleting, and("move", id, from_column, to_column)is undone by moving back. Start by supporting undo only for create/delete/move; extend later. - For dependencies, keep the module 7 convention: edge A→B means "B depends on A". Before adding a dependency, simulate it and check
has_cycle; if it creates one, reject it with a clear message.
Incremental milestones: (1) menu + create/list/look up on the dict; (2) board columns and moves; (3) undo/redo; (4) urgent inbox with lazy deletion; (5) tags and the inverted index; (6) hierarchical projects with budget; (7) dependencies + work order; (8) event log and polish.
"Done" criteria: no menu operation scans the whole dict except "list everything"; creating 10,000 test tasks degrades neither lookup by id nor extraction of urgent tasks; undoing immediately after any operation leaves the system in the previous state (check it by comparing the dict before and after); adding a circular dependency is impossible.
Project 2: Sprint planner
Brief
Given a backlog of tasks — each with id, title, priority, hours and a depends_on list — and a sprint capacity in hours (e.g. 40), generate a list of valid sprints: no task appears before its dependencies, no sprint exceeds the capacity and, all else being equal, higher-priority tasks go in first. In addition, the planner must compute the project's critical path and flag the bottlenecks: tasks that, if delayed, delay the entire delivery.
Requirements by structure
| Piece | Required structure | Target cost |
|---|---|---|
| Backlog indexed by id | dict |
O(1) per lookup |
| Dependency graph | Graph (adjacency list) |
O(V+E) construction |
| Upfront validation (no cycles, dependencies exist) | has_cycle (colors) |
O(V+E) |
| Order by levels (what can happen "at the same time") | Layered Kahn (the level-based variant from module 7) | O(V+E) |
| Choosing within a level by priority | Heap (priority, counter, id) |
O(log n) per extraction |
| Critical path and slack | critical_path over the DAG (hours as weights) |
O(V+E) |
Suggested solution guide
The core algorithm is the level-based Kahn from the module 7 scheduler, with a twist: within each level the order matters, because capacity is limited.
- Validate the backlog: every dependency points to an existing id and
has_cyclereturns false. If not, report and stop — a plan over an invalid graph means nothing. - Compute each task's in-degree. Those with in-degree 0 are eligible: put them in a heap keyed by
(priority, counter, id). - To fill a sprint: extract eligible tasks from the heap while they fit in the sprint's remaining hours. A design decision you must make and document: if the highest-priority task doesn't fit but a lower-priority one does, do you skip it (use the capacity) or close the sprint (respect strict priority)? Both are defensible; a
dequeof "didn't fit" tasks retried before the next sprint is an elegant middle ground. - When a sprint is "completed", decrease the in-degree of the tasks that depend on its tasks; those that reach 0 enter the eligible heap. Repeat until the backlog is empty.
- Critical path: with
hoursas the weight, compute for each task its earliest finish time (maximum over its dependencies + its own hours, in topological order) and, walking backwards, the latest one. The tasks with zero slack form the critical path: highlight them in the output ("if Migrate server slips, everything slips").
Incremental milestones: (1) backlog loading and validation; (2) flat topological order; (3) parallel levels; (4) sprints with capacity and priority; (5) critical path and warnings; (6) readable output (a per-sprint table with hours used/free).
"Done" criteria: with an artificial backlog of 1,000 tasks and random cycle-free dependencies, it plans in well under a second; no task appears in a sprint earlier than any of its dependencies (write an automatic verifier: it's a loop with a set of completed tasks — O(V+E) — and it's part of the project); the sum of hours in each sprint never exceeds the capacity; the total estimated duration matches the length of the critical path when the capacity is "infinite".
Project 3: Task search engine
Brief
Build TaskFlow's search engine: given a corpus of tasks, it must answer free-text queries ("monthly client report") by returning the k most relevant tasks, at interactive speed even with tens of thousands of tasks. Optionally, autocomplete while typing. This is the most open-ended project: there are design decisions with no single right answer, and documenting them is part of the work.
Requirements by structure
| Piece | Required structure | Why |
|---|---|---|
| Inverted index word→ids | dict of set (or defaultdict(set)) |
Search without scanning the corpus: O(1) average per word |
| Frequencies for ranking | A Counter per document |
Counting is its trade |
| Top-k results | Heap (heapq.nlargest or your own Heap) |
O(n log k), without sorting everything |
| Repeated queries | Memoization with a dict (query→results cache) |
Module 5; invalidate it when indexing new tasks |
| (Optional) Autocomplete | A prefix dict, a sorted list + bisect, or a trie (08-03) |
Comparing the three is the exercise |
Suggested solution guide
- Indexing. Normalize each title/description (lowercase, accents stripped, split on spaces and punctuation — careful: "café"→"cafe" is acceptable here; document the decision). For each word, add the id to that word's
setin the inverted index. Also store aCounterof words per task for the ranking. Indexing should be O(total number of words). - Querying. Tokenize the query exactly like the corpus (same normalization or nothing will match!). Retrieve the id
sets for each word. Design decision: intersection (all the words: precise, few results) or union (any word: flexible, many)? Suggestion: union, and let the ranking reward whoever covers more words. - Ranking. A simple, sufficient score: the sum, for each query word, of its frequency in the task, with a bonus per distinct word covered; break ties by task priority (lower number first!). Extract the top-k with a heap, never by sorting the full candidate list.
- Autocomplete (optional). Implement at least the 08-01 version (sorted list +
bisect). If you're up for the trie: a node is adictcharacter→child plus an end-of-word flag; inserting and prefix search are O(length). Compare the memory and speed of both against your corpus and write two paragraphs with the conclusion — that little report is worth more than the code. - Cache. A
dictnormalized_query→results. When you index a new task, empty it (or invalidate only the affected queries, using the inverted index itself, if you want an extra challenge).
Incremental milestones: (1) normalizer + inverted index with single-word search; (2) multi-word queries with union; (3) ranking + top-k with a heap; (4) cache with invalidation; (5) autocomplete; (6) comparative measurement (next section).
"Done" criteria: with 20,000 synthetic tasks (generate titles by combining a vocabulary of ~200 words), a query answers in milliseconds; searching for a nonexistent word returns empty without an error; adding a task makes it findable immediately; a warm-cache search is measurably and clearly faster than a cold one.
How to self-assess with timeit
Every structure's promise is a Big O; your final task in the course is to audit your own promises, as we did in module 1. The method: measure the same operation with n, 10n and 100n elements and check the growth shape — O(1) barely moves, O(log n) adds a fixed amount per jump, O(n) multiplies by 10, O(n²) by 100.
import timeit
def measure(fn, repetitions=1000):
"""Average time (in microseconds) of one call to fn."""
total = timeit.timeit(fn, number=repetitions)
return total / repetitions * 1_000_000
# Example: audit Project 1's lookup by id at three sizes
for n in (1_000, 10_000, 100_000):
app = build_taskflow_with(n) # your test-data generator
us = measure(lambda: app.lookup(n // 2))
print(f"n={n:>7}: {us:8.2f} µs per lookup")timeit runs the operation many times and averages, removing the noise of a single measurement; we measure with the "middle" task to avoid favoring edge cases. Suggested test cases, one per key promise:
| Project | Audited operation | Promise | Failure signal |
|---|---|---|---|
| 1 | lookup(id) with n = 10³/10⁴/10⁵ |
O(1) | Time grows with n → you're walking a list somewhere |
| 1 | next_urgent() after many priority changes |
O(log n) | Linear growth → lazy deletion isn't purging, or you're re-sorting the list |
| 2 | Planning a backlog of 10²/10³/10⁴ tasks | O(V+E) | Quadratic growth → you're finding dependents by scanning the whole backlog |
| 3 | Cold query with a corpus of 10³/10⁴ | ~O(candidates) | It grows with the total corpus, not with the candidates → you're not using the index |
| 3 | Repeated query (warm cache) | O(1) | Same as cold → the cache isn't intercepting (are you normalizing before caching?) |
If a measurement contradicts the theory, congratulations: you've just found the best exercise of the course. Chase down the cause with the symptoms table from 08-01.
Common Mistakes and Tips
We adapt the section to advice on project approach:
- Starting with the interface. The pretty menu is the classic trap: hours of
input/printthat exercise nothing. Core first, tested from a script; interface last. - Not writing generatable test data. You need functions that manufacture 10,000 synthetic tasks in a second; without them there's no verifiable milestone and no
timeitpossible. Write them in milestone 1. - Synchronizing by hand. If the interface updates the
dicthere and the index there, desynchronization is a matter of time. Every write goes through the core; that's the lesson of the 08-01 pattern. - Structure perfectionism. Torn between
dequeandLinkedListfor a board column? Pick one, note why, move on. Changing it later will cost little precisely if you respected the ADT (cross-cutting concept 1). - Skipping the written justification. The "operation → structure → cost" line per piece turns the project into portfolio and interview material. Without it, it's just code that works today.
- Comparing yourself with real libraries. Your search engine doesn't compete with Elasticsearch. The goal is for your cost promises to hold and for you to be able to explain them.
Exercises
In this lesson, the projects are the exercises: pick at least one (ideally all three, in order) and carry it through to its "done" criteria. There is no finished solution to copy — each project's guide is your map, and the timeit measurements are your automatic grader.
Solutions
The "solution" to each project is a self-assessment in three checks, common to all three:
- Functional: the project's "done" criteria hold with your generated test data (including the automatic verifier, in the planner's case).
- Cost: the measurements in the
timeittable show the promised growth shape when n is multiplied by 10 and by 100. - Judgment: you can walk through your code and, piece by piece, recite "dominant operation → structure → cost → why not the alternatives". If any piece fails this interrogation, revisit 08-01; if they all pass, the course has done its job with you.
Conclusion
Congratulations: you have reached the end of the Data Structures course. You started with a loose dict called task and you finish able to design, justify and measure a complete system — board, undo, urgent tasks, indexes, hierarchies, dependencies and search — choosing for each piece the structure that makes the operation that truly matters cheap. That is the philosophy we hope you carry with you: data first, then code. Algorithms get forgotten and looked up again; judgment, once acquired, stays. Practice it in every code review, in every design and in every interview: always ask which operations dominate and which structure serves them best. TaskFlow is yours, and so is the judgment. It has been a pleasure building with you. This is where the course ends — and where you go from here is up to you.
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
