Abstract data types and representation
An abstract data type specifies operations and behaviour; a data structure implements them. A stack promises last-in-first-out removal regardless of whether it uses an array or links. Separate the operation contract from its cost. Choose around workload: frequent indexed reads, front removals, key lookups and ordered scans favour different structures. An implementation should also specify empty behaviour, duplicate policy and ownership of mutable values.
Is FIFO a representation or a behavioural rule?
Worked solution
A behavioural rule; several representations can implement it.
Arrays and dynamic arrays
An array places elements in indexed slots, enabling constant-time indexed access in the usual RAM model. A dynamic array grows its backing storage when capacity runs out. A single append may copy n entries, yet geometrically increasing capacity gives amortised O(1) append: across many appends, total copies grow only linearly. Inserting at the front shifts entries and is O(n). Python lists are dynamic arrays of references, not packed arbitrary Python objects. Locality often makes sequential array scans practical despite abstractly similar costs elsewhere.
Does amortised O(1) mean every append is O(1)?
Worked solution
No; occasional resizing can be O(n).
Linked lists and local updates
A linked node stores a value and a reference to the next node. Prepending only changes a few references. Indexed access requires following links, so reaching position i costs O(i). Inserting after an already-known node is O(1), but finding that node may cost O(n). Deleting requires access to the predecessor in a singly linked list. Links consume extra memory and can reduce locality. The stack lab makes these references visible rather than hiding the implementation inside a built-in list.
Is finding and deleting an arbitrary linked item always O(1)?
Worked solution
No, locating the item and predecessor can require a scan.
Stacks and queues
Stacks remove the newest item first; queues remove the oldest. Undo history uses a stack; waiting tasks often use a queue. A linked stack pushes and pops at its head in O(1). A deque supports efficient operations at both ends and avoids Python list pop(0), which shifts remaining references. Empty removal needs a defined result or exception. Later, recursion uses a call stack and breadth-first graph search uses a queue; the behavioural order determines the algorithm.
Which structure processes requests in arrival order?
Worked solution
A FIFO queue.
Hash tables, collisions and load
A hash function maps keys to bucket indices. Different keys may collide, so a table must compare original keys within a chain or use another collision strategy. Updating an existing key should replace its value rather than add a duplicate. Expected O(1) lookup depends on appropriate hashing and controlled load; worst-case chains can be O(n). The load factor is entries/buckets. Resize and rehash when necessary. Our byte-sum hash is intentionally poor: anagrams collide. Python dictionaries use a much more sophisticated implementation; do not substitute this toy for them.
Does equal hash imply equal key?
Worked solution
No; compare keys to distinguish collisions.
Common misconceptions
- Counting only pointer changes omits the search needed to locate a node.
- A hash is not a unique identity.
Lab setup
Download each script and run it in a terminal with Python 3.11 or later: python m06_structures.py. On Windows, py -3 is an alternative; on some systems use python3. The labs use only the standard library. Predict the result before running, then complete the variations. Run without -O so assertions remain enabled. Outputs below were captured by the builder. Code and output are identical in both language editions.
Lab 1 — Linked stack and queue
Follow head and next references. Compare the removal order for identical inputs.
"""A linked stack and a FIFO queue with explicit empty behaviour."""
from collections import deque
class Node:
def __init__(self, value, next_node=None):
self.value, self.next = value, next_node
class Stack:
def __init__(self):
self.head = None
def push(self, value):
self.head = Node(value, self.head)
def pop(self):
if self.head is None:
raise IndexError("empty stack")
node = self.head
self.head = node.next
return node.value
stack, queue = Stack(), deque()
for book in ["Dune", "Foundation", "Solaris"]:
stack.push(book); queue.append(book)
print("stack:", [stack.pop() for _ in range(3)])
print("queue:", [queue.popleft() for _ in range(3)])
try:
stack.pop()
except IndexError:
print("empty pop rejected")
stack: ['Solaris', 'Foundation', 'Dune']
queue: ['Dune', 'Foundation', 'Solaris']
empty pop rejected
- Add a peek operation without removal.
- Test one push/pop and an empty queue.
- Draw the chain after two pushes.
Worked solution
peek returns head.value after the same empty check, without updating head. Two pushes A then B give B→A→None. deque.popleft raises IndexError when empty.
Lab 2 — Collision handling
ab and ba share a bucket but remain different keys. The update changes ab's value without growing its chain.
"""A deliberately small chained table makes collisions visible."""
class Table:
def __init__(self, buckets=4):
if buckets < 1:
raise ValueError("positive bucket count required")
self.buckets = [[] for _ in range(buckets)]
def slot(self, key):
return sum(key.encode("utf-8")) % len(self.buckets)
def put(self, key, value):
chain = self.buckets[self.slot(key)]
for i, (stored, _) in enumerate(chain):
if stored == key:
chain[i] = (key, value); return
chain.append((key, value))
def get(self, key):
for stored, value in self.buckets[self.slot(key)]:
if stored == key:
return value
raise KeyError(key)
table = Table()
for key, value in [("ab", 1), ("ba", 2), ("c", 3), ("ab", 4)]:
table.put(key, value)
print("chains:", table.buckets)
assert table.get("ab") == 4 and table.get("ba") == 2
try:
table.get("missing")
except KeyError:
print("missing key rejected")
chains: [[], [], [], [('ab', 4), ('ba', 2), ('c', 3)]]
missing key rejected
- Use one bucket and count scanned entries.
- Add delete with an explicit missing-key rule.
- Double bucket count and reinsert all entries.
Worked solution
One bucket creates a linear chain. Delete searches for a matching key and removes that pair, raising KeyError if absent. Rehash by recomputing each key's slot with the new bucket count; copying old chains unchanged is incorrect.
Exercises with worked solutions
Try before opening the solution. ★ applies an idea; ★★ combines ideas; ★★★ asks for design or proof.
Push A,B,C; pop twice. What remains?
Worked solution
Pops C then B; A remains.
Enqueue A,B,C; dequeue twice.
Worked solution
Removes A then B; C remains.
Compare reading index 500 in an array and singly linked list.
Worked solution
Array: O(1) indexed access. Linked list: about 500 link traversals, O(n) in the general worst case.
There are 18 entries and 8 buckets. Compute load.
Worked solution
18/8=2.25 entries per bucket on average; individual chains can differ greatly.
Can every string map uniquely into 256 buckets?
Worked solution
No. More than 256 distinct strings force collisions by pigeonhole.
If capacities double, bound total copies before capacity 16.
Worked solution
1+2+4+8=15. Generally a geometric sum is less than the final doubled capacity, explaining linear total resize work.
Choose for repeated lookup by immutable book ID with no sorted-order requirement.
Worked solution
A hash map gives expected O(1) lookups; specify missing-key behaviour and key uniqueness. If ordered range queries become important, reconsider with a sorted structure.
Why does O(1) insertion after a known node not mean O(1) insertion by title?
Worked solution
The title query must locate the node first, potentially scanning the whole list.
Self-check quiz
Choose an answer for feedback; reset to retry. A text answer key is available without JavaScript.
A stack is?
list.pop(0) typically costs?
A collision means?
Load factor is?
Linked random access is usually?
Expected constant hash lookup requires?
Answer key
- A — Newest item leaves first.
- B — Remaining references shift.
- C — Collision handling preserves both keys.
- A — It summarises occupancy.
- B — Follow next links.
- C — Worst cases can still be linear.
Guided reading
- Python deque — Compare endpoint operations with list front removal.
Review and the next step
Explain a workload where each structure is appropriate. Trace the stack's references and a collision update. Module 07 uses stacks and queues to traverse recursive structures.
Key terms
| Term | Meaning |
|---|---|
| ADT | An operation-and-behaviour contract. |
| Amortised cost | Cost per operation averaged over a sequence with a worst-case total bound. |
| Collision | Different keys receiving the same bucket index. |