Advanced Algorithms — Exam Cheat Sheet

UFCFYR-15-2 · BSc Computer Science · Dr Kun Wei · Weeks 1–5

UFCFYR-15-2

Time Complexity & Big-O

O(1) O(log n) O(n) O(n log n) O(n²) O(2ⁿ) O(n!)

← faster                                                  slower →

Pattern → Complexity

Code patternComplexityWhy
Single loop 0..nO(n)n iterations
Two sequential loops of nO(n)2n = O(n)
Two sequential loops of n and mO(n+m)independent
Nested loops (both 0..n)O(n²)n×n iterations
Nested: outer n, inner halvedO(n log n)n × log n
i = i // 2 while loopO(log n)halved each step
Recursive divide-in-halfO(log n)e.g. binary search
Recursion branching factor 2O(2ⁿ)e.g. naive Fibonacci

Python List Operations

OperationCost
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 listO(n)
x in setO(1)
dict[key]O(1)

* amortised; resize is O(n)

! Drop constants & lower-order terms: O(3n² + 2n + 7) = O(n²). Always state the worst-case unless asked otherwise.

Python Built-in Data Structures

Comparison Table

TypeOrderedMutableDupsHash
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

OperationCost
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

  1. Operand → copy to output
  2. ( → push to stack
  3. ) → pop to output until (
  4. Operator: pop any higher/equal-precedence ops to output, then push
  5. 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
  • heapq in Python gives a min-heap (negate values for max-heap)

Linked Lists

Operations & Costs

OperationLinked ListArray
Access by indexO(n)O(1)
SearchO(n)O(n)
Insert at headO(1)O(n)
Insert at tailO(n)*O(1)†
Delete (given node)O(1)O(n)

*O(1) if tail pointer kept  †amortised

Types

  • Singlyvalue + next pointer only → forward traversal
  • Doublyprev + 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 FnUse
MurmurHashPython dicts/sets
SHA-256Encryption / signatures
Argon2Password hashing
MD5Fast, 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 / k

n = occupied entries    k = table size

LanguageDefault λTable size
Python dict2/3Power of 2 (init 8)
Java HashMap0.75Power of 2 (init 16)

When λ exceeds threshold → rehash (create larger table, reinsert all)

! Dict keys must be hashable — lists cannot be dict keys; tuples can.

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

leftparentright
OperationAvgWorst
SearchO(log n)O(n)*
InsertO(log n)O(n)*
DeleteO(log n)O(n)*

*when tree is fully unbalanced (e.g. sorted input)

BST Deletion (3 Cases)

  1. No children — set parent pointer to NULL
  2. One child — parent pointer skips node, points to child
  3. 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

OrderPatternUse case
Pre-orderRoot → L → RCopy a tree; DFS path
In-orderL → Root → RSorted output from BST
Post-orderL → R → RootDelete tree (leaves first)
BFS / LevelLevel by levelShortest path (unweighted)
DFSDeep firstCycle detect, connected components
In a BST, in-order traversal always gives sorted (ascending) output.

Example — Tree: A(B(D,E), C(F,G))

OrderOutput
Pre-orderA B D E C F G
In-orderD B E A F C G
Post-orderD E B F G C A

BFS vs DFS

BFSDFS
StructureQueueStack / recursion
DirectionLevel by levelDeep → 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:

  1. Every node is Red or Black
  2. Root is always Black
  3. Children of a Red node are Black
  4. Every root→NULL path has the same number of Black nodes
  5. 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

AVLRed-Black
LookupFasterSlower
Insert/DeleteSlowerFaster
BalanceStricterRelaxed
Memory/nodeint (BF)1 bit
Used inDBsLang libs

Other Variants

TreeKey ideaUse
SplayAccess → rootCaches
HeapParent ≤/≥ childPriority queue
TreapBST + heap priorityRandomised sets
B-TreeMulti-key nodesDB indexing, FS

Sorting Algorithms

Master Comparison Table

AlgorithmBestAverageWorstSpaceIn-placeStableApproach
Bubble SortO(n)O(n²)O(n²)O(1)Brute force
Selection SortO(n²)O(n²)O(n²)O(1)Brute force
Merge SortO(n log n)O(n log n)O(n log n)O(n)D&C
Quick SortO(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?

ScenarioBest choiceWhy
Nearly sorted dataBubble SortBest-case O(n) with early exit
Minimise swapsSelection SortOnly 1 swap per pass
Guaranteed O(n log n)Merge SortNo worst-case degradation
Memory constrainedQuick Sort (in-place)O(log n) stack space only
Large random datasetQuick SortBest average performance in practice
Need stable sortMerge SortPreserves equal element order

Divide & Conquer

Strategy

  1. Divide — break problem into smaller subproblems
  2. Conquer — solve each recursively
  3. 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&CDP
SubproblemsIndependentOverlapping
StyleRecursiveRecursive or iterative
Recomputes?YesNo (stores)
ExamplesMerge/Quick sortFibonacci, 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:

  1. Can we include item i? (weight[i] ≤ w)
  2. Compare profit of including vs. excluding
  3. 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

TypeSpaceEdge lookupBest for
Edge listO(E)O(E)Simple/sparse
Adjacency matrixO(V²)O(1)Dense graphs
Adjacency listO(V+E)O(deg)Sparse (most common)

Adjacency list is the standard choice — efficient space and lookup for sparse graphs.

Graph Traversal

BFSDFS
StructureQueueStack / recursion
OrderLevel by levelDeep, then backtrack
Visited array?✅ (cycles)✅ (cycles)
TimeO(V+E)O(V+E)
Shortest path?✅ (unweighted)
A graph traversal converts the graph into a tree.

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

  1. Mark all nodes unvisited; source distance = 0, rest = ∞
  2. Set source as current node
  3. For each unvisited neighbour: new_dist = cur_dist + edge_weight
  4. If new_dist < neighbour_dist → update neighbour
  5. Mark current node visited
  6. Pick unvisited node with smallest distance → new current
  7. Repeat from step 3 until destination visited or all visited
! Negative edge weights → use Bellman-Ford instead (O(VE))

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 mistakeTruth
"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

TrapRemember
List as dict key❌ Not hashable — use tuple
Mutating a tuple❌ Immutable — use list
set orderingNo guaranteed order
Stack with listOK but deque is better
Queue with listO(n) for dequeue — use deque

Tree Traps

TrapRemember
AVL BF must be ±1Strictly −1, 0, or +1
Red-Black root colourAlways Black
Red node's childrenAlways Black
In-order gives sortedTrue for BST only
Post-order deletes safelyProcesses children first

Algorithm Quick-Select Decision Guide

SituationAlgorithmComplexity
Search in sorted arrayBinary SearchO(log n)
Shortest path, no negative weightsDijkstraO((V+E) log V)
Shortest path, negative weights allowedBellman-FordO(VE)
Sort large random datasetQuick SortO(n log n) avg
Sort, guaranteed O(n log n)Merge SortO(n log n)
Overlapping subproblemsDynamic Programmingproblem-specific
Independent subproblems, divide inputDivide & Conquerproblem-specific
Frequent lookups, DB queriesAVL TreeO(log n)
Frequent inserts/deletesRed-Black TreeO(log n)
Max/min repeatedly extractedHeap / Priority QueueO(log n)
Key-value store, O(1) lookupHash Table (dict)O(1) avg

UFCFYR-15-2 Advanced Algorithms · MSc Computer Science · Weeks 1–5 · Generated 5 March 2026  ·  Further Reading & Deep Dives →