Time Complexity & Big-O
← faster slower →
Pattern → Complexity
| Code pattern | Complexity | Why |
|---|---|---|
| Single loop 0..n | O(n) | n iterations |
| Two sequential loops of n | O(n) | 2n = O(n) |
| Two sequential loops of n and m | O(n+m) | independent |
| Nested loops (both 0..n) | O(n²) | n×n iterations |
| Nested: outer n, inner halved | O(n log n) | n × log n |
i = i // 2 while loop | O(log n) | halved each step |
| Recursive divide-in-half | O(log n) | e.g. binary search |
| Recursion branching factor 2 | O(2ⁿ) | e.g. naive Fibonacci |
Python List Operations
| Operation | Cost |
|---|---|
list[i] | O(1) |
len(list) | O(1) |
list.append(x) | O(1)* |
list.insert(0, x) | O(n) |
list + [x] | O(n) |
x in list | O(n) |
x in set | O(1) |
dict[key] | O(1) |
* amortised; resize is O(n)
Python Built-in Data Structures
Comparison Table
| Type | Ordered | Mutable | Dups | Hash |
|---|---|---|---|---|
list [] | ✅ | ✅ | ✅ | ❌ |
tuple () | ✅ | ❌ | ✅ | ✅ |
set {} | ❌ | ✅* | ❌ | ✅ |
dict {k:v} | ✅ 3.7+ | ✅ | Keys ❌ | ✅ |
*set items must be immutable (hashable)
When to use each
- Tuple — immutable records, dict keys, returning multiple values, faster than list
- Set — deduplicate (
list(set(l))), O(1) membership test - Dict — O(1) lookup by key, counting (
Counter), grouping - List — ordered sequence, index access, dynamic size
Tuple is faster than list because it uses a single memory block vs. list's two-block structure (object info + data pointers).
Stacks
Core Properties
LIFO — Last In, First Out
| Operation | Cost |
|---|---|
| push(x) | O(1) |
| pop() | O(1) |
| peek() | O(1) |
| size() | O(1) |
Implement with: list or collections.deque (preferred — O(1) both ends)
Applications
- Program call stack / recursion
- Undo / redo operations
- Infix → postfix conversion
- Postfix expression evaluation
- Syntax / bracket matching
- DFS (iterative)
- Browser back button
Infix → Postfix Rules
- Operand → copy to output
(→ push to stack)→ pop to output until(- Operator: pop any higher/equal-precedence ops to output, then push
- End: pop all remaining stack → output
Example: (a+b)/(b+c) → ab+bc+/
Precedence: * / > + −
Queues & Priority Queues
Queue — FIFO (First In, First Out)
- Enqueue → add to rear
- Dequeue → remove from front
- Both O(1) with
collections.deque
Applications: task scheduling, BFS, print queues, buffering
Priority Queue
- Every item has a priority; higher priority dequeued first
- Equal priority → FIFO order
- Implementations: array, linked list, heap (best), binary tree
heapqin Python gives a min-heap (negate values for max-heap)
Linked Lists
Operations & Costs
| Operation | Linked List | Array |
|---|---|---|
| Access by index | O(n) | O(1) |
| Search | O(n) | O(n) |
| Insert at head | O(1) | O(n) |
| Insert at tail | O(n)* | O(1)† |
| Delete (given node) | O(1) | O(n) |
*O(1) if tail pointer kept †amortised
Types
- Singly —
value + nextpointer only → forward traversal - Doubly —
prev + value + next→ O(1) delete if node known - Circular — tail's
next→ head; good for round-robin
Use / Avoid
✅ Use when:
- Frequent inserts/deletes in middle
- Unknown size at compile time
- No need for random access
❌ Avoid when:
- Need index / random access
- Memory is constrained (pointer overhead)
- Cache performance matters
Hash Tables
Mechanics
Hash function maps key → integer index.
Example (modulo):
# Table size = 10 sum("John Smith") = 948 948 mod 10 = 8 ← stored at index 8
Good hash function: fast, distributes keys evenly
| Hash Fn | Use |
|---|---|
| MurmurHash | Python dicts/sets |
| SHA-256 | Encryption / signatures |
| Argon2 | Password hashing |
| MD5 | Fast, not secure |
Collision Resolution
Linear Probing
- On collision → try next slot:
(h + i) mod k - Faster when table is sparse
- Risk: clustering
Chaining
- Each slot holds a linked list of entries
- Better when table is nearly full
- Extra memory for pointers
Load Factor
n = occupied entries k = table size
| Language | Default λ | Table size |
|---|---|---|
| Python dict | 2/3 | Power of 2 (init 8) |
| Java HashMap | 0.75 | Power of 2 (init 16) |
When λ exceeds threshold → rehash (create larger table, reinsert all)
Trees & Binary Search Trees
Terminology
- Root — top node, no parent
- Leaf / Terminal — no children
- Internal node — has ≥1 child
- Height — longest root-to-leaf path
- Siblings — same parent
- Edge / Branch — connection between nodes
BST Property
| Operation | Avg | Worst |
|---|---|---|
| Search | O(log n) | O(n)* |
| Insert | O(log n) | O(n)* |
| Delete | O(log n) | O(n)* |
*when tree is fully unbalanced (e.g. sorted input)
BST Deletion (3 Cases)
- No children — set parent pointer to NULL
- One child — parent pointer skips node, points to child
- Two children — replace value with in-order predecessor (or successor), then delete that predecessor (falls to case 1)
BST Insertion Pseudocode
def insert(value): node = Node(value) if root is NULL: root = node; return cur = root while True: if value > cur.value: if cur.right is NULL: cur.right = node; break else: cur = cur.right else: if cur.left is NULL: cur.left = node; break else: cur = cur.left
Tree Traversal Orders
Orders at a Glance
| Order | Pattern | Use case |
|---|---|---|
| Pre-order | Root → L → R | Copy a tree; DFS path |
| In-order | L → Root → R | Sorted output from BST |
| Post-order | L → R → Root | Delete tree (leaves first) |
| BFS / Level | Level by level | Shortest path (unweighted) |
| DFS | Deep first | Cycle detect, connected components |
Example — Tree: A(B(D,E), C(F,G))
| Order | Output |
|---|---|
| Pre-order | A B D E C F G |
| In-order | D B E A F C G |
| Post-order | D E B F G C A |
BFS vs DFS
| BFS | DFS | |
|---|---|---|
| Structure | Queue | Stack / recursion |
| Direction | Level by level | Deep → backtrack |
| Visited array? | ✅ (graphs) | ✅ (graphs) |
Recursive In-Order (Python)
def inorder(root): if root is None: return [] return inorder(root.left) + [root.val] + inorder(root.right)
BFS with Queue (Python)
from collections import deque def bfs(root): q = deque([root]) while q: cur = q.popleft() print(cur.val) if cur.left: q.append(cur.left) if cur.right: q.append(cur.right)
Balanced BST Variants
AVL Tree
First self-balancing BST (1962)
Balance Factor (BF) = Height(L) − Height(R)
Must be −1, 0, or +1 at every node
If |BF| > 1 → rotate to rebalance
Rotations: LL, RR, LR (double), RL (double)
✅ Faster lookups (stricter balance)
❌ Slower inserts/deletes (more rotations)
Used in: databases, Linux memory management
Red-Black Tree
Self-balancing BST with node colours (1972)
Rules:
- Every node is Red or Black
- Root is always Black
- Children of a Red node are Black
- Every root→NULL path has the same number of Black nodes
- All NULL nodes are Black
✅ Faster inserts/deletes (relaxed balance, fewer rotations)
Only 1 bit extra per node (the colour)
Used in: C++ map/set, Linux CFS scheduler, Linux epoll
AVL vs Red-Black
| AVL | Red-Black | |
|---|---|---|
| Lookup | Faster | Slower |
| Insert/Delete | Slower | Faster |
| Balance | Stricter | Relaxed |
| Memory/node | int (BF) | 1 bit |
| Used in | DBs | Lang libs |
Other Variants
| Tree | Key idea | Use |
|---|---|---|
| Splay | Access → root | Caches |
| Heap | Parent ≤/≥ child | Priority queue |
| Treap | BST + heap priority | Randomised sets |
| B-Tree | Multi-key nodes | DB indexing, FS |
Sorting Algorithms
Master Comparison Table
| Algorithm | Best | Average | Worst | Space | In-place | Stable | Approach |
|---|---|---|---|---|---|---|---|
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | ✅ | ✅ | Brute force |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) | ✅ | ❌ | Brute force |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | ❌ | ✅ | D&C |
| Quick Sort | O(n log n) | O(n log n) | O(n²)* | O(log n) | ✅ | ❌ | D&C |
*Quick Sort worst case: when pivot is always min/max (e.g. already sorted input)
Merge Sort — Pseudocode
def merge_sort(lst): if len(lst) <= 1: return lst mid = len(lst) // 2 L = merge_sort(lst[:mid]) R = merge_sort(lst[mid:]) return merge(L, R) def merge(L, R): out, i, j = [], 0, 0 while i < len(L) and j < len(R): if L[i] < R[j]: out.append(L[i]); i += 1 else: out.append(R[j]); j += 1 return out + L[i:] + R[j:]
Quick Sort — Not-in-place
def quick_sort(lst): if len(lst) < 2: return lst pivot = lst[0] # or random choice left = [x for x in lst[1:] if x <= pivot] right = [x for x in lst[1:] if x > pivot] return quick_sort(left) + [pivot] + quick_sort(right)
In-place partition
# pivot = last element; i from low, j from high while i < j: while lst[i] < pivot: i += 1 while lst[j] > pivot: j -= 1 if i < j: lst[i], lst[j] = lst[j], lst[i]; i += 1 recurse(low, i-1); recurse(i, high)
When to choose which algorithm?
| Scenario | Best choice | Why |
|---|---|---|
| Nearly sorted data | Bubble Sort | Best-case O(n) with early exit |
| Minimise swaps | Selection Sort | Only 1 swap per pass |
| Guaranteed O(n log n) | Merge Sort | No worst-case degradation |
| Memory constrained | Quick Sort (in-place) | O(log n) stack space only |
| Large random dataset | Quick Sort | Best average performance in practice |
| Need stable sort | Merge Sort | Preserves equal element order |
Binary Search
Requirements & Complexity
IMPORTANT: Array must be sorted
Complexity: O(log n)
Each step halves the search space.
Iterative (preferred)
def binary_search(lst, target): lo, hi = 0, len(lst) - 1 while lo <= hi: mid = lo + (hi - lo) // 2 # avoids overflow if lst[mid] == target: return mid elif lst[mid] < target: lo = mid + 1 else: hi = mid - 1 return -1 # not found
Recursive Version
def bs_rec(lst, target, lo, hi): if lo > hi: return -1 mid = lo + (hi - lo) // 2 if lst[mid] == target: return mid elif lst[mid] < target: return bs_rec(lst, target, mid+1, hi) else: return bs_rec(lst, target, lo, mid-1)
mid = lo + (hi - lo) // 2 not (lo + hi) // 2 to avoid integer overflow in languages with fixed-size ints.
Divide & Conquer
Strategy
- Divide — break problem into smaller subproblems
- Conquer — solve each recursively
- Combine — merge results
Subproblems are independent (no overlap).
Usually recursive.
D&C Examples
- Merge Sort — split + merge
- Quick Sort — partition + recurse
- Binary Search — split search space
- Majority Element (LC 169):
split → find majority in L and R → count both candidates → return higher
When D&C goes wrong
If subproblems overlap (same sub-calculation repeated), D&C wastes time recomputing.
Example: Naive Fibonacci
fib(5) calls fib(3) twice, fib(2) three times…
→ O(2ⁿ)
→ Use Dynamic Programming instead
Dynamic Programming
Core Idea
Solve overlapping subproblems once, store results — avoid recomputation.
Two key properties required:
- Optimal Substructure — optimal solution built from optimal sub-solutions
- Overlapping Subproblems — same sub-calculations repeated
| D&C | DP | |
|---|---|---|
| Subproblems | Independent | Overlapping |
| Style | Recursive | Recursive or iterative |
| Recomputes? | Yes | No (stores) |
| Examples | Merge/Quick sort | Fibonacci, Knapsack |
Two Approaches
Top-Down (Memoization) — recursive + cache
memo = {} def fib(n): if n in memo: return memo[n] if n <= 1: return n memo[n] = fib(n-1) + fib(n-2) return memo[n] # O(n) time & space
Bottom-Up (Tabulation) — iterative, fill table from small → large
def fib(n): dp = [0, 1] for i in range(2, n+1): dp.append(dp[i-1] + dp[i-2]) return dp[n] # O(n) time & space
0/1 Knapsack Problem
Problem: N items, each with weight[i] and profit[i]. Bag capacity W. Maximise total profit without exceeding W.
DP Recurrence:
dp[i][w] = max(
dp[i-1][w], # exclude item i
profit[i] + dp[i-1][w - weight[i]] # include item i
)
Only include item i if weight[i] ≤ w
For each subproblem:
- Can we include item i? (weight[i] ≤ w)
- Compare profit of including vs. excluding
- Take the max
Time: O(N × W)
Space: O(N × W) → optimisable to O(W)
Graphs
Fundamentals
G = (V, E) — vertices + edges
- Order = |V|
- Size = |E|
- Degree = edges incident to a vertex
- Weighted — edges have costs
- Directed — edges have direction
Path — sequence of vertices
Simple path — all vertices distinct
Length = number of edges (vertices − 1)
Representations
| Type | Space | Edge lookup | Best for |
|---|---|---|---|
| Edge list | O(E) | O(E) | Simple/sparse |
| Adjacency matrix | O(V²) | O(1) | Dense graphs |
| Adjacency list | O(V+E) | O(deg) | Sparse (most common) |
Adjacency list is the standard choice — efficient space and lookup for sparse graphs.
Graph Traversal
| BFS | DFS | |
|---|---|---|
| Structure | Queue | Stack / recursion |
| Order | Level by level | Deep, then backtrack |
| Visited array? | ✅ (cycles) | ✅ (cycles) |
| Time | O(V+E) | O(V+E) |
| Shortest path? | ✅ (unweighted) | ❌ |
Dijkstra's Shortest Path Algorithm
Requirements & Complexity
- Weighted graph, no negative weights
- Single-source shortest path to all vertices
- Complexity: O((V + E) log V) with min-heap
Steps
- Mark all nodes unvisited; source distance = 0, rest = ∞
- Set source as current node
- For each unvisited neighbour:
new_dist = cur_dist + edge_weight - If
new_dist < neighbour_dist→ update neighbour - Mark current node visited
- Pick unvisited node with smallest distance → new current
- Repeat from step 3 until destination visited or all visited
Pseudocode
import heapq def dijkstra(graph, src): dist = {v: float('inf') for v in graph} dist[src] = 0 pq = [(0, src)] # (distance, vertex) while pq: d, u = heapq.heappop(pq) if d > dist[u]: continue # stale entry for v, w in graph[u]: # neighbour, weight if dist[u] + w < dist[v]: dist[v] = dist[u] + w heapq.heappush(pq, (dist[v], v)) return dist
Real-world uses
- GPS navigation (shortest/fastest route)
- OSPF routing protocol (Open Shortest Path First)
- Network packet routing
Common Exam Traps & Must-Knows
Complexity Traps
| Common mistake | Truth |
|---|---|
| "Two loops = O(n²)" | Sequential loops = O(n) |
| "Merge sort is in-place" | No — needs O(n) extra space |
| "Quick sort is always O(n log n)" | Worst case O(n²) |
| "BST search is O(log n)" | Only if balanced; can be O(n) |
| "Dijkstra works everywhere" | Fails with negative weights |
Data Structure Traps
| Trap | Remember |
|---|---|
| List as dict key | ❌ Not hashable — use tuple |
| Mutating a tuple | ❌ Immutable — use list |
set ordering | No guaranteed order |
Stack with list | OK but deque is better |
Queue with list | O(n) for dequeue — use deque |
Tree Traps
| Trap | Remember |
|---|---|
| AVL BF must be ±1 | Strictly −1, 0, or +1 |
| Red-Black root colour | Always Black |
| Red node's children | Always Black |
| In-order gives sorted | True for BST only |
| Post-order deletes safely | Processes children first |
Algorithm Quick-Select Decision Guide
| Situation | Algorithm | Complexity |
|---|---|---|
| Search in sorted array | Binary Search | O(log n) |
| Shortest path, no negative weights | Dijkstra | O((V+E) log V) |
| Shortest path, negative weights allowed | Bellman-Ford | O(VE) |
| Sort large random dataset | Quick Sort | O(n log n) avg |
| Sort, guaranteed O(n log n) | Merge Sort | O(n log n) |
| Overlapping subproblems | Dynamic Programming | problem-specific |
| Independent subproblems, divide input | Divide & Conquer | problem-specific |
| Frequent lookups, DB queries | AVL Tree | O(log n) |
| Frequent inserts/deletes | Red-Black Tree | O(log n) |
| Max/min repeatedly extracted | Heap / Priority Queue | O(log n) |
| Key-value store, O(1) lookup | Hash Table (dict) | O(1) avg |
UFCFYR-15-2 Advanced Algorithms · MSc Computer Science · Weeks 1–5 · Generated 5 March 2026 · Further Reading & Deep Dives →