Computational thinking and algorithms

Class 12 Computer Science, unit 2, book pages 40–58

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.
Applying the four pillars
A school wants a program that prepares report cards from students' marks. Show how each pillar of computational thinking is used.
  • 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.

Common ways to classify data structures (a helpful summary; the book introduces each structure separately).
TypeMeaningExamples
LinearElements are arranged one after another in a sequenceArray, linked list, stack, queue
Non-linearElements are arranged in a hierarchy or a network; one element can connect to manyTree, graph
StaticSize is fixed when the structure is createdArray
DynamicSize can grow or shrink while the program runsLinked 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.

Fig. 2.1: An array of 7 elements. The elements sit in contiguous memory locations 100 to 106, and each is reached by its index 0 to 6.

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].
Fig. 2.2: A one-dimensional (1-D) array. One index is enough to reach an element, for example marks[3] is 12.
Fig. 2.3: A two-dimensional (2-D) array (matrix) of 4 rows and 5 columns. Two indexes, row and column, are needed to reach an element.

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)
Creating and accessing 1-D and 2-D arrays (lists) in Python.
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]
Output

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.
Fig. 2.5 and 2.7: A singly linked list node has 2 parts (data, next). A doubly linked list node has 3 parts (prev, data, next).

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).

Fig. 2.6: A singly linked list. Each node points to the next one; the last node points to NULL.

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.

Fig. 2.8: A doubly linked list. Each node points forward (next) and backward (prev). 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.

Fig. 2.9: A circular (singly) linked list. The next pointer of the last node goes back to the first node, so traversal never reaches NULL.
Comparison of linked list types
PointSinglyDoublyCircular
Parts of a nodedata, nextprev, data, nextdata, next (or prev, data, next)
Direction of traversalForward onlyForward and backwardRound and round
Last node points toNULLNULL (next)The first node
Memory per nodeLessMore (two pointers)Same as the type it is built on
Array versus linked list
ArrayLinked list
Fixed size, declared at the startDynamic size, grows and shrinks
Contiguous memoryNodes anywhere in memory, joined by pointers
Random access by index in Sequential access from head in
Insertion or deletion in the middle needs shiftingInsertion or deletion only changes pointers
No extra memory for linksExtra 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()
Singly linked list with insertion, deletion and traversal.
A -> B -> C -> D -> NULL
A -> B -> D -> NULL
Output

2.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).
Fig. 2.11: Stack operations. push(C) places C on the top; pop() removes the top element C, so B becomes the new top. Both work only at the top (LIFO).

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())
An array (list) based stack with a fixed capacity.
Stack overflow: cannot push D
Stack: ['A', 'B', 'C']  top = C
Popped: C
Stack: ['A', 'B']  top = B
Output

Applications 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).
Fig. 2.12: A queue. New elements join at the rear (enqueue 9); elements leave from the front (dequeue 2). The first in is the first out (FIFO).

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 operations with collections.deque.
Queue: ['Print A', 'Print B', 'Print C']
Front: Print A
Dequeued: Print A
Queue: ['Print B', 'Print C', 'Print D']
Is empty? False
Output

Applications 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

Stack versus queue
PointStackQueue
PrincipleLIFO: last in, first outFIFO: first in, first out
Ends usedOne end (top) for both adding and removingTwo ends: add at rear, remove at front
Add operationPushEnqueue
Remove operationPopDequeue
Peek operationTopFront
Used inUndo, back button, recursion, DFSScheduling, buffering, print queue, BFS
Real-life examplePile of plates or booksLine 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.

Fig. 2.13: A graph with 6 vertices (nodes 1 to 6) joined by 6 edges.

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.
Fig. 2.14 and 2.15: (a) a connected graph: every vertex can be reached from every other vertex. (b) a directed graph: each edge has a direction shown by an arrow.

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))
Adjacency list, breadth-first search (queue) and depth-first search (stack) for the graph of Fig. 2.13.
BFS from 5: [5, 3, 1, 4, 2, 6]
DFS from 5: [5, 3, 1, 2, 4, 6]
Output

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.
Fig. 2.16: Tree terms. A is the root (level 0). D is the parent of I and J, which are its children. F, G and H are siblings. Nodes with no children (I, J, K, F, G, H) are leaves.

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.

