- Abstraction = removing unnecessary detail to focus on what matters for a given purpose
- The abstraction is a simplified model — not the real thing
- Helps manage complexity in large systems
- Example: a map abstracts away building interiors, keeping only roads and names
- You must be able to: identify what to include/exclude when creating an abstraction, and explain why abstraction is necessary
- Devise an abstract model for a scenario (e.g. a library, a traffic system)
- Inputs — what data goes in; Outputs — what comes out
- Preconditions — what must be true before a solution can work (e.g. binary search requires a sorted list)
- Caching — storing results of expensive computations so they don't need to be repeated. Benefits: faster access. Drawbacks: takes memory, data can go stale
- Reusable components — functions/modules designed to be used in many programs. Reduces duplication, easier to test and maintain
- Break a problem into components (decomposition)
- Identify the order of steps needed — sequencing matters
- Identify sub-procedures: discrete, reusable chunks of a solution
- Example: "Book a train ticket" → search routes → check availability → enter details → pay → confirm
- Each sub-procedure can be independently tested and developed
- Identify decision points in a solution — where the outcome could go different ways
- Determine the logical conditions that govern each decision (e.g. IF score > 50 THEN pass)
- Understand how decisions affect flow through a program (branching, loops)
- Ties closely to Boolean logic: AND, OR, NOT conditions
- Example: a login system — correct password? → branch to welcome page or error
- Identify parts of a problem that can happen at the same time
- Concurrent processing — multiple tasks processed in overlapping time periods (not necessarily simultaneously)
- Benefits: faster overall execution, better use of multi-core processors
- Trade-offs: race conditions, harder to debug, synchronisation overhead, shared resource conflicts
- Example: rendering a webpage — fetching HTML, images and CSS can happen concurrently
An abstraction always leaves things out. The question is always: what did we choose to leave out and why?
Good abstraction includes:
- Data/attributes relevant to the purpose
- Actions/behaviours that need to be modelled
- Relationships between components
Good abstraction excludes:
- Details irrelevant to the problem
- Physical/real-world noise
- Anything that would increase complexity without benefit
| Benefit | Drawback |
|---|---|
| Reduces time to access frequently used data | Cache uses memory — expensive |
| Avoids recomputing expensive results | Cached data can become stale/outdated |
| Reduces load on slower storage (e.g. disk, network) | Cache misses add overhead |
| Can dramatically speed up repeated operations | Managing cache invalidation is complex |
Sequence
Instructions execute one after another in order. The default behaviour of any program.
Iteration
Repeating a block of code. FOR loops — known number of repeats. WHILE — condition checked before. DO…UNTIL — condition checked after (always runs at least once).
Branching
IF/ELSEIF/ELSE or SWITCH/CASE statements — directing flow based on a condition.
if score >= 70 then print("Distinction") elseif score >= 50 then print("Pass") else print("Fail") endif
Recursion
A function that calls itself. Must have a base case (stopping condition) and a recursive case.
function factorial(n) if n == 0 then return 1 // base case else return n * factorial(n-1) endif endfunction
- Uses the call stack — each call adds a stack frame
- Elegant for problems with natural recursive structure (trees, factorials)
- Risk: stack overflow if recursion depth too large
Iteration
Uses loops. Generally more memory-efficient than recursion.
function factorial(n) result = 1 for i = 1 to n result = result * i next i return result endfunction
- No risk of stack overflow
- Often faster (no function call overhead)
- Can be less readable for recursive-natured problems
| Recursion | Iteration | |
|---|---|---|
| Memory | Higher (call stack) | Lower |
| Speed | Slower (overhead) | Faster |
| Readability | Often cleaner for recursive problems | More verbose for some |
| Stack overflow risk | Yes | No |
| Best for | Trees, divide & conquer | Simple repetition |
Local Variables
- Declared inside a function/procedure
- Only accessible within that scope
- Created when function is called, destroyed when it returns
- Preferred — prevents unintended side effects
Global Variables
- Declared outside all functions — accessible everywhere
- Persist for the lifetime of the program
- Risky: any function can accidentally modify them
- Use sparingly — only when truly needed across many functions
Function
Returns a value. Called as part of an expression.
function square(x) return x * x endfunction result = square(5) // result = 25
Procedure
Does NOT return a value. Performs an action.
procedure printLine(msg) print(msg) print("--------") endprocedure printLine("Hello")
Parameter Passing
By Value — a copy of the value is passed. Changes inside the function do NOT affect the original variable.
By Reference — the memory address is passed. Changes inside the function DO affect the original variable.
Key Concepts
- Class — a blueprint/template defining attributes and methods
- Object — an instance of a class
- Attribute — a variable belonging to a class (data)
- Method — a function belonging to a class (behaviour)
- Constructor — special method called when object is created
Three Pillars
- Encapsulation — data and methods bundled together; internal data hidden from outside (private attributes)
- Inheritance — a subclass inherits attributes/methods from a superclass; promotes reuse
- Polymorphism — different classes can respond to the same method name in different ways (overriding)
// OCR pseudocode style class Animal private name public procedure new(animalName) name = animalName endprocedure public function getName() return name endfunction public procedure speak() print("...") endprocedure endclass class Dog inherits Animal public procedure speak() // overrides parent method print("Woof!") endprocedure endclass myDog = new Dog("Rex") myDog.speak() // outputs: Woof!
- The problem can be broken down into logical steps
- There is a clear, unambiguous solution that can be expressed algorithmically
- It involves processing data in a well-defined way
- It would be too large/slow for a human to solve in reasonable time
- It can be automated and repeated
Decomposition
Breaking a large problem into smaller, more manageable sub-problems. Each sub-problem can be solved independently and combined.
- Easier to assign to different developers
- Each part easier to test in isolation
- Matches to modular programming
Divide and Conquer
A specific algorithmic strategy: divide the problem into sub-problems of the same type, conquer each recursively, combine results.
- Examples: merge sort, binary search, quicksort
- Usually gives O(log n) or O(n log n) complexity
- Much better than brute-force for large datasets
| Method | Description | Example |
|---|---|---|
| Backtracking | Try a path; if it fails, undo and try another. Systematic trial and error. | Maze solving, Sudoku solver |
| Data Mining | Searching large datasets to find patterns, correlations or useful information. | Recommender systems, fraud detection |
| Heuristics | A "good enough" approach when an optimal solution is too costly to compute. Sacrifices accuracy for speed. | A* pathfinding, NP-hard approximations |
| Performance Modelling | Simulating a system to predict behaviour before building it — cheaper than real testing. | Simulating server load, traffic modelling |
| Pipelining | Breaking a task into stages processed concurrently (each stage works on a different item simultaneously). | CPU instruction pipeline, assembly lines |
| Visualisation | Representing data graphically to help understand complex relationships and patterns. | Network topology diagrams, data dashboards |
Big O notation describes the upper bound of an algorithm's time (or space) complexity as the input size n grows. It tells you the worst case — how bad can it get?
The goal: minimise Big O. An algorithm's time complexity is more important to exam boards than space complexity (though you must know both).
| Big O | Name | Description | Example |
|---|---|---|---|
| O(1) | Constant | Time does not change regardless of input size | Accessing an array element by index, push/pop on stack |
| O(log n) | Logarithmic | Time grows very slowly — input halved each step | Binary search, binary tree operations |
| O(n) | Linear | Time grows proportionally with input size | Linear search, traversing a linked list |
| O(n²) | Polynomial | Time grows as n squared — nested loops | Bubble sort, insertion sort, selection sort |
| O(2ⁿ) | Exponential | Time doubles for every extra element — extremely slow | Brute-force password cracking, recursive Fibonacci (naïve) |
Note: O(n log n) is where merge sort and quicksort sit — between linear and polynomial.
// O(1) — no loops, independent of n x = array[0] print(x)
// O(n) — single loop over n items for i = 0 to n-1 print(array[i]) next i
// O(n²) — nested loops for i = 0 to n-1 for j = 0 to n-1 print(array[i][j]) next j next i
// O(log n) — halving the problem // e.g. binary search reduces // search space by half each step
Space complexity = how much extra memory an algorithm needs relative to input size n.
- Algorithms that work in-place (like bubble sort) use O(1) extra space
- Merge sort creates copies of sub-arrays → O(n) space
- Recursive algorithms store stack frames → space complexity depends on recursion depth
- In exams: the board mainly asks about time complexity; know space complexity exists and what it means
Implemented as an array with a single top pointer, initialised at -1 (empty stack).
Operations
// Push — add to top procedure push(stack, item) if top < maxSize-1 then top = top + 1 stack[top] = item else print("Stack overflow") endif endprocedure // Pop — remove from top function pop(stack) if top >= 0 then item = stack[top] top = top - 1 return item else print("Stack underflow") endif endfunction // Peek — see top without removing function peek(stack) return stack[top] endfunction // isEmpty function isEmpty() return top == -1 endfunction
Key Points
- Top pointer starts at -1 (empty)
- Push: increment top, then insert
- Pop: read top, then decrement
- Overflow = push onto full stack
- Underflow = pop from empty stack
- Size = top + 1
Uses of Stacks
- Call stack (function calls in recursion)
- Undo functionality in editors
- Browser back button
- Expression evaluation
Complexity
Implemented as an array with front and back pointers. Items join at the back, leave from the front.
// Enqueue — add to back procedure enqueue(queue, item) if back < maxSize-1 then queue[back] = item back = back + 1 else print("Queue full") endif endprocedure // Dequeue — remove from front function dequeue(queue) if front != back then item = queue[front] front = front + 1 return item else print("Queue empty") endif endfunction // isEmpty: front == back // Size: back - front
Key Points
- Front and back both start at 0
- Empty when front == back
- Size = back − front
- A circular queue wraps the back pointer round to reuse freed space at the front
Uses of Queues
- CPU scheduling (process queues)
- Printer job queues
- Breadth-first search (uses a queue)
- Keyboard input buffer
A dynamic data structure where each element (node) stores a value and a pointer to the next node. The list ends when a node points to null/None.
Traversal
procedure traverse(head) current = head while current != null print(current.data) current = current.next endwhile endprocedure
Advantages over arrays
- Dynamic size — no need to declare size in advance
- Efficient insertions/deletions (just update pointers)
Disadvantages
- No random access — must traverse from head: O(n)
- Extra memory for pointers
- Not cache-friendly
Complexity
A tree is a hierarchical structure with a root node. A binary search tree (BST) has the property: left child < parent < right child.
Depth-First (Post-Order)
OCR spec specifically names post-order for depth-first. Visit left subtree → right subtree → root.
procedure postOrder(node) if node != null then postOrder(node.left) postOrder(node.right) print(node.data) endif endprocedure
Breadth-First Traversal
Visits nodes level by level (top to bottom, left to right). Uses a queue.
procedure breadthFirst(root) queue.enqueue(root) while queue NOT empty node = queue.dequeue() print(node.data) if node.left != null queue.enqueue(node.left) endif if node.right != null queue.enqueue(node.right) endif endwhile endprocedure
Repeatedly compares adjacent pairs and swaps them if out of order. After each pass, the largest unsorted element "bubbles" to its correct position at the end.
function bubbleSort(array) n = length(array) swapped = True while swapped swapped = False for i = 0 to n-2 if array[i] > array[i+1] then temp = array[i] array[i] = array[i+1] array[i+1] = temp swapped = True endif next i endwhile return array endfunction
Trace: [5, 3, 1, 4]
| Pass | Array after pass |
|---|---|
| 1 | [3, 1, 4, 5] — 5 settled |
| 2 | [1, 3, 4, 5] — 4 settled |
| 3 | [1, 3, 4, 5] — sorted |
Key Points
- The swapped flag is an optimisation — stops early if no swaps in a pass (best case O(n))
- After pass k, the last k elements are guaranteed sorted
- Simple but slow — rarely used in practice
Builds a sorted portion from left to right. Takes each element and inserts it into its correct position in the already-sorted portion to its left.
function insertionSort(array) n = length(array) for i = 1 to n-1 key = array[i] j = i - 1 while j >= 0 AND array[j] > key array[j+1] = array[j] j = j - 1 endwhile array[j+1] = key next i return array endfunction
Trace: [5, 3, 1, 4]
| i | key | Array |
|---|---|---|
| 1 | 3 | [3, 5, 1, 4] |
| 2 | 1 | [1, 3, 5, 4] |
| 3 | 4 | [1, 3, 4, 5] |
Key Points
- The sorted portion grows by one element each iteration
- Very efficient for nearly sorted data
- Note: the sorted elements are NOT necessarily the smallest i elements
Divide and conquer: recursively split the array in half until individual elements, then merge sorted halves back together.
function mergeSort(array) if length(array) <= 1 then return array endif mid = length(array) DIV 2 left = mergeSort(array[0:mid]) right = mergeSort(array[mid:]) return merge(left, right) endfunction function merge(left, right) result = [] while left != [] AND right != [] if left[0] <= right[0] then result.append(left[0]) left = left[1:] else result.append(right[0]) right = right[1:] endif endwhile return result + left + right endfunction
Trace: [5, 3, 1, 4]
Split: [5,3] | [1,4]
Split again: [5] [3] [1] [4]
Merge [5][3] → [3,5]
Merge [1][4] → [1,4]
Merge [3,5][1,4] → [1,3,4,5]
Key Points
- Always O(n log n) — most consistent sort
- Requires O(n) extra space for the merging step
- The exact implementation of merge is not required — just how it works conceptually
Selects a pivot, partitions elements into those less than and greater than the pivot, then recursively sorts each partition.
function quickSort(array) if length(array) <= 1 then return array endif pivot = array[mid] left = [x for x in array if x < pivot] middle = [x for x in array if x == pivot] right = [x for x in array if x > pivot] return quickSort(left) + middle + quickSort(right) endfunction
Trace: [5, 3, 1, 4, 2] — pivot = 3
Pivot = 3. Left = [1,2], Middle = [3], Right = [4,5]
Sort [1,2]: pivot=1 → [1,2] ✓
Sort [4,5]: pivot=4 → [4,5] ✓
Combine: [1,2,3,4,5] ✓
Key Points
- Pivot choice matters — middle element is common in exams
- Once a pivot is placed, it never moves
- Worst case O(n²) when pivot is always smallest/largest
- O(log n) space (recursive call stack)
Check each element one by one from the start until the target is found or the list is exhausted.
function linearSearch(array, target) for i = 0 to length(array)-1 if array[i] == target then return i // found at index i endif next i return -1 // not found endfunction
Properties
- Complexity: O(n)
- Works on unsorted data
- Simple to implement
- Very inefficient for large datasets
- Best case: target is first element O(1)
- Worst case: target not in list — checks all n elements
Divide and conquer. Check the midpoint — if it's the target, done. If target is smaller, search left half; if larger, search right half. Repeat.
function binarySearch(array, target) first = 0 last = length(array) - 1 while first <= last mid = (first + last) DIV 2 if array[mid] == target then return mid elseif array[mid] < target then first = mid + 1 else last = mid - 1 endif endwhile return -1 // not found endfunction
Trace: find 7 in [1,3,5,7,9,11]
| first | last | mid | array[mid] | Action |
|---|---|---|---|---|
| 0 | 5 | 2 | 5 | 7 > 5 → first = 3 |
| 3 | 5 | 4 | 9 | 7 < 9 → last = 3 |
| 3 | 3 | 3 | 7 | Found! Return 3 |
Properties
| Linear Search | Binary Search | |
|---|---|---|
| Data sorted? | Not required | Required |
| Time complexity | O(n) | O(log n) |
| Best for | Small/unsorted data | Large sorted data |
| Implementation | Simple | More complex |
| 10,000 items worst case | ~10,000 steps | ~14 steps |
Finds the shortest path from a source node to all other nodes in a weighted graph. Uses a greedy approach — always processes the unvisited node with the smallest known distance.
Steps:
Assign distance 0 to source, ∞ to all others. Mark all as unvisited.
Select the unvisited node with the smallest distance (start with source).
For each unvisited neighbour, calculate: current node's distance + edge weight. If this is less than the neighbour's current distance, update it.
Mark current node as visited. Visited nodes are never revisited.
Repeat from step 2 until destination is visited (or all nodes visited).
Worked Example
Dijkstra's Table (Source = A, Target = F)
| Step | Visited | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|
| Start | — | 0 | ∞ | ∞ | ∞ | ∞ | ∞ |
| Visit A | A | 0 | 4 | 2 | ∞ | ∞ | ∞ |
| Visit C (dist 2) | A,C | 0 | 4 | 2 | ∞ | 5 | ∞ |
| Visit B (dist 4) | A,C,B | 0 | 4 | 2 | 9 | 5 | ∞ |
| Visit E (dist 5) | A,C,B,E | 0 | 4 | 2 | 9 | 5 | 11 |
| Visit D (dist 9) | A,C,B,E,D | 0 | 4 | 2 | 9 | 5 | 10 |
| Visit F (dist 10) | All | 0 | 4 | 2 | 9 | 5 | 10 |
Shortest path A→F = 10, via A→C→E→F
A heuristic extension of Dijkstra's. Adds an estimated distance to the destination at each node to guide the search more intelligently.
The Key Formula
f(n) = g(n) + h(n) g(n) = actual cost from start to node n h(n) = heuristic estimate from node n to destination f(n) = total estimated cost of path through n
How it works
Calculate f(n) = g(n) + h(n) for each neighbour
Always expand the node with the lowest f(n)
Stop when destination is reached
vs Dijkstra's
| Dijkstra's | A* | |
|---|---|---|
| Heuristic? | No | Yes h(n) |
| Optimal? | Yes (always) | Yes (if h(n) ≤ true distance) |
| Speed | Slower | Faster in practice |
| Use case | All-pairs shortest paths | Single target navigation |