Computational thinking and algorithms
This unit joins computational thinking (decomposition, pattern recognition, abstraction, algorithm design) with the data structures that algorithms work on: arrays, linked lists, stacks, queues, graphs and trees, including tree traversals and binary search trees. It then shows how to evaluate a computational solution for correctness, clarity and efficiency, using time and space complexity in Big O notation, with binary search, linear search and case studies as examples.
Learning outcomes (2)
- Identify and apply complex algorithms on data structures such as trees and binary search.
- Understand and evaluate computational solutions in terms of efficiency, clarity and correctness.
IntroComputational thinking and its four pillarsOften examined
Computational thinking (CT) is a problem-solving process based on the ideas of computer science. It lets us solve a big, complex problem by breaking it into small parts and then dealing with each part in a logical, step-by-step way. CT is not only for programmers. Doctors, engineers, business people and students use the same way of thinking in daily life.
- Decomposition
- Breaking a problem into smaller, more manageable parts. Example: building a website is split into designing, coding, backend set-up and testing.
- Pattern recognition
- Finding similarities or trends inside a problem or across similar problems, so that one solution can be reused. Example: noticing that every subject's marks are processed in the same way.
- Abstraction
- Focusing on the important information and ignoring details that do not matter for the problem. Example: a map of bus routes shows stops and routes, not every building.
- Algorithm design
- Writing clear, step-by-step instructions that solve the problem. Example: a fixed procedure that sorts numbers from smallest to largest.
- Decomposition: split the job into parts: enter marks, calculate total and percentage for each student, assign a grade, print the report card.
- Pattern recognition: the same steps (add, average, grade) repeat for every student and every subject, so one routine can be used in a loop.
- Abstraction: keep only name, roll number and marks; ignore details such as a student's address or seat number, which are not needed for the result.
- Algorithm design: write the steps in order: for each student, total = sum of marks; percentage = total / maximum × 100; if percentage ≥ 80 then grade A+, and so on.
At HSSC-II level, CT is used for two jobs: to design algorithms and to evaluate them for correctness, clarity and efficiency. The choice of data structure strongly affects how well a solution performs, so this unit first studies data structures (2.1) and then how to evaluate solutions (2.2).
2.1Data structuresOften examined
A program works with data: marks, names, prices, messages. How that data is arranged in memory decides how quickly we can find, add or remove an item. A data structure is a way of organising and storing data so that it can be accessed and modified efficiently.
Data structures give a framework for managing data in programs. They decide how data is arranged in memory and so affect the speed of operations such as insertion (adding), deletion (removing) and retrieval (finding or reading). For example, an array gives quick access to any element by its index, while a linked list makes it easy to add and remove elements. The right choice (array, linked list, stack, queue, tree or graph) depends on the needs of the application and the operations it performs most often.
| Type | Meaning | Examples |
|---|---|---|
| Linear | Elements are arranged one after another in a sequence | Array, linked list, stack, queue |
| Non-linear | Elements are arranged in a hierarchy or a network; one element can connect to many | Tree, graph |
| Static | Size is fixed when the structure is created | Array |
| Dynamic | Size can grow or shrink while the program runs | Linked list (and stacks or queues built on it) |
2.1.1ArraysOften examined
An array is the simplest data structure. It stores a collection of elements of the same type (and size) in contiguous (side by side) memory locations. Each element is identified by an index. In Python and most languages the first index is 0.
Why arrays are efficient
- They need no extra data (metadata such as pointers) to store the elements, so no memory is wasted on links.
- Because the locations are contiguous, the address of any element can be calculated directly from its index. So any element can be reached in one step: this is called random (direct) access and takes time.
For Fig. 2.1 with 1 byte per element: address of A[4] = 100 + 4 × 1 = 104, which holds E.
Limitations of arrays
- Fixed size: the size must be declared at the start. Resizing later is costly, because a new, larger array must be created and all elements shifted (copied) into it.
- Same type only: an array is not a good choice when the elements are of different types.
- Slow insertion and deletion in the middle: to insert at position , all elements after must move one place to the right.
Types of arrays
- One-dimensional (1-D) array
- Elements of the same type stored in a single row (list). An element is reached with one index, for example marks[3].
- Two-dimensional (2-D) array
- Also called a matrix. Elements are stored as a table of rows and columns. An element is reached with two indexes, row and column, for example M[2][3].
Arrays in Python
Python's built-in list is used as an array. A 2-D array is a list of lists. (Python lists can hold mixed types, but for array work we keep one type.)
marks = [2, 4, 8, 12, 16, 18] # 1-D array
print("marks[3] =", marks[3]) # direct access by index
marks[0] = 5 # modify an element
print("Updated:", marks)
M = [[1, 2, 3, 4, 5], # 2-D array: 4 rows, 5 columns
[6, 7, 8, 9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20]]
print("M[2][3] =", M[2][3]) # row 2, column 3
for row in M: # traverse row by row
print(row)marks[3] = 12
Updated: [5, 4, 8, 12, 16, 18]
M[2][3] = 14
[1, 2, 3, 4, 5]
[6, 7, 8, 9, 10]
[11, 12, 13, 14, 15]
[16, 17, 18, 19, 20]2.1.2Linked listsOften examined
In an array the elements sit next to each other. In a linked list they can be anywhere in memory; each element remembers where the next one is. A linked list is a data structure in which elements, called nodes, are connected using pointers. Each node stores a value and a pointer, which is the reference (memory address) of the next node in the sequence. The first node is reached through a pointer called head.
Advantages of linked lists
- Dynamic size: no need to declare the size at the start; the list grows and shrinks as nodes are added or removed.
- Easy insertion and deletion at any position: only a few pointers are changed; no elements are shifted as in an array.
- They can store different types of data, and memory is allocated only when a node is needed.
Disadvantages
- No random access: to reach the 50th node we must start at head and follow 49 links, so access takes time.
- Extra memory is used for the pointer(s) in every node.
Singly linked list
The basic type. Each node has two parts: the data to be stored and a reference to the next node. The last node's next pointer is NULL (in Python, None) because it does not refer to any node. Traversal is possible in one direction only (forward).
Doubly linked list
More complex than a singly linked list but more useful. Each node has three parts: a reference to the previous node, the data, and a reference to the next node. The major advantage is that the list can be traversed in both directions. The prev of the first node and the next of the last node are NULL.
Circular linked list
A linked list in which the nodes form a circle (loop). The last node does not point to NULL; it points back to the first node. So a traversal never reaches NULL, and it must stop when it comes back to the starting node. A circular list can be made from either a singly or a doubly linked list. In a circular doubly linked list, the next of the last node points to the first node and the prev of the first node points to the last node.
| Point | Singly | Doubly | Circular |
|---|---|---|---|
| Parts of a node | data, next | prev, data, next | data, next (or prev, data, next) |
| Direction of traversal | Forward only | Forward and backward | Round and round |
| Last node points to | NULL | NULL (next) | The first node |
| Memory per node | Less | More (two pointers) | Same as the type it is built on |
| Array | Linked list |
|---|---|
| Fixed size, declared at the start | Dynamic size, grows and shrinks |
| Contiguous memory | Nodes anywhere in memory, joined by pointers |
| Random access by index in | Sequential access from head in |
| Insertion or deletion in the middle needs shifting | Insertion or deletion only changes pointers |
| No extra memory for links | Extra memory for each pointer |
A singly linked list in Python
class Node:
def __init__(self, data):
self.data = data # value stored in the node
self.next = None # reference to the next node (None = NULL)
class LinkedList:
def __init__(self):
self.head = None
def insert_at_beginning(self, data):
node = Node(data)
node.next = self.head # new node points to old first node
self.head = node # head now points to the new node
def append(self, data):
node = Node(data)
if self.head is None:
self.head = node
return
cur = self.head
while cur.next is not None: # walk to the last node
cur = cur.next
cur.next = node
def delete(self, value):
cur, prev = self.head, None
while cur is not None and cur.data != value:
prev, cur = cur, cur.next
if cur is None:
return # value not found
if prev is None:
self.head = cur.next # deleting the first node
else:
prev.next = cur.next # bypass the deleted node
def display(self):
cur, items = self.head, []
while cur is not None:
items.append(str(cur.data))
cur = cur.next
print(" -> ".join(items + ["NULL"]))
ll = LinkedList()
for x in ["B", "C", "D"]:
ll.append(x)
ll.insert_at_beginning("A")
ll.display()
ll.delete("C")
ll.display()A -> B -> C -> D -> NULL
A -> B -> D -> NULL2.1.3StacksOften examined
Think of a pile of plates. You put a new plate on the top and you also take a plate from the top. A stack is a linear data structure that follows the LIFO (Last-In, First-Out) principle: the element added most recently is the first one to be removed. All operations take place at one end, called the top. This makes a stack simple, and it is useful for algorithms that must manage tasks in a specific (reverse) order. A stack is memory efficient; its size may be fixed (array based) or dynamic (linked list based), depending on the implementation.
- Push
- Add a new element at the top of the stack.
- Pop
- Remove the existing element from the top of the stack (and return it).
- Top (peek)
- Return the top element without removing it.
- IsEmpty
- Check whether the stack is empty.
- IsFull
- Check whether the stack is full (for a fixed-size stack).
Stack in Python
class Stack:
def __init__(self, capacity):
self.items = []
self.capacity = capacity
def is_empty(self):
return len(self.items) == 0
def is_full(self):
return len(self.items) == self.capacity
def push(self, x):
if self.is_full():
print("Stack overflow: cannot push", x)
else:
self.items.append(x) # add at the top (end of list)
def pop(self):
if self.is_empty():
print("Stack underflow")
return None
return self.items.pop() # remove from the top
def top(self):
return None if self.is_empty() else self.items[-1]
s = Stack(3)
s.push("A"); s.push("B"); s.push("C")
s.push("D") # stack is full
print("Stack:", s.items, " top =", s.top())
print("Popped:", s.pop())
print("Stack:", s.items, " top =", s.top())Stack overflow: cannot push D
Stack: ['A', 'B', 'C'] top = C
Popped: C
Stack: ['A', 'B'] top = BApplications of stacks
- Undo in text editors and the back button of a web browser (the last action or page is undone first).
- Function calls and recursion: the computer keeps a call stack; the most recent call finishes first.
- Reversing a word or list, and checking balanced brackets in expressions.
- Depth-first search (DFS) in graphs and trees uses a stack.
2.1.4QueuesOften examined
Think of students standing in a line at the canteen. The student who came first is served first, and new students join at the back. A queue is a linear data structure that follows the FIFO (First-In, First-Out) principle: the first element added is the first one removed. Unlike a stack, a queue works at two ends: new elements are added at the rear (back or tail) and elements are removed from the front (head). It is useful for tasks that must be processed in the order they arrive.
- Enqueue
- Add a new element at the end (back, tail or rear) of the queue.
- Dequeue
- Remove the existing element from the start (front or head) of the queue.
- Front (peek)
- Return the front element without removing it.
- IsEmpty
- Check whether the queue is empty.
- IsFull
- Check whether the queue is full (for a fixed-size queue).
Queue in Python
Python's removes from the front in time, so it is the usual way to build a queue.
from collections import deque
q = deque()
for job in ["Print A", "Print B", "Print C"]:
q.append(job) # enqueue at the rear
print("Queue:", list(q))
print("Front:", q[0]) # peek at the front
print("Dequeued:", q.popleft()) # dequeue from the front
q.append("Print D")
print("Queue:", list(q))
print("Is empty?", len(q) == 0)Queue: ['Print A', 'Print B', 'Print C']
Front: Print A
Dequeued: Print A
Queue: ['Print B', 'Print C', 'Print D']
Is empty? FalseApplications of queues
- Scheduling: the CPU and printers serve jobs in the order they arrive (print queue).
- Buffering: keyboard input, video streaming and network packets are held in a queue until they are processed.
- Breadth-first search (BFS) in graphs uses a queue.
- Real life: customers at a bank counter, calls waiting at a call centre.
Stack versus queue
| Point | Stack | Queue |
|---|---|---|
| Principle | LIFO: last in, first out | FIFO: first in, first out |
| Ends used | One end (top) for both adding and removing | Two ends: add at rear, remove at front |
| Add operation | Push | Enqueue |
| Remove operation | Pop | Dequeue |
| Peek operation | Top | Front |
| Used in | Undo, back button, recursion, DFS | Scheduling, buffering, print queue, BFS |
| Real-life example | Pile of plates or books | Line of people at a ticket window |
2.1.5Graphs
A road map shows cities joined by roads. A graph is a non-linear data structure made of nodes (vertices) connected to each other by edges. Graphs model complex relationships and networks: friends in a social network, cities and roads, computers in a network. They are used in network analysis, finding shortest paths (for example Dijkstra's algorithm) and checking connectivity.
Basic operations on a graph
- Add vertex
- Add a new node to the graph.
- Add edge
- Create a connection between two nodes.
- Remove vertex or edge
- Delete a node or an edge from the graph.
- Traversal
- Visit the nodes of the graph in a specific order, for example breadth-first search (BFS) or depth-first search (DFS).
Types of graphs
- Connected graph
- A graph in which at least one path exists between every pair of nodes. Starting from any node you can reach any other node.
- Directed graph
- A graph whose edges have a direction, shown by arrows. An edge from 1 to 0 lets you go from 1 to 0 but not back, unless there is also an edge from 0 to 1.
Storing and traversing a graph in Python
A graph is commonly stored as an adjacency list: for each vertex, a list of its neighbours. BFS visits nodes level by level using a queue; DFS goes as deep as possible first using a stack (here, recursion).
from collections import deque
graph = { # adjacency list of Fig. 2.13
1: [3, 2],
2: [1, 4, 6],
3: [5, 1, 4],
4: [3, 2],
5: [3],
6: [2],
}
def bfs(start):
visited, order = {start}, []
q = deque([start])
while q:
v = q.popleft() # dequeue
order.append(v)
for n in graph[v]:
if n not in visited:
visited.add(n)
q.append(n) # enqueue
return order
def dfs(v, visited=None):
if visited is None:
visited = []
visited.append(v)
for n in graph[v]:
if n not in visited:
dfs(n, visited) # recursion uses the call stack
return visited
print("BFS from 5:", bfs(5))
print("DFS from 5:", dfs(5))BFS from 5: [5, 3, 1, 4, 2, 6]
DFS from 5: [5, 3, 1, 2, 4, 6]2.1.6Trees and binary search treesOften examined
A family tree or the folders on your computer have one item at the top that branches into more items below. A tree is a non-linear data structure that represents a hierarchy of nodes. Each node consists of a value and pointers to its children.
- Root
- The topmost node of the tree. It has no parent.
- Parent and child
- Directly connected nodes form a parent-child relationship: the higher node is the parent and the lower node is the child.
- Leaf node
- A node that has no child.
- Edge
- The link (relationship) between two nodes.
- Siblings
- Nodes that have the same parent.
- Level
- The distance of a node from the root; the root is at level 0.
- Subtree
- A node together with all of its descendants; it is itself a tree.
- Height
- The number of edges on the longest path from the root to a leaf.
Trees are important for understanding complex relationships between data, and they are the base of many advanced data structures. In computer science, the directory (folder) structure of an operating system and the tag structure of an HTML page are the best examples of trees.
Basic operations on trees
- Insertion
- Add a new node at a specified location in the tree.
- Deletion
- Remove an existing node.
- Search
- Find a specific node in the tree.
- Traversal
- Visit all nodes of the tree in a specific order (in-order, pre-order, post-order).
Binary tree and binary search tree (BST)
A binary tree is a tree in which every node has at most two children, called the left child and the right child. A binary search tree (BST) is a binary tree with an ordering rule: for every node, all keys in its left subtree are smaller than the node's key and all keys in its right subtree are larger. This rule is what makes searching fast: at each node we compare once and go only left or only right, like binary search on a sorted list.
- Compare 60 with the root 50: 60 > 50, so go right.
- Compare 60 with 70: 60 < 70, so go left.
- Compare 60 with 60: equal, so the key is found.
class Node:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
def insert(root, key):
if root is None:
return Node(key)
if key < root.key:
root.left = insert(root.left, key) # smaller keys go left
else:
root.right = insert(root.right, key) # larger keys go right
return root
def search(root, key):
steps = 0
while root is not None:
steps += 1
if key == root.key:
return True, steps
root = root.left if key < root.key else root.right
return False, steps
root = None
for k in [50, 30, 70, 20, 40, 60, 80]:
root = insert(root, k)
print("Search 60:", search(root, 60))
print("Search 45:", search(root, 45))Search 60: (True, 3)
Search 45: (False, 3)2.1.6Tree traversalOften examined
Tree traversal means visiting every node of a tree exactly once in a systematic order. There are several methods and each has a different purpose. For binary trees the three common depth-first methods differ only in when the root is visited: before, between or after its two subtrees.
| Traversal | Order of operations | Memory aid |
|---|---|---|
| In-order | 1. Traverse the left subtree. 2. Visit the root. 3. Traverse the right subtree. | Left, Root, Right (root in the middle) |
| Pre-order | 1. Visit the root. 2. Traverse the left subtree. 3. Traverse the right subtree. | Root, Left, Right (root first: pre) |
| Post-order | 1. Traverse the left subtree. 2. Traverse the right subtree. 3. Visit the root. | Left, Right, Root (root last: post) |
In-order traversal (Fig. 2.17)
Start at root 1 and go down the left side to the leftmost node 4. Visit 4, then its parent 2, then 2's right child 5. The left subtree of 1 is finished, so visit 1, then traverse its right subtree: 3 has no left child, so visit 3, then 6. Order: 4, 2, 5, 1, 3, 6. In-order is often used with binary search trees, because it visits the keys in ascending (sorted) order.
Pre-order traversal (Fig. 2.18)
Visit the root 1 first, then the whole left subtree in pre-order (2, then 4, then 5), then the right subtree (3, then 6). Order: 1, 2, 4, 5, 3, 6. Pre-order is useful when we must work with a node before its children, for example to copy a tree or to print a folder before the files inside it.
Post-order traversal (Fig. 2.19)
Finish the left subtree (4, 5, then 2), then the right subtree (6, then 3), and visit the root 1 last. Order: 4, 5, 2, 6, 3, 1. Post-order is used for deleting nodes or freeing memory, because it processes the children before their parent; it is also used to find the total size of a folder.
- In-order (L, Root, R): left subtree of 1 gives 4, 2, 5; then 1; right subtree gives 6, 3, 7. Result: 4, 2, 5, 1, 6, 3, 7.
- Pre-order (Root, L, R): 1; then 2, 4, 5; then 3, 6, 7. Result: 1, 2, 4, 5, 3, 6, 7.
- Post-order (L, R, Root): 4, 5, 2; then 6, 7, 3; then 1. Result: 4, 5, 2, 6, 7, 3, 1.
Traversals in Python
class Node:
def __init__(self, key, left=None, right=None):
self.key, self.left, self.right = key, left, right
def inorder(n):
if n:
inorder(n.left); print(n.key, end=" "); inorder(n.right)
def preorder(n):
if n:
print(n.key, end=" "); preorder(n.left); preorder(n.right)
def postorder(n):
if n:
postorder(n.left); postorder(n.right); print(n.key, end=" ")
# tree of Fig. 2.17 to 2.19
root = Node(1, Node(2, Node(4), Node(5)), Node(3, None, Node(6)))
print("In-order: ", end=" "); inorder(root); print()
print("Pre-order: ", end=" "); preorder(root); print()
print("Post-order:", end=" "); postorder(root); print()In-order: 4 2 5 1 3 6
Pre-order: 1 2 4 5 3 6
Post-order: 4 5 2 6 3 1 2.1How data structures support computational thinking
- Problem decomposition: different data structures help break a problem into manageable parts, which makes algorithms easier to design and implement.
- Efficiency considerations: they show trade-offs between operations, for example fast access (array) versus fast insertion (linked list), which helps us choose the most suitable structure for a problem.
- Algorithmic thinking: understanding these structures helps in developing and analysing algorithms, and improves problem solving by choosing the right tool for each task.
So data structures are not just containers for data; they shape the way we think about a problem and design its algorithm.
| Need of the problem | Suitable structure | Reason |
|---|---|---|
| Fast access to the th item | Array | Direct access by index |
| Frequent insertion and deletion, unknown size | Linked list | Only pointers change; dynamic size |
| Undo, back button, reversing | Stack | LIFO order |
| Serve requests in arrival order | Queue | FIFO order |
| Hierarchy (folders, HTML tags), fast search | Tree or BST | Parent-child structure; search in when balanced |
| Networks, routes, friendships | Graph | Any node can connect to any other |
2.2, 2.2.1Evaluating computational solutions: correctnessOften examined
There is usually more than one algorithm for a problem. To decide which one is better we evaluate it against three criteria: correctness (does it give the right answer?), clarity (is it easy to understand?) and efficiency (does it use little time and memory?).
Correctness
An algorithm is correct if it produces the expected output for all valid inputs. To check correctness we run it on many test cases that cover different situations, including edge cases: unusual or boundary inputs such as an empty list, a list with one element, repeated values, negative numbers or zero.
Bubble sort repeatedly compares neighbouring elements and swaps them if they are in the wrong order. After each pass the largest remaining element \"bubbles\" to its correct place at the end.
def bubble_sort(arr):
a = arr[:] # work on a copy
n = len(a)
for i in range(n - 1): # passes
for j in range(n - 1 - i): # compare neighbours
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j] # swap
return a
tests = [
([5, 2, 9, 1, 5, 6], [1, 2, 5, 5, 6, 9]), # book's test case
([], []), # edge case: empty
([7], [7]), # edge case: one element
([3, 2, 1], [1, 2, 3]), # reverse order
([-4, 0, -9, 2], [-9, -4, 0, 2]), # negative numbers
]
for data, expected in tests:
result = bubble_sort(data)
print(data, "->", result, "PASS" if result == expected else "FAIL")[5, 2, 9, 1, 5, 6] -> [1, 2, 5, 5, 6, 9] PASS
[] -> [] PASS
[7] -> [7] PASS
[3, 2, 1] -> [1, 2, 3] PASS
[-4, 0, -9, 2] -> [-9, -4, 0, 2] PASSBecause it gives the sorted array for all these cases, including the edge cases, bubble sort can be considered correct.
Test it with keys that exist (first, middle and last positions), keys that do not exist (smaller than all, larger than all, in between), and arrays of different sizes including an empty array. The full program and its tests are in the binary search section below.
2.2.2Clarity
An algorithm is clear if its steps are logically structured and easy to understand. Clear code is easier to check, debug, modify and share with other programmers.
How to make an algorithm clear
- Use descriptive names for variables and functions (total_marks, not tm).
- Order the steps logically, one task per step.
- Add documentation and comments that explain what the code does.
- Use proper indentation and simple, standard constructs instead of clever one-line tricks.
Example: two Python functions that calculate the factorial of a number.
# Case 1: clear code
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n - 1)
# Case 2: less clear code
def fct(n):
return 1 if n == 0 else n * fct(n - 1)
print(factorial(5), fct(5))120 120Both give 120 for , so both are correct. The first case is easier to read: the name factorial tells what the function does and the condition if n == 0 is written clearly on its own line. The second case uses a short name (fct) and a ternary operator on one line; it is valid but less understandable for someone new to the code.
2.2.3Efficiency: time complexity, space complexity and Big OOften examined
Efficiency refers to how well an algorithm uses computer resources, mainly time and memory. It is usually expressed using Big O notation, for example , , . Here is the input size: the size of the problem, such as the number of items in a list.
- Time complexity
- A measure of the execution time of an algorithm with respect to the size of its input. It is estimated by counting the basic steps (comparisons, assignments) the algorithm performs as grows, not by using a stopwatch, because real time depends on the computer.
- Space complexity
- A measure of the amount of memory an algorithm uses with respect to the size of its input (extra variables, arrays, recursion stack).
- Worst case
- The input that makes the algorithm do the most work. For searching an array one by one, the worst case is when the item is at the last index or not present.
- Big O notation
- A notation that gives an upper bound of an algorithm's running time or space as the input grows. It describes the efficiency under the most challenging (worst-case) conditions and ignores constants and smaller terms.
| Point | Time complexity | Space complexity |
|---|---|---|
| Measures | Running time (number of steps) | Memory used |
| Depends on | Loops, comparisons, recursive calls | Extra variables, data structures, recursion depth |
| Linear search | ||
| Binary search (recursive) | ||
| Aim | Fewer steps | Less memory |
Common Big O notations
| Notation | Name | Meaning | Example |
|---|---|---|---|
| Constant time | Time does not change with input size | Accessing an array element by index, push or pop on a stack | |
| Logarithmic time | Time grows very slowly; each step halves the search space | Binary search, looking up a word in a dictionary | |
| Linear time | Time grows in proportion to ; double the input, double the time | A loop over every element, linear search | |
| Linearithmic time | Linear times logarithmic growth; the problem is split into parts and each part processed | Merge sort, quick sort (average) | |
| Quadratic time | Nested loops compare each element with every other; double the input, four times the time | Bubble sort, selection sort |
import math
print(f"{'n':>6} {'log2 n':>7} {'n log2 n':>9} {'n^2':>10}")
for n in [10, 100, 1000, 10000]:
print(f"{n:>6} {math.log2(n):>7.1f} {n * math.log2(n):>9.0f} {n * n:>10}") n log2 n n log2 n n^2
10 3.3 33 100
100 6.6 664 10000
1000 10.0 9966 1000000
10000 13.3 132877 100000000The table shows why efficiency matters for large inputs: for 10,000 items an algorithm needs about 13 steps, an algorithm 10,000 steps and an algorithm 100 million steps.
2.2.3Example 1: binary searchOften examined
Binary search locates a target value in a sorted array by repeatedly dividing the search interval in half. It is a divide and conquer algorithm.
Algorithm steps
- Start by examining the number in the centre (middle) of the list.
- Check whether this middle number equals the one we are looking for.
- If it matches, we have located the number.
- If the target is smaller (on the left part of the list), repeat the process on the left half, ignoring the right half.
- If the target is larger (in the right half), ignore the left half and repeat the process on the right half.
- Continue until the number is found or the remaining part of the list is empty (the number is not in the list).
Trace
Search for 23 in the sorted list [2, 5, 8, 12, 16, 23, 38, 56, 72, 91] (indexes 0 to 9). low and high mark the ends of the current range; mid = (low + high) // 2.
| Step | low | high | mid | A[mid] | Decision |
|---|---|---|---|---|---|
| 1 | 0 | 9 | 4 | 16 | 23 > 16, search right half: low = 5 |
| 2 | 5 | 9 | 7 | 56 | 23 < 56, search left half: high = 6 |
| 3 | 5 | 6 | 5 | 23 | Found at index 5 |
def binary_search(arr, target):
low, high = 0, len(arr) - 1
while low <= high:
mid = (low + high) // 2 # middle index
if arr[mid] == target:
return mid # found
elif target < arr[mid]:
high = mid - 1 # search left half
else:
low = mid + 1 # search right half
return -1 # not found
data = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print("23 at index", binary_search(data, 23))
print("50 at index", binary_search(data, 50))
# correctness tests (book: key 3 in [1, 2, 3, 4, 5] gives index 2)
tests = [([1, 2, 3, 4, 5], 3, 2), ([1, 2, 3, 4, 5], 1, 0),
([1, 2, 3, 4, 5], 5, 4), ([1, 2, 3, 4, 5], 6, -1),
([], 3, -1), ([7], 7, 0)]
for arr, key, expected in tests:
r = binary_search(arr, key)
print(arr, key, "->", r, "PASS" if r == expected else "FAIL")23 at index 5
50 at index -1
[1, 2, 3, 4, 5] 3 -> 2 PASS
[1, 2, 3, 4, 5] 1 -> 0 PASS
[1, 2, 3, 4, 5] 5 -> 4 PASS
[1, 2, 3, 4, 5] 6 -> -1 PASS
[] 3 -> -1 PASS
[7] 7 -> 0 PASSDerivation Time complexity of binary search
Aim. Show that binary search needs about steps, so its time complexity is .
- The array has elements and is sorted.
- Worst case: the target is found only when one element is left, or is not present.
- Each comparison with the middle element discards half of the current range.
- So after steps the number of elements left is
- The search stops when one element is left.
- Take logarithm to base 2 of both sides to find the number of steps.
Doubling the input adds only one more step. For at most about halvings (11 comparisons) are needed, compared with up to 1024 comparisons in linear search.
Space complexity
The book gives the space complexity of binary search as (logarithmic space). This is for the recursive version: each recursive call adds a new frame to the call stack, and in the worst case the number of recursive calls equals the number of times can be halved, .
def binary_search_rec(arr, target, low, high, depth=1):
if low > high:
return -1, depth
mid = (low + high) // 2
if arr[mid] == target:
return mid, depth
if target < arr[mid]:
return binary_search_rec(arr, target, low, mid - 1, depth + 1)
return binary_search_rec(arr, target, mid + 1, high, depth + 1)
arr = list(range(1, 1025)) # 1024 sorted numbers
print("(index, recursion depth):", binary_search_rec(arr, 1024, 0, len(arr) - 1))(index, recursion depth): (1023, 11)2.2.3Example 2: linear searchOften examined
In linear (sequential) search each element of the array is examined one by one until the target value is found or the end of the array is reached. It works on unsorted as well as sorted data.
Algorithm steps
- Start at the beginning of the array and examine each element one by one.
- Compare the target value with each element of the array.
- The search is completed when the target value matches an element.
- If the array ends and no match is found, conclude that the target is not in the array.
def linear_search(arr, target):
for i in range(len(arr)):
if arr[i] == target:
return i # found at index i
return -1 # not found
items = [34, 7, 23, 32, 5, 62]
print("23 at index", linear_search(items, 23))
print("62 at index", linear_search(items, 62), "(worst case: last element)")
print("10 at index", linear_search(items, 10))23 at index 2
62 at index 5 (worst case: last element)
10 at index -1Time complexity: O(n)
The number of operations grows linearly with the input size. For an array of elements we may check the first, second, and so on up to the th element. In the worst case (target at the last index or not present) every one of the elements is examined, so the time complexity is . (Best case: target at the first index, 1 comparison.)
Space complexity: O(1)
Linear search uses a fixed amount of extra memory whatever the size of the input: only a few variables for the current index and the target. So its space complexity is constant, .
| Point | Linear search | Binary search |
|---|---|---|
| Data must be sorted? | No | Yes |
| Method | Check elements one by one | Compare with the middle, discard half |
| Time complexity (worst) | ||
| Space complexity | recursive, iterative | |
| Comparisons for 1,000,000 items (worst) | 1,000,000 | about 20 |
| Best for | Small or unsorted lists | Large sorted lists |
2.2.1, 2.2.3Sorting algorithms: bubble sort and merge sort
Sorting algorithms arrange data in a particular order, for example ascending. The book uses bubble sort to explain correctness and names merge sort and quick sort as algorithms. Comparing them is a good exercise in evaluating solutions.
Bubble sort
Compare each pair of neighbouring elements and swap them if they are in the wrong order. One pass moves the largest element to the end; repeat passes. It uses two nested loops, so it makes about comparisons: time complexity . It sorts in place, so extra space is . It is very easy to understand and code (high clarity) but slow for large lists.
Merge sort
A divide and conquer algorithm: (1) divide the list into two halves, (2) sort each half by merge sort (recursively), (3) merge the two sorted halves into one sorted list. The list can be halved times and each level of merging handles all elements, so the time complexity is . It needs an extra list for merging, so space is . It is harder to understand than bubble sort because of recursion.
def merge_sort(a):
if len(a) <= 1:
return a # a list of 0 or 1 items is sorted
mid = len(a) // 2
left = merge_sort(a[:mid]) # divide and sort each half
right = merge_sort(a[mid:])
merged, i, j = [], 0, 0
while i < len(left) and j < len(right): # merge two sorted halves
if left[i] <= right[j]:
merged.append(left[i]); i += 1
else:
merged.append(right[j]); j += 1
return merged + left[i:] + right[j:]
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))
print(merge_sort(["Zara", "Ali", "Hina", "Bilal"]))[3, 9, 10, 27, 38, 43, 82]
['Ali', 'Bilal', 'Hina', 'Zara']Quick sort
Quick sort picks a pivot element, places smaller elements to its left and larger ones to its right (partition), then sorts the two parts recursively. Its average time is (worst case with bad pivots).
| Point | Bubble sort | Merge sort |
|---|---|---|
| Idea | Swap neighbours repeatedly | Divide, sort halves, merge |
| Time complexity | ||
| Extra space | ||
| Clarity | Very simple | Needs recursion; more complex |
| Best use | Small or nearly sorted lists, teaching | Large lists |
2.2.4Case studies: evaluating two algorithmsOften examined
The book evaluates complete solutions under four headings: algorithm, efficiency (time and space complexity), clarity and correctness. The exercise asks you to do the same, so learn this pattern.
Problem 1: greatest common divisor (GCD)
Problem statement: given two integers, find their greatest common divisor. Solution: the Euclidean algorithm.
- Take two integers and where .
- While is not zero: set to , and to (the remainder of the old divided by the old ). Both assignments are done together.
- When becomes zero, contains the GCD.
def gcd(a, b):
while b != 0:
print(f"a = {a}, b = {b}, a % b = {a % b}")
a, b = b, a % b # both updated at the same time
return a
print("GCD =", gcd(48, 18))a = 48, b = 18, a % b = 12
a = 18, b = 12, a % b = 6
a = 12, b = 6, a % b = 0
GCD = 6- Efficiency: time complexity , because the remainder shrinks quickly (it at least halves every two steps); space complexity , as only a few variables are used.
- Clarity: the algorithm is elegant and clear: a simple loop with one assignment.
- Correctness: it is based on the mathematical fact and , so it always gives the right GCD.
Problem 2: count the vowels in a string
Problem statement: for an input string, count the number of vowels. Solution: simple iteration.
- Initialise a counter to zero.
- Iterate through each character in the string.
- For each character, check whether it is a vowel (one of a, e, i, o, u).
- If it is a vowel, increment the counter. After the loop, the counter holds the number of vowels.
def count_vowels(text):
count = 0
for ch in text.lower(): # lower() also counts capital vowels
if ch in "aeiou":
count += 1
return count
print(count_vowels("Computational Thinking"))
print(count_vowels("")) # edge case: empty string8
0- Efficiency: time complexity , where is the length of the string (each character is checked once); space complexity , as only a counter is used.
- Clarity: simple and clear: basic iteration and a conditional check.
- Correctness: it counts the vowels correctly by examining every character; converting to lower case makes capital vowels count too.
SummaryTypes of algorithms (unit summary)
The unit summary lists the main families of algorithms. Know one example of each for MCQs.
| Type | Idea | Examples |
|---|---|---|
| Sorting algorithms | Arrange data in a particular order | Quick sort, bubble sort, merge sort |
| Searching algorithms | Find a specific item in data | Linear search, binary search |
| Graph algorithms | Work with nodes and edges | Dijkstra's algorithm, depth-first search (DFS), BFS |
| Dynamic programming | Break a problem into smaller overlapping subproblems and store (reuse) their solutions | Fibonacci numbers |
| Greedy algorithms | Make the best (locally optimal) choice at each step, hoping for an overall optimum | Huffman coding |
| Backtracking algorithms | Try a choice, and undo it (backtrack) when it leads to a dead end | N-Queens problem, Sudoku solver |
def fibonacci(n):
fib = [0, 1] # store solutions of subproblems
for i in range(2, n + 1):
fib.append(fib[i - 1] + fib[i - 2]) # reuse stored answers
return fib[n]
print([fibonacci(i) for i in range(10)])[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]Formula sheet
Key terms
- Computational thinking
- A problem-solving process based on computer science principles: decomposition, pattern recognition, abstraction and algorithm design.
- Decomposition
- Breaking a problem into smaller, manageable parts.
- Pattern recognition
- Identifying similarities or trends to simplify a problem.
- Abstraction
- Focusing on important information and ignoring irrelevant details.
- Algorithm
- A finite set of clear, step-by-step instructions to solve a problem.
- Data structure
- A way of organising and storing data so that it can be accessed and modified efficiently.
- Array
- A collection of elements of the same type in contiguous memory locations, each reached by an index.
- Index
- The position number of an element in an array, starting from 0.
- 2-D array (matrix)
- An array arranged in rows and columns; an element needs two indexes.
- Linked list
- A linear data structure of nodes joined by pointers; each node holds data and the address of the next node.
- Node
- One element of a linked list, tree or graph, holding data and link(s).
- Pointer (reference)
- A value that holds the memory address of another node.
- Head
- The pointer to the first node of a linked list.
- NULL
- A special value meaning a pointer refers to no node.
- Singly linked list
- A linked list whose nodes have data and a next pointer; traversal is forward only.
- Doubly linked list
- A linked list whose nodes have prev, data and next; traversal in both directions.
- Circular linked list
- A linked list in which the last node points back to the first node.
- Stack
- A LIFO data structure where insertion and deletion happen at the top.
- LIFO
- Last-In, First-Out: the element added last is removed first.
- Push / Pop
- Add an element to / remove an element from the top of a stack.
- Stack overflow / underflow
- Pushing onto a full stack / popping from an empty stack.
- Queue
- A FIFO data structure where elements join at the rear and leave from the front.
- FIFO
- First-In, First-Out: the element added first is removed first.
- Enqueue / Dequeue
- Add an element at the rear / remove an element from the front of a queue.
- Graph
- A non-linear data structure of vertices (nodes) connected by edges.
- Vertex / Edge
- A node of a graph / a connection between two nodes.
- Connected graph
- A graph with at least one path between every pair of vertices.
- Directed graph
- A graph whose edges have a direction.
- Tree
- A non-linear hierarchical data structure of nodes with a single root.
- Root
- The topmost node of a tree.
- Leaf node
- A tree node with no children.
- Siblings
- Nodes with the same parent.
- Subtree
- A node together with all its descendants.
- Binary tree
- A tree in which each node has at most two children.
- Binary search tree (BST)
- A binary tree where left-subtree keys are smaller and right-subtree keys are larger than the node.
- Tree traversal
- Visiting every node of a tree once in a systematic order.
- In-order traversal
- Left subtree, root, right subtree.
- Pre-order traversal
- Root, left subtree, right subtree.
- Post-order traversal
- Left subtree, right subtree, root.
- BFS / DFS
- Breadth-first search (level by level, uses a queue) / depth-first search (as deep as possible, uses a stack).
- Correctness
- An algorithm's ability to give the expected output for all valid inputs.
- Edge case
- An unusual or boundary input, such as an empty list or a single element.
- Clarity
- How logically structured and easy to understand an algorithm is.
- Efficiency
- How well an algorithm uses resources such as time and memory.
- Time complexity
- How the running time of an algorithm grows with input size.
- Space complexity
- How the memory used by an algorithm grows with input size.
- Big O notation
- Notation giving an upper bound (usually worst case) on the growth of time or space with input size.
- Worst case
- The input that makes an algorithm do the most work.
- Binary search
- Searching a sorted array by repeatedly halving the search range; O(\log n).
- Linear search
- Checking elements one by one; O(n).
- Divide and conquer
- Solving a problem by splitting it into smaller parts, solving them and combining the results (binary search, merge sort).
- Greedy algorithm
- An algorithm that makes the best choice at each step (Huffman coding).
- Dynamic programming
- Solving overlapping subproblems once and storing their answers (Fibonacci numbers).
- Backtracking
- Trying choices and undoing them at dead ends (N-Queens, Sudoku).
Multiple choice questionsp. 56–57
- AA method of communication
- BA way to store and organize data in memoryAnswer
- CA system for internet access
- DA graphical interface design
A data structure is a way of organising and storing data in memory so that it can be accessed and modified efficiently. The other options describe networking or interface design.
- AArray
- BStack
- CLinked ListAnswer
- DQueue
A linked list grows and shrinks at run time (nodes are created when needed) and insertion or deletion only changes pointers. An array has a fixed size and needs shifting.
- AData and address of previous node
- BOnly data
- CData and address of next nodeAnswer
- DIndex value and data
A singly linked list node has two parts: the data and the reference (address) of the next node. The address of the previous node is kept only in a doubly linked list.
- ARandom access
- BUses less memory
- CAllows traversal in both directionsAnswer
- DFaster arithmetic operations
Each node of a doubly linked list stores both prev and next pointers, so the list can be traversed forward and backward. It actually uses more memory, and neither list gives random access.
- ADoubly linked list
- BCircular linked listAnswer
- CSingly linked list
- DLinear linked list
In a circular linked list the last node points to the first node instead of NULL, forming a loop.
- AEnqueue
- BDequeue
- CPopAnswer
- DTop
Pop removes the top element. Top only reads it without removing it; enqueue and dequeue are queue operations.
- ARoot node
- BLeaf nodeAnswer
- CSibling node
- DParent node
A leaf node has no child. The root has no parent, and a parent by definition has at least one child.
- AArray
- BStackAnswer
- CQueue
- DTree
A stack adds and removes elements at the top, so the last element pushed is the first popped (LIFO). A queue is FIFO.
- AQuick SortAnswer
- BBinary Search
- CDepth-First Search
- DEuclidean Algorithm
Quick sort arranges data in order. Binary search finds an item, depth-first search traverses a graph and the Euclidean algorithm finds a GCD.
- ABubble Sort
- BHuffman CodingAnswer
- CDepth-First Search
- DBinary Search
Huffman coding repeatedly joins the two least frequent symbols, the best choice at each step, which is the greedy approach.
- AO(1)
- BO(n)
- CO(\log n)Answer
- DO(n^2)
The book takes the recursive version: each recursive call adds a stack frame and there are at most \log_2 n calls, so space is O(\log n). (An iterative version needs only O(1), but the book's expected answer is O(\log n).)
- AStack
- BQueue
- CArrayAnswer
- DTree
Array elements are stored in contiguous memory, so any element's address is calculated from its index and reached in one step.
- ADecompositionAnswer
- BPattern Recognition
- CAbstraction
- DAlgorithm Design
Separating the whole job into smaller tasks (marks by subject, averages, grades) is decomposition. Algorithm design comes later, when the exact steps of each task are written.
- AO(1)
- BO(\log n)
- CO(n)Answer
- DO(n^2)
In the worst case linear search checks all n elements one by one, so its time grows linearly: O(n).
- AThey memorized how traffic lights work in real life.
- BThey created a flowchart outlining how traffic signal timing changes with vehicle count.Answer
- CThey guessed a solution based on their intuition.
- DThey programmed a random timer for each signal.
A flowchart that links signal timing to vehicle count shows decomposition, abstraction and a step-by-step algorithm. Memorising, guessing or random timers involve no logical problem solving.
Short response questions (SRQs)p. 57–58
Problems 1 to 5 follow the case-study pattern of 2.2.4: algorithm, efficiency (time and space complexity), clarity and correctness. Every program was run and its output is shown.
- Take the integer n; if it is negative, use its absolute value. Set sum = 0.
- While n > 0: add the last digit n \% 10 to sum, then remove that digit with n = n // 10 (integer division).
- When n becomes 0, sum holds the sum of the digits. Output sum.
def sum_of_digits(n):
n = abs(n) # handle negative numbers
total = 0
while n > 0:
total += n % 10 # last digit
n //= 10 # remove last digit
return total
for x in [4096, 7, 0, -123]:
print(x, "->", sum_of_digits(x))4096 -> 19
7 -> 7
0 -> 0
-123 -> 6- Time complexity: O(d), where d is the number of digits, because the loop runs once per digit. Since d \approx \log_{10} n, this is also O(\log n).
- Space complexity: O(1), because only two variables (n and total) are used, whatever the size of the number.
The algorithm is clear: one loop with two simple arithmetic steps. Descriptive names (sum_of_digits, total) and a comment for each step make its purpose obvious to any reader.
Each pass adds exactly one digit (n \% 10) and removes it (n // 10), so every digit is added once. It gives 4 + 0 + 9 + 6 = 19 for 4096 and handles edge cases: 0 gives 0, a single digit gives itself, and a negative number is made positive first.
- Input a non-negative integer n. Set result = 1.
- For i = 2, 3, ..., n: multiply result by i.
- Output result. (For n = 0 or 1 the loop does not run and result stays 1.)
def factorial(n):
if n < 0:
return None # factorial is not defined for negatives
result = 1
for i in range(2, n + 1):
result *= i
return result
for x in [0, 1, 5, 10]:
print(f"{x}! =", factorial(x))0! = 1
1! = 1
5! = 120
10! = 3628800- Time complexity: O(n), because the loop performs about n multiplications.
- Space complexity: O(1), because only result and the loop variable are stored. (A recursive version would need O(n) stack space, which is why the iterative approach is preferred.)
The iterative method is simple and clear: one loop that multiplies the numbers from 2 to n. It follows the definition n! = 1 \times 2 \times \cdots \times n directly, and the name factorial with the variable result shows what the code does.
It multiplies every integer from 2 to n exactly once, which is the definition of n!. It gives 5! = 120 and 10! = 3628800, and it handles the edge cases 0! = 1 and 1! = 1 correctly; negative input is rejected.
- If the list is empty, report that there is no largest number.
- Set largest = the first element of the list.
- Scan the remaining elements one by one; if an element is greater than largest, set largest to that element.
- After the scan, output largest.
def find_largest(nums):
if len(nums) == 0:
return None # edge case: empty list
largest = nums[0]
for x in nums[1:]:
if x > largest:
largest = x
return largest
print(find_largest([12, 45, 7, 89, 23]))
print(find_largest([-5, -2, -9]))
print(find_largest([42]))
print(find_largest([]))89
-2
42
None- Time complexity: O(n): each of the n elements is compared once (n - 1 comparisons).
- Space complexity: O(1): only the variable largest (and the loop variable) is needed.
The linear scan is simple and clear: one loop and one comparison. A meaningful name such as largest and a separate check for the empty list make the logic easy to follow and to test.
After checking element k, largest is the maximum of the first k elements; after the last element it is the maximum of the whole list. Starting with the first element (not 0) makes it correct for all-negative lists such as [-5, -2, -9], which gives -2.
- If n < 2, it is not prime.
- For each i from 2 up to \sqrt{n}: if n \% i = 0, then i divides n, so n is not prime; stop.
- If no divisor is found, n is prime.
def is_prime(n):
if n < 2:
return False
i = 2
while i * i <= n: # test divisors up to sqrt(n)
if n % i == 0:
return False
i += 1
return True
print([x for x in range(1, 30) if is_prime(x)])
print(97, is_prime(97), "|", 91, is_prime(91))[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
97 True | 91 False- Time complexity: O(\sqrt{n}), because at most about \sqrt{n} divisors are tested. (Testing all numbers from 2 to n-1 would be O(n); stopping at \sqrt{n} is enough because if n = a \times b, one factor is at most \sqrt{n}.)
- Space complexity: O(1): only n and the counter i are stored.
The test is short and clear: one loop that checks divisibility with the remainder operator. The function name is_prime and a comment explaining the \sqrt{n} limit make the idea easy to understand.
A number is prime exactly when no integer from 2 to \sqrt{n} divides it, so the test is mathematically correct. Edge cases are handled: 0 and 1 are not prime, 2 and 3 are prime, and 91 = 7 × 13 is correctly rejected while 97 is accepted.
- If the list is empty, report that the average cannot be calculated (avoid division by zero).
- Set total = 0 and add every number of the list to total.
- Average = total ÷ (number of elements). Output the average.
def average(nums):
if len(nums) == 0:
return None # avoid division by zero
total = 0
for x in nums:
total += x
return total / len(nums)
print(average([70, 85, 90, 65]))
print(average([5]))
print(average([]))77.5
5.0
None- Time complexity: O(n): each element is added once, then one division is done.
- Space complexity: O(1): only total (and the loop variable) is stored.
The method is simple and clear: sum the numbers, then divide by the count. It matches the everyday meaning of average, and the names total and average make the code readable.
The average is defined as the sum of the values divided by their number, which is exactly what the algorithm computes: (70 + 85 + 90 + 65) ÷ 4 = 77.5. The empty-list edge case is checked first so the program never divides by zero.
Abstraction means focusing on the important details of a problem and ignoring the unnecessary ones. For a timetable app, the student keeps only the information the timetable really needs: days, periods (start and end times), subjects, teachers and rooms.
Details such as the colour of the classroom, the teacher's address or the number of chairs are ignored. This makes the problem smaller, so the student can model it simply, for example as a 2-D array with days as rows and periods as columns, and write the app faster with fewer errors.
The shopkeeper should use a searching algorithm, and for a long list the best one is binary search, after sorting the product list (for example by name or code).
Binary search compares the target with the middle item and discards half the list at each step, so it takes O(\log n) time. For 10,000 products it needs about 14 comparisons, while linear search (O(n)) may need all 10,000. Linear search is suitable only if the list is short or cannot be kept sorted.
Extended response questions (ERQs)p. 58
Bubble sort
Bubble sort compares neighbouring names and swaps them if they are out of alphabetical order. After each pass the largest remaining name moves to the end. It needs n-1 passes with nested loops.
- Time complexity: O(n^2). The number of comparisons is \dfrac{n(n-1)}{2}. For n = 1000: \dfrac{1000 \times 999}{2} = 499{,}500 comparisons.
- Space complexity: O(1); it sorts in place.
- Clarity: very high. The idea (swap neighbours) is easy to understand, code and trace by hand.
Merge sort
Merge sort uses divide and conquer: split the list of names into two halves, sort each half recursively, then merge the two sorted halves.
- Time complexity: O(n\log n). The list can be halved \log_2 1000 \approx 10 times and each level merges all 1000 names, so at most about 1000 \times 10 = 10{,}000 comparisons.
- Space complexity: O(n) for the temporary lists used while merging.
- Clarity: lower than bubble sort, because it uses recursion and a separate merge step, but it is still well structured.
Measured comparison
import random
random.seed(1)
names = ["".join(random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(6))
for _ in range(1000)]
def bubble_sort(a):
a, count = a[:], 0
for i in range(len(a) - 1):
for j in range(len(a) - 1 - i):
count += 1
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
return a, count
def merge_sort(a):
if len(a) <= 1:
return a, 0
mid = len(a) // 2
left, c1 = merge_sort(a[:mid])
right, c2 = merge_sort(a[mid:])
merged, i, j, c = [], 0, 0, c1 + c2
while i < len(left) and j < len(right):
c += 1
if left[i] <= right[j]:
merged.append(left[i]); i += 1
else:
merged.append(right[j]); j += 1
return merged + left[i:] + right[j:], c
b, cb = bubble_sort(names)
m, cm = merge_sort(names)
print("Same sorted result:", b == m == sorted(names))
print("Bubble sort comparisons:", cb)
print("Merge sort comparisons: ", cm)Same sorted result: True
Bubble sort comparisons: 499500
Merge sort comparisons: 8743| Criterion | Bubble sort | Merge sort |
|---|---|---|
| Time complexity | O(n^2) | O(n\log n) |
| Comparisons for 1,000 names | 499,500 | under 10,000 |
| Extra memory | O(1) | O(n) |
| Clarity | Very simple | Moderate (recursion) |
Conclusion
Both algorithms are correct. Merge sort is more suitable for 1,000 student names: it needs about 50 times fewer comparisons, and the gap grows quickly as the list gets longer (for 10,000 names bubble sort needs about 50 million comparisons). The small extra memory for 1,000 names is not a problem. Bubble sort is acceptable only for very small or nearly sorted lists, or for teaching, where its clarity is an advantage.
Stack
A stack is a linear data structure that follows LIFO (Last-In, First-Out). Elements are added and removed at one end, the top. Operations: push (add at top), pop (remove from top), top/peek, IsEmpty, IsFull.
Queue
A queue is a linear data structure that follows FIFO (First-In, First-Out). Elements are added at the rear and removed from the front. Operations: enqueue (add at rear), dequeue (remove from front), front/peek, IsEmpty, IsFull.
| Point | Stack | Queue |
|---|---|---|
| Principle | LIFO | FIFO |
| Ends used | One end (top) | Two ends (rear for insertion, front for deletion) |
| Insert / delete | Push / pop | Enqueue / dequeue |
| Order of output | Reverse of input order | Same as input order |
| Algorithms | DFS, recursion, undo, expression evaluation | BFS, scheduling, buffering |
Effect on algorithm design
- The data structure fixes the order in which items are processed. If the most recent item must be handled first (going back, undoing, nested function calls), the algorithm is designed around a stack. If items must be handled fairly in arrival order, it is designed around a queue.
- Traversal strategy depends on it: using a stack in graph traversal gives depth-first search (go deep, then backtrack); using a queue gives breadth-first search (visit level by level, which finds the shortest path in an unweighted graph).
- Both allow O(1) insertion and deletion at their ends, so algorithms built on them are efficient, but neither allows fast access to middle elements; if that is needed another structure must be chosen.
from collections import deque
items = ["Page 1", "Page 2", "Page 3"]
stack = []
for p in items: stack.append(p) # push
print("Stack pops: ", [stack.pop() for _ in range(3)])
queue = deque()
for p in items: queue.append(p) # enqueue
print("Queue serves:", [queue.popleft() for _ in range(3)])Stack pops: ['Page 3', 'Page 2', 'Page 1']
Queue serves: ['Page 1', 'Page 2', 'Page 3']Real-life examples
- Stack: a pile of plates or books; the plate placed last is taken first. In computing, the browser back button returns to the most recently visited page.
- Queue: people standing in line at a bank counter or ticket window; the first person in line is served first. In computing, a printer prints documents in the order they were sent.
Importance of tree traversal
Unlike an array, a tree has no single natural order, so we need a systematic method to visit every node exactly once. Traversal is the basis of almost every tree operation: searching, printing, copying, evaluating expressions, and deleting a tree. The order chosen decides what the result means, for example sorted output from a BST or a correct copy of a folder structure.
The three traversals
| Traversal | Order | Use case |
|---|---|---|
| In-order | Left subtree, Root, Right subtree | Gives keys of a BST in ascending (sorted) order |
| Pre-order | Root, Left subtree, Right subtree | Copying a tree; processing a node before its children (printing a folder before its contents) |
| Post-order | Left subtree, Right subtree, Root | Deleting a tree or freeing memory; children are processed before the parent (folder size) |
Example
- In-order: go to leftmost node 4, then 2, then 5; then root 1; then right subtree 3, 6. Result: 4, 2, 5, 1, 3, 6.
- Pre-order: root 1 first, then 2, 4, 5, then 3, 6. Result: 1, 2, 4, 5, 3, 6.
- Post-order: 4, 5, then 2; then 6, then 3; root 1 last. Result: 4, 5, 2, 6, 3, 1.
class Node:
def __init__(self, key, left=None, right=None):
self.key, self.left, self.right = key, left, right
def inorder(n): return inorder(n.left) + [n.key] + inorder(n.right) if n else []
def preorder(n): return [n.key] + preorder(n.left) + preorder(n.right) if n else []
def postorder(n): return postorder(n.left) + postorder(n.right) + [n.key] if n else []
root = Node(1, Node(2, Node(4), Node(5)), Node(3, None, Node(6)))
print("In-order: ", inorder(root))
print("Pre-order: ", preorder(root))
print("Post-order:", postorder(root))
bst = Node(50, Node(30, Node(20), Node(40)), Node(70, Node(60), Node(80)))
print("In-order of a BST:", inorder(bst))In-order: [4, 2, 5, 1, 3, 6]
Pre-order: [1, 2, 4, 5, 3, 6]
Post-order: [4, 5, 2, 6, 3, 1]
In-order of a BST: [20, 30, 40, 50, 60, 70, 80]Use cases compared
- In-order on the BST (50, 30, 70, 20, 40, 60, 80) prints 20, 30, 40, 50, 60, 70, 80, so it is used to list records in sorted order.
- Pre-order visits the root first, so saving a tree in pre-order and rebuilding it in the same order reproduces the same shape (copying or serialising a tree).
- Post-order visits the root last, so every child is deleted before its parent and no node is lost; operating systems compute a folder's total size this way.
Important exam questions (not in the exercise)
Likely FBISE short (3-mark) and long (5-mark) questions from this unit that are not in the book's exercise. Every program was run and its output is shown.
Computational thinking (CT) is a problem-solving process based on computer science principles. It solves a complex problem by breaking it into manageable parts and approaching them logically and systematically, so that a computer (or a person) can carry out the solution.
- Decomposition: breaking a problem into smaller parts.
- Pattern recognition: finding similarities or trends.
- Abstraction: keeping the important details and ignoring the rest.
- Algorithm design: writing clear step-by-step instructions.
Pattern recognition is identifying similarities or trends within a problem or across similar problems, so that a solution found once can be reused. It saves time and simplifies complex tasks.
Example: while writing a result program, we notice that the steps for every student are the same (add marks, find percentage, assign grade). So one routine is written and repeated in a loop for all students. In data analysis, recognising users' behaviour patterns helps improve a product's design.
| Array | Linked list |
|---|---|
| Fixed size, declared at the start | Dynamic size; grows and shrinks at run time |
| Elements in contiguous memory, reached directly by index in O(1) | Nodes anywhere in memory joined by pointers; reached one by one from head in O(n) |
| Insertion or deletion in the middle needs shifting of elements | Insertion or deletion only changes pointers |
An array also needs no extra memory for links, while every linked list node stores at least one pointer.
A circular linked list is a linked list in which the last node does not point to NULL; it points back to the first node, so the nodes form a loop. It can be built from a singly or a doubly linked list.
Advantage: from any node we can reach every other node by moving forward, and there is no NULL at the end, so repeated rounds are easy. Use: round-robin CPU scheduling, where each program gets a turn and then the turn returns to the first program; a music playlist on repeat.
Stack overflow occurs when we try to push an element onto a stack that is already full (a fixed-size stack). Stack underflow occurs when we try to pop an element from an empty stack.
They are prevented by checking before each operation: call IsFull before Push and IsEmpty before Pop. If the check is true, the operation is refused and a message is shown instead of the program crashing or corrupting memory.
| Operation | Stack (bottom to top) | Returned |
|---|---|---|
| Push(5) | 5 | |
| Push(8) | 5, 8 | |
| Pop() | 5 | 8 |
| Push(3) | 5, 3 | |
| Push(9) | 5, 3, 9 | |
| Pop() | 5, 3 | 9 |
| Push(1) | 5, 3, 1 | |
| Top() | 5, 3, 1 | 1 |
Final stack (bottom to top): 5, 3, 1. The last Top() returns 1 without removing it. Each Pop removed the most recently pushed element, which shows the LIFO principle.
A graph is a non-linear data structure consisting of vertices (nodes) connected by edges. It models networks and relationships such as roads between cities or friendships in a social network.
In a directed graph every edge has a direction (shown by an arrow): an edge from A to B allows movement from A to B only, as in one-way roads or Twitter/X followers. In an undirected graph edges have no direction: an edge between A and B can be used both ways, as in two-way roads or Facebook friendships.
- Root
- The topmost node of a tree; it has no parent.
- Leaf
- A node with no children.
- Siblings
- Nodes that share the same parent.
- Level
- Distance of a node from the root; the root is at level 0, its children at level 1.
- Subtree
- A node together with all its descendants; it forms a smaller tree inside the tree.
A binary search tree is a binary tree (each node has at most two children) in which, for every node, all keys in the left subtree are smaller and all keys in the right subtree are larger than the node's key.
Searching is fast because at each node one comparison tells us to go only left or only right, so about half of the remaining tree is discarded each time. In a balanced BST the search takes about \log_2 n steps, O(\log n). For example, 60 is found in 3 comparisons in a 7-node BST with root 50.
Edge cases are unusual or boundary inputs at the limits of what an algorithm must handle, such as an empty list, a list of one element, repeated values, zero, negative numbers or the largest allowed value.
An algorithm is correct only if it gives the expected output for all valid inputs. Many errors appear only at the edges, for example dividing by zero when averaging an empty list, or starting a maximum search at 0 for a list of negative numbers. Testing edge cases therefore reveals hidden errors.
Big O notation describes the efficiency of an algorithm by giving an upper bound on how its running time or memory grows with the input size n, ignoring constants and smaller terms. Examples: O(1), O(\log n), O(n), O(n^2).
We consider the worst case because it guarantees that the algorithm will never take longer than this bound, whatever the input. For example, linear search is O(n) because in the worst case the item is last or missing and all n elements are checked.
- O(1) constant: array access by index.
- O(\log n) logarithmic: binary search.
- O(n) linear: linear search.
- O(n\log n) linearithmic: merge sort.
- O(n^2) quadratic: bubble sort (nested loops).
For n = 1000 these are roughly 1, 10, 1000, 10,000 and 1,000,000 steps. So an algorithm further to the left in this order is more efficient, and the difference becomes very large as the input grows.
Binary search compares the target with the middle element and then throws away one half. This is only valid if the list is sorted: then every element on the left is smaller and every element on the right is larger than the middle one, so the discarded half cannot contain the target.
In an unsorted list this fails. Example: search for 9 in [2, 9, 5, 1, 7]. The middle element is 5; since 9 > 5, binary search discards the left half and looks in [1, 7], so it reports 9 as missing although 9 is at index 1. For unsorted data linear search must be used, or the data must be sorted first.
| a | b | a % b |
|---|---|---|
| 84 | 36 | 12 |
| 36 | 12 | 0 |
| 12 | 0 | stop |
Each step replaces (a, b) by (b,\ a \bmod b): (84, 36) \to (36, 12) \to (12, 0). When b becomes 0, a holds the answer, so GCD(84, 36) = 12. Only 2 divisions were needed, showing why its time complexity is O(\log \min(a,b)) and space O(1).
A greedy algorithm makes the best-looking (locally optimal) choice at each step and never reconsiders it, hoping to reach an overall optimum. Example: Huffman coding, which always joins the two least frequent symbols; also giving change with the largest coin first.
Dynamic programming breaks a problem into smaller overlapping subproblems, solves each once and stores the answers for reuse. Example: Fibonacci numbers, where each F(n) = F(n-1) + F(n-2) is computed from stored values, reducing exponential time to O(n).
- Problem decomposition: each data structure models one part of a problem (a queue of customers, a tree of folders), which helps break the problem into manageable components.
- Efficiency considerations: they show trade-offs such as access time versus insertion time (array versus linked list), helping to pick the best structure.
- Algorithmic thinking: knowing them helps design and analyse algorithms, for example using a stack for undo or a queue for scheduling.
Definition and steps
Binary search finds a target in a sorted array by repeatedly halving the search range. Keep two indexes low and high. Find mid = (low + high) // 2. If A[mid] equals the target, stop. If the target is smaller, set high = mid - 1 (search the left half); if larger, set low = mid + 1 (search the right half). If low passes high, the target is not present.
Program
def binary_search(arr, target):
low, high = 0, len(arr) - 1
step = 0
while low <= high:
step += 1
mid = (low + high) // 2
print(f"step {step}: low={low} high={high} mid={mid} A[mid]={arr[mid]}")
if arr[mid] == target:
return mid
if target < arr[mid]:
high = mid - 1
else:
low = mid + 1
return -1
A = [3, 8, 15, 21, 30, 42, 57, 66, 79]
print("Index of 66:", binary_search(A, 66))step 1: low=0 high=8 mid=4 A[mid]=30
step 2: low=5 high=8 mid=6 A[mid]=57
step 3: low=7 high=8 mid=7 A[mid]=66
Index of 66: 7Trace
Step 1: the middle element (index 4) is 30; 66 > 30, so low = 5. Step 2: mid = 6 holds 57; 66 > 57, so low = 7. Step 3: mid = 7 holds 66, so the target is found at index 7. Only 3 comparisons were needed for 9 elements, while linear search would need 8.
Time complexity
- After each comparison the range halves: n,\ n/2,\ n/4,\ \ldots,\ n/2^k.
- The search ends when one element remains:\frac{n}{2^k} = 1 \Rightarrow k = \log_2 n
- So the worst-case time complexity is O(\log n). Space is O(1) for this iterative version (O(\log n) for the recursive version, because of the call stack).
Definition
A linked list is a linear, dynamic data structure made of nodes. Each node stores data and a pointer (reference) to the next node. The first node is reached through head. It grows and shrinks at run time and allows insertion and deletion by changing pointers only.
Types
- Singly linked list: each node has data and next; the last node points to NULL; traversal is forward only.
- Doubly linked list: each node has prev, data and next; traversal in both directions; uses more memory.
- Circular linked list: the last node points back to the first node; traversal never reaches NULL.
Program
class Node:
def __init__(self, data):
self.data = data
self.next = None
head = None
for value in [40, 30, 20, 10]: # insert each at the beginning
node = Node(value)
node.next = head
head = node
cur = head
while cur: # traverse from head to NULL
print(cur.data, end=" -> ")
cur = cur.next
print("NULL")10 -> 20 -> 30 -> 40 -> NULLBig O notation
Big O notation expresses the efficiency of an algorithm as an upper bound on the growth of its running time (time complexity) or memory use (space complexity) as the input size n increases. It usually describes the worst case and ignores constants: 3n + 5 steps is written O(n).
Common classes
| Class | Name | Example | Steps for n = 1024 |
|---|---|---|---|
| O(1) | Constant | Access A[i]; push or pop | 1 |
| O(\log n) | Logarithmic | Binary search | 10 |
| O(n) | Linear | Linear search, sum of a list | 1024 |
| O(n\log n) | Linearithmic | Merge sort, quick sort (average) | 10,240 |
| O(n^2) | Quadratic | Bubble sort (nested loops) | 1,048,576 |
Growth
def count_steps(n):
linear = sum(1 for i in range(n)) # one loop
quadratic = sum(1 for i in range(n) for j in range(n)) # nested loops
halving, m = 0, n
while m > 1: # halving loop
m //= 2
halving += 1
return halving, linear, quadratic
for n in [8, 64, 512]:
print(n, count_steps(n))8 (3, 8, 64)
64 (6, 64, 4096)
512 (9, 512, 262144)When n is multiplied by 8, the halving loop adds only 3 steps, the single loop does 8 times more work, and the nested loops do 64 times more work. This is why algorithms with lower Big O are preferred for large data.
Operations
- Enqueue
- Add an element at the rear; refused if the queue is full.
- Dequeue
- Remove and return the element at the front; refused if the queue is empty.
- Front
- Return the front element without removing it.
- IsEmpty / IsFull
- Check whether the queue has no elements, or has reached its capacity.
Program
class Queue:
def __init__(self, capacity):
self.items = []
self.capacity = capacity
def is_empty(self):
return len(self.items) == 0
def is_full(self):
return len(self.items) == self.capacity
def enqueue(self, x):
if self.is_full():
print("Queue is full, cannot add", x)
else:
self.items.append(x) # add at rear
def dequeue(self):
if self.is_empty():
print("Queue is empty")
return None
return self.items.pop(0) # remove from front
def front(self):
return None if self.is_empty() else self.items[0]
q = Queue(3)
for c in ["Ali", "Sara", "Omar", "Hina"]:
q.enqueue(c)
print("Queue:", q.items, "front =", q.front())
print("Served:", q.dequeue())
q.enqueue("Hina")
print("Queue:", q.items)Queue is full, cannot add Hina
Queue: ['Ali', 'Sara', 'Omar'] front = Ali
Served: Ali
Queue: ['Sara', 'Omar', 'Hina']Applications
- CPU and printer scheduling: jobs are served in the order they arrive.
- Buffering and BFS: keyboard buffers, video streaming and breadth-first search of graphs process items in FIFO order.
Building the BST
- 45 becomes the root.
- 25 < 45: left child of 45. 65 > 45: right child of 45.
- 15 < 45, 15 < 25: left child of 25. 35 < 45, 35 > 25: right child of 25.
- 55 > 45, 55 < 65: left child of 65. 75 > 45, 75 > 65: right child of 65.
Traversals
class Node:
def __init__(self, k):
self.key, self.left, self.right = k, None, None
def insert(r, k):
if r is None:
return Node(k)
if k < r.key:
r.left = insert(r.left, k)
else:
r.right = insert(r.right, k)
return r
def inorder(n): return inorder(n.left) + [n.key] + inorder(n.right) if n else []
def preorder(n): return [n.key] + preorder(n.left) + preorder(n.right) if n else []
def postorder(n): return postorder(n.left) + postorder(n.right) + [n.key] if n else []
root = None
for k in [45, 25, 65, 15, 35, 55, 75]:
root = insert(root, k)
print("In-order: ", inorder(root))
print("Pre-order: ", preorder(root))
print("Post-order:", postorder(root))In-order: [15, 25, 35, 45, 55, 65, 75]
Pre-order: [45, 25, 15, 35, 65, 55, 75]
Post-order: [15, 35, 25, 55, 75, 65, 45]The in-order result is in ascending order, as expected for a BST. Pre-order starts with the root 45 and post-order ends with it.
Correctness
A solution is correct if it gives the expected output for all valid inputs. It is checked with test cases, including edge cases. Linear search must return the right index when the item is first, in the middle or last, and -1 when the item is absent or the list is empty.
def linear_search(items, target):
"""Return the index of target in items, or -1 if it is absent."""
for index in range(len(items)):
if items[index] == target:
return index
return -1
tests = [([4, 9, 2], 4, 0), ([4, 9, 2], 2, 2), ([4, 9, 2], 7, -1), ([], 7, -1)]
for items, target, expected in tests:
got = linear_search(items, target)
print(items, target, "->", got, "PASS" if got == expected else "FAIL")[4, 9, 2] 4 -> 0 PASS
[4, 9, 2] 2 -> 2 PASS
[4, 9, 2] 7 -> -1 PASS
[] 7 -> -1 PASSClarity
A solution is clear if its steps are logical and easy to follow. The function above uses descriptive names (linear_search, items, target, index), a docstring explaining its purpose and a single simple loop, so any reader can understand and maintain it.
Efficiency
Efficiency is how well the solution uses time and memory. Linear search checks up to n elements, so its time complexity is O(n); it uses only a loop variable, so its space complexity is O(1). It is efficient enough for short or unsorted lists; for a large sorted list binary search (O(\log n)) is more efficient.
FBISE 2026 questions
Questions from the FBISE HSSC-II 2026 (1st Annual) papers that belong to this unit, answered the way an examiner expects.
- ALinked List
- BQueue
- CStackAnswer
- DArray
A stack adds (push) and removes (pop) elements only at its top, so the last element added is the first removed. A queue is FIFO.
- ALeafAnswer
- BRoot
- CParent
- DSubtree
A leaf node has no child. The root is the node with no parent; a parent has at least one child; a subtree is a part of a tree, not a node.
| Stack | Queue |
|---|---|
| Follows LIFO: the last element added is removed first. | Follows FIFO: the first element added is removed first. |
| Insertion and deletion at one end, the top. | Insertion at the rear, deletion at the front. |
| Operations: push, pop, top. | Operations: enqueue, dequeue, front. |
| Used in undo, back button, recursion, DFS. | Used in scheduling, buffering, print queue, BFS. |
Real-life example of a stack: a pile of plates in a cafeteria; the plate put on last is taken first. Real-life example of a queue: students standing in line at a canteen or bank counter; the person who came first is served first.
Linked list: a linear data structure in which elements, called nodes, are connected by pointers. Each node stores the data and the reference (memory address) of the next node; the last node points to NULL. The first node is reached through head.
Advantages
- Dynamic size: it can grow or shrink during execution; no size is declared at the start, so memory is not wasted.
- Easy insertion and deletion at any position: only pointers are changed; no shifting of elements as in an array.
- It can store different types of data, and a doubly linked list can be traversed in both directions.
Time complexity measures how the execution time of an algorithm grows with the size of its input n. It is found by counting the basic steps (comparisons, assignments) and is written in Big O notation. Example: linear search is O(n), binary search is O(\log n).
Space complexity measures how much memory an algorithm uses with respect to the input size: extra variables, arrays and the recursion stack. Example: linear search needs only a few variables, O(1); recursive binary search uses O(\log n) stack space.
So time complexity judges a solution by speed, and space complexity by memory; a good solution keeps both low, and sometimes one is traded for the other (merge sort is faster than bubble sort but uses O(n) extra memory).
A queue is a FIFO (First-In, First-Out) data structure with two ends, front and rear.
- Enqueue
- Adds a new element at the end (rear, back or tail) of the queue. Before adding, the queue is checked with IsFull.
- Dequeue
- Removes the existing element from the start (front or head) of the queue, that is, the element that has waited longest. Before removing, IsEmpty is checked.
- Front
- Returns the element at the front of the queue without removing it, so we can see which item will be served next.
Example: queue [3, 4, 5]; enqueue(6) gives [3, 4, 5, 6]; dequeue() removes 3; front() now returns 4.
Advantage: array elements are stored in contiguous memory locations and identified by indexes, so any element can be reached directly (random access) in O(1) time. Arrays also need no extra memory (metadata or pointers) to store the elements, so they are efficient.
Types of arrays
- One-dimensional (1-D) array: elements of the same type stored in a single row; one index reaches an element, for example marks = [2, 4, 8, 12], marks[2] is 8.
- Two-dimensional (2-D) array: also called a matrix; elements stored in rows and columns; two indexes (row, column) reach an element, for example M[1][2].
Tree traversal means visiting every node of a tree exactly once in a systematic order. For a binary tree the methods differ in when the root is visited.
Pre-order traversal (2.5 marks)
- 1. Visit the root node.
- 2. Traverse the left subtree in pre-order.
- 3. Traverse the right subtree in pre-order.
Example: for the tree shown, visit root 1, then the left subtree (2, then its children 4 and 5), then the right subtree (3, then 6). Order: 1, 2, 4, 5, 3, 6. Use: processing a node before its children, such as copying a tree or printing a folder before its contents.
Post-order traversal (2.5 marks)
- 1. Traverse the left subtree in post-order.
- 2. Traverse the right subtree in post-order.
- 3. Visit the root node.
Example: for the same tree, finish the left subtree (4, 5, then 2), then the right subtree (6, then 3), and visit the root 1 last. Order: 4, 5, 2, 6, 3, 1. Use: deleting nodes or freeing memory, because children are processed before their parent.
class Node:
def __init__(self, key, left=None, right=None):
self.key, self.left, self.right = key, left, right
def preorder(n): return [n.key] + preorder(n.left) + preorder(n.right) if n else []
def postorder(n): return postorder(n.left) + postorder(n.right) + [n.key] if n else []
root = Node(1, Node(2, Node(4), Node(5)), Node(3, None, Node(6)))
print("Pre-order: ", preorder(root))
print("Post-order:", postorder(root))Pre-order: [1, 2, 4, 5, 3, 6]
Post-order: [4, 5, 2, 6, 3, 1]- AThe first nodeAnswer
- BNULL
- CThe previous node
- DThe middle node
In a circular linked list the last node's next pointer goes back to the first node, forming a loop; only non-circular lists end with NULL.
- ALevel-order
- BPost-order
- CIn-orderAnswer
- DPre-order
Left, Root, Right is in-order traversal. Pre-order is Root, Left, Right and post-order is Left, Right, Root; level-order visits level by level.
A queue is a FIFO (First-In, First-Out) data structure: elements join at the rear and leave from the front. Three of its operations are:
- Enqueue
- Adds a new element at the end (rear) of the queue, for example enqueue(9) on [3, 4, 5] gives [3, 4, 5, 9].
- Dequeue
- Removes the element at the start (front) of the queue; on [3, 4, 5, 9] it removes 3.
- Front
- Returns the front element without removing it.
Other operations are IsEmpty (checks whether the queue has no elements) and IsFull (checks whether a fixed-size queue is full).
An array is the simplest data structure: a collection of elements of the same type stored in contiguous memory locations. Each element is identified by an index (starting from 0), so any element can be accessed directly. Its size is fixed when it is declared.
Example: marks = [2, 4, 8, 12, 16, 18] is a 1-D array of 6 integers. marks[0] is 2 and marks[3] is 12. If the array starts at address 100 and each element takes 1 unit, marks[3] is at address 100 + 3 = 103. A 2-D array (matrix) such as M[4][5] needs two indexes, row and column.
An algorithm is correct if it produces the expected output for all valid inputs. Correctness is checked by running the algorithm on many test cases covering different situations, including edge cases such as an empty list, a single element, repeated values or negative numbers. If it always gives the expected result, it is considered correct.
Example: bubble sort on the unsorted array [5, 2, 9, 1, 5, 6] must give [1, 2, 5, 5, 6, 9], and it must also work for [] and [7]. Similarly, binary search on the sorted array [1, 2, 3, 4, 5] with key 3 must return index 2, and -1 for a missing key such as 6.
| Singly linked list | Doubly linked list |
|---|---|
| Each node has 2 parts: data and next. | Each node has 3 parts: prev, data and next. |
| Traversal in one direction only (forward). | Traversal in both directions (forward and backward). |
| Less memory per node (one pointer). | More memory per node (two pointers). |
| Last node's next is NULL. | First node's prev and last node's next are NULL. |
Example: singly: head → [A|next] → [B|next] → [C|NULL]. Doubly: NULL ← [prev|1|next] ⇄ [prev|2|next] ⇄ [prev|3|next] → NULL. A browser's back and forward buttons suit a doubly linked list, since we move both ways.
A stack is a LIFO (Last-In, First-Out) structure in which all operations take place at the top.
- Push
- Adds a new element at the top of the stack (after checking IsFull to avoid overflow).
- Pop
- Removes the existing element from the top of the stack and returns it (after checking IsEmpty to avoid underflow).
- Top
- Returns the top element without removing it, so we can see the most recently added item.
Example: push(A), push(B), push(C) gives [A, B, C]; top() returns C; pop() removes C, leaving [A, B].
To find the sum of the numbers (digits) in a list of n elements, we set total = 0 and add each element once in a loop.
digits = [4, 0, 9, 6]
total = 0
for d in digits: # the loop runs n times
total += d
print("Sum =", total)Sum = 19- **Time complexity: O(n).** Each of the n elements is visited and added once, so the number of steps grows linearly with the length of the list.
- **Space complexity: O(1).** Only one extra variable (total) and a loop variable are used, whatever the size of the list.
(For the book's version, the sum of the digits of one integer with d digits, time is O(d), that is O(\log n) for the number n, and space is again O(1).)
Tree traversal is the systematic visiting of every node of a tree exactly once. In a binary tree each node has a left and a right subtree, and the traversal methods differ in when the root is visited.
In-order traversal (2.5 marks)
- 1. Traverse the left subtree in in-order.
- 2. Visit the root node.
- 3. Traverse the right subtree in in-order.
Example: in the tree shown, first go to the leftmost node 4, then its parent 2, then 5; the left subtree is done, so visit root 1; then the right subtree gives 6, 3, 7. Order: 4, 2, 5, 1, 6, 3, 7. Use: in a binary search tree, in-order visits the keys in ascending (sorted) order.
Pre-order traversal (2.5 marks)
- 1. Visit the root node.
- 2. Traverse the left subtree in pre-order.
- 3. Traverse the right subtree in pre-order.
Example: visit root 1, then the left subtree (2, 4, 5), then the right subtree (3, 6, 7). Order: 1, 2, 4, 5, 3, 6, 7. Use: when a node must be processed before its children, for example copying a tree or listing a folder before the files inside it.
class Node:
def __init__(self, key, left=None, right=None):
self.key, self.left, self.right = key, left, right
def inorder(n): return inorder(n.left) + [n.key] + inorder(n.right) if n else []
def preorder(n): return [n.key] + preorder(n.left) + preorder(n.right) if n else []
root = Node(1, Node(2, Node(4), Node(5)), Node(3, Node(6), Node(7)))
print("In-order: ", inorder(root))
print("Pre-order:", preorder(root))In-order: [4, 2, 5, 1, 6, 3, 7]
Pre-order: [1, 2, 4, 5, 3, 6, 7]Exam focus
- Tree traversals: in-order, pre-order and post-order with the order of operations and an example tree. (long)Came as the 5-mark Section C question in both 2026 sets (Set B Q.4 OR, Set D Q.3 OR) and as an MCQ.Go to answer
- Differentiate between stack and queue with real-life examples; purposes of push, pop, top and enqueue, dequeue, front. (short)Four 2026 short questions and an MCQ came from stacks and queues.Go to answer
- What is a linked list? Advantages; singly versus doubly; circular linked list. (short)Asked in both 2026 sets (Set B Q.2 (iii), Set D Q.2 (ix)) and as an MCQ.Go to answer
- What is an array? Advantages and its two types (1-D and 2-D). (short)Asked in both 2026 sets (Set B Q.2 (ix), Set D Q.2 (ii)).Go to answer
- Difference between time complexity and space complexity; Big O classes with examples. (short)Set B Q.2 (vi) OR; Big O is the core of evaluating efficiency.Go to answer
- Correctness of an algorithm with an example (test cases and edge cases). (short)Set D Q.2 (viii) OR; correctness, clarity and efficiency are the unit's second SLO.Go to answer
- Time and space complexity of case-study problems (sum of digits, factorial, largest, prime, average, GCD, vowels). (short)Set D Q.2 (xi) asked exactly this type; the book's SRQs follow the same pattern.Go to answer
- Binary search: steps, program, trace and derivation of O(\log n); compare with linear search. (long)The key algorithm of the unit; MCQs on its complexity are common.Go to answer
- Bubble sort versus merge sort for a large list. (long)Book ERQ 1; tests time complexity and clarity together.Go to answer
- Tree terms (root, leaf, parent, child, siblings, level) and the BST property. (mcq)Leaf node was a 2026 MCQ; BSTs are named in the SLOs.Go to answer
- Four pillars of computational thinking with examples. (short)Opening topic of the unit and a source of scenario MCQs.Go to answer