A binary search tree (BST) built by inserting 50, 30, 70, 20, 40, 60, 80. Every left subtree holds smaller keys and every right subtree holds larger keys.
Searching a BST
Search for 60 in the BST above and count the comparisons.
  1. Compare 60 with the root 50: 60 > 50, so go right.
  2. Compare 60 with 70: 60 < 70, so go left.
  3. 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))
Building a BST and searching it. The result shows (found, number of comparisons).
Search 60: (True, 3)
Search 45: (False, 3)
Output

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.

The three depth-first traversals of a binary tree
TraversalOrder of operationsMemory aid
In-order1. Traverse the left subtree. 2. Visit the root. 3. Traverse the right subtree.Left, Root, Right (root in the middle)
Pre-order1. Visit the root. 2. Traverse the left subtree. 3. Traverse the right subtree.Root, Left, Right (root first: pre)
Post-order1. Traverse the left subtree. 2. Traverse the right subtree. 3. Visit the root.Left, Right, Root (root last: post)
Fig. 2.17 to 2.19: The binary tree used by the book for the three depth-first traversals, with the order in which each traversal visits the nodes.

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.

Fig. 2.20: A complete binary tree of 7 nodes and its in-order, pre-order and post-order traversals.
Fig. 2.20: a tree of 7 nodes
Write the in-order, pre-order and post-order traversals of the tree in Fig. 2.20.
  1. 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.
  2. Pre-order (Root, L, R): 1; then 2, 4, 5; then 3, 6, 7. Result: 1, 2, 4, 5, 3, 6, 7.
  3. 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()
Recursive in-order, pre-order and post-order traversals of the book's tree.
In-order:   4 2 5 1 3 6 
Pre-order:  1 2 4 5 3 6 
Post-order: 4 5 2 6 3 1 
Output

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.

Choosing a data structure
Need of the problemSuitable structureReason
Fast access to the th itemArrayDirect access by index
Frequent insertion and deletion, unknown sizeLinked listOnly pointers change; dynamic size
Undo, back button, reversingStackLIFO order
Serve requests in arrival orderQueueFIFO order
Hierarchy (folders, HTML tags), fast searchTree or BSTParent-child structure; search in when balanced
Networks, routes, friendshipsGraphAny 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.

Example 1: bubble sort
Check the correctness of bubble sort. Test case: the unsorted array [5, 2, 9, 1, 5, 6] should become [1, 2, 5, 5, 6, 9].

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")
Bubble sort tested on the book's case and on edge cases.
[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] PASS
Output

Because it gives the sorted array for all these cases, including the edge cases, bubble sort can be considered correct.

Example 2: binary search
Given the sorted array [1, 2, 3, 4, 5] and the search key 3, binary search should return index 2 (zero-based indexing). How do we verify its correctness?

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))
Both functions are correct, but the first is clearer.
120 120
Output

Both 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.
Time complexity versus space complexity
PointTime complexitySpace complexity
MeasuresRunning time (number of steps)Memory used
Depends onLoops, comparisons, recursive callsExtra variables, data structures, recursion depth
Linear search
Binary search (recursive)
AimFewer stepsLess memory

Common Big O notations

Common Big O classes, from fastest to slowest
NotationNameMeaningExample
Constant timeTime does not change with input sizeAccessing an array element by index, push or pop on a stack
Logarithmic timeTime grows very slowly; each step halves the search spaceBinary search, looking up a word in a dictionary
Linear timeTime grows in proportion to ; double the input, double the timeA loop over every element, linear search
Linearithmic timeLinear times logarithmic growth; the problem is split into parts and each part processedMerge sort, quick sort (average)
Quadratic timeNested loops compare each element with every other; double the input, four times the timeBubble sort, selection sort
Growth of common running times
2
4
6
8
10
12
10
20
30
40
O
Number of steps against input size . (dashed line) stays flat, grows very slowly, grows in a straight line, while and rise steeply.
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}")
How the number of steps grows with n for each class.
     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  100000000
Output

The 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.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"]))
Merge sort on numbers and on names.
[3, 9, 10, 27, 38, 43, 82]
['Ali', 'Bilal', 'Hina', 'Zara']
Output

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).

