Week 6 follows chapter 4 in the Book "Problem Solving with Algorithms and Data Structures using Python", so we encourage you to read it as homework.
class Stack:
def __init__(self):
self._data = [] # internal list — hidden from caller
def push(self, item): # O(1)
self._data.append(item)
def pop(self): # O(1)
if self.is_empty():
raise IndexError('Stack underflow')
return self._data.pop()
def peek(self): # O(1) — look without removing
return self._data[-1]
def is_empty(self):
return len(self._data) == 0
def size(self):
return len(self._data)
def __repr__(self):
return f'Stack({self._data})'
Rule of thumb: never use list.pop(0) as a queue — use collections.deque.
# Every pop(0) shifts ALL elements left
queue = []
queue.append('A') # enqueue
queue.append('B')
queue.pop(0) # O(n) — shifts B
# At n = 1,000,000 items:
# each pop moves 999,999 elements!from collections import deque
q = deque()
q.append('A') # enqueue rear
q.append('B')
q.popleft() # O(1) — no shift
# deque is a doubly-linked list
# internally — O(1) at both ends.from collections import deque
class Queue:
def __init__(self):
self._data = deque()
def enqueue(self, item): # O(1)
self._data.append(item)
def dequeue(self): # O(1)
if self.is_empty():
raise IndexError('Queue underflow')
return self._data.popleft()
def peek(self):
return self._data[0]
def size(self):
return len(self._data)
def is_empty(self):
return len(self._data) == 0
Same parameter k as Exercise 2.4: each round, k−1 rotations then eliminate the front.
def hot_potato(names, k):
q = Queue()
for name in names:
q.enqueue(name)
while q.size() > 1:
for _ in range(k - 1): # rotate front to rear
q.enqueue(q.dequeue())
q.dequeue() # eliminate front
return q.dequeue()
names = ['Bill','David','Susan','Jane','Kent','Brad']
print(hot_potato(names, 8)) # → 'Susan' (same as seminar notebook with this k)Key pattern: dequeue from front, re-enqueue at rear — perfect FIFO round-robin.
from collections import deque
class Deque:
def __init__(self):
self._data = deque()
def add_front(self, item): # O(1)
self._data.appendleft(item)
def add_rear(self, item): # O(1)
self._data.append(item)
def remove_front(self): # O(1)
if self.is_empty():
raise IndexError('Deque underflow')
return self._data.popleft()
def remove_rear(self): # O(1)
if self.is_empty():
raise IndexError('Deque underflow')
return self._data.pop()
def peek_front(self):
return self._data[0]
def peek_rear(self):
return self._data[-1]
def size(self):
return len(self._data)
def is_empty(self):
return len(self._data) == 0
Core state: references to head (and optionally tail) plus a logical size.
LinkedListADT
+ is_empty() -> bool
+ size() -> int
+ prepend(x: T) -> None # O(1)
+ append(x: T) -> None # O(1) with tail, else O(n)
+ remove_front() -> T # O(1)
+ find(x: T) -> bool # O(n)
+ insert_after(target, x) # O(n) to find target
Performance idea: fast updates at known positions, but sequential traversal for search/index access.
Use linked lists when many inserts/deletes happen near ends or around known nodes.
| Operation | Cost | Why |
|---|---|---|
| prepend (insert at head) | O(1) | Redirect head reference only |
| append (insert at tail) | O(n) | Must traverse to last node |
| search(value) | O(n) | Follow next references one by one |
| delete(value) | O(n) | Must find node first, then redirect |
| access by index | O(n) | No address arithmetic — must traverse |
| insert after known node | O(1) | Redirect two references; node already in hand |
next only.prev reference → O(1) insert/delete at both ends.collections.deque is under the hood.deque.appendleft() in Python, you are calling a doubly linked list insert.from collections import deque
d = deque([10, 20, 30])
d.appendleft(5) # insert at front — O(1)
d.append(40) # insert at rear — O(1)
d.popleft() # → 5
d.pop() # → 40
# 1. Get a giant integer using Python's hash()
print(hash('alice')) # e.g. 4632849283746523
# 2. Our hash table has capacity = 10 buckets (indices 0 to 9).
# How do we fit this giant number into our smaller array?
# Solution: modulo (%) gives the remainder.
capacity = 10
bucket_for_alice = hash('alice') % capacity # remainder by 10 is 3
bucket_for_bob = hash('bob') % capacity # remainder by 10 is 7
# Any huge number % 10 will ALWAYS be in 0..9.
# So, 'alice' goes to bucket 3, 'bob' goes to bucket 7.
# Key properties of a good hash function:
# Deterministic: same input → always same output
# Fast to compute: O(1)
# Spreads values evenly: minimizes collisions
hash('cat') % 10 == 4 AND hash('bat') % 10 == 4SimpleHashMap: chain = self._buckets[idx].dict / set use open addressing (probing, not chaining per bucket).dict and set is a hash table.hash(key); collisions are resolved by open addressing (probe for another bucket index) — not by scanning a chain like our SimpleHashMap GIF.'key' in my_set follows same logic; presence is still average O(1).dict stores (key, value) pairs; set stores only keys — both give O(1) lookup.| Structure | Access Rule | Add | Remove | Python Built-in |
|---|---|---|---|---|
| Array (list) | Index based | append at end | pop/remove | list |
| Linked List | Sequential traversal | O(1) at head | O(n) find + unlink | (manual Node class) |
| Stack | LIFO | push → top | pop ← top | list (append/pop) |
| Queue | FIFO | enqueue → rear | dequeue ← front | collections.deque |
| Deque | Both ends | appendleft / append | popleft / pop | collections.deque |
| Structure | Access by index | Search | Insert | Delete |
|---|---|---|---|---|
| Array (list) | O(1) | O(n) | O(n)* | O(n) |
| Linked List | O(n) | O(n) | O(1)† | O(1)† |
| Stack | — | — | O(1) | O(1) |
| Queue (deque) | — | — | O(1) | O(1) |
| Hash Table | — | O(1)* | O(1)* | O(1)* |
* Average case: hash table O(1) assumes a good hash and load factor; worst case O(n) with many collisions. Array insert at arbitrary index is O(n); append at end is amortized O(1).
† Linked list: O(1) insert/delete when you have a reference to the node or act at an end; finding the position is O(n).
sorted(a+b) cheat)to_list + prepend, or pointer prev/cur/nxt(), [], {} checker (LIFO matching)k−1 times, eliminate, repeat until one player remainsSimpleHashMap only (get / put, no dict/Counter)| Feature | Stack | Queue | Deque | Linked List | Hash Table |
|---|---|---|---|---|---|
| Data | LIFO (top only) | FIFO (front to rear) | Both ends | Nodes + next reference | Key to value |
| Implementation idea | Use a list for top push/pop | Use collections.deque | Use collections.deque | Store head reference and Node.next | Use buckets and chaining (dict/set) |
| Access cost | Peek top: O(1), index: O(n) | Peek front: O(1) | Ends: O(1), index: O(n) | Index is O(n) | Lookup avg: O(1) |
| Insert/remove cost | push/pop at top: O(1) | enqueue/dequeue at ends: O(1) | append/popleft/pop: O(1) | insert at head: O(1), search: O(n) | insert/delete/lookup avg: O(1) |
| Typical uses | undo, call stack, bracket checking | BFS, scheduling, print queue | sliding window, palindrome checks | insert at head, reference-based structures | caching, fast membership tests |
| Memory cost | O(n) items | O(n) items | O(n) items | O(n) nodes + reference per node | More space for buckets |
| Example operations | push, pop, peek | enqueue, dequeue, peek | appendleft, append, popleft, pop | prepend, append, search, delete | insert, get, remove |