University of Geneva — Algorithmics & Data Structures
Learning Objectives¶
By the end of this seminar you will be able to:
Explain the fundamental properties of arrays, linked lists, stacks, queues, and hash tables
Implement each data structure in Python from first principles
Analyse the time and space complexity of each structure’s core operations
Choose the appropriate data structure for a given algorithmic problem
Slides: Animations for LIFO stacks, FIFO queues, deques, linked-list insertion, and hash chaining are in the lecture deck Data Structures (
docs/_static/slides/en/06_data_structures.htmlwhen built). Extra practice problems from earlier versions of this notebook are kept in06_data_structures_impl_full.ipynb.
Part 1: Theory and implementations¶
1.1 Why Data Structures Matter¶
A data structure is a systematic way of organising data in a computer so that it can be accessed and modified efficiently. Choosing the wrong data structure can be the difference between an algorithm that runs in milliseconds and one that takes hours.
Consider a real-world analogy:
A stack of plates in a cafeteria — you always take from the top and place on top (LIFO).
A queue at a bank — first in, first served (FIFO).
A phone book — sorted alphabetically so you can binary-search by name.
A dictionary/HashMap — look up any word directly without scanning all entries.
Each structure has a sweet spot: operations it performs in constant or logarithmic time, and operations that are expensive.
| Data Structure | Access | Search | Insertion | Deletion | Primary Use Case |
|---|---|---|---|---|---|
| Array | O(1) | O(n) | O(n) amortised O(1) at end | O(n) | Random access, known-size collections |
| Linked List | O(n) | O(n) | O(1) at head | O(1) with reference | Frequent insertions/deletions at ends |
| Stack | O(n) | O(n) | O(1) push | O(1) pop | Undo systems, expression parsing, DFS |
| Queue | O(n) | O(n) | O(1) enqueue | O(1) dequeue | BFS, scheduling, buffering |
| Hash Table | O(1) avg | O(1) avg | O(1) avg | O(1) avg | Fast lookup by key, caching, sets |
Note: O(1) amortised for Python list append means that occasionally the list must resize (O(n)), but averaged over many operations the cost per operation is constant.
1.2 Arrays in Python¶
Python’s built-in list is a dynamic array. It stores references to objects contiguously in memory, enabling O(1) random access by index.
Key properties:
Random access:
arr[i]is O(1) because the address of elementiisbase_address + i * element_size.Append: Amortised O(1) — Python over-allocates capacity (grows by ~12.5% each resize).
Insert at arbitrary position: O(n) — all elements after the insertion point must shift right.
Delete at arbitrary position: O(n) — same reason.
Slides: See the slide Python
listas a dynamic array in the Data Structures deck for a compact diagram of indexing and capacity growth.
1.3 Linked Lists¶
A linked list is a linear data structure in which each element (called a node) contains:
A data field storing the element’s value.
A next reference (reference) to the following node.
The last node’s next is None, marking the end of the list.
Advantages over arrays:
Insertion/deletion at the head (or with a reference to the predecessor) is O(1) — no shifting.
Size is not fixed; nodes are allocated individually on the heap.
Disadvantages:
No random access — to reach element
i, you must traverse from the head: O(n).Extra memory per node for the reference (typically 8 bytes on a 64-bit system).
Poor cache locality compared to arrays (nodes may be scattered in memory).
Example (search). Suppose the list is 3 → 5 → 7 → None and we want to find 7. Starting at the head, we compare at 3, then 5, then 7 — we stop as soon as we find the value. In the worst case (value absent or at the tail) we may visit every node, so search costs O(n).
Given only head, follow next until you find the target or reach None:
def contains(head, target):
cur = head
while cur is not None:
if cur.data == target:
return True
cur = cur.next
return False
# Worst case: visit every node → O(n)# ---------------------------------------------------------------------------
# Singly Linked List implementation
# ---------------------------------------------------------------------------
class Node:
"""A single node in a singly linked list."""
def __init__(self, data):
self.data = data # The payload stored in this node
self.next = None # reference to the next node (None = end of list)
class LinkedList:
"""Singly linked list: O(1) prepend at head, O(n) append at tail (traverse to end)."""
def __init__(self):
self.head = None # The first node; None when the list is empty
self.size = 0 # Track length to avoid O(n) traversal for len()
# ------------------------------------------------------------------
# Insertion operations
# ------------------------------------------------------------------
def prepend(self, data):
"""Insert a new node at the FRONT of the list — O(1)."""
new_node = Node(data)
new_node.next = self.head # New node points to old head
self.head = new_node # Head now points to new node
self.size += 1
def append(self, data):
"""Insert a new node at the BACK of the list — O(n)."""
new_node = Node(data)
if self.head is None: # Empty list: new node becomes head
self.head = new_node
else:
# Traverse to the last node
current = self.head
while current.next is not None:
current = current.next
current.next = new_node # Attach new node at the tail
self.size += 1
def insert_after(self, target_data, new_data):
"""Insert new_data immediately after the first node with target_data — O(n)."""
current = self.head
while current is not None:
if current.data == target_data:
new_node = Node(new_data)
new_node.next = current.next
current.next = new_node
self.size += 1
return
current = current.next
raise ValueError(f"Value {target_data} not found in list.")
# ------------------------------------------------------------------
# Deletion
# ------------------------------------------------------------------
def delete(self, data):
"""Remove the first node containing data — O(n)."""
if self.head is None:
raise IndexError("Cannot delete from an empty list.")
# Special case: the node to delete is the head
if self.head.data == data:
self.head = self.head.next
self.size -= 1
return
# General case: traverse until we find the predecessor of the target node
current = self.head
while current.next is not None:
if current.next.data == data:
current.next = current.next.next # Bypass the target node
self.size -= 1
return
current = current.next
raise ValueError(f"Value {data} not found in list.")
# ------------------------------------------------------------------
# Traversal / utility
# ------------------------------------------------------------------
def to_list(self):
"""Return the linked list contents as a Python list — O(n)."""
result = []
current = self.head
while current is not None:
result.append(current.data)
current = current.next
return result
def __repr__(self):
return ' -> '.join(str(v) for v in self.to_list()) + ' -> None'
# ---------------------------------------------------------------------------
# Demonstration
# ---------------------------------------------------------------------------
ll = LinkedList()
for val in [10, 20, 30, 40, 50]:
ll.append(val)
print("After appending 10..50 :", ll)
ll.prepend(5)
print("After prepend(5) :", ll)
ll.insert_after(20, 25)
print("After insert_after(20,25):", ll)
ll.delete(25)
print("After delete(25) :", ll)After appending 10..50 : 10 -> 20 -> 30 -> 40 -> 50 -> None
After prepend(5) : 5 -> 10 -> 20 -> 30 -> 40 -> 50 -> None
After insert_after(20,25): 5 -> 10 -> 20 -> 25 -> 30 -> 40 -> 50 -> None
After delete(25) : 5 -> 10 -> 20 -> 30 -> 40 -> 50 -> None
1.4 Stacks¶
A stack is an abstract data type that follows the Last-In, First-Out (LIFO) principle. The last element pushed onto the stack is the first one popped off.
Core operations:
| Operation | Description | Complexity |
|---|---|---|
push(x) | Add element x to the top | O(1) |
pop() | Remove and return the top element | O(1) |
peek() / top() | Return top element without removing it | O(1) |
is_empty() | True if the stack has no elements | O(1) |
size() | Number of elements | O(1) |
Applications: function call stack, undo/redo, expression evaluation, bracket matching, DFS.
# ---------------------------------------------------------------------------
# Stack implementation using a Python list as the underlying storage.
# The END of the list is the TOP of the stack (append/pop are both O(1)).
# ---------------------------------------------------------------------------
class Stack:
"""LIFO stack backed by a Python list."""
def __init__(self, capacity=None):
"""
Parameters
----------
capacity : int or None
Maximum number of elements (None = unbounded).
"""
self._data = [] # Internal storage
self._capacity = capacity # Optional fixed capacity
def push(self, item):
"""Push item onto the top of the stack — O(1) amortised."""
if self._capacity is not None and len(self._data) >= self._capacity:
raise OverflowError("Stack overflow: capacity exceeded.")
self._data.append(item)
def pop(self):
"""Remove and return the top element — O(1)."""
if self.is_empty():
raise IndexError("Stack underflow: cannot pop from an empty stack.")
return self._data.pop() # list.pop() removes from the end in O(1)
def peek(self):
"""Return the top element without removing it — O(1)."""
if self.is_empty():
raise IndexError("Stack is empty.")
return self._data[-1]
def is_empty(self):
"""Return True if the stack contains no elements — O(1)."""
return len(self._data) == 0
def size(self):
"""Return the number of elements in the stack — O(1)."""
return len(self._data)
def __repr__(self):
if self.is_empty():
return "Stack(empty)"
return "Stack(bottom -> top): " + str(self._data)
# Quick demo (no plotting — see slides for LIFO animation)
s = Stack()
for v in [10, 20, 30, 40]:
s.push(v)
print(s)
Stack(bottom -> top): [10, 20, 30, 40]
Application: Balanced parentheses¶
Problem: does every ( get a matching ) in the right order?
((()))— OK (every pair closes in order).(()(— not OK (one(never gets a matching)).())— not OK (a)appears when there is no(left to match).
Rule of thumb: every ) must match some ( that appeared earlier in the string.
Why a stack: the stack remembers open brackets still waiting for a partner. The top of the stack is always the ( that must match the next ). That is exactly LIFO — same idea as the push/pop demo above.
Walking through ((()))¶
Rule: on ( → push; on ) → pop one ( from the stack. At the end the stack must be empty.
Start with an empty stack.
See
(→ push → stack holds one(.See
(→ push → stack holds two(.See
(→ push → stack holds three(.See
)→ pop → two(left.See
)→ pop → one(left.See
)→ pop → stack is empty.End of string and stack empty → balanced.
Walking through (()(¶
Same rule: ( push, ) pop. String: (()(
See
(→ push → one(on the stack.See
(→ push → two(on the stack.See
)→ pop → one(on the stack.See
(→ push → two(on the stack.End of string — but the stack is not empty.
So we still have unclosed
(→ not balanced.
def is_balanced(expr):
stack = Stack()
for ch in expr:
if ch == '(':
stack.push(ch)
elif ch == ')':
if stack.is_empty():
return False # ')' with no matching '('
stack.pop()
return stack.is_empty() # True only if all '(' were matched
print(is_balanced('((()))')) # True
print(is_balanced('(()(')) # False
print(is_balanced('())')) # False
True
False
False
1.5 Queues¶
A queue is an abstract data type that follows the First-In, First-Out (FIFO) principle. Think of a queue at the checkout: the first person to arrive is the first to be served.
| Operation | Description | Complexity |
|---|---|---|
enqueue(x) | Add x to the BACK of the queue | O(1) |
dequeue() | Remove and return the element at the FRONT | O(1) |
peek() | Inspect front element without removing | O(1) |
is_empty() | True if queue is empty | O(1) |
Important: Do NOT use a plain Python list with list.pop(0) as a queue — that is O(n) because all elements must shift. Instead, use collections.deque which provides O(1) operations at both ends.
Applications: BFS graph traversal, OS process scheduling, print spooling, network packet buffering.
from collections import deque
# ---------------------------------------------------------------------------
# Queue implementation using collections.deque
# deque (double-ended queue) provides O(1) appendleft and pop from either end.
# We use: append() to enqueue at the right, popleft() to dequeue from the left.
# ---------------------------------------------------------------------------
class Queue:
"""FIFO queue backed by collections.deque."""
def __init__(self):
self._data = deque() # deque gives O(1) at both ends
def enqueue(self, item):
"""Add item to the BACK of the queue — O(1)."""
self._data.append(item)
def dequeue(self):
"""Remove and return the FRONT element — O(1)."""
if self.is_empty():
raise IndexError("Queue underflow: cannot dequeue from an empty queue.")
return self._data.popleft()
def peek(self):
"""Return the front element without removing it — O(1)."""
if self.is_empty():
raise IndexError("Queue is empty.")
return self._data[0]
def is_empty(self):
return len(self._data) == 0
def size(self):
return len(self._data)
def __repr__(self):
return "Queue(front -> back): " + str(list(self._data))
# ---------------------------------------------------------------------------
# Demonstrate FIFO vs LIFO
# ---------------------------------------------------------------------------
print("=== QUEUE (FIFO) ===")
q = Queue()
for v in [1, 2, 3, 4, 5]:
q.enqueue(v)
print(f" enqueue({v}) -> {q}")
print()
for _ in range(3):
val = q.dequeue()
print(f" dequeue() -> {val} | {q}")
print()
print("=== STACK (LIFO) ===")
st = Stack()
for v in [1, 2, 3, 4, 5]:
st.push(v)
print(f" push({v}) -> {st}")
print()
for _ in range(3):
val = st.pop()
print(f" pop() -> {val} | {st}")=== QUEUE (FIFO) ===
enqueue(1) -> Queue(front -> back): [1]
enqueue(2) -> Queue(front -> back): [1, 2]
enqueue(3) -> Queue(front -> back): [1, 2, 3]
enqueue(4) -> Queue(front -> back): [1, 2, 3, 4]
enqueue(5) -> Queue(front -> back): [1, 2, 3, 4, 5]
dequeue() -> 1 | Queue(front -> back): [2, 3, 4, 5]
dequeue() -> 2 | Queue(front -> back): [3, 4, 5]
dequeue() -> 3 | Queue(front -> back): [4, 5]
=== STACK (LIFO) ===
push(1) -> Stack(bottom -> top): [1]
push(2) -> Stack(bottom -> top): [1, 2]
push(3) -> Stack(bottom -> top): [1, 2, 3]
push(4) -> Stack(bottom -> top): [1, 2, 3, 4]
push(5) -> Stack(bottom -> top): [1, 2, 3, 4, 5]
pop() -> 5 | Stack(bottom -> top): [1, 2, 3, 4]
pop() -> 4 | Stack(bottom -> top): [1, 2, 3]
pop() -> 3 | Stack(bottom -> top): [1, 2]
1.6 Hash tables (dictionaries — the idea)¶
You already use dictionaries in Python: scores["Ada"] = 91. The question is: how does Python find "Ada" without scanning every name?
Imagine a row of numbered lockers (0, 1, 2, …). You have a key (e.g. a student name) and a value (e.g. a grade). The hash table picks a locker number from the key, stores the pair there, and later opens that same locker to read it back. If picking the number is fast, lookup feels “instant” — that is the average O(1) behaviour you see in the big table in 1.1.
Three steps in words:
Turn the key into a big integer using a hash function (in Python: the built-in
hash()— see the next code cell).Turn that integer into a small locker index, e.g.
hash(key) % M, whereMis how many lockers you have.Store or look up the
(key, value)pair in that locker.
Collision means two different keys end up wanting the same locker. That is normal; the table must handle it. Two common ideas: keep a short list of pairs in each locker (chaining — what SimpleHashMap below does), or search the next free locker (open addressing — what CPython’s built-in dict uses internally). You do not need the details of open addressing for this course; knowing that collisions exist is enough.
Python’s dict and set are highly optimised hash tables. Our SimpleHashMap is a small teaching version with chaining, but it uses the same idea: hash(key) to choose a bucket.
# ---------------------------------------------------------------------------
# Python's built-in hash() — used by dict, set, and our SimpleHashMap
# ---------------------------------------------------------------------------
# hash() maps a key to an integer. In one run of the program, the same key
# always gets the same hash (so lookups are consistent).
print("hash('hello') =", hash("hello"))
print("hash('hello') again =", hash("hello")) # same value in one run
print("hash(42) =", hash(42))
print("hash((1, 2)) =", hash((1, 2))) # tuple OK if contents are hashable
# Tables only have finitely many "lockers" (buckets). Modulo picks which one:
M = 8
for key in ["cat", "dog", "bird", "Ada", "Bob"]:
bucket = hash(key) % M
print(f"{key!r:6} -> bucket {bucket}")
# Only *hashable* types work — lists and dicts are not allowed as dict keys:
# hash([]) # TypeError: unhashable type: 'list'
hash('hello') = 3758311864781760680
hash('hello') again = 3758311864781760680
hash(42) = 42
hash((1, 2)) = -3550055125485641917
'cat' -> bucket 7
'dog' -> bucket 0
'bird' -> bucket 2
'Ada' -> bucket 4
'Bob' -> bucket 3
# ---------------------------------------------------------------------------
# A simple hash map from scratch using SEPARATE CHAINING for collision resolution.
# Each bucket is a list of (key, value) pairs (a chain).
# ---------------------------------------------------------------------------
class SimpleHashMap:
"""
Hash map with separate chaining.
Average-case O(1) get/set/delete (assuming a good hash and low load factor).
"""
DEFAULT_CAPACITY = 16 # Number of buckets; should be a power of 2
LOAD_FACTOR_THRESHOLD = 0.75 # Resize when this fraction of buckets are occupied
def __init__(self, capacity=DEFAULT_CAPACITY):
self._capacity = capacity
self._buckets = [[] for _ in range(self._capacity)] # Each bucket = a chain (list)
self._size = 0 # Number of key-value pairs stored
# ------------------------------------------------------------------
# Internal hash function
# ------------------------------------------------------------------
def _bucket_index(self, key):
"""Map a key to a bucket index using Python's built-in hash."""
return hash(key) % self._capacity
# ------------------------------------------------------------------
# Core operations
# ------------------------------------------------------------------
def put(self, key, value):
"""
Insert or update key -> value.
Average O(1); O(n) worst case (all keys collide into one bucket).
"""
idx = self._bucket_index(key)
chain = self._buckets[idx]
# Check if key already exists in the chain -> update
for i, (k, v) in enumerate(chain):
if k == key:
chain[i] = (key, value) # Update value in place
return
# Key not found -> append new pair
chain.append((key, value))
self._size += 1
# Resize if load factor exceeded
if self._size / self._capacity > self.LOAD_FACTOR_THRESHOLD:
self._resize()
def get(self, key, default=None):
"""Return the value associated with key, or default if not found."""
idx = self._bucket_index(key)
for k, v in self._buckets[idx]:
if k == key:
return v
return default
def delete(self, key):
"""Remove key from the map. Raise KeyError if not found."""
idx = self._bucket_index(key)
chain = self._buckets[idx]
for i, (k, v) in enumerate(chain):
if k == key:
chain.pop(i)
self._size -= 1
return
raise KeyError(key)
def _resize(self):
"""Double capacity and rehash all entries — O(n)."""
old_buckets = self._buckets
self._capacity *= 2
self._buckets = [[] for _ in range(self._capacity)]
self._size = 0
for chain in old_buckets:
for k, v in chain:
self.put(k, v) # Re-insert into new bucket array
def load_factor(self):
return self._size / self._capacity
def __repr__(self):
items = []
for chain in self._buckets:
items.extend(chain)
return '{' + ', '.join(f'{k!r}: {v!r}' for k, v in items) + '}'
# ---------------------------------------------------------------------------
# Demonstrate the hash map
# ---------------------------------------------------------------------------
hm = SimpleHashMap(capacity=8)
students = [('Alice', 92), ('Bob', 85), ('Charlie', 78), ('Diana', 95), ('Eve', 88)]
for name, grade in students:
hm.put(name, grade)
print("HashMap:", hm)
print(f"Load factor: {hm.load_factor():.2f}")
print(f"get('Alice') = {hm.get('Alice')}")
print(f"get('Zara') = {hm.get('Zara', 'Not found')}")
hm.put('Alice', 97) # Update existing key
print(f"After update, get('Alice') = {hm.get('Alice')}")
HashMap: {'Diana': 95, 'Bob': 85, 'Alice': 92, 'Charlie': 78, 'Eve': 88}
Load factor: 0.62
get('Alice') = 92
get('Zara') = Not found
After update, get('Alice') = 97
Part 2: Exercises¶
Work through 2.1–2.5 in order. Use only plain Python lists, or the LinkedList, Stack, Queue, and SimpleHashMap classes from this notebook unless the task says otherwise. Add your own assert checks where it helps.
These exercises are guided: complete the lines marked # YOUR CODE or replace pass. If you are stuck, re-read the matching theory section (Section 1.2 for 2.1, 1.3 for 2.2, 1.4 for 2.3, 1.5 for 2.4, 1.6 for 2.5).
At a glance: 2.1 plain lists · 2.2 LinkedList · 2.3 Stack · 2.4 Queue · 2.5 SimpleHashMap — each exercise practises one idea from Part 1.
Exercise 2.1 — Merge two sorted lists¶
Given two sorted lists a and b (non-decreasing order), return a new sorted list that contains all elements from both, in O(n) time where n = len(a) + len(b).
Use two indices (merge like in merge-sort). Do not call sorted(a + b) as your solution.
Recipe (merge like two sorted piles of cards):
Set
i, j = 0, 0and an empty listout.While both
iandjare still valid indices: comparea[i]andb[j], append the smaller value toout, and advance the index you took from.When one list is exhausted, extend
outwith everything left in the other list (two shortwhileloops).
Examples:
assert merge_sorted([1, 3, 5], [2, 4, 6]) == [1, 2, 3, 4, 5, 6]
assert merge_sorted([], [1, 2]) == [1, 2]
assert merge_sorted([3], [1, 2]) == [1, 2, 3]def merge_sorted(a: list, b: list) -> list:
"""Return merged sorted list in O(len(a)+len(b))."""
out = []
i, j = 0, 0
# Merge while both lists still have elements
while i < len(a) and j < len(b):
if a[i] <= b[j]:
# YOUR CODE: append a[i] to out and increment i
pass
else:
# YOUR CODE: append b[j] to out and increment j
pass
# Remainder of the longer list
while i < len(a):
# YOUR CODE: append from a
pass
while j < len(b):
# YOUR CODE: append from b
pass
return out
Exercise 2.2 — Reverse a linked list¶
Implement reverse_linked_list(ll: LinkedList) -> None so that after the call, ll traverses backwards compared to before. ll.size stays the same; update ll.head correctly.
Minimum target: the list reads in reverse order (e.g. 1 -> 2 -> 3 becomes 3 -> 2 -> 1).
Choose one track:
Level A (easier): allowed solution — read values with
to_list(), reset the list (head = None,size = 0), then rebuild usingprependin reverse order. Good if pointer juggling feels unclear.Level B (pointer exercise): rewire
nextpointers only — O(n) time, O(1) extra space. Keep three names:prev,cur,nxt(hint: savenxtbefore you overwritecur.next).
Example: After reverse_linked_list(ll) on 1 -> 2 -> 3 -> None, you should have 3 -> 2 -> 1 -> None.
def reverse_linked_list(ll: "LinkedList") -> None:
"""Reverse nodes; update ll.head (ll.size unchanged)."""
# --- Level A (easier): rebuild using to_list + prepend (default skeleton) ---
vals = ll.to_list()
ll.head = None
ll.size = 0
for v in reversed(vals):
pass # YOUR CODE: prepend v onto ll
# --- Level B (pointer reverse): comment out Level A above, then implement ---
# prev = None
# cur = ll.head
# while cur is not None:
# nxt = cur.next
# cur.next = prev
# prev = cur
# cur = nxt
# ll.head = prev
Exercise 2.3 — Balanced brackets with a stack¶
Implement is_balanced_brackets(expression: str) -> bool using the Stack class from this notebook.
Support ( ), [ ], and { }. Each closing bracket must match the most recent unmatched opening bracket (see Section 1.4).
Closing → opening (for checking after a pop):
| Closing | Opening |
|---|---|
) | ( |
] | [ |
} | { |
Characters: only bracket characters participate — ignore spaces, letters, and digits (do not push them on the stack).
| Expression | Balanced? |
|---|---|
"{[]()}" | Yes |
"([)]" | No |
"((()))" | Yes |
Also: In one sentence, explain why a queue would be a poor ADT for this problem (optional comment in your cell is fine).
def is_balanced_brackets(expression: str) -> bool:
"""True if (), [], {} are all matched in LIFO order."""
pairs = {")": "(", "]": "[", "}": "{"}
stack = Stack()
for ch in expression:
if ch in "([{":
stack.push(ch)
elif ch in ")]}":
if stack.is_empty():
return False
# YOUR CODE: pop opening bracket and compare to pairs[ch]
pass
# else: ignore non-bracket characters
return stack.is_empty()
Exercise 2.4 — Hot potato (queue)¶
Implement hot_potato(names: list[str], k: int) -> str using the Queue class from Section 1.5.
Algorithm:
Enqueue every name.
While more than one player remains:
Repeat
k - 1times:dequeuethe front person and enqueue them at the back (rotate).Once more:
dequeuethe front person — they are eliminated.
Return the last remaining name.
Trace with names = ["A", "B", "C"] and k = 2:
After one elimination round you should remove B. Continue until only C wins.
assert hot_potato(["A", "B", "C"], 2) == "C"def hot_potato(names: list[str], k: int) -> str:
"""Last remaining name using Queue and the rules in Exercise 2.4."""
q = Queue()
for n in names:
q.enqueue(n)
while q.size() > 1:
for _ in range(k - 1):
# YOUR CODE: move front player to the rear (same pattern as the slides)
pass
# YOUR CODE: eliminate the player at the front after those rotations
pass
return q.dequeue()
Exercise 2.5 — Word counts with SimpleHashMap¶
Implement word_counts(text: str) -> SimpleHashMap. Split text on whitespace (split()), count how many times each word appears.
Steps: create a SimpleHashMap → for each word → get the current count (use 0 if missing) → put the count plus one.
Use only SimpleHashMap from this notebook — no built-in dict, collections.Counter, or defaultdict.
Example:
m = word_counts("cat dog cat")
# After your implementation: get("cat") -> 2, get("dog") -> 1def word_counts(text: str) -> "SimpleHashMap":
"""Word frequencies using SimpleHashMap only."""
m = SimpleHashMap()
for w in text.split():
c = m.get(w, 0)
# YOUR CODE: store count + 1 for this word
pass
return m