Bubble sort versus merge sort
PointBubble sortMerge sort
IdeaSwap neighbours repeatedlyDivide, sort halves, merge
Time complexity
Extra space
ClarityVery simpleNeeds recursion; more complex
Best useSmall or nearly sorted lists, teachingLarge 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.

Algorithm
  1. Take two integers and where .
  2. While is not zero: set to , and to (the remainder of the old divided by the old ). Both assignments are done together.
  3. 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))
Euclidean algorithm with a trace of each step for 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
Output
  • 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.

Algorithm
  1. Initialise a counter to zero.
  2. Iterate through each character in the string.
  3. For each character, check whether it is a vowel (one of a, e, i, o, u).
  4. 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 string
Counting vowels.
8
0
Output
  • 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.

Families of algorithms
TypeIdeaExamples
Sorting algorithmsArrange data in a particular orderQuick sort, bubble sort, merge sort
Searching algorithmsFind a specific item in dataLinear search, binary search
Graph algorithmsWork with nodes and edgesDijkstra's algorithm, depth-first search (DFS), BFS
Dynamic programmingBreak a problem into smaller overlapping subproblems and store (reuse) their solutionsFibonacci numbers
Greedy algorithmsMake the best (locally optimal) choice at each step, hoping for an overall optimumHuffman coding
Backtracking algorithmsTry a choice, and undo it (backtrack) when it leads to a dead endN-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)])
Dynamic programming: each Fibonacci number is built from two stored earlier answers, so it takes O(n) time instead of exponential time.
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Output

Formula sheet

Address of an array element
is the base address (address of A[0]), the index, the size of one element in bytes.
Elements left after k steps of binary search
is the number of elements, the number of halving steps.
Worst-case steps of binary search
is the size of the sorted array.
Worst-case comparisons of linear search
is the number of elements.
Comparisons in bubble sort
is the number of elements; passes with nested loops.
Merge sort
Each of the levels merges all elements.
Order of growth
Constant, logarithmic, linear, linearithmic, quadratic.
Euclidean algorithm
are integers; is the remainder (Python: a % b).

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

1
Which of the following best defines a data structure?
  1. AA method of communication
  2. BA way to store and organize data in memoryAnswer
  3. CA system for internet access
  4. DA graphical interface design
Why

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.

2
Which data structure allows dynamic memory allocation and easy insertion/deletion?
  1. AArray
  2. BStack
  3. CLinked ListAnswer
  4. DQueue
Why

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.

3
In a singly linked list, each node contains:
  1. AData and address of previous node
  2. BOnly data
  3. CData and address of next nodeAnswer
  4. DIndex value and data
Why

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.

4
The main advantage of a doubly linked list over a singly linked list is:
  1. ARandom access
  2. BUses less memory
  3. CAllows traversal in both directionsAnswer
  4. DFaster arithmetic operations
Why

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.

52026 Annual I, Set D, MCQ 3
What type of linked list connects the last node back to the first?
  1. ADoubly linked list
  2. BCircular linked listAnswer
  3. CSingly linked list
  4. DLinear linked list
Why

In a circular linked list the last node points to the first node instead of NULL, forming a loop.

6
Which operation removes the top element from a stack?
  1. AEnqueue
  2. BDequeue
  3. CPopAnswer
  4. DTop
Why

Pop removes the top element. Top only reads it without removing it; enqueue and dequeue are queue operations.

72026 Annual I, Set B, MCQ 5
Which node in a tree does not have any children?
  1. ARoot node
  2. BLeaf nodeAnswer
  3. CSibling node
  4. DParent node
Why

A leaf node has no child. The root has no parent, and a parent by definition has at least one child.

82026 Annual I, Set B, MCQ 4
Which data structure operates on a Last-In, First-Out (LIFO) principle?
  1. AArray
  2. BStackAnswer
  3. CQueue
  4. DTree
Why

A stack adds and removes elements at the top, so the last element pushed is the first popped (LIFO). A queue is FIFO.

9
Which algorithm is used for sorting data?
  1. AQuick SortAnswer
  2. BBinary Search
  3. CDepth-First Search
  4. DEuclidean Algorithm
Why

Quick sort arranges data in order. Binary search finds an item, depth-first search traverses a graph and the Euclidean algorithm finds a GCD.

