Practical 10: the Combined Application
Chapter Thirty-Four
Syllabus topic Module 2, practical 10, "Combined Application: Design a simple program that uses multiple data structures ."
Pages 243 to 252 of 297
Aim
To design a simple program that uses several data structures together, choosing each one for a reason.
The application: a library issue desk
Small enough to write, large enough to need real structures. The desk has to do six things:
| What the desk does | How often | The operation it needs |
|---|---|---|
| look a book up by its accession number | constantly | find by exact key |
| list the catalogue in title order | at the end of the day | the keys in sorted order |
| hold reservations for a book that is out | while a book is out | first come, first served |
| undo the last few desk actions | rarely, when a mistake is made | most recent first |
| report the most borrowed titles | once a week | sorted by a count |
| suggest what else a borrower might like | on request | who else borrowed what |
Read that table again, because it is the whole design. Six operations, and each one names a structure. Nothing is chosen because it is on the syllabus.
| Need | Structure | Why that one |
|---|---|---|
| find by exact key | hash table | O(1) average. Order does not matter here |
| catalogue in title order | binary search tree | the in-order walk is sorted, free |
| reservations | queue | FIFO is the only fair rule for a waiting list |
| undo | stack | LIFO: the most recent action is undone first |
| most borrowed | sorted array | built once a week and then read; a sort is cheaper than maintaining order |
| suggestions | graph | the relation is between any two borrowers, which nothing linear can hold |
That last one is MU's Course Objective 6, which names graphs although no practical row does.
The program
"""A library issue desk built from five data structures.
Each structure is here because of the operation named in its docstring, which is
what MU's Course Objective 8 asks for: choose, and justify the choice.
"""
from collections import deque
# --------------------------------------------------------------------------
# 1. the CATALOGUE: a hash table, because a book is looked up by its exact
# accession number far more often than anything else happens.
# --------------------------------------------------------------------------
def accession_hash(key, base=31):
"""A polynomial hash, written out so bucket numbers are reproducible."""
total = 0
for character in str(key):
total = total * base + ord(character)
return total
class Catalogue:
"""Hash table with separate chaining. Lookup by accession number, O(1)."""
def __init__(self, buckets=11):
self.buckets = [[] for _ in range(buckets)]
self.count = 0
def _index(self, key):
return accession_hash(key) % len(self.buckets)
def add(self, accession, title, author):
chain = self.buckets[self._index(accession)]
for position, (existing, _) in enumerate(chain):
if existing == accession:
chain[position] = (accession, (title, author))
return False
chain.append((accession, (title, author)))
self.count += 1
return True
def find(self, accession):
"""(title, author) or None. Searches one bucket's chain only."""
for existing, value in self.buckets[self._index(accession)]:
if existing == accession:
return value
return None
def __len__(self):
return self.count
def all_items(self):
for chain in self.buckets:
for accession, value in chain:
yield accession, value
def chain_lengths(self):
return [len(chain) for chain in self.buckets]
# --------------------------------------------------------------------------
# 2. the TITLE INDEX: a binary search tree, because the catalogue has to be
# listed in title order and an in-order walk gives that for nothing.
# --------------------------------------------------------------------------
class TitleNode:
def __init__(self, title, accession):
self.title = title
self.accessions = [accession]
self.left = None
self.right = None
class TitleIndex:
"""BST keyed on title. in_order() is the sorted catalogue, free."""
def __init__(self):
self.root = None
self.count = 0
def add(self, title, accession):
if self.root is None:
self.root = TitleNode(title, accession)
self.count += 1
return
here = self.root
while True:
if title == here.title:
here.accessions.append(accession)
return
if title < here.title:
if here.left is None:
here.left = TitleNode(title, accession)
self.count += 1
return
here = here.left
else:
if here.right is None:
here.right = TitleNode(title, accession)
self.count += 1
return
here = here.right
def find(self, title):
"""(accessions, comparisons). O(log n) when the tree is balanced."""
here = self.root
comparisons = 0
while here is not None:
comparisons += 1
if title == here.title:
return here.accessions, comparisons
here = here.left if title < here.title else here.right
return None, comparisons
def in_order(self):
"""Every title in order, which is the whole point of using a tree."""
out = []
def walk(node):
if node is not None:
walk(node.left)
out.append((node.title, node.accessions))
walk(node.right)
walk(self.root)
return out
def height(self):
def deepest(node):
return -1 if node is None else 1 + max(deepest(node.left),
deepest(node.right))
return deepest(self.root)
# --------------------------------------------------------------------------
# 3. RESERVATIONS: a queue per book, because a waiting list is fair only if
# it is first come, first served.
# --------------------------------------------------------------------------
class Reservations:
"""A FIFO queue per accession number. deque, so both ends are O(1)."""
def __init__(self):
self.queues = {}
def reserve(self, accession, borrower):
queue = self.queues.setdefault(accession, deque())
if borrower in queue:
return -1
queue.append(borrower)
return len(queue)
def next_in_line(self, accession):
queue = self.queues.get(accession)
if not queue:
return None
return queue.popleft()
def waiting(self, accession):
return list(self.queues.get(accession, ()))
# --------------------------------------------------------------------------
# 4. UNDO: a stack, because the most recent action is the one to undo.
# --------------------------------------------------------------------------
class UndoStack:
"""LIFO, over a fixed array, so it can overflow like a real one."""
def __init__(self, capacity=20):
self.slots = [None] * capacity
self.top = -1
def is_empty(self):
return self.top == -1
def push(self, action):
if self.top == len(self.slots) - 1:
# the oldest action is dropped rather than refusing the newest
self.slots.pop(0)
self.slots.append(None)
self.top -= 1
self.top += 1
self.slots[self.top] = action
def pop(self):
if self.is_empty():
raise IndexError("nothing left to undo")
action = self.slots[self.top]
self.slots[self.top] = None
self.top -= 1
return action
def peek(self):
if self.is_empty():
return None
return self.slots[self.top]
def __len__(self):
return self.top + 1
# --------------------------------------------------------------------------
# 5. BORROWER GRAPH: two borrowers are joined when they have borrowed the same
# book. Nothing linear can hold a relation between any two things.
# --------------------------------------------------------------------------
class BorrowerGraph:
"""Undirected graph, adjacency list. Used for 'who else read this'."""
def __init__(self):
self.neighbours = {}
self.borrowed = {}
def record(self, borrower, accession):
self.neighbours.setdefault(borrower, set())
others = self.borrowed.setdefault(accession, set())
for other in others:
if other != borrower:
self.neighbours[borrower].add(other)
self.neighbours.setdefault(other, set()).add(borrower)
others.add(borrower)
def also_borrowed_by(self, borrower):
return sorted(self.neighbours.get(borrower, ()))
def suggestions(self, borrower, catalogue):
"""Books read by people who read what this borrower read. BFS, depth 1."""
mine = {a for a, who in self.borrowed.items() if borrower in who}
out = {}
for neighbour in self.neighbours.get(borrower, ()):
for accession, who in self.borrowed.items():
if neighbour in who and accession not in mine:
found = catalogue.find(accession)
if found:
out.setdefault(found[0], set()).add(neighbour)
return {title: sorted(who) for title, who in sorted(out.items())}
def connected_group(self, start):
"""Everyone reachable from a borrower. BFS with a seen set."""
if start not in self.neighbours:
return []
seen = {start}
order = []
waiting = deque([start])
while waiting:
here = waiting.popleft()
order.append(here)
for neighbour in sorted(self.neighbours[here]):
if neighbour not in seen:
seen.add(neighbour)
waiting.append(neighbour)
return order
# --------------------------------------------------------------------------
# the DESK, which ties them together
# --------------------------------------------------------------------------
class IssueDesk:
def __init__(self):
self.catalogue = Catalogue()
self.titles = TitleIndex()
self.reservations = Reservations()
self.undo = UndoStack()
self.graph = BorrowerGraph()
self.on_loan = {}
self.borrow_count = {}
def add_book(self, accession, title, author):
if self.catalogue.add(accession, title, author):
self.titles.add(title, accession)
self.borrow_count.setdefault(accession, 0)
return f"added {accession} {title!r}"
return f"{accession} was already in the catalogue, details updated"
def issue(self, accession, borrower):
book = self.catalogue.find(accession)
if book is None:
return f"no such accession number {accession}"
if accession in self.on_loan:
place = self.reservations.reserve(accession, borrower)
if place == -1:
return f"{borrower} is already waiting for {book[0]!r}"
return (f"{book[0]!r} is out with {self.on_loan[accession]}; "
f"{borrower} is number {place} in the queue")
self.on_loan[accession] = borrower
self.borrow_count[accession] += 1
self.graph.record(borrower, accession)
self.undo.push(("issue", accession, borrower))
return f"issued {book[0]!r} to {borrower}"
def give_back(self, accession):
if accession not in self.on_loan:
return f"{accession} is not on loan"
was = self.on_loan.pop(accession)
self.undo.push(("return", accession, was))
nxt = self.reservations.next_in_line(accession)
title = self.catalogue.find(accession)[0]
if nxt is None:
return f"{title!r} returned by {was}, back on the shelf"
self.on_loan[accession] = nxt
self.borrow_count[accession] += 1
self.graph.record(nxt, accession)
return (f"{title!r} returned by {was} and issued straight to {nxt}, "
f"who was first in the queue")
def undo_last(self):
if self.undo.is_empty():
return "nothing left to undo"
what, accession, who = self.undo.pop()
title = self.catalogue.find(accession)[0]
if what == "issue":
self.on_loan.pop(accession, None)
self.borrow_count[accession] -= 1
return f"undid the issue of {title!r} to {who}"
self.on_loan[accession] = who
return f"undid the return of {title!r}, it is with {who} again"
def sorted_catalogue(self):
return self.titles.in_order()
def most_borrowed(self, how_many=3):
"""A SORTED ARRAY, built once and then read. Ties broken by title."""
rows = []
for accession, count in self.borrow_count.items():
title = self.catalogue.find(accession)[0]
rows.append((count, title, accession))
rows.sort(key=lambda row: (-row[0], row[1]))
return rows[:how_many]Practical 10: the Combined Application
The desk, run
from library import IssueDesk
desk = IssueDesk()
print("adding books:")
books = [("A104", "Data Structures", "Karumanchi"),
("A101", "Let Us Python", "Kanetkar"),
("A109", "Operating System Concepts", "Silberschatz"),
("A102", "Learning Python", "Lutz"),
("A115", "Introduction to Algorithms", "Cormen"),
("A107", "Python: The Complete Reference", "Brown")]
for accession, title, author in books:
print(" ", desk.add_book(accession, title, author))
print()
print(f"the catalogue holds {len(desk.catalogue)} books")
print("the hash table's chain lengths:", desk.catalogue.chain_lengths())
print("the title tree's height:", desk.titles.height())Practical 10: the Combined Application
adding books:
added A104 'Data Structures'
added A101 'Let Us Python'
added A109 'Operating System Concepts'
added A102 'Learning Python'
added A115 'Introduction to Algorithms'
added A107 'Python: The Complete Reference'
the catalogue holds 6 books
the hash table's chain lengths: [1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0]
the title tree's height: 3Practical 10: the Combined Application
from library import IssueDesk
desk = IssueDesk()
for accession, title, author in [("A104", "Data Structures", "Karumanchi"),
("A101", "Let Us Python", "Kanetkar"),
("A109", "Operating System Concepts", "Silberschatz"),
("A102", "Learning Python", "Lutz"),
("A115", "Introduction to Algorithms", "Cormen"),
("A107", "Python: The Complete Reference", "Brown")]:
desk.add_book(accession, title, author)
print("1. LOOKUP by accession number, which is the hash table:")
for accession in ["A104", "A115", "A999"]:
found = desk.catalogue.find(accession)
print(f" {accession} -> {found if found else 'not in the catalogue'}")
print()
print("2. the CATALOGUE IN TITLE ORDER, which is the tree's in-order walk:")
for title, accessions in desk.sorted_catalogue():
print(f" {title:<34} {accessions}")
print()
print("3. ISSUING, and the RESERVATION QUEUE when a book is already out:")
for accession, borrower in [("A104", "Aarti"), ("A104", "Bhavesh"),
("A104", "Chetan"), ("A101", "Aarti"),
("A101", "Divya")]:
print(" ", desk.issue(accession, borrower))
print(" waiting for A104:", desk.reservations.waiting("A104"))
print()
print("4. RETURNING, and the queue serving itself:")
print(" ", desk.give_back("A104"))
print(" waiting for A104 now:", desk.reservations.waiting("A104"))
print(" ", desk.give_back("A104"))
print(" waiting for A104 now:", desk.reservations.waiting("A104"))
print()
print("5. UNDO, which is the stack:")
print(" the stack holds", len(desk.undo), "action(s); the top is",
desk.undo.peek())
print(" ", desk.undo_last())
print(" ", desk.undo_last())
print(" the stack holds", len(desk.undo), "action(s) now")
print()
print("6. MOST BORROWED, which is the sorted array:")
for count, title, accession in desk.most_borrowed(3):
print(f" {count} loan(s) {title} ({accession})")1. LOOKUP by accession number, which is the hash table:
A104 -> ('Data Structures', 'Karumanchi')
A115 -> ('Introduction to Algorithms', 'Cormen')
A999 -> not in the catalogue
2. the CATALOGUE IN TITLE ORDER, which is the tree's in-order walk:
Data Structures ['A104']
Introduction to Algorithms ['A115']
Learning Python ['A102']
Let Us Python ['A101']
Operating System Concepts ['A109']
Python: The Complete Reference ['A107']
3. ISSUING, and the RESERVATION QUEUE when a book is already out:
issued 'Data Structures' to Aarti
'Data Structures' is out with Aarti; Bhavesh is number 1 in the queue
'Data Structures' is out with Aarti; Chetan is number 2 in the queue
issued 'Let Us Python' to Aarti
'Let Us Python' is out with Aarti; Divya is number 1 in the queue
waiting for A104: ['Bhavesh', 'Chetan']
4. RETURNING, and the queue serving itself:
'Data Structures' returned by Aarti and issued straight to Bhavesh, who was first in the queue
waiting for A104 now: ['Chetan']
'Data Structures' returned by Bhavesh and issued straight to Chetan, who was first in the queue
waiting for A104 now: []
5. UNDO, which is the stack:
the stack holds 4 action(s); the top is ('return', 'A104', 'Bhavesh')
undid the return of 'Data Structures', it is with Bhavesh again
undid the return of 'Data Structures', it is with Aarti again
the stack holds 2 action(s) now
6. MOST BORROWED, which is the sorted array:
3 loan(s) Data Structures (A104)
1 loan(s) Let Us Python (A101)
0 loan(s) Introduction to Algorithms (A115)Practical 10: the Combined Application
Read the reservation lines. Aarti took A104, and Bhavesh and Chetan joined the queue behind her. When Aarti gave it back, the book went straight to Bhavesh, who was first in line, and the queue then held only Chetan. That is FIFO doing the only fair thing, and it is why the reservations are a queue and not anything else.
Read the undo lines too: the last action is undone first, which is LIFO, and that is why undo is a stack.
The graph, which is MU's Course Objective 6
from library import IssueDesk
desk = IssueDesk()
for accession, title, author in [("A104", "Data Structures", "Karumanchi"),
("A101", "Let Us Python", "Kanetkar"),
("A109", "Operating System Concepts", "Silberschatz"),
("A102", "Learning Python", "Lutz"),
("A115", "Introduction to Algorithms", "Cormen")]:
desk.add_book(accession, title, author)
# a small borrowing history: several people, several books
history = [("A104", "Aarti"), ("A101", "Aarti"), ("A104", "Bhavesh"),
("A109", "Bhavesh"), ("A101", "Chetan"), ("A115", "Chetan"),
("A102", "Divya"), ("A115", "Divya"), ("A109", "Eshan")]
for accession, borrower in history:
desk.graph.record(borrower, accession)
print("who has read a book in common with whom:")
for borrower in ["Aarti", "Bhavesh", "Chetan", "Divya", "Eshan"]:
print(f" {borrower:<9} {desk.graph.also_borrowed_by(borrower)}")
print()
print("suggestions for Aarti, from what her neighbours read that she has not:")
for title, who in desk.graph.suggestions("Aarti", desk.catalogue).items():
print(f" {title:<34} read by {', '.join(who)}")
print()
print("everyone reachable from Aarti, breadth first:")
print(" ", desk.graph.connected_group("Aarti"))
print(" note that Eshan is reached, although he shares no book with Aarti:")
print(" Aarti -> Bhavesh (A104) -> Eshan (A109). That is a PATH, and only a")
print(" graph can hold it.")who has read a book in common with whom:
Aarti ['Bhavesh', 'Chetan']
Bhavesh ['Aarti', 'Eshan']
Chetan ['Aarti', 'Divya']
Divya ['Chetan']
Eshan ['Bhavesh']
suggestions for Aarti, from what her neighbours read that she has not:
Introduction to Algorithms read by Chetan
Operating System Concepts read by Bhavesh
everyone reachable from Aarti, breadth first:
['Aarti', 'Bhavesh', 'Chetan', 'Eshan', 'Divya']
note that Eshan is reached, although he shares no book with Aarti:
Aarti -> Bhavesh (A104) -> Eshan (A109). That is a PATH, and only a
graph can hold it.Practical 10: the Combined Application
That last line is the justification. A hash table can say what Aarti borrowed. A tree can list the catalogue. Neither can answer "who is connected to Aarti through somebody else", because the relation is between any two borrowers and has no order and no single key. Only a graph holds it, and the answer is a breadth first search with a seen set, exactly as [Python for Data Structures, and the Cost of an Operation] set out.
What each structure costs, in this program
from library import IssueDesk
desk = IssueDesk()
for n in range(60):
desk.add_book(f"A{n:04d}", f"Title {n:02d}", "Author")
print(f"60 books in the catalogue")
print(f" hash table chain lengths : {desk.catalogue.chain_lengths()}")
print(f" longest chain : {max(desk.catalogue.chain_lengths())}")
print(f" load factor : {len(desk.catalogue) / 11:.2f}")
print(f" title tree height : {desk.titles.height()}")
print()
print("finding a title in the tree, with the comparisons:")
for title in ["Title 00", "Title 30", "Title 59", "Title 99"]:
accessions, comparisons = desk.titles.find(title)
found = accessions if accessions else "not found"
print(f" {title:<10} {str(found):<12} {comparisons} comparison(s)")60 books in the catalogue
hash table chain lengths : [6, 5, 6, 5, 6, 5, 6, 5, 5, 6, 5]
longest chain : 6
load factor : 5.45
title tree height : 59
finding a title in the tree, with the comparisons:
Title 00 ['A0000'] 1 comparison(s)
Title 30 ['A0030'] 31 comparison(s)
Title 59 ['A0059'] 60 comparison(s)
Title 99 not found 60 comparison(s)Note the tree's height. The titles were inserted in ascending order, which is exactly the degenerate case of [Practical 5: the Binary Search Tree, Create, Insert and Search], so the tree is a linked list and the searches cost a walk rather than a halving.
That is a real defect in this program, honestly reported, and the cure is the one that chapter gives:
from library import IssueDesk
import random
random.seed(7)
titles = [f"Title {n:02d}" for n in range(60)]
ordered = IssueDesk()
for n, title in enumerate(titles):
ordered.add_book(f"A{n:04d}", title, "Author")
shuffled_titles = titles[:]
random.shuffle(shuffled_titles)
shuffled = IssueDesk()
for n, title in enumerate(shuffled_titles):
shuffled.add_book(f"B{n:04d}", title, "Author")
print(f" titles added in ascending order : height {ordered.titles.height()}")
print(f" titles added in shuffled order : height {shuffled.titles.height()}")
print()
for desk, label in [(ordered, "ascending"), (shuffled, "shuffled ")]:
_, comparisons = desk.titles.find("Title 59")
print(f" finding the last title, {label}: {comparisons} comparison(s)")
print()
print("both list the catalogue in the same order, which is the point:")
print(" ", [t for t, _ in ordered.sorted_catalogue()][:5], "...")
print(" ", [t for t, _ in shuffled.sorted_catalogue()][:5], "...")Practical 10: the Combined Application
titles added in ascending order : height 59
titles added in shuffled order : height 10
finding the last title, ascending: 60 comparison(s)
finding the last title, shuffled : 5 comparison(s)
both list the catalogue in the same order, which is the point:
['Title 00', 'Title 01', 'Title 02', 'Title 03', 'Title 04'] ...
['Title 00', 'Title 01', 'Title 02', 'Title 03', 'Title 04'] ...Both trees give the same sorted catalogue and one of them takes far fewer comparisons to search. In a real desk the books arrive in accession order, not title order, so the tree comes out reasonably balanced by itself; adding a batch from a sorted file is the case to watch for.
A second design, so you can pick either
MU's row is open, so here is a different application with different justifications. Write whichever you prefer; what is marked is the reasoning.
| Need | Structure | Why |
|---|---|---|
| an expression typed by the user | stack | infix to postfix and then evaluation, practical 3 |
| the variables it uses | hash table | looked up by name, constantly, O(1) |
| the history of expressions entered | stack | the most recent is recalled first |
| the results, in numeric order | BST | the in-order walk is the sorted list of answers |
| the pending calculations in a batch | queue | they are worked in the order they were submitted |
from collections import deque
PRECEDENCE = {"+": 1, "-": 1, "*": 2, "/": 2}
def to_postfix(tokens, variables):
"""Stack of operators; names are looked up in the hash table on the way out."""
output, stack = [], []
for token in tokens:
if token in PRECEDENCE:
while stack and stack[-1] != "(" and \
PRECEDENCE[stack[-1]] >= PRECEDENCE[token]:
output.append(stack.pop())
stack.append(token)
elif token == "(":
stack.append(token)
elif token == ")":
while stack and stack[-1] != "(":
output.append(stack.pop())
stack.pop()
else:
output.append(variables.get(token, token))
while stack:
output.append(stack.pop())
return output
def evaluate(postfix):
stack = []
for token in postfix:
if token in PRECEDENCE:
right, left = stack.pop(), stack.pop()
stack.append({"+": left + right, "-": left - right,
"*": left * right, "/": left / right}[token])
else:
stack.append(float(token))
return stack.pop()
variables = {"marks": 78, "total": 100, "credits": 4} # a hash table
pending = deque() # a queue
history = [] # a stack
answers = [] # kept sorted
for text in ["marks / total * 100", "marks * credits", "( marks + 10 ) / 2",
"total - marks"]:
pending.append(text)
print("working the queue in the order the expressions were submitted:")
while pending:
text = pending.popleft()
tokens = text.split()
postfix = to_postfix(tokens, variables)
value = evaluate(postfix)
history.append((text, value))
answers.append(value)
print(f" {text:<22} postfix {' '.join(str(t) for t in postfix):<20} "
f"= {value}")
print()
print("the history, most recent first, which is the stack:")
while history:
text, value = history.pop()
print(f" {text:<22} = {value}")
print()
print("the answers in numeric order, which a BST would give as its in-order walk:")
print(" ", sorted(answers))Practical 10: the Combined Application
working the queue in the order the expressions were submitted:
marks / total * 100 postfix 78 100 / 100 * = 78.0
marks * credits postfix 78 4 * = 312.0
( marks + 10 ) / 2 postfix 78 10 + 2 / = 44.0
total - marks postfix 100 78 - = 22.0
the history, most recent first, which is the stack:
total - marks = 22.0
( marks + 10 ) / 2 = 44.0
marks * credits = 312.0
marks / total * 100 = 78.0
the answers in numeric order, which a BST would give as its in-order walk:
[22.0, 44.0, 78.0, 312.0]What to write when the examiner says "design a program"
The answer has three parts and the third is where the marks are.
- The application, in two sentences. Small and concrete.
- What it has to do, as a list of operations, with how often each happens.
- A table: need, structure, why. One row per structure, and the "why" names the operation that
structure makes cheap, not the structure's features.
A good "why" reads: a hash table, because the desk looks a book up by accession number on every transaction and never needs the catalogue in accession order.
A bad "why" reads: a hash table, because hash tables are fast.
Procedure
- Pick an application small enough to write and big enough to need three or more structures.
- Write the operations list first, with how often each one happens. That list chooses the
structures.
- Write the need, structure, why table before writing any code.
- Implement each structure as its own class, with a docstring naming the operation it is there for.
- Tie them together in one class whose methods are the desk's actions.
- Run a realistic session: add records, look one up, list them in order, queue a reservation, return
the item and watch the queue serve itself, undo two actions, and report the top three.
- Print the internal state as you go: the chain lengths, the tree height, the queue contents, the stack
depth.
- Add the graph query that nothing else can answer, and show a path of length two.
- Report each structure's cost in this program, including any defect, such as the tree's height when the
keys arrive sorted.
- Write the justification table into the journal beside the output.
Result
Six books were added to a hash table catalogue and a binary search tree title index. Lookup by accession number searched one bucket's chain. The in-order walk of the tree listed the catalogue in title order. Issuing a book that was already out put the borrower in a queue, and returning it issued the book straight to the first in line, leaving the rest waiting, which is FIFO. Undo removed the two most recent actions in reverse order, which is LIFO. The most borrowed report was a sorted array built from the counts. The graph answered a question none of the others can: Eshan is reachable from Aarti through Bhavesh, although Aarti and Eshan share no book. With 60 titles added in ascending order the tree's height was 59, a degenerate tree, and the same titles shuffled gave a much smaller height and far fewer comparisons while producing the same sorted catalogue.
Practical 10: the Combined Application
Where marks are lost
- No justification. The structures are the easy half; the reasons are the exercise.
- A "why" that describes the structure instead of naming the operation it makes cheap.
- Using every structure on the syllabus whether the application needs it or not.
- One structure doing everything, usually a Python dictionary, with the others mentioned and unused.
- No output. The session run is the evidence that the structures work together.
- Not printing the internal state. The chain lengths, the tree height and the queue contents are what
show the structures are real.
- Not noticing the sorted insertion defect in the tree, or not reporting it.
- A queue served from the wrong end, which makes the reservation list unfair.
For the journal
The aim in MU's words. The operations table first, with how often each happens, because that table is what chooses the structures. Then the need, structure, why table, with each "why" naming an operation. Then the code, each class with its docstring. Then the session run in full, and beside it the internal state: chain lengths, tree height, the queue before and after a return, the stack depth. Then the graph query with the two step path, and one sentence saying no other structure here can answer it. Then the tree height with the titles inserted in order and shuffled, reported as a defect and a cure. The conclusion: each structure was chosen by naming the operation it makes cheap, and the same data in the wrong structure would have made one of the desk's six jobs slow.
Quick revision
- The operations list chooses the structures, not the other way round.
- hash table for lookup by exact key, O(1) average, when order does not matter.
- BST when you need fast lookup and the sorted order, which the in-order walk gives free.
- queue for a waiting list, because FIFO is the only fair rule.
- stack for undo, because the most recent action goes first.
- sorted array for a report built once and then read; sorting once beats keeping order all the time.
- graph when the relation is between any two things, which nothing linear can hold.
- A good justification names the operation: "a hash table, because we look up by accession number on
Practical 10: the Combined Application
every transaction". A bad one names the feature: "because hash tables are fast".
- Print the internal state: chain lengths, tree height, queue contents, stack depth.
- Watch for the tree going degenerate when keys arrive sorted, and say so.
Questions you should be able to answer
1. How do you decide which structures a program needs? Write the list of operations it performs and how often each happens. Each frequent operation names the structure that makes it cheap.
2. Why a hash table for the catalogue? Because the desk looks a book up by its exact accession number on every transaction, which a hash table does in O(1) on average, and it never needs the books in accession order.
3. Why a tree as well, when the hash table already holds everything? Because the catalogue must be listed in title order, and a hash table has no order at all. A binary search tree's in-order walk gives the sorted list for nothing.
4. Why a queue for reservations and not a stack? Because a waiting list is only fair if the first person to ask is the first served, which is FIFO. A stack would serve the most recent request first.
5. Why a stack for undo? Because the most recent action must be undone first, which is LIFO.
6. Why a sorted array for the most borrowed report and not a tree? Because the report is built once a week and then read. Sorting once is cheaper than keeping a structure in order through every loan.
7. What can the graph answer that nothing else here can? Whether two borrowers are connected through other people. In the run, Eshan is reachable from Aarti through Bhavesh although they share no book, and that is a path, which only a graph holds.
8. What was wrong with the title tree when 60 books were added in title order? It degenerated into a linked list with a height of 59, so searching it walked instead of halving. Shuffling the insertion order fixed it and gave the same sorted catalogue.
9. What makes a good justification? Naming the operation the structure makes cheap, and how often that operation happens. Naming a property of the structure instead is what loses the mark.
10. What would you print to show the structures are really there? The hash table's chain lengths and load factor, the tree's height, the queue's contents before and after a return, and the stack's depth.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.