munotes®

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 doesHow oftenThe operation it needs
look a book up by its accession numberconstantlyfind by exact key
list the catalogue in title orderat the end of the daythe keys in sorted order
hold reservations for a book that is outwhile a book is outfirst come, first served
undo the last few desk actionsrarely, when a mistake is mademost recent first
report the most borrowed titlesonce a weeksorted by a count
suggest what else a borrower might likeon requestwho 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.

NeedStructureWhy that one
find by exact keyhash tableO(1) average. Order does not matter here
catalogue in title orderbinary search treethe in-order walk is sorted, free
reservationsqueueFIFO is the only fair rule for a waiting list
undostackLIFO: the most recent action is undone first
most borrowedsorted arraybuilt once a week and then read; a sort is cheaper than maintaining order
suggestionsgraphthe 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]
munotes.in243

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())
munotes.in244

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: 3
munotes.in245

Practical 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)
munotes.in246

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.
munotes.in247

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], "...")
munotes.in248

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.

NeedStructureWhy
an expression typed by the userstackinfix to postfix and then evaluation, practical 3
the variables it useshash tablelooked up by name, constantly, O(1)
the history of expressions enteredstackthe most recent is recalled first
the results, in numeric orderBSTthe in-order walk is the sorted list of answers
the pending calculations in a batchqueuethey 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))
munotes.in249

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.

  1. The application, in two sentences. Small and concrete.
  2. What it has to do, as a list of operations, with how often each happens.
  3. 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

  1. Pick an application small enough to write and big enough to need three or more structures.
  2. Write the operations list first, with how often each one happens. That list chooses the

structures.

  1. Write the need, structure, why table before writing any code.
  2. Implement each structure as its own class, with a docstring naming the operation it is there for.
  3. Tie them together in one class whose methods are the desk's actions.
  4. 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.

  1. Print the internal state as you go: the chain lengths, the tree height, the queue contents, the stack

depth.

  1. Add the graph query that nothing else can answer, and show a path of length two.
  2. Report each structure's cost in this program, including any defect, such as the tree's height when the

keys arrive sorted.

  1. 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.

munotes.in250

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
munotes.in251

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.

munotes.in252

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.

Report or request
Done!