10
Which algorithm is an example of a Greedy Algorithm?
  1. ABubble Sort
  2. BHuffman CodingAnswer
  3. CDepth-First Search
  4. DBinary Search
Why

Huffman coding repeatedly joins the two least frequent symbols, the best choice at each step, which is the greedy approach.

11
What is the space complexity of the Binary Search algorithm?
  1. AO(1)
  2. BO(n)
  3. CO(\log n)Answer
  4. DO(n^2)
Why

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).)

12
Which data structure allows random access to elements by index?
  1. AStack
  2. BQueue
  3. CArrayAnswer
  4. DTree
Why

Array elements are stored in contiguous memory, so any element's address is calculated from its index and reached in one step.

13
A school wants to automate the process of generating student report cards based on input marks. Which step of computational thinking would most likely involve identifying how to separate marks by subject, calculate averages, and assign grades?
  1. ADecompositionAnswer
  2. BPattern Recognition
  3. CAbstraction
  4. DAlgorithm Design
Why

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.

14
What is the time complexity of Linear Search?
  1. AO(1)
  2. BO(\log n)
  3. CO(n)Answer
  4. DO(n^2)
Why

In the worst case linear search checks all n elements one by one, so its time grows linearly: O(n).

15
During an inter-school tech competition, students were asked to develop a solution for managing traffic signals based on traffic density. Which approach best shows their use of computational thinking?
  1. AThey memorized how traffic lights work in real life.
  2. BThey created a flowchart outlining how traffic signal timing changes with vehicle count.Answer
  3. CThey guessed a solution based on their intuition.
  4. DThey programmed a random timer for each signal.
Why

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.

12026 Annual I, Set D, Q.2 (xi)3 marks
Problem 1: Finding the sum of digits. Problem statement: Given an integer, calculate the sum of its digits. Solution: Digit extraction and summation. Give answer of the following: (a) algorithm, (b) efficiency (time complexity and space complexity), (c) clarity, (d) correctness.
Solution
(a) Algorithm
  1. Take the integer n; if it is negative, use its absolute value. Set sum = 0.
  2. While n > 0: add the last digit n \% 10 to sum, then remove that digit with n = n // 10 (integer division).
  3. 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
Output
(b) Efficiency
  • 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.
(c) Clarity

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.

(d) Correctness

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.

23 marks
Problem 2: Finding the factorial of a number. Problem statement: Compute the factorial of a non-negative integer n (i.e., n!). Solution: Iterative approach. Give answer of the following: (a) algorithm, (b) efficiency (time complexity and space complexity), (c) clarity, (d) correctness.
Solution
(a) Algorithm
  1. Input a non-negative integer n. Set result = 1.
  2. For i = 2, 3, ..., n: multiply result by i.
  3. 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
Output
(b) Efficiency
  • 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.)
(c) Clarity

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.

(d) Correctness

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.

33 marks
Problem 3: Finding the largest number in a list. Problem statement: Given a list of integers, find the largest number in the list. Solution: Linear scan. Give answer of the following: (a) algorithm, (b) efficiency (time complexity and space complexity), (c) clarity, (d) correctness.
Solution
(a) Algorithm
  1. If the list is empty, report that there is no largest number.
  2. Set largest = the first element of the list.
  3. Scan the remaining elements one by one; if an element is greater than largest, set largest to that element.
  4. 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
Output
(b) Efficiency
  • 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.
(c) Clarity

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.

(d) Correctness

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.

43 marks
Problem 4: Checking for prime numbers. Problem statement: Determine if a given integer n is a prime number (i.e., it has no divisors other than 1 and itself). Solution: Simple divisibility test. Give answer of the following: (a) algorithm, (b) efficiency (time complexity and space complexity), (c) clarity, (d) correctness.
Solution
(a) Algorithm
  1. If n < 2, it is not prime.
  2. For each i from 2 up to \sqrt{n}: if n \% i = 0, then i divides n, so n is not prime; stop.
  3. 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
Output
(b) Efficiency
  • 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.
(c) Clarity

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.

(d) Correctness

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.

