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

  1. How to approach the projects
  2. Project 1: Full TaskFlow in the console
  3. Project 2: Sprint planner
  4. Project 3: Task search engine
  5. How to self-assess with timeit
  6. 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 timeit measurements (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: a TaskFlow class that encapsulates the coordination. Start from the TaskFlowCore skeleton 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.

  1. Validate the backlog: every dependency points to an existing id and has_cycle returns false. If not, report and stop — a plan over an invalid graph means nothing.
  2. Compute each task's in-degree. Those with in-degree 0 are eligible: put them in a heap keyed by (priority, counter, id).
  3. 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 deque of "didn't fit" tasks retried before the next sprint is an elegant middle ground.
  4. 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.
  5. Critical path: with hours as 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 set in the inverted index. Also store a Counter of 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 a dict character→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 dict normalized_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/print that 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 timeit possible. Write them in milestone 1.
  • Synchronizing by hand. If the interface updates the dict here 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 deque and LinkedList for 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:

  1. Functional: the project's "done" criteria hold with your generated test data (including the automatic verifier, in the planner's case).
  2. Cost: the measurements in the timeit table show the promised growth shape when n is multiplied by 10 and by 100.
  3. 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.

© Copyright 2026. All rights reserved