Algorithms & Programs
A2 Level — Unit 3: Programming & System Development
Recursion
Recursion – a technique in which a function (or procedure) calls itself in order to solve a problem. Every recursive solution must have a base case (a condition that stops the recursion) and a recursive case (where the function calls itself with a smaller or simpler input).
Base Case and Recursive Case
Every recursive function has two essential parts:
- Base case: The simplest instance of the problem that can be answered directly without further recursion. Without a base case, the recursion would continue forever (in practice, until the call stack overflows).
- Recursive case: The function calls itself with a modified argument that moves closer to the base case.
The Call Stack and Stack Frames
Call stack – a region of memory that stores information about active function calls. Each time a function is called, a new stack frame is pushed onto the call stack containing the function’s local variables, parameters, and return address. When the function returns, its frame is popped off the stack.
When a recursive function calls itself, each call creates a new stack frame. These frames accumulate on the stack until a base case is reached. Then, the frames are popped one by one as each call returns its result to the caller.
Key points about the call stack:
- The stack has a finite size. Too many recursive calls cause a stack overflow.
- Each stack frame stores the function’s local variables, parameters, and the return address (where to resume after the call returns).
- Frames are processed in LIFO (Last In, First Out) order.
Example: Factorial
The factorial of n is defined as:
- 0! = 1 (base case)
- n! = n x (n-1)! for n > 0 (recursive case)
def factorial(n):
if n == 0: # Base case
return 1
else: # Recursive case
return n * factorial(n - 1)
print(factorial(5)) # Output: 120
Function Factorial(n As Integer) As Long
If n = 0 Then ' Base case
Return 1
Else ' Recursive case
Return n * Factorial(n - 1)
End If
End Function
Sub Main()
Console.WriteLine(Factorial(5)) ' Output: 120
End Sub
Tracing the call stack for factorial(4):
| Call | Stack state (top to bottom) | Returns |
|---|---|---|
| factorial(4) | factorial(4) | 4 * factorial(3) |
| factorial(3) | factorial(3), factorial(4) | 3 * factorial(2) |
| factorial(2) | factorial(2), factorial(3), factorial(4) | 2 * factorial(1) |
| factorial(1) | factorial(1), factorial(2), factorial(3), factorial(4) | 1 * factorial(0) |
| factorial(0) | factorial(0), factorial(1), factorial(2), factorial(3), factorial(4) | 1 (base case) |
Then the stack unwinds: 1 * 1 = 1, then 2 * 1 = 2, then 3 * 2 = 6, then 4 * 6 = 24.
Example: Fibonacci
The Fibonacci sequence is defined as:
- fib(0) = 0 (base case)
- fib(1) = 1 (base case)
- fib(n) = fib(n-1) + fib(n-2) for n > 1 (recursive case)
def fibonacci(n):
if n == 0: # Base case 1
return 0
elif n == 1: # Base case 2
return 1
else: # Recursive case
return fibonacci(n - 1) + fibonacci(n - 2)
for i in range(10):
print(fibonacci(i), end=" ")
# Output: 0 1 1 2 3 5 8 13 21 34
Function Fibonacci(n As Integer) As Long
If n = 0 Then ' Base case 1
Return 0
ElseIf n = 1 Then ' Base case 2
Return 1
Else ' Recursive case
Return Fibonacci(n - 1) + Fibonacci(n - 2)
End If
End Function
Sub Main()
For i As Integer = 0 To 9
Console.Write(Fibonacci(i) & " ")
Next
' Output: 0 1 1 2 3 5 8 13 21 34
End Sub
The naive recursive Fibonacci is extremely inefficient because it recalculates the same values many times. For example, fibonacci(5) calls fibonacci(3) twice and fibonacci(2) three times. Its time complexity is O(2^n). This is a classic exam point – you may be asked to contrast this with the O(n) iterative approach.
Elegance of Recursion vs Iteration
Elegance in programming refers to a solution that is concise, clear, and closely mirrors the mathematical or logical definition of the problem. Recursive solutions are often considered elegant because they express the problem in terms of itself.
Comparison
| Aspect | Recursion | Iteration |
|---|---|---|
| Code length | Often shorter and more readable | May require more code (loop, variables) |
| Mirrors definition | Directly reflects mathematical definitions | Must manually manage state |
| Memory usage | Uses call stack space (O(n) or more) | Typically O(1) extra space |
| Risk of overflow | Stack overflow for deep recursion | No stack overflow risk |
| Performance | Function call overhead per recursion | Generally faster due to no call overhead |
| Suitability | Tree traversals, divide-and-conquer, backtracking | Simple loops, accumulation |
Factorial: Recursive vs Iterative
# Recursive -- elegant but uses O(n) stack space
def factorial_recursive(n):
if n == 0:
return 1
return n * factorial_recursive(n - 1)
# Iterative -- less elegant but O(1) extra space
def factorial_iterative(n):
result = 1
for i in range(2, n + 1):
result *= i
return result
' Recursive
Function FactorialRecursive(n As Integer) As Long
If n = 0 Then Return 1
Return n * FactorialRecursive(n - 1)
End Function
' Iterative
Function FactorialIterative(n As Integer) As Long
Dim result As Long = 1
For i As Integer = 2 To n
result *= i
Next
Return result
End Function
When Recursion is Truly Elegant
Some problems are naturally recursive and become awkward or complex when written iteratively:
- Tree traversals – visiting every node in a binary tree is naturally expressed recursively.
- Divide-and-conquer algorithms – merge sort and quick sort split the problem into subproblems.
- Backtracking – problems like maze solving or the N-queens puzzle benefit from recursive exploration of possibilities.
For these problems, an iterative solution would require manually managing a stack data structure, which is essentially reimplementing what the call stack does automatically.
The WJEC specification specifically asks about the “potential elegance” of recursion. Be prepared to argue both sides: recursion is elegant because it is concise and mirrors the problem definition, but iteration is sometimes preferable for performance and memory reasons. Always mention the trade-off.
Merge Sort
Merge sort – a divide-and-conquer sorting algorithm that recursively splits a list in half, sorts each half, and then merges the two sorted halves back together. It is a stable sort with a guaranteed time complexity of O(n log n) in all cases.
Algorithm
- Divide: If the list has more than one element, split it into two roughly equal halves.
- Conquer: Recursively sort each half.
- Merge: Combine the two sorted halves into one sorted list by comparing elements from the front of each half and appending the smaller one.
Worked Example
Sort the list: [38, 27, 43, 3, 9, 82, 10]
Splitting phase:
graph TD
A["38, 27, 43, 3, 9, 82, 10"] --> B["38, 27, 43"]
A --> C["3, 9, 82, 10"]
B --> D["38"]
B --> E["27, 43"]
C --> F["3, 9"]
C --> G["82, 10"]
E --> H["27"]
E --> I["43"]
F --> J["3"]
F --> K["9"]
G --> L["82"]
G --> M["10"]
Merging phase (bottom-up):
[27] [43] --> [27, 43]
[38] + [27, 43] --> [27, 38, 43]
[3] [9] --> [3, 9]
[82] [10] --> [10, 82]
[3, 9] + [10, 82] --> [3, 9, 10, 82]
[27, 38, 43] + [3, 9, 10, 82] --> [3, 9, 10, 27, 38, 43, 82]
Code
def merge_sort(arr):
if len(arr) <= 1: # Base case
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid]) # Recursively sort left half
right = merge_sort(arr[mid:]) # Recursively sort right half
return merge(left, right)
def merge(left, right):
"""Merge two sorted lists into one sorted list."""
result = []
i = 0 # Pointer for left list
j = 0 # Pointer for right list
while i < len(left) and j < len(right):
if left[i] <= right[j]: # <= ensures stability
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
# Append any remaining elements
result.extend(left[i:])
result.extend(right[j:])
return result
data = [38, 27, 43, 3, 9, 82, 10]
print(merge_sort(data))
# Output: [3, 9, 10, 27, 38, 43, 82]
Function MergeSort(arr As List(Of Integer)) As List(Of Integer)
If arr.Count <= 1 Then ' Base case
Return arr
End If
Dim mid As Integer = arr.Count \ 2
Dim left As List(Of Integer) = MergeSort(arr.GetRange(0, mid))
Dim right As List(Of Integer) = MergeSort(arr.GetRange(mid, arr.Count - mid))
Return Merge(left, right)
End Function
Function Merge(left As List(Of Integer), right As List(Of Integer)) As List(Of Integer)
Dim result As New List(Of Integer)
Dim i As Integer = 0
Dim j As Integer = 0
While i < left.Count AndAlso j < right.Count
If left(i) <= right(j) Then ' <= ensures stability
result.Add(left(i))
i += 1
Else
result.Add(right(j))
j += 1
End If
End While
' Append remaining elements
While i < left.Count
result.Add(left(i))
i += 1
End While
While j < right.Count
result.Add(right(j))
j += 1
End While
Return result
End Function
Sub Main()
Dim data As New List(Of Integer) From {38, 27, 43, 3, 9, 82, 10}
Dim sorted As List(Of Integer) = MergeSort(data)
Console.WriteLine(String.Join(", ", sorted))
' Output: 3, 9, 10, 27, 38, 43, 82
End Sub
Characteristics of Merge Sort
| Characteristic | Detail |
|---|---|
| Type | Divide and conquer |
| Time complexity (best) | O(n log n) |
| Time complexity (average) | O(n log n) |
| Time complexity (worst) | O(n log n) |
| Space complexity | O(n) – requires additional space for merging |
| Stable | Yes – equal elements maintain their original relative order |
| In-place | No – requires O(n) extra memory |
| Recursive | Yes |
Merge sort always takes O(n log n) time regardless of the initial order of the data. This makes it predictable, but it uses O(n) extra memory. This is a key distinction from quick sort, which is in-place but has a worse worst case.
Quick Sort
Quick sort – a divide-and-conquer sorting algorithm that selects a pivot element, partitions the list into elements less than the pivot and elements greater than the pivot, then recursively sorts the two partitions. Average time complexity is O(n log n) but worst case is O(n^2).
Algorithm
- Choose a pivot – commonly the first element, last element, middle element, or a random element.
- Partition – rearrange elements so that all elements less than the pivot are to its left, and all elements greater are to its right. The pivot is now in its final sorted position.
- Recurse – recursively apply quick sort to the left partition and the right partition.
- Base case – a list with 0 or 1 elements is already sorted.
Pivot Selection
The choice of pivot affects performance:
| Strategy | Description | Risk |
|---|---|---|
| First element | Simple but poor for already-sorted data | O(n^2) on sorted input |
| Last element | Simple but same risk as first | O(n^2) on sorted input |
| Middle element | Reasonable compromise | Less likely to hit worst case |
| Median-of-three | Median of first, middle, last | Good balance of simplicity and performance |
| Random | Choose a random element | Statistically avoids worst case |
Worked Example
Sort the list: [7, 2, 1, 6, 8, 5, 3, 4] using last element as pivot.
Pass 1: Pivot = 4
Partition around 4: elements < 4 go left, elements > 4 go right.
[2, 1, 3] [4] [7, 6, 8, 5]
4 is now in its final position (index 3).
Pass 2a: Sort left partition [2, 1, 3], pivot = 3
[2, 1] [3]
3 is in its final position.
Pass 2b: Sort right partition [7, 6, 8, 5], pivot = 5
[5] [7, 6, 8]
5 is in its final position.
Pass 3a: Sort [2, 1], pivot = 1
[1] [2]
Pass 3b: Sort [7, 6, 8], pivot = 8
[7, 6] [8]
Pass 4: Sort [7, 6], pivot = 6
[6] [7]
Final sorted list: [1, 2, 3, 4, 5, 6, 7, 8]
Code
def quick_sort(arr):
if len(arr) <= 1: # Base case
return arr
pivot = arr[-1] # Choose last element as pivot
left = [] # Elements less than pivot
right = [] # Elements greater than pivot
equal = [] # Elements equal to pivot
for item in arr:
if item < pivot:
left.append(item)
elif item > pivot:
right.append(item)
else:
equal.append(item)
return quick_sort(left) + equal + quick_sort(right)
data = [7, 2, 1, 6, 8, 5, 3, 4]
print(quick_sort(data))
# Output: [1, 2, 3, 4, 5, 6, 7, 8]
An in-place version (more memory-efficient, uses partitioning):
def quick_sort_in_place(arr, low, high):
if low < high:
pivot_index = partition(arr, low, high)
quick_sort_in_place(arr, low, pivot_index - 1)
quick_sort_in_place(arr, pivot_index + 1, high)
def partition(arr, low, high):
pivot = arr[high] # Pivot is last element
i = low - 1 # Index of smaller element boundary
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i] # Swap
arr[i + 1], arr[high] = arr[high], arr[i + 1] # Place pivot
return i + 1
data = [7, 2, 1, 6, 8, 5, 3, 4]
quick_sort_in_place(data, 0, len(data) - 1)
print(data)
# Output: [1, 2, 3, 4, 5, 6, 7, 8]
' Simple version (not in-place, easier to understand)
Function QuickSort(arr As List(Of Integer)) As List(Of Integer)
If arr.Count <= 1 Then ' Base case
Return arr
End If
Dim pivot As Integer = arr(arr.Count - 1) ' Last element as pivot
Dim left As New List(Of Integer)
Dim right As New List(Of Integer)
Dim equal As New List(Of Integer)
For Each item As Integer In arr
If item < pivot Then
left.Add(item)
ElseIf item > pivot Then
right.Add(item)
Else
equal.Add(item)
End If
Next
Dim result As New List(Of Integer)
result.AddRange(QuickSort(left))
result.AddRange(equal)
result.AddRange(QuickSort(right))
Return result
End Function
' In-place version
Sub QuickSortInPlace(arr As List(Of Integer), low As Integer, high As Integer)
If low < high Then
Dim pivotIndex As Integer = Partition(arr, low, high)
QuickSortInPlace(arr, low, pivotIndex - 1)
QuickSortInPlace(arr, pivotIndex + 1, high)
End If
End Sub
Function Partition(arr As List(Of Integer), low As Integer, high As Integer) As Integer
Dim pivot As Integer = arr(high)
Dim i As Integer = low - 1
For j As Integer = low To high - 1
If arr(j) <= pivot Then
i += 1
' Swap arr(i) and arr(j)
Dim temp As Integer = arr(i)
arr(i) = arr(j)
arr(j) = temp
End If
Next
' Place pivot in correct position
Dim temp2 As Integer = arr(i + 1)
arr(i + 1) = arr(high)
arr(high) = temp2
Return i + 1
End Function
Sub Main()
Dim data As New List(Of Integer) From {7, 2, 1, 6, 8, 5, 3, 4}
QuickSortInPlace(data, 0, data.Count - 1)
Console.WriteLine(String.Join(", ", data))
' Output: 1, 2, 3, 4, 5, 6, 7, 8
End Sub
Characteristics of Quick Sort
| Characteristic | Detail |
|---|---|
| Type | Divide and conquer |
| Time complexity (best) | O(n log n) |
| Time complexity (average) | O(n log n) |
| Time complexity (worst) | O(n^2) – when pivot is always smallest or largest |
| Space complexity | O(log n) for the call stack (in-place version) |
| Stable | No – relative order of equal elements may change |
| In-place | Yes (in-place version) – does not require O(n) extra memory |
| Recursive | Yes |
Quick sort’s worst case O(n^2) occurs when the pivot is consistently the smallest or largest element – this happens with already-sorted data if you pick the first or last element as pivot. Despite this, quick sort is often faster in practice than merge sort because it has better cache performance and lower constant factors. The exam may ask you to identify scenarios where each algorithm is preferable.
Comparing Sorting Algorithms
The following table compares the key sorting algorithms you need to know at A2 level:
| Algorithm | Best Case | Average Case | Worst Case | Space | Stable | In-Place | Method |
|---|---|---|---|---|---|---|---|
| Bubble sort | O(n) | O(n^2) | O(n^2) | O(1) | Yes | Yes | Comparison / swapping |
| Insertion sort | O(n) | O(n^2) | O(n^2) | O(1) | Yes | Yes | Comparison / insertion |
| Merge sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes | No | Divide and conquer |
| Quick sort | O(n log n) | O(n log n) | O(n^2) | O(log n) | No | Yes | Divide and conquer |
Key Points for Comparison
- Bubble sort and insertion sort are O(n^2) algorithms. They are simple but only practical for small datasets. Insertion sort is efficient on nearly-sorted data (O(n) best case).
- Merge sort guarantees O(n log n) in all cases but requires O(n) extra space. It is the best choice when consistent performance is needed and memory is not constrained.
- Quick sort is O(n log n) on average and typically the fastest in practice due to good cache locality. However, its O(n^2) worst case can be a problem. It is in-place (O(log n) stack space).
- For large datasets, merge sort and quick sort are preferred. For small or nearly-sorted datasets, insertion sort can be very efficient.
The exam often asks you to recommend a sorting algorithm for a given scenario. Consider: How large is the dataset? Is memory constrained? Is the data nearly sorted? Is stability required? Use these factors to justify your choice.
Searching Tree-Based Data Structures
Binary Search Tree (BST) – a binary tree in which, for every node, all values in its left subtree are less than the node’s value, and all values in its right subtree are greater than the node’s value. This property enables efficient searching.
BST Search Algorithm
To search for a value in a BST:
- Start at the root.
- If the current node is null, the value is not in the tree – return “not found”.
- If the value equals the current node’s data, return the node – found.
- If the value is less than the current node’s data, search the left subtree.
- If the value is greater than the current node’s data, search the right subtree.
This is inherently recursive and mirrors binary search on a sorted array.
Worked Example
Given this BST:
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
Search for 7:
- Start at 8. 7 < 8, go left.
- At 3. 7 > 3, go right.
- At 6. 7 > 6, go right.
- At 7. Found.
Visited 4 nodes out of 9. This is much faster than checking all elements.
Search for 5:
- Start at 8. 5 < 8, go left.
- At 3. 5 > 3, go right.
- At 6. 5 < 6, go left.
- At 4. 5 > 4, go right.
- Right child is null. Not found.
Code
class Node:
def __init__(self, data):
self.data = data
self.left = None
self.right = None
def bst_search(root, target):
"""Search for a value in a BST. Returns the node if found, None otherwise."""
if root is None: # Base case: not found
return None
if target == root.data: # Base case: found
return root
elif target < root.data: # Recursive case: search left
return bst_search(root.left, target)
else: # Recursive case: search right
return bst_search(root.right, target)
def bst_insert(root, data):
"""Insert a value into a BST."""
if root is None:
return Node(data)
if data < root.data:
root.left = bst_insert(root.left, data)
elif data > root.data:
root.right = bst_insert(root.right, data)
return root
# Build the example tree
root = None
for val in [8, 3, 10, 1, 6, 14, 4, 7, 13]:
root = bst_insert(root, val)
# Search
result = bst_search(root, 7)
print("Found" if result else "Not found") # Output: Found
result = bst_search(root, 5)
print("Found" if result else "Not found") # Output: Not found
Class Node
Public Data As Integer
Public Left As Node
Public Right As Node
Sub New(data As Integer)
Me.Data = data
Me.Left = Nothing
Me.Right = Nothing
End Sub
End Class
Function BSTSearch(root As Node, target As Integer) As Node
If root Is Nothing Then ' Base case: not found
Return Nothing
End If
If target = root.Data Then ' Base case: found
Return root
ElseIf target < root.Data Then ' Search left
Return BSTSearch(root.Left, target)
Else ' Search right
Return BSTSearch(root.Right, target)
End If
End Function
Function BSTInsert(root As Node, data As Integer) As Node
If root Is Nothing Then
Return New Node(data)
End If
If data < root.Data Then
root.Left = BSTInsert(root.Left, data)
ElseIf data > root.Data Then
root.Right = BSTInsert(root.Right, data)
End If
Return root
End Function
Sub Main()
Dim root As Node = Nothing
For Each val As Integer In {8, 3, 10, 1, 6, 14, 4, 7, 13}
root = BSTInsert(root, val)
Next
Dim result As Node = BSTSearch(root, 7)
Console.WriteLine(If(result IsNot Nothing, "Found", "Not found")) ' Output: Found
result = BSTSearch(root, 5)
Console.WriteLine(If(result IsNot Nothing, "Found", "Not found")) ' Output: Not found
End Sub
BST Search Complexity
| Case | Time Complexity | When it occurs |
|---|---|---|
| Best | O(1) | Target is at the root |
| Average | O(log n) | Tree is reasonably balanced |
| Worst | O(n) | Tree is completely unbalanced (degenerates to a linked list) |
The average case O(log n) arises because a balanced BST has a height of approximately log2(n), and each comparison eliminates half the remaining nodes.
Big O Notation
Big O notation – a mathematical notation that describes the upper bound of an algorithm’s growth rate as the input size increases. It classifies algorithms by how their time or space requirements scale, ignoring constants and lower-order terms. For example, O(n^2) means the time roughly quadruples when the input doubles.
Common Time Complexities
| Big O | Name | Description | Example |
|---|---|---|---|
| O(1) | Constant | Time does not change with input size | Accessing an array element by index |
| O(log n) | Logarithmic | Time increases slowly; halving the problem each step | Binary search |
| O(n) | Linear | Time grows proportionally to input size | Linear search, traversing a list |
| O(n log n) | Linearithmic | Slightly worse than linear; typical of efficient sorts | Merge sort, quick sort (average) |
| O(n^2) | Quadratic | Time grows with the square of input size | Bubble sort, insertion sort (worst), nested loops |
| O(2^n) | Exponential | Time doubles with each additional input element | Naive recursive Fibonacci, brute-force subset enumeration |
Growth Rate Comparison
For an input of size n, the approximate number of operations:
| n | O(1) | O(log n) | O(n) | O(n log n) | O(n^2) | O(2^n) |
|---|---|---|---|---|---|---|
| 1 | 1 | 0 | 1 | 0 | 1 | 2 |
| 10 | 1 | 3 | 10 | 33 | 100 | 1,024 |
| 100 | 1 | 7 | 100 | 664 | 10,000 | 1.27 x 10^30 |
| 1,000 | 1 | 10 | 1,000 | 9,966 | 1,000,000 | too large |
| 10,000 | 1 | 13 | 10,000 | 132,877 | 100,000,000 | too large |
This table makes it clear why O(2^n) algorithms are impractical for anything beyond very small inputs, and why O(n log n) sorting algorithms are vastly superior to O(n^2) ones for large datasets.
Graphical Description of Growth Rates
Operations
^
| / O(2^n)
| /
| /
| /
| /
| ___---------- O(n^2)
| __---
| __--
| _- ___----------- O(n log n)
| / __----
| / __---
| / _-- __________________ O(n)
| / / ______/
| //_____/ _____________________ O(log n)
| //___________/
| //_________________________________ O(1)
+----------------------------------------------> n
Space Complexity
Space complexity – a measure of how much additional memory an algorithm requires relative to the input size. Like time complexity, it is expressed using Big O notation. It typically measures auxiliary space (extra space beyond the input itself).
| Algorithm | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Linear search | O(n) | O(1) | Only needs a loop variable |
| Binary search (iterative) | O(log n) | O(1) | Only needs low, high, mid variables |
| Binary search (recursive) | O(log n) | O(log n) | Call stack depth |
| Bubble sort | O(n^2) | O(1) | Sorts in place |
| Merge sort | O(n log n) | O(n) | Needs temporary arrays for merging |
| Quick sort (in-place) | O(n log n) avg | O(log n) | Call stack depth |
| BST search | O(log n) avg | O(log n) recursive / O(1) iterative | Depends on implementation |
The exam may ask you to determine the Big O complexity of a given code snippet. Key strategies: count the nesting depth of loops (one loop = O(n), nested loop = O(n^2)), look for halving (suggests O(log n)), and look for recursive calls that split the problem (suggests O(n log n) for divide-and-conquer). Always state the worst case unless told otherwise.
Reverse Polish Notation (Postfix Notation)
Reverse Polish Notation (RPN) – also called postfix notation – is a way of writing arithmetic expressions where the operator comes after its operands. For example, the infix expression 3 + 4 is written as 3 4 + in RPN. It eliminates the need for brackets and operator precedence rules.
Why RPN is Used
| Reason | Explanation |
|---|---|
| No brackets needed | Operator order is unambiguous without parentheses |
| No precedence rules | Operators are applied in the order they appear |
| Easy to evaluate | Can be evaluated using a simple stack-based algorithm |
| Efficient for computers | Used internally by compilers and calculators (especially HP calculators) |
| Maps to hardware | Stack-based CPUs can execute postfix directly |
Infix vs Postfix Examples
| Infix | Postfix (RPN) |
|---|---|
| 3 + 4 | 3 4 + |
| 3 + 4 * 5 | 3 4 5 * + |
| (3 + 4) * 5 | 3 4 + 5 * |
| (A + B) * (C - D) | A B + C D - * |
| A + B * C - D / E | A B C * + D E / - |
Converting Infix to Postfix: The Shunting Yard Algorithm
Shunting yard algorithm – an algorithm invented by Edsger Dijkstra that converts infix expressions to postfix (or prefix) notation using a stack. It processes tokens left to right, using the stack to hold operators temporarily while respecting precedence and associativity.
Algorithm Steps
- Create an empty operator stack and an empty output queue.
- Read tokens from left to right:
- If the token is a number/operand: add it directly to the output queue.
- If the token is an operator (O1):
- While there is an operator O2 on top of the stack, and either:
- O2 has greater precedence than O1, or
- O2 has equal precedence and O1 is left-associative
- Pop O2 from the stack to the output queue.
- Push O1 onto the stack.
- While there is an operator O2 on top of the stack, and either:
- If the token is a left bracket
(: push it onto the stack. - If the token is a right bracket
): pop operators from the stack to the output queue until a left bracket is found. Discard both brackets.
- When all tokens are read, pop all remaining operators from the stack to the output queue.
Operator Precedence
| Operator | Precedence | Associativity |
|---|---|---|
| ^ (power) | 3 (highest) | Right |
| * / | 2 | Left |
| + - | 1 (lowest) | Left |
Worked Example
Convert: ( 3 + 4 ) * 5 - 6 / 2
| Token | Action | Output Queue | Operator Stack |
|---|---|---|---|
| ( | Push to stack | ( | |
| 3 | Output | 3 | ( |
| + | Push to stack | 3 | ( + |
| 4 | Output | 3 4 | ( + |
| ) | Pop until ( | 3 4 + | |
| * | Push to stack | 3 4 + | * |
| 5 | Output | 3 4 + 5 | * |
| - | Pop * (higher prec), push - | 3 4 + 5 * | - |
| 6 | Output | 3 4 + 5 * 6 | - |
| / | Push (higher prec than -) | 3 4 + 5 * 6 | - / |
| 2 | Output | 3 4 + 5 * 6 2 | - / |
| End | Pop all | 3 4 + 5 * 6 2 / - |
Result: 3 4 + 5 * 6 2 / -
Verification: (3 + 4) * 5 - 6 / 2 = 7 * 5 - 3 = 35 - 3 = 32.
Worked Example 2
Convert: A + B * C - D
| Token | Action | Output Queue | Operator Stack |
|---|---|---|---|
| A | Output | A | |
| + | Push | A | + |
| B | Output | A B | + |
| * | Push (* higher than +) | A B | + * |
| C | Output | A B C | + * |
| - | Pop * (higher prec), pop + (equal prec, left-assoc), push - | A B C * + | - |
| D | Output | A B C * + D | - |
| End | Pop all | A B C * + D - |
Result: A B C * + D -
Code: Shunting Yard Algorithm
def infix_to_postfix(expression):
"""Convert infix expression string to postfix using shunting yard algorithm."""
precedence = {'+': 1, '-': 1, '*': 2, '/': 2, '^': 3}
right_associative = {'^'}
output = []
operator_stack = []
tokens = expression.split()
for token in tokens:
if token not in precedence and token not in ('(', ')'):
# Operand (number or variable)
output.append(token)
elif token == '(':
operator_stack.append(token)
elif token == ')':
# Pop until matching left bracket
while operator_stack and operator_stack[-1] != '(':
output.append(operator_stack.pop())
operator_stack.pop() # Remove the '('
else:
# Operator
while (operator_stack and
operator_stack[-1] != '(' and
operator_stack[-1] in precedence and
(precedence[operator_stack[-1]] > precedence[token] or
(precedence[operator_stack[-1]] == precedence[token]
and token not in right_associative))):
output.append(operator_stack.pop())
operator_stack.append(token)
# Pop remaining operators
while operator_stack:
output.append(operator_stack.pop())
return ' '.join(output)
# Test examples
print(infix_to_postfix("( 3 + 4 ) * 5 - 6 / 2"))
# Output: 3 4 + 5 * 6 2 / -
print(infix_to_postfix("A + B * C - D"))
# Output: A B C * + D -
Function InfixToPostfix(expression As String) As String
Dim precedence As New Dictionary(Of String, Integer) From {
{"+", 1}, {"-", 1}, {"*", 2}, {"/", 2}, {"^", 3}
}
Dim rightAssociative As New HashSet(Of String) From {"^"}
Dim output As New List(Of String)
Dim operatorStack As New Stack(Of String)
Dim tokens() As String = expression.Split(" "c)
For Each token As String In tokens
If Not precedence.ContainsKey(token) AndAlso token <> "(" AndAlso token <> ")" Then
' Operand
output.Add(token)
ElseIf token = "(" Then
operatorStack.Push(token)
ElseIf token = ")" Then
' Pop until matching left bracket
While operatorStack.Count > 0 AndAlso operatorStack.Peek() <> "("
output.Add(operatorStack.Pop())
End While
operatorStack.Pop() ' Remove the "("
Else
' Operator
While operatorStack.Count > 0 AndAlso
operatorStack.Peek() <> "(" AndAlso
precedence.ContainsKey(operatorStack.Peek()) AndAlso
(precedence(operatorStack.Peek()) > precedence(token) OrElse
(precedence(operatorStack.Peek()) = precedence(token) AndAlso
Not rightAssociative.Contains(token)))
output.Add(operatorStack.Pop())
End While
operatorStack.Push(token)
End If
Next
' Pop remaining operators
While operatorStack.Count > 0
output.Add(operatorStack.Pop())
End While
Return String.Join(" ", output)
End Function
Sub Main()
Console.WriteLine(InfixToPostfix("( 3 + 4 ) * 5 - 6 / 2"))
' Output: 3 4 + 5 * 6 2 / -
Console.WriteLine(InfixToPostfix("A + B * C - D"))
' Output: A B C * + D -
End Sub
Evaluating Postfix Expressions Using a Stack
Postfix evaluation – the process of computing the result of a postfix expression using a stack. Operands are pushed onto the stack; when an operator is encountered, the required number of operands are popped, the operation is applied, and the result is pushed back.
Algorithm
- Read tokens from left to right.
- If the token is a number, push it onto the stack.
- If the token is an operator, pop two operands from the stack (the first popped is the right operand, the second is the left operand), apply the operator, and push the result.
- After all tokens are processed, the single value remaining on the stack is the result.
Worked Example
Evaluate: 3 4 + 5 * 6 2 / -
| Token | Action | Stack (top on right) |
|---|---|---|
| 3 | Push | 3 |
| 4 | Push | 3, 4 |
| + | Pop 4 and 3, compute 3+4=7, push | 7 |
| 5 | Push | 7, 5 |
| * | Pop 5 and 7, compute 7*5=35, push | 35 |
| 6 | Push | 35, 6 |
| 2 | Push | 35, 6, 2 |
| / | Pop 2 and 6, compute 6/2=3, push | 35, 3 |
| - | Pop 3 and 35, compute 35-3=32, push | 32 |
Result: 32
This matches (3 + 4) * 5 - 6 / 2 = 32.
Worked Example 2
Evaluate: 5 3 2 * + 4 -
| Token | Action | Stack |
|---|---|---|
| 5 | Push | 5 |
| 3 | Push | 5, 3 |
| 2 | Push | 5, 3, 2 |
| * | Pop 2 and 3, compute 3*2=6, push | 5, 6 |
| + | Pop 6 and 5, compute 5+6=11, push | 11 |
| 4 | Push | 11, 4 |
| - | Pop 4 and 11, compute 11-4=7, push | 7 |
Result: 7 (equivalent infix: 5 + 3 * 2 - 4 = 7)
Code
def evaluate_postfix(expression):
"""Evaluate a postfix expression and return the numeric result."""
stack = []
tokens = expression.split()
operators = {'+', '-', '*', '/'}
for token in tokens:
if token in operators:
right = stack.pop() # First popped = right operand
left = stack.pop() # Second popped = left operand
if token == '+':
stack.append(left + right)
elif token == '-':
stack.append(left - right)
elif token == '*':
stack.append(left * right)
elif token == '/':
stack.append(left / right)
else:
stack.append(float(token))
return stack[0]
print(evaluate_postfix("3 4 + 5 * 6 2 / -")) # Output: 32.0
print(evaluate_postfix("5 3 2 * + 4 -")) # Output: 7.0
Function EvaluatePostfix(expression As String) As Double
Dim stack As New Stack(Of Double)
Dim tokens() As String = expression.Split(" "c)
Dim operators As New HashSet(Of String) From {"+", "-", "*", "/"}
For Each token As String In tokens
If operators.Contains(token) Then
Dim right As Double = stack.Pop() ' First popped = right operand
Dim left As Double = stack.Pop() ' Second popped = left operand
Select Case token
Case "+"
stack.Push(left + right)
Case "-"
stack.Push(left - right)
Case "*"
stack.Push(left * right)
Case "/"
stack.Push(left / right)
End Select
Else
stack.Push(Double.Parse(token))
End If
Next
Return stack.Pop()
End Function
Sub Main()
Console.WriteLine(EvaluatePostfix("3 4 + 5 * 6 2 / -")) ' Output: 32
Console.WriteLine(EvaluatePostfix("5 3 2 * + 4 -")) ' Output: 7
End Sub
When evaluating postfix expressions, the order of operands matters for subtraction and division. The second value popped is the left operand and the first value popped is the right operand. So if you pop B then A, the result is A operator B, not B operator A. Getting this wrong is a very common mistake.
Binary Expression Trees from Postfix Notation
Binary expression tree – a binary tree that represents an arithmetic expression. Each leaf node contains an operand (number or variable), and each internal node contains an operator. The left and right children of an operator node are its operands (or sub-expressions).
Building a Binary Expression Tree from Postfix
The algorithm is very similar to postfix evaluation, but instead of computing values, we build tree nodes:
- Read tokens left to right.
- If the token is an operand, create a leaf node and push it onto the stack.
- If the token is an operator, pop two nodes from the stack (first popped becomes right child, second becomes left child), create a new node with the operator and these children, and push the new node.
- The final node on the stack is the root of the expression tree.
Worked Example
Build a tree from: 3 4 + 5 *
| Token | Action | Stack (tree nodes) |
|---|---|---|
| 3 | Create leaf [3], push | [3] |
| 4 | Create leaf [4], push | [3], [4] |
| + | Pop [4] and [3], create [+] with left=[3], right=[4], push | [+] |
| 5 | Create leaf [5], push | [+], [5] |
| * | Pop [5] and [+], create [*] with left=[+], right=[5], push | [*] |
Resulting tree:
graph TD
A["*"] --> B["+"]
A --> C["5"]
B --> D["3"]
B --> E["4"]
This represents the infix expression: (3 + 4) * 5 = 35.
Larger Worked Example
Build a tree from: A B + C D - *
| Token | Action | Stack |
|---|---|---|
| A | Leaf [A], push | [A] |
| B | Leaf [B], push | [A], [B] |
| + | Pop [B] and [A], create [+], push | [+] |
| C | Leaf [C], push | [+], [C] |
| D | Leaf [D], push | [+], [C], [D] |
| - | Pop [D] and [C], create [-], push | [+], [-] |
| * | Pop [-] and [+], create [*], push | [*] |
Resulting tree:
graph TD
A["*"] --> B["+"]
A --> C["-"]
B --> D["A"]
B --> E["B"]
C --> F["C"]
C --> G["D"]
This represents: (A + B) * (C - D).
Traversals of Expression Trees
Different tree traversals produce different notations:
| Traversal | Order | Produces | Result for above tree |
|---|---|---|---|
| In-order | Left, Root, Right | Infix notation | A + B * C - D (needs brackets for correctness) |
| Pre-order | Root, Left, Right | Prefix notation | * + A B - C D |
| Post-order | Left, Right, Root | Postfix notation | A B + C D - * |
Code
class ExpressionNode:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
def build_expression_tree(postfix):
"""Build a binary expression tree from a postfix expression."""
stack = []
operators = {'+', '-', '*', '/', '^'}
tokens = postfix.split()
for token in tokens:
node = ExpressionNode(token)
if token in operators:
node.right = stack.pop() # First popped = right child
node.left = stack.pop() # Second popped = left child
stack.append(node)
return stack[0] # Root of the tree
def inorder(node):
"""In-order traversal: produces infix notation (with brackets)."""
if node is None:
return ""
if node.left is None and node.right is None:
return node.value
return f"({inorder(node.left)} {node.value} {inorder(node.right)})"
def preorder(node):
"""Pre-order traversal: produces prefix notation."""
if node is None:
return ""
if node.left is None and node.right is None:
return node.value
return f"{node.value} {preorder(node.left)} {preorder(node.right)}"
def postorder(node):
"""Post-order traversal: produces postfix notation."""
if node is None:
return ""
if node.left is None and node.right is None:
return node.value
return f"{postorder(node.left)} {postorder(node.right)} {node.value}"
# Build tree from postfix: A B + C D - *
root = build_expression_tree("A B + C D - *")
print("Infix: ", inorder(root)) # Output: ((A + B) * (C - D))
print("Prefix: ", preorder(root)) # Output: * + A B - C D
print("Postfix:", postorder(root)) # Output: A B + C D - *
Class ExpressionNode
Public Value As String
Public Left As ExpressionNode
Public Right As ExpressionNode
Sub New(value As String)
Me.Value = value
Me.Left = Nothing
Me.Right = Nothing
End Sub
End Class
Function BuildExpressionTree(postfix As String) As ExpressionNode
Dim stack As New Stack(Of ExpressionNode)
Dim operators As New HashSet(Of String) From {"+", "-", "*", "/", "^"}
Dim tokens() As String = postfix.Split(" "c)
For Each token As String In tokens
Dim node As New ExpressionNode(token)
If operators.Contains(token) Then
node.Right = stack.Pop() ' First popped = right child
node.Left = stack.Pop() ' Second popped = left child
End If
stack.Push(node)
Next
Return stack.Pop() ' Root of the tree
End Function
Function Inorder(node As ExpressionNode) As String
If node Is Nothing Then Return ""
If node.Left Is Nothing AndAlso node.Right Is Nothing Then
Return node.Value
End If
Return $"({Inorder(node.Left)} {node.Value} {Inorder(node.Right)})"
End Function
Function Preorder(node As ExpressionNode) As String
If node Is Nothing Then Return ""
If node.Left Is Nothing AndAlso node.Right Is Nothing Then
Return node.Value
End If
Return $"{node.Value} {Preorder(node.Left)} {Preorder(node.Right)}"
End Function
Function Postorder(node As ExpressionNode) As String
If node Is Nothing Then Return ""
If node.Left Is Nothing AndAlso node.Right Is Nothing Then
Return node.Value
End If
Return $"{Postorder(node.Left)} {Postorder(node.Right)} {node.Value}"
End Function
Sub Main()
' Build tree from postfix: A B + C D - *
Dim root As ExpressionNode = BuildExpressionTree("A B + C D - *")
Console.WriteLine("Infix: " & Inorder(root)) ' Output: ((A + B) * (C - D))
Console.WriteLine("Prefix: " & Preorder(root)) ' Output: * + A B - C D
Console.WriteLine("Postfix: " & Postorder(root)) ' Output: A B + C D - *
End Sub
Binary expression trees connect three major topics: postfix notation, trees, and traversals. The exam may ask you to build a tree from a postfix expression, or to perform traversals to produce different notations. Remember: in-order gives infix (but you need brackets), pre-order gives prefix (Polish notation), and post-order gives postfix (Reverse Polish Notation). Practise building trees by hand – draw the tree and verify by performing a post-order traversal to recover the original postfix expression.