53 marks
Problem 5: Finding the average of a list. Problem statement: Calculate the average value of a list of numbers. Solution: Summation and division. Give answer of the following: (a) algorithm, (b) efficiency (time complexity and space complexity), (c) clarity, (d) correctness.
Solution
(a) Algorithm
  1. If the list is empty, report that the average cannot be calculated (avoid division by zero).
  2. Set total = 0 and add every number of the list to total.
  3. 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
Output
(b) Efficiency
  • 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.
(c) Clarity

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.

(d) Correctness

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.

63 marks
How can abstraction help a student create a simple timetable app for school?
Solution

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.

73 marks
A shopkeeper wants to use a program to quickly find a product in a long list. Which type of algorithm should they use and why?
Solution

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

18 marks
Compare and evaluate the efficiency of Bubble Sort and Merge Sort in sorting a list of 1,000 student names. Discuss in terms of time complexity and clarity. Which is more suitable and why?
Solution

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)
Counting comparisons for 1,000 random names.
Same sorted result: True
Bubble sort comparisons: 499500
Merge sort comparisons:  8743
Output
Evaluation for n = 1000
CriterionBubble sortMerge sort
Time complexityO(n^2)O(n\log n)
Comparisons for 1,000 names499,500under 10,000
Extra memoryO(1)O(n)
ClarityVery simpleModerate (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.

22026 Annual I, Set B, Q.2 (ii)8 marks
Describe the difference between Stack and Queue. How does their operation affect algorithm design? Provide one real-life example for each data structure.
Solution

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.

Fig. 2.11: Stack operations. push(C) places C on the top; pop() removes the top element C, so B becomes the new top. Both work only at the top (LIFO).
Fig. 2.12: A queue. New elements join at the rear (enqueue 9); elements leave from the front (dequeue 2). The first in is the first out (FIFO).
Stack versus queue
PointStackQueue
PrincipleLIFOFIFO
Ends usedOne end (top)Two ends (rear for insertion, front for deletion)
Insert / deletePush / popEnqueue / dequeue
Order of outputReverse of input orderSame as input order
AlgorithmsDFS, recursion, undo, expression evaluationBFS, 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)])
Same input, opposite output order.
Stack pops:   ['Page 3', 'Page 2', 'Page 1']
Queue serves: ['Page 1', 'Page 2', 'Page 3']
Output

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.
32026 Annual I, Set B, Q.4 OR2026 Annual I, Set D, Q.3 OR8 marks
Explain the importance of tree traversal techniques. Compare in-order, pre-order, and post-order traversal with examples and their use cases.
Solution

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

Comparison
TraversalOrderUse case
In-orderLeft subtree, Root, Right subtreeGives keys of a BST in ascending (sorted) order
Pre-orderRoot, Left subtree, Right subtreeCopying a tree; processing a node before its children (printing a folder before its contents)
Post-orderLeft subtree, Right subtree, RootDeleting a tree or freeing memory; children are processed before the parent (folder size)

Example

Fig. 2.17 to 2.19: The binary tree used by the book for the three depth-first traversals, with the order in which each traversal visits the nodes.
  1. 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.
  2. Pre-order: root 1 first, then 2, 4, 5, then 3, 6. Result: 1, 2, 4, 5, 3, 6.
  3. 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))
The three traversals, and in-order on a BST giving sorted keys.
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]
Output

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.

S13 marks
What is computational thinking? Name its four pillars.
Solution

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.
S23 marks
What is pattern recognition in computational thinking? Give an example.
Solution

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.

S33 marks
Give three differences between an array and a linked list.
Solution
ArrayLinked list
Fixed size, declared at the startDynamic 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 elementsInsertion or deletion only changes pointers

An array also needs no extra memory for links, while every linked list node stores at least one pointer.

S43 marks
What is a circular linked list? Give one advantage and one use.
Solution

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.

S53 marks
What are stack overflow and stack underflow? How are they prevented?
Solution

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.

S63 marks
Trace the following stack operations and give the final contents of the stack and the value returned by the last Top: Push(5), Push(8), Pop(), Push(3), Push(9), Pop(), Push(1), Top().
Solution
OperationStack (bottom to top)Returned
Push(5)5
Push(8)5, 8
Pop()58
Push(3)5, 3
Push(9)5, 3, 9
Pop()5, 39
Push(1)5, 3, 1
Top()5, 3, 11

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.

S73 marks
Define a graph. Differentiate between a directed and an undirected graph.
Solution

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.

S83 marks
Define the following tree terms: root, leaf, siblings, level, subtree.
Solution
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.
S93 marks
What is a binary search tree (BST)? Why is searching in a BST fast?
Solution

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.

S103 marks
What are edge cases? Why are they important in checking the correctness of an algorithm?
Solution

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.

S113 marks
What is Big O notation? Why do we usually consider the worst case?
Solution

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.

S123 marks
Arrange the following in order of increasing growth rate and name each: O(n^2), O(1), O(n\log n), O(\log n), O(n).
Solution
O(1) < O(\log n) < O(n) < O(n\log n) < O(n^2)
  • 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.

S133 marks
Why does binary search require the list to be sorted?
Solution

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.

S143 marks
Trace the Euclidean algorithm to find GCD(84, 36).
Solution
aba % b
843612
36120
120stop

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).

S153 marks
Differentiate between greedy algorithms and dynamic programming with one example each.
Solution

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).

S163 marks
How do data structures support computational thinking?
Solution
  • 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.
L15 marks
Explain the binary search algorithm. Write a Python program for it, trace it for a given list, and derive its time complexity.
Solution

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))
Binary search with a printed trace, searching for 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: 7
Output

Trace

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

  1. After each comparison the range halves: n,\ n/2,\ n/4,\ \ldots,\ n/2^k.
  2. The search ends when one element remains:
    \frac{n}{2^k} = 1 \Rightarrow k = \log_2 n
  3. 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).
L25 marks
What is a linked list? Explain its types with diagrams and write a Python program to create and display a singly linked list.
Solution

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.
Fig. 2.6: A singly linked list. Each node points to the next one; the last node points to NULL.
Fig. 2.8: A doubly linked list. Each node points forward (next) and backward (prev). The prev of the first node and the next of the last node are NULL.
Fig. 2.9: A circular (singly) linked list. The next pointer of the last node goes back to the first node, so 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")
Creating a singly linked list by inserting at the beginning, then traversing it.
10 -> 20 -> 30 -> 40 -> NULL
Output
L35 marks
Explain Big O notation. Describe the common complexity classes with examples and show how they grow with input size.
Solution

Big 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

ClassNameExampleSteps for n = 1024
O(1)ConstantAccess A[i]; push or pop1
O(\log n)LogarithmicBinary search10
O(n)LinearLinear search, sum of a list1024
O(n\log n)LinearithmicMerge sort, quick sort (average)10,240
O(n^2)QuadraticBubble sort (nested loops)1,048,576

Growth

Growth of running time
Steps against input size n for the main classes; the dashed line is O(1).
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))
Counting loop steps: (logarithmic, linear, quadratic) for n = 8, 64, 512.
8 (3, 8, 64)
64 (6, 64, 4096)
512 (9, 512, 262144)
Output

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.

L45 marks
Write a Python program that implements a queue with enqueue, dequeue, front, is_empty and is_full operations. Explain each operation and give two applications of queues.
Solution

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)
A fixed-capacity queue. (pop(0) is simple to read; collections.deque is faster for large queues.)
Queue is full, cannot add Hina
Queue: ['Ali', 'Sara', 'Omar'] front = Ali
Served: Ali
Queue: ['Sara', 'Omar', 'Hina']
Output

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.
L55 marks
Insert the keys 45, 25, 65, 15, 35, 55, 75 into an empty binary search tree. Draw the tree and write its in-order, pre-order and post-order traversals.
Solution

Building the BST

  1. 45 becomes the root.
  2. 25 < 45: left child of 45. 65 > 45: right child of 45.
  3. 15 < 45, 15 < 25: left child of 25. 35 < 45, 35 > 25: right child of 25.
  4. 55 > 45, 55 < 65: left child of 65. 75 > 45, 75 > 65: right child of 65.
The BST after inserting 45, 25, 65, 15, 35, 55, 75.

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]
Output

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.

L65 marks
Explain the three criteria for evaluating a computational solution (correctness, clarity, efficiency), using the linear search algorithm as an example.
Solution

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")
Linear search checked with normal and edge-case tests.
[4, 9, 2] 4 -> 0 PASS
[4, 9, 2] 2 -> 2 PASS
[4, 9, 2] 7 -> -1 PASS
[] 7 -> -1 PASS
Output

Clarity

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.

2026 Annual I, Set B, MCQ 41 mark
Which data structure uses the LIFO (Last-In, First-Out) principle?
  1. ALinked List
  2. BQueue
  3. CStackAnswer
  4. DArray
Why

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.

2026 Annual I, Set B, MCQ 51 mark
Which node in a tree has no children?
  1. ALeafAnswer
  2. BRoot
  3. CParent
  4. DSubtree
Why

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.

2026 Annual I, Set B, Q.2 (ii)1.5+1.5 = 3 marks
Differentiate between stack and queue with real life examples.
Solution
StackQueue
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.

2026 Annual I, Set B, Q.2 (iii)1+2 = 3 marks
What is 'linked list'? Also write down its advantages.
Solution

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.
2026 Annual I, Set B, Q.2 (vi) OR1.5+1.5 = 3 marks
What is the difference between time complexity and space complexity in evaluating computational solutions?
Solution

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).

2026 Annual I, Set B, Q.2 (viii) OR3 marks
Write down the purpose of Enqueue, Dequeue and Front operations of Queue.
Solution

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.

2026 Annual I, Set B, Q.2 (ix)1+2 = 3 marks
What is the advantage of using arrays? Write its two types.
Solution

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].
2026 Annual I, Set B, Q.4 OR2.5+2.5 = 5 marks
Explain the following tree traversal methods: Pre-order and Post-order. Describe their order of operations with one example each.
Solution

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.

Fig. 2.17 to 2.19: The binary tree used by the book for the three depth-first traversals, with the order in which each traversal visits the nodes.

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))
Checking both orders in Python.
Pre-order:  [1, 2, 4, 5, 3, 6]
Post-order: [4, 5, 2, 6, 3, 1]
Output
2026 Annual I, Set D, MCQ 31 mark
In a circular linked list, the last node points to:
  1. AThe first nodeAnswer
  2. BNULL
  3. CThe previous node
  4. DThe middle node
Why

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.

2026 Annual I, Set D, MCQ 41 mark
Which tree traversal visits the left subtree first, then the root, and finally the right subtree?
  1. ALevel-order
  2. BPost-order
  3. CIn-orderAnswer
  4. DPre-order
Why

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.

2026 Annual I, Set D, Q.2 (i) OR1x3 = 3 marks
Write down any three operations of Queue.
Solution

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).

2026 Annual I, Set D, Q.2 (ii)2+1 = 3 marks
What is an array? Explain with an example.
Solution

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.

2026 Annual I, Set D, Q.2 (viii) OR2+1 = 3 marks
What is the concept of correctness in algorithm? Give an example.
Solution

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.

2026 Annual I, Set D, Q.2 (ix)1.5+1.5 = 3 marks
How does a doubly linked list differ from a singly linked list? Give example.
Solution
Singly linked listDoubly 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.

2026 Annual I, Set D, Q.2 (x) OR1x3 = 3 marks
Write down the purpose of push, pop and top operations of stack.
Solution

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].

2026 Annual I, Set D, Q.2 (xi)3 marks
What will be the time and space complexity of finding the sum of digits in a list?
Solution

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
Output
  • **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).)

2026 Annual I, Set D, Q.3 OR2.5+2.5 = 5 marks
Explain the following tree traversal methods: In-order and Pre-order. Describe their order of operations with one example each.
Solution

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.

Fig. 2.20: A complete binary tree of 7 nodes and its in-order, pre-order and post-order traversals.

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))
Checking both orders in Python.
In-order:  [4, 2, 5, 1, 6, 3, 7]
Pre-order: [1, 2, 4, 5, 3, 6, 7]
Output

Exam focus

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. Bubble sort versus merge sort for a large list. (long)
    Book ERQ 1; tests time complexity and clarity together.
    Go to answer
  10. 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
  11. Four pillars of computational thinking with examples. (short)
    Opening topic of the unit and a source of scenario MCQs.
    Go to answer

Mark this unit as done to fill the Class 12 Computer Science ring.

Next, unit 3
Programming fundamentals

© 2026 FBISE Fold. Independent study notes, not affiliated with the Federal Board of Intermediate and Secondary Education.