B2.1.1 Construct, trace programs with variables (AO3)
B2.1.1_1 Data types: Boolean, char, decimal, integer, string
Example Program (Python)
# Declare variables with appropriate data types
student_name = "Alice" # String: Stores textual data
is_passing = True # Boolean: True/False for logical conditions
grade1 = 85 # Integer: Whole number for grades
grade2 = 90
weight = 75.5 # Decimal (float): For precise numerical values
initial = 'A' # Char (string of length 1): Single character
# Calculate average grade
average = (grade1 + grade2) / 2
# Output results
print(f"Student: {student_name}, Initial: {initial}")
print(f"Average Grade: {average}")
print(f"Passing: {is_passing}")
student_name = "Alice" # String: Stores textual data
is_passing = True # Boolean: True/False for logical conditions
grade1 = 85 # Integer: Whole number for grades
grade2 = 90
weight = 75.5 # Decimal (float): For precise numerical values
initial = 'A' # Char (string of length 1): Single character
# Calculate average grade
average = (grade1 + grade2) / 2
# Output results
print(f"Student: {student_name}, Initial: {initial}")
print(f"Average Grade: {average}")
print(f"Passing: {is_passing}")
Tracing the Program
- Initialize: student_name = "Alice", is_passing = True, grade1 = 85, grade2 = 90, weight = 75.5, initial = 'A'
- Calculate: average = (85 + 90) / 2 = 87.5
- Output: Prints "Student: Alice, Initial: A", "Average Grade: 87.5", "Passing: True"
Boolean
- Stores True or False for logical conditions
- Example: is_passing = grade >= 60
Char
- Single character (in Python, a string of length 1)
- Example: initial = 'A'
Decimal (Float)
- Represents numbers with decimal points
- Example: weight = 75.5 for precise measurements
Integer
- Whole numbers for counts or discrete values
- Example: grade1 = 85
String
- Text data for names, descriptions, etc.
- Example: student_name = "Alice"
Purpose
- Selecting appropriate data types ensures accurate data storage, manipulation, and program functionality
- Tracing verifies program logic by tracking variable changes and confirming expected outputs
B2.1.2 Construct programs for substring manipulation (AO3)
B2.1.2_1 Identify, extract, alter, concatenate, replace substrings
Identify
- Locate specific substrings using methods like find() or index()
- Example: text = "Hello World"; pos = text.find("World") returns 6
Extract
- Retrieve a portion using slicing or substring methods
- Example: text = "Hello World"; substring = text[6:11] extracts "World"
Alter
- Modify parts by creating a new string with changes
- Example: text = "hello"; text = text.upper() results in "HELLO"
Concatenate
- Combine strings using + operator or join() method
- Example: first = "Hello"; last = "World"; full = first + " " + last
Replace
- Substitute a substring using replace() method
- Example: text = "Hello World"; new_text = text.replace("World", "Python")
Example Program (Python)
# Program to manipulate a string
text = "Welcome to IB Computer Science"
# Identify: Find position of substring
pos = text.find("Computer")
print(f"Position of 'Computer': {pos}") # Output: 11
# Extract: Get substring
substring = text[11:19]
print(f"Extracted substring: {substring}") # Output: Computer
# Alter: Convert to uppercase
upper_text = substring.upper()
print(f"Uppercase substring: {upper_text}") # Output: COMPUTER
# Concatenate: Add prefix
new_text = "Subject: " + text
print(f"Concatenated: {new_text}") # Output: Subject: Welcome to IB Computer Science
# Replace: Change substring
replaced_text = text.replace("Computer Science", "Programming")
print(f"Replaced text: {replaced_text}") # Output: Welcome to IB Programming
text = "Welcome to IB Computer Science"
# Identify: Find position of substring
pos = text.find("Computer")
print(f"Position of 'Computer': {pos}") # Output: 11
# Extract: Get substring
substring = text[11:19]
print(f"Extracted substring: {substring}") # Output: Computer
# Alter: Convert to uppercase
upper_text = substring.upper()
print(f"Uppercase substring: {upper_text}") # Output: COMPUTER
# Concatenate: Add prefix
new_text = "Subject: " + text
print(f"Concatenated: {new_text}") # Output: Subject: Welcome to IB Computer Science
# Replace: Change substring
replaced_text = text.replace("Computer Science", "Programming")
print(f"Replaced text: {replaced_text}") # Output: Welcome to IB Programming
Construction and Tracing
- Construct: Write code to perform substring operations based on requirements
- Trace: Step through the code, tracking variable values:
- text = "Welcome to IB Computer Science"
- pos = 14 (from find("Computer"))
- substring = "Computer" (from slicing [14:22])
- upper_text = "COMPUTER" (from upper())
- new_text = "Subject: Welcome to IB Computer Science" (from concatenation)
- replaced_text = "Welcome to IB Programming" (from replace())
Use Case
- A program processing email addresses to extract usernames (e.g., user@domain.com → user)
- Concatenate with a greeting, and replace domains for standardization
- Ensuring correct manipulation through tracing
B2.1.3 Describe exception handling (AO2)
B2.1.3_1 Failure points: unexpected inputs, resource unavailability, logic errors
Unexpected Inputs
- Occurs when user data does not match expected format or type
- Example: Entering "abc" when expecting an integer
- Can cause crashes (e.g., ValueError) or incorrect behavior
Resource Unavailability
- Failure due to inaccessible resources (files, databases, network)
- Example: Attempting to open a non-existent file
- Results in FileNotFoundError, disrupting execution
Logic Errors
- Errors in program logic (division by zero, out-of-bounds access)
- Example: Dividing by a variable that equals zero
- Causes runtime errors or incorrect outputs
B2.1.3_2 Role in program development
Purpose
- Exception handling ensures programs gracefully handle errors
- Improves robustness and user experience
- Prevents crashes by catching and managing errors
- Allows programs to recover or fail gracefully
Benefits in Development
- Enhances reliability by anticipating and addressing potential failure points
- Improves debugging by providing clear error messages and controlled error handling
- Supports user-friendly applications by informing users of issues without abrupt termination
Use in Testing
- Helps developers identify and handle edge cases during testing
- Ensures comprehensive error management
- Example: Testing a calculator program for division by zero
B2.1.3_3 Constructs: try/catch (Java), try/except (Python), finally block
Try/Catch (Java)
try {
int result = 10 / 0; // Potential ZeroDivisionError
} catch (ArithmeticException e) {
System.out.println("Error: Division by zero");
}
int result = 10 / 0; // Potential ZeroDivisionError
} catch (ArithmeticException e) {
System.out.println("Error: Division by zero");
}
- Catches specific exceptions to prevent crashes
Try/Except (Python)
try:
result = 10 / 0 # Potential ZeroDivisionError
except ZeroDivisionError:
print("Error: Division by zero")
result = 10 / 0 # Potential ZeroDivisionError
except ZeroDivisionError:
print("Error: Division by zero")
- Manages runtime errors, allowing program continuation
Finally Block (Java and Python)
try:
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
print("Error: File not found")
finally:
file.close() # Ensures file is closed
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
print("Error: File not found")
finally:
file.close() # Ensures file is closed
- Executes code regardless of whether an exception occurs
- Often used for cleanup tasks (e.g., closing files)
- Ensures resources are properly released, preventing leaks
Use Case
- A Python program reading user input for a calculation uses try/except to handle invalid inputs
- Finally block logs the operation's completion
- Ensures robustness and proper resource management
B2.1.4 Construct, use debugging techniques (AO3)
B2.1.4_1 Techniques: trace tables, breakpoints, print statements, step-by-step execution
Trace Tables
- Table tracking variable values at each step
- Identifies logic errors or unexpected behavior
- Example: For a loop calculating sum:
total = 0
for i in range(1, 4):
total += i
print(total)
for i in range(1, 4):
total += i
print(total)
| Step | i | total |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 2 | 3 |
| 3 | 3 | 6 |
Breakpoints
- Pause execution at specific lines to inspect state
- Used with debuggers (e.g., Python's pdb, IDEs)
- Example: Setting breakpoint at total += i to check values
- Pinpoints where errors occur
Print Statements
- Insert print() to output variable values
- Quick monitoring without debugger
- Example:
total = 0
for i in range(1, 4):
total += i
print(f"i={i}, total={total}")
for i in range(1, 4):
total += i
print(f"i={i}, total={total}")
- Output: i=1, total=1, i=2, total=3, i=3, total=6
Step-by-Step Execution
- Execute code line-by-line using debugger
- Observe behavior and variable changes in real-time
- Tools: pdb, VS Code, PyCharm debuggers
- Identifies exact lines where errors occur
Construction and Use
# Example: Debug a program to find the maximum number
numbers = [4, 7, 2, 9, 1]
max_num = numbers[0]
for num in numbers:
print(f"Checking num={num}, max_num={max_num}") # Print statement
if num > max_num:
max_num = num
print(f"Maximum: {max_num}")
numbers = [4, 7, 2, 9, 1]
max_num = numbers[0]
for num in numbers:
print(f"Checking num={num}, max_num={max_num}") # Print statement
if num > max_num:
max_num = num
print(f"Maximum: {max_num}")
| Step | num | max_num |
|---|---|---|
| 1 | 4 | 4 |
| 2 | 7 | 7 |
| 3 | 2 | 7 |
| 4 | 9 | 9 |
| 5 | 1 | 9 |
B2.2.1 Compare static vs dynamic data structures (AO3)
B2.2.1_1 Memory allocation, resizing mechanisms
Static Data Structures
Memory Allocation
- Memory allocated at compile time
- Fixed size that cannot change during runtime
- Example: numbers = [0] * 5 creates a Python list with 5 elements
Resizing Mechanism
- Cannot be resized dynamically
- Size is set during declaration
- To change size, a new structure must be created and data copied
Dynamic Data Structures
Memory Allocation
- Memory allocated at runtime
- Flexible size adjustments as needed
- Example: numbers = [1, 2, 3]; numbers.append(4) dynamically adds an element
Resizing Mechanism
- Automatically resizes when capacity exceeded
- Allocates new memory and copies data
- Example: Python lists double capacity when full
B2.2.1_2 Pros, cons: speed, memory, flexibility
| Aspect | Static Data Structures | Dynamic Data Structures |
|---|---|---|
| Speed | Faster access (O(1)) due to contiguous memory and fixed size | Slower due to resizing overhead (O(n) in worst case) |
| Memory | More efficient, no overhead for resizing | Higher overhead for resizing and extra capacity |
| Flexibility | Inflexible, fixed size | Highly flexible, grows/shrinks as needed |
| Use Case | Fixed number of student grades (exactly 10) | Tracking user inputs in a chat application |
Static Pros
- Faster access due to contiguous memory
- Memory-efficient with no overhead
Static Cons
- Inflexible size cannot change
- Wasted space if not fully utilized
Dynamic Pros
- Flexible size adjusts as needed
- Easy to use with automatic memory management
Dynamic Cons
- Slower due to resizing overhead
- Higher memory usage for extra capacity
B2.2.2 Construct programs with arrays, lists (AO3)
B2.2.2_1 1D/2D arrays, ArrayLists (Java)
1D Arrays
- Fixed-size, contiguous collection of elements
- Accessed by index
# 1D array using Python list
grades = [85, 90, 78, 92]
grades[2] = 80 # Update element
print(grades[0]) # Access: 85
grades = [85, 90, 78, 92]
grades[2] = 80 # Update element
print(grades[0]) # Access: 85
- Use: Store sequences like student grades
2D Arrays
- Matrix-like structure with rows and columns
- Accessed by two indices
# 2D array using nested lists
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
matrix[1][2] = 10 # Update element
print(matrix[0][1]) # Access: 2
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
matrix[1][2] = 10 # Update element
print(matrix[0][1]) # Access: 2
- Use: Represent grids, tables, or seating charts
ArrayLists (Java)
- Dynamic, resizable list in Java
- Part of java.util package
import java.util.ArrayList;
// Initialize ArrayList
ArrayList<Integer> scores = new ArrayList<>();
scores.add(85); // Add element
scores.add(90);
scores.set(1, 95); // Update element
System.out.println(scores.get(0)); // Access: 85
// Initialize ArrayList
ArrayList<Integer> scores = new ArrayList<>();
scores.add(85); // Add element
scores.add(90);
scores.set(1, 95); // Update element
System.out.println(scores.get(0)); // Access: 85
- Use: Store dynamic collections like user inputs
B2.2.2_2 1D/2D lists (Python)
1D Lists
- Python's built-in dynamic lists
- Resizable and can store mixed data types
# 1D list
names = ["Alice", "Bob", "Charlie"]
names.append("David") # Add element
names[1] = "Beth" # Update element
print(names[0]) # Access: Alice
names = ["Alice", "Bob", "Charlie"]
names.append("David") # Add element
names[1] = "Beth" # Update element
print(names[0]) # Access: Alice
- Use: Store dynamic sequences like customer names
2D Lists
- Nested lists representing a grid or table
- Dynamic resizing capabilities
# 2D list
grid = [[0, 1, 0], [1, 0, 1], [0, 1, 0]]
grid[1][1] = 2 # Update element
print(grid[0][2]) # Access: 0
grid.append([1, 1, 1]) # Add new row
grid = [[0, 1, 0], [1, 0, 1], [0, 1, 0]]
grid[1][1] = 2 # Update element
print(grid[0][2]) # Access: 0
grid.append([1, 1, 1]) # Add new row
- Use: Represent dynamic matrices like game boards
B2.2.2_3 Add, remove, traverse dynamic list elements
Add Elements
- Append or insert elements to expand size
students = ["Alice", "Bob"]
students.append("Charlie") # Add to end
students.insert(1, "Beth") # Add at index 1
print(students) # ['Alice', 'Beth', 'Bob', 'Charlie']
students.append("Charlie") # Add to end
students.insert(1, "Beth") # Add at index 1
print(students) # ['Alice', 'Beth', 'Bob', 'Charlie']
Remove Elements
- Remove by value, index, or clear entire list
students = ["Alice", "Beth", "Bob", "Charlie"]
students.remove("Beth") # Remove by value
students.pop(1) # Remove by index (Bob)
print(students) # ['Alice', 'Charlie']
students.remove("Beth") # Remove by value
students.pop(1) # Remove by index (Bob)
print(students) # ['Alice', 'Charlie']
Traverse Elements
- Iterate through elements to process or analyze
scores = [85, 90, 78, 92]
for score in scores:
print(f"Score: {score}")
# Access by index
for i in range(len(scores)):
scores[i] += 5 # Add 5 to each
print(scores) # [90, 95, 83, 97]
for score in scores:
print(f"Score: {score}")
# Access by index
for i in range(len(scores)):
scores[i] += 5 # Add 5 to each
print(scores) # [90, 95, 83, 97]
Use Case
- A program managing a student roster:
- Adds new students (append)
- Removes dropouts (remove)
- Traverses the list to calculate average grades or print names
- Ensures dynamic updates and efficient data processing
B2.2.3 Explain stack (LIFO) (AO2)
B2.2.3_1 Operations: push, pop, peek, isEmpty
Push
- Adds element to the top of the stack
- Example: Adding a plate to a stack of plates
stack = []
stack.append(5) # Push 5 onto stack
stack.append(5) # Push 5 onto stack
Pop
- Removes and returns the top element
- Example: Taking the top plate off
top = stack.pop() # Removes and returns 5
Peek
- Returns top element without removing it
- Example: Checking the top plate
top = stack[-1] if stack else None # Views top element
isEmpty
- Checks if stack is empty
- Returns True if no elements remain
is_empty = len(stack) == 0 # Returns True if empty
B2.2.3_2 Impact on performance, memory
Performance
- Push/Pop: O(1) time complexity
- Operations occur at the top (constant-time access)
- Peek/isEmpty: O(1) time complexity
- Only access top element or check length
- Example: Fast for function call management
Memory
- Uses contiguous or dynamic memory
- Minimal overhead for simple operations
- Dynamic resizing may cause occasional O(n) operations
- Example: Function call stack uses small, fixed space per element
Trade-offs
- Efficient for LIFO operations but limited for random access or searching (O(n))
- Memory-efficient for sequential data but may waste space if underused
B2.2.3_3 Appropriate stack use
Function Call Stack
- Manages function calls and returns
- Tracks execution context
- Example: Python's call stack in recursion
Undo/Redo Operations
- Tracks actions in reverse order
- Example: Text editor undo feature
Expression Evaluation
- Processes expressions or validates syntax
- Example: Checking balanced parentheses
Backtracking Algorithms
- Supports algorithms like depth-first search
- Example: Solving a maze by storing visited paths
Python Example
# Stack for checking balanced parentheses
def is_balanced(expression):
stack = []
for char in expression:
if char == '(':
stack.append(char) # Push
elif char == ')':
if len(stack) == 0: # Check isEmpty
return False
stack.pop() # Pop
return len(stack) == 0
print(is_balanced("(a + b)")) # Output: True
print(is_balanced("(a + b))")) # Output: False
def is_balanced(expression):
stack = []
for char in expression:
if char == '(':
stack.append(char) # Push
elif char == ')':
if len(stack) == 0: # Check isEmpty
return False
stack.pop() # Pop
return len(stack) == 0
print(is_balanced("(a + b)")) # Output: True
print(is_balanced("(a + b))")) # Output: False
Appropriateness
- Ideal for LIFO access, reversing actions, or managing nested operations
- Inappropriate for random access or frequent searching
B2.2.4 Explain queue (FIFO) (AO2)
B2.2.4_1 Operations: enqueue, dequeue, front, isEmpty
Enqueue
- Adds element to the back of the queue
- Example: Adding a customer to a ticket line
queue = []
queue.append(10) # Enqueue 10
queue.append(10) # Enqueue 10
Dequeue
- Removes and returns the front element
- Example: Serving the first customer
front = queue.pop(0) # Dequeue first element (10)
Front
- Returns the front element without removing
- Example: Checking the next customer
front = queue[0] if queue else None # View front element
isEmpty
- Checks if queue is empty
- Returns True if no elements remain
is_empty = len(queue) == 0 # Returns True if empty
B2.2.4_2 Impact on performance, memory
Performance
- Enqueue: O(1) using dynamic list (append)
- Dequeue: O(n) in basic Python list due to shifting
- Optimized implementations (collections.deque) achieve O(1)
- Front/isEmpty: O(1) time complexity
# Optimized implementation
from collections import deque
queue = deque()
queue.append(10) # O(1)
queue.popleft() # O(1)
from collections import deque
queue = deque()
queue.append(10) # O(1)
queue.popleft() # O(1)
Memory
- Uses contiguous or dynamic memory
- Overhead for resizing in dynamic implementations
- Efficient for sequential processing
- May waste memory if pre-allocated capacity is underused
- Example: Print job queue uses minimal space per job
Trade-offs
- Fast for FIFO operations but inefficient for random access (O(n))
- Optimized implementations like deque improve performance
B2.2.4_3 Appropriate queue use
Task Scheduling
- Manages tasks in arrival order
- Example: CPU process scheduling
Event Handling
- Processes events/messages in order received
- Example: Server queueing user requests
Breadth-First Search
- Explores nodes level by level
- Example: Finding shortest path in a maze
Buffering
- Handles data streams
- Example: Video player queueing frames
Python Example
from collections import deque
# Queue for processing print jobs
def process_print_jobs(jobs):
queue = deque()
for job in jobs:
queue.append(job) # Enqueue job
while len(queue) > 0: # Check isEmpty
current_job = queue.popleft() # Dequeue front job
print(f"Processing: {current_job}")
return len(queue) == 0
print_jobs = ["Doc1.pdf", "Doc2.pdf", "Doc3.pdf"]
print(process_print_jobs(print_jobs)) # Processes each job, returns True
# Queue for processing print jobs
def process_print_jobs(jobs):
queue = deque()
for job in jobs:
queue.append(job) # Enqueue job
while len(queue) > 0: # Check isEmpty
current_job = queue.popleft() # Dequeue front job
print(f"Processing: {current_job}")
return len(queue) == 0
print_jobs = ["Doc1.pdf", "Doc2.pdf", "Doc3.pdf"]
print(process_print_jobs(print_jobs)) # Processes each job, returns True
Appropriateness
- Ideal for FIFO scenarios, processing tasks in order of arrival
- Inappropriate for LIFO or random access, where stacks or arrays are better
B2.3.1 Construct programs implementing correct instruction sequence (AO3)
B2.3.1_1 The impact of instruction order on program functionality
Why Order Matters
- Programs execute instructions sequentially — each line runs in the order it appears
- Changing the order of instructions can produce entirely different (often incorrect) results
- Variables must be declared and assigned before they are used
- Computations that depend on previous results must appear after those results are computed
Incorrect Order
# ERROR: using average before it is calculated
print(f"Average: {average}") # NameError!
score1 = 80
score2 = 90
average = (score1 + score2) / 2
print(f"Average: {average}") # NameError!
score1 = 80
score2 = 90
average = (score1 + score2) / 2
- Causes a NameError:
averageis not yet defined when printed
Correct Order
# Correct: compute before printing
score1 = 80
score2 = 90
average = (score1 + score2) / 2
print(f"Average: {average}") # Output: 85.0
score1 = 80
score2 = 90
average = (score1 + score2) / 2
print(f"Average: {average}") # Output: 85.0
- Variables are defined before use — program runs correctly
B2.3.1_2 Ways to avoid errors: infinite loops, deadlock, incorrect output
Infinite Loops
- A loop that never terminates because the exit condition is never met
- Cause: missing update to the loop variable, or logic error in condition
- Prevention: ensure the loop variable is modified inside the loop body and the condition will eventually be false
# INFINITE — count never changes
count = 0
while count < 5:
print("Hello") # count is never incremented!
# FIXED
count = 0
while count < 5:
print("Hello")
count += 1 # Loop variable updated
count = 0
while count < 5:
print("Hello") # count is never incremented!
# FIXED
count = 0
while count < 5:
print("Hello")
count += 1 # Loop variable updated
Deadlock
- Two or more processes are each waiting for the other to release a resource — neither can proceed
- Common in concurrent / multi-threaded programs
- Prevention: use consistent resource-locking order, timeouts, or avoid circular dependencies between processes
# Conceptual example (pseudo-code)
# Process A holds Lock1, waits for Lock2
# Process B holds Lock2, waits for Lock1
# → deadlock: neither can continue
# Prevention: always acquire locks in the same order
# Both processes acquire Lock1 first, then Lock2
# Process A holds Lock1, waits for Lock2
# Process B holds Lock2, waits for Lock1
# → deadlock: neither can continue
# Prevention: always acquire locks in the same order
# Both processes acquire Lock1 first, then Lock2
Incorrect Output
- Program runs without errors but produces wrong results
- Caused by logic errors: wrong formula, off-by-one index, incorrect operator
- Prevention: trace tables, unit testing, careful code review
# Off-by-one error example
scores = [70, 80, 90]
# WRONG: misses last element
for i in range(len(scores) - 1):
print(scores[i])
# CORRECT
for i in range(len(scores)):
print(scores[i])
scores = [70, 80, 90]
# WRONG: misses last element
for i in range(len(scores) - 1):
print(scores[i])
# CORRECT
for i in range(len(scores)):
print(scores[i])
General Best Practices
- Plan program logic with pseudocode or flowcharts before coding
- Use trace tables to step through logic manually
- Test with boundary values (e.g., 0, negative numbers, empty lists)
- Use meaningful variable names to reduce logic mistakes
B2.3.2 Construct programs with selection structures (AO3)
B2.3.2_1 if, else, elif / else if — conditional execution
What is Selection?
- Selection allows a program to choose which block of code to execute based on a condition
- Enables programs to respond differently to different inputs or states
- Core constructs: if, else, elif (Python) / else if (Java)
Python: if / elif / else
grade = 74
if grade >= 90:
print("A")
elif grade >= 80:
print("B")
elif grade >= 70:
print("C") # Output: C
else:
print("Fail")
if grade >= 90:
print("A")
elif grade >= 80:
print("B")
elif grade >= 70:
print("C") # Output: C
else:
print("Fail")
Java: if / else if / else
int grade = 74;
if (grade >= 90) {
System.out.println("A");
} else if (grade >= 80) {
System.out.println("B");
} else if (grade >= 70) {
System.out.println("C"); // Output: C
} else {
System.out.println("Fail");
}
if (grade >= 90) {
System.out.println("A");
} else if (grade >= 80) {
System.out.println("B");
} else if (grade >= 70) {
System.out.println("C"); // Output: C
} else {
System.out.println("Fail");
}
B2.3.2_2 Boolean operators (AND, OR, NOT) and relational operators (<, <=, >, >=, ==, !=)
Relational Operators
- == equal to != not equal to
- < less than <= less than or equal
- > greater than >= greater than or equal
- Always produce a Boolean result (True or False)
Boolean Operators
- AND — both conditions must be True
- OR — at least one condition must be True
- NOT — inverts a Boolean value
- Used to combine multiple conditions in a single
ifstatement
Combined Example (Python)
age = 17
has_permission = True
score = 55
# AND: both conditions required
if age >= 16 and has_permission:
print("Access granted") # Output: Access granted
# OR: either condition sufficient
if score < 40 or score > 100:
print("Invalid score")
else:
print("Score accepted") # Output: Score accepted
# NOT: invert condition
if not has_permission:
print("Access denied")
else:
print("Welcome!") # Output: Welcome!
has_permission = True
score = 55
# AND: both conditions required
if age >= 16 and has_permission:
print("Access granted") # Output: Access granted
# OR: either condition sufficient
if score < 40 or score > 100:
print("Invalid score")
else:
print("Score accepted") # Output: Score accepted
# NOT: invert condition
if not has_permission:
print("Access denied")
else:
print("Welcome!") # Output: Welcome!
Use Case
- A login system checks:
if username == stored_name and password == stored_pass - A ticket system applies discounts:
if age < 12 or age >= 65 - A sensor triggers an alert:
if temperature > 100 and pressure > 200
B2.3.3 Construct programs with looping structures (AO3)
B2.3.3_1 Counted loops vs conditional loops
Counted Loops (for)
- Repeat a known, fixed number of times
- Best when the number of iterations is determined before the loop starts
- Uses a loop variable that steps through a range or collection
# Print squares of 1 to 5
for i in range(1, 6):
print(f"{i}² = {i**2}")
# Output: 1²=1, 2²=4, 3²=9, 4²=16, 5²=25
# Traverse a list
names = ["Alice", "Bob", "Carol"]
for name in names:
print(f"Hello, {name}")
for i in range(1, 6):
print(f"{i}² = {i**2}")
# Output: 1²=1, 2²=4, 3²=9, 4²=16, 5²=25
# Traverse a list
names = ["Alice", "Bob", "Carol"]
for name in names:
print(f"Hello, {name}")
Conditional Loops (while)
- Repeat while a condition remains True
- Best when the number of iterations is not known in advance
- Loop variable must be updated inside the loop to avoid infinite loops
# Keep asking until valid input
user_input = ""
while user_input != "quit":
user_input = input("Enter command: ")
print(f"You entered: {user_input}")
print("Session ended")
user_input = ""
while user_input != "quit":
user_input = input("Enter command: ")
print(f"You entered: {user_input}")
print("Session ended")
B2.3.3_2 Conditional statements within loops using Boolean and relational operators
Example: Filtering and Accumulating
# Count and sum only passing grades (>= 50)
grades = [72, 45, 88, 33, 91, 60]
total = 0
count = 0
for grade in grades:
if grade >= 50: # Condition inside loop
total += grade
count += 1
if count > 0:
print(f"Passing average: {total / count:.1f}") # Output: 77.8
grades = [72, 45, 88, 33, 91, 60]
total = 0
count = 0
for grade in grades:
if grade >= 50: # Condition inside loop
total += grade
count += 1
if count > 0:
print(f"Passing average: {total / count:.1f}") # Output: 77.8
Example: Early Exit with Boolean Operators
# Search for first student with grade A (>= 90) who is also enrolled
students = [("Alice", 85, True), ("Bob", 92, True), ("Carol", 95, False)]
found = False
i = 0
while i < len(students) and not found:
name, grade, enrolled = students[i]
if grade >= 90 and enrolled:
print(f"Top student: {name} ({grade})") # Output: Bob (92)
found = True
i += 1
students = [("Alice", 85, True), ("Bob", 92, True), ("Carol", 95, False)]
found = False
i = 0
while i < len(students) and not found:
name, grade, enrolled = students[i]
if grade >= 90 and enrolled:
print(f"Top student: {name} ({grade})") # Output: Bob (92)
found = True
i += 1
Trace Table for Filtering Example
| grade | grade >= 50 | total | count |
|---|---|---|---|
| 72 | True | 72 | 1 |
| 45 | False | 72 | 1 |
| 88 | True | 160 | 2 |
| 33 | False | 160 | 2 |
| 91 | True | 251 | 3 |
| 60 | True | 311 | 4 |
- Final average: 311 / 4 = 77.75
B2.3.4 Construct functions and modularization (AO3)
B2.3.4_1 Functions — reusable blocks with different inputs
What is a Function?
- A named block of code that performs a specific task and can be called multiple times
- Accepts parameters (inputs) and optionally returns a value
- Eliminates repeated code — define once, call many times with different arguments
Python Functions
def calculate_average(scores):
# Returns the average of a list
if len(scores) == 0:
return 0
return sum(scores) / len(scores)
def letter_grade(average):
# Returns a letter grade string
if average >= 90: return "A"
elif average >= 75: return "B"
elif average >= 60: return "C"
else: return "Fail"
# Call with different inputs
class_a = [88, 76, 95]
class_b = [55, 62, 48]
avg_a = calculate_average(class_a) # 86.3
avg_b = calculate_average(class_b) # 55.0
print(letter_grade(avg_a)) # B
print(letter_grade(avg_b)) # Fail
# Returns the average of a list
if len(scores) == 0:
return 0
return sum(scores) / len(scores)
def letter_grade(average):
# Returns a letter grade string
if average >= 90: return "A"
elif average >= 75: return "B"
elif average >= 60: return "C"
else: return "Fail"
# Call with different inputs
class_a = [88, 76, 95]
class_b = [55, 62, 48]
avg_a = calculate_average(class_a) # 86.3
avg_b = calculate_average(class_b) # 55.0
print(letter_grade(avg_a)) # B
print(letter_grade(avg_b)) # Fail
Java Methods (Functions)
public static double calculateAverage(int[] scores) {
if (scores.length == 0) return 0;
int total = 0;
for (int s : scores) total += s;
return (double) total / scores.length;
}
public static String letterGrade(double avg) {
if (avg >= 90) return "A";
else if (avg >= 75) return "B";
else if (avg >= 60) return "C";
else return "Fail";
}
if (scores.length == 0) return 0;
int total = 0;
for (int s : scores) total += s;
return (double) total / scores.length;
}
public static String letterGrade(double avg) {
if (avg >= 90) return "A";
else if (avg >= 75) return "B";
else if (avg >= 60) return "C";
else return "Fail";
}
B2.3.4_2 Modularization and scope (local vs global)
Modularization
- Breaking a program into smaller, self-contained modules (functions, classes, files)
- Each module has a single, clear responsibility
- Modules can be reused across different programs
- Makes code easier to read, test, maintain, and extend
Local Scope
- Variables declared inside a function exist only for that function's duration
- Cannot be accessed from outside the function
- Prevents unintended modification from other parts of the program
def greet():
message = "Hello" # local
print(message)
greet()
# print(message) → NameError!
message = "Hello" # local
print(message)
greet()
# print(message) → NameError!
Global Scope
- Variables declared outside all functions are accessible everywhere
- Should be used sparingly — global state makes programs harder to debug
- In Python, use the global keyword to modify a global variable inside a function
total = 0 # global variable
def add_score(score):
global total
total += score
add_score(85)
add_score(90)
print(total) # Output: 175
def add_score(score):
global total
total += score
add_score(85)
add_score(90)
print(total) # Output: 175
Benefits of Modularization
- Reusability: write once, use in many places
- Maintainability: change one function without affecting others
- Testability: test each function in isolation
- Readability: meaningful function names make code self-documenting
- Collaboration: team members can work on separate modules
Modularized Program Example
# Modularized student grade report program
def get_grades():
# Module 1: data input
return [88, 72, 95, 64, 80]
def calculate_average(grades):
# Module 2: calculation
return sum(grades) / len(grades)
def generate_report(grades, average):
# Module 3: output
print(f"Grades: {grades}")
print(f"Average: {average:.1f}")
print(f"Result: {'Pass' if average >= 60 else 'Fail'}")
# Main program — calls each module in sequence
grades = get_grades()
avg = calculate_average(grades)
generate_report(grades, avg)
# Output: Grades: [88,72,95,64,80] Average: 79.8 Result: Pass
def get_grades():
# Module 1: data input
return [88, 72, 95, 64, 80]
def calculate_average(grades):
# Module 2: calculation
return sum(grades) / len(grades)
def generate_report(grades, average):
# Module 3: output
print(f"Grades: {grades}")
print(f"Average: {average:.1f}")
print(f"Result: {'Pass' if average >= 60 else 'Fail'}")
# Main program — calls each module in sequence
grades = get_grades()
avg = calculate_average(grades)
generate_report(grades, avg)
# Output: Grades: [88,72,95,64,80] Average: 79.8 Result: Pass
B2.4.1 Describe algorithm efficiency using Big O notation (AO2)
B2.4.1_1 Time and space complexity
Time Complexity
- Measures how execution time grows as the input size (n) increases
- Describes the worst-case number of operations
- Not the actual time in seconds — it describes the rate of growth
- Example: an algorithm that checks every item in a list of n items takes time proportional to n
Space Complexity
- Measures how memory usage grows as input size increases
- Includes variables, data structures, and call stack
- A sorting algorithm that creates a copy of the input uses O(n) extra space
- An in-place algorithm that uses only a few variables uses O(1) extra space
B2.4.1_2 Calculating Big O — common complexities
What is Big O?
- Big O notation expresses the upper bound of an algorithm's growth rate
- Constants and lower-order terms are dropped: 3n + 5 → O(n)
- Focus is on how the algorithm scales as n becomes very large
| Big O | Name | Description | Example |
|---|---|---|---|
| O(1) | Constant | Same time regardless of input size | Access list[0] |
| O(log n) | Logarithmic | Halves the problem each step | Binary search |
| O(n) | Linear | One operation per element | Linear search |
| O(n log n) | Linearithmic | Efficient sorting | Merge sort, quicksort |
| O(n²) | Quadratic | Nested loops over entire input | Bubble sort, selection sort |
| O(2ⁿ) | Exponential | Doubles with each added element | Brute-force subset enumeration |
O(1) — Constant
def get_first(lst):
return lst[0] # Always 1 operation
return lst[0] # Always 1 operation
O(n) — Linear
def find_max(lst):
max_val = lst[0]
for x in lst: # n iterations
if x > max_val: max_val = x
return max_val
max_val = lst[0]
for x in lst: # n iterations
if x > max_val: max_val = x
return max_val
O(n²) — Quadratic
def all_pairs(lst):
for i in range(len(lst)): # n
for j in range(len(lst)): # × n
print(lst[i], lst[j])
for i in range(len(lst)): # n
for j in range(len(lst)): # × n
print(lst[i], lst[j])
O(log n) — Logarithmic
def binary_search(lst, target):
low, high = 0, len(lst) - 1
while low <= high: # halves each time
mid = (low + high) // 2
if lst[mid] == target: return mid
elif lst[mid] < target: low = mid + 1
else: high = mid - 1
low, high = 0, len(lst) - 1
while low <= high: # halves each time
mid = (low + high) // 2
if lst[mid] == target: return mid
elif lst[mid] < target: low = mid + 1
else: high = mid - 1
B2.4.1_3 Algorithm choice based on scalability
Choosing the Right Algorithm
- For small datasets, a simple O(n²) algorithm is acceptable and often easier to implement
- For large datasets, an O(n log n) or O(log n) algorithm is essential — an O(n²) algorithm on 1,000,000 items would require 10¹² operations
- Always consider both time and space — a faster algorithm may use significantly more memory
| n | O(log n) | O(n) | O(n log n) | O(n²) |
|---|---|---|---|---|
| 10 | 3 | 10 | 33 | 100 |
| 100 | 7 | 100 | 664 | 10,000 |
| 1,000 | 10 | 1,000 | 10,000 | 1,000,000 |
| 1,000,000 | 20 | 1,000,000 | 20,000,000 | 10¹² |
B2.4.2 Construct and trace linear and binary search (AO3)
B2.4.2_1 Linear Search — O(n)
How it Works
- Checks each element one by one from start to end
- Works on unsorted or sorted data
- Best case: target is first element — O(1)
- Worst case: target is last or not present — O(n)
Python Implementation
def linear_search(lst, target):
for i in range(len(lst)):
if lst[i] == target:
return i # Found: return index
return -1 # Not found
phones = ["Alice", "Bob", "Carol", "David"]
print(linear_search(phones, "Carol")) # 2
print(linear_search(phones, "Eve")) # -1
for i in range(len(lst)):
if lst[i] == target:
return i # Found: return index
return -1 # Not found
phones = ["Alice", "Bob", "Carol", "David"]
print(linear_search(phones, "Carol")) # 2
print(linear_search(phones, "Eve")) # -1
Trace for linear_search(["Alice","Bob","Carol","David"], "Carol")
| i | lst[i] | == "Carol"? | Action |
|---|---|---|---|
| 0 | "Alice" | False | Continue |
| 1 | "Bob" | False | Continue |
| 2 | "Carol" | True | Return 2 |
B2.4.2_2 Binary Search — O(log n)
How it Works
- Requires a sorted list
- Compares target to the middle element
- If target is smaller → search the left half
- If target is larger → search the right half
- Halves the search space each step → O(log n)
Python Implementation
def binary_search(lst, target):
low = 0
high = len(lst) - 1
while low <= high:
mid = (low + high) // 2
if lst[mid] == target:
return mid
elif lst[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
low = 0
high = len(lst) - 1
while low <= high:
mid = (low + high) // 2
if lst[mid] == target:
return mid
elif lst[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
Trace for binary_search([10, 23, 37, 45, 58, 72, 89], 58)
| Step | low | high | mid | lst[mid] | Action |
|---|---|---|---|---|---|
| 1 | 0 | 6 | 3 | 45 | 45 < 58 → low = 4 |
| 2 | 4 | 6 | 5 | 72 | 72 > 58 → high = 4 |
| 3 | 4 | 4 | 4 | 58 | Found! Return 4 |
- Only 3 comparisons to find the target in 7 elements — vs up to 7 for linear search
B2.4.2_3 Comparing search efficiency and use cases
| Aspect | Linear Search | Binary Search |
|---|---|---|
| Time Complexity | O(n) | O(log n) |
| Requires Sorted Data? | No | Yes |
| Best Case | O(1) — target is first element | O(1) — target is middle element |
| Worst Case | O(n) — target not present | O(log n) — target not present |
| Use Case | Unsorted list; small datasets; searching by phone number to find a name | Large sorted dataset; searching a sorted/indexed name list to find a phone number |
B2.4.3 Construct and trace bubble sort and selection sort (AO3)
B2.4.3_1 Bubble Sort
How it Works
- Repeatedly compares adjacent pairs and swaps them if out of order
- Each pass "bubbles" the largest unsorted element to its correct position
- Continues until no swaps are made in a full pass (sorted)
- Time: O(n²) worst/average; O(n) best (already sorted with optimisation)
- Space: O(1) — in-place, only needs one temporary variable
Python Implementation
def bubble_sort(lst):
n = len(lst)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if lst[j] > lst[j + 1]:
lst[j], lst[j+1] = lst[j+1], lst[j]
swapped = True
if not swapped: break # Early exit if sorted
return lst
data = [64, 25, 12, 22, 11]
print(bubble_sort(data)) # [11,12,22,25,64]
n = len(lst)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if lst[j] > lst[j + 1]:
lst[j], lst[j+1] = lst[j+1], lst[j]
swapped = True
if not swapped: break # Early exit if sorted
return lst
data = [64, 25, 12, 22, 11]
print(bubble_sort(data)) # [11,12,22,25,64]
Trace for [64, 25, 12, 22, 11]
| Pass | After Pass | Swaps? |
|---|---|---|
| 1 | [25, 12, 22, 11, 64] | Yes |
| 2 | [12, 22, 11, 25, 64] | Yes |
| 3 | [12, 11, 22, 25, 64] | Yes |
| 4 | [11, 12, 22, 25, 64] | Yes |
| 5 | [11, 12, 22, 25, 64] | No → stop |
- Bold = element placed in final sorted position that pass
B2.4.3_2 Selection Sort
How it Works
- Divides list into a sorted left portion and an unsorted right portion
- Each pass finds the minimum in the unsorted portion and swaps it to the end of the sorted portion
- Time: always O(n²) — no early exit like bubble sort
- Space: O(1) — in-place
- Makes fewer swaps than bubble sort (at most n−1 swaps)
Python Implementation
def selection_sort(lst):
n = len(lst)
for i in range(n):
# Find minimum in unsorted portion
min_idx = i
for j in range(i + 1, n):
if lst[j] < lst[min_idx]:
min_idx = j
# Swap minimum into sorted position
lst[i], lst[min_idx] = lst[min_idx], lst[i]
return lst
data = [64, 25, 12, 22, 11]
print(selection_sort(data)) # [11,12,22,25,64]
n = len(lst)
for i in range(n):
# Find minimum in unsorted portion
min_idx = i
for j in range(i + 1, n):
if lst[j] < lst[min_idx]:
min_idx = j
# Swap minimum into sorted position
lst[i], lst[min_idx] = lst[min_idx], lst[i]
return lst
data = [64, 25, 12, 22, 11]
print(selection_sort(data)) # [11,12,22,25,64]
Trace for [64, 25, 12, 22, 11]
| Pass (i) | Min Found | Swap | After Pass |
|---|---|---|---|
| 0 | 11 (idx 4) | 64 ↔ 11 | [11, 25, 12, 22, 64] |
| 1 | 12 (idx 2) | 25 ↔ 12 | [11, 12, 25, 22, 64] |
| 2 | 22 (idx 3) | 25 ↔ 22 | [11, 12, 22, 25, 64] |
| 3 | 25 (idx 3) | No swap | [11, 12, 22, 25, 64] |
| 4 | 64 (idx 4) | No swap | [11, 12, 22, 25, 64] |
- Bold = element confirmed in its final sorted position
B2.4.3_3 Comparing bubble sort and selection sort
| Aspect | Bubble Sort | Selection Sort |
|---|---|---|
| Time (worst/avg) | O(n²) | O(n²) |
| Time (best) | O(n) with optimisation (already sorted) | O(n²) — always scans full unsorted portion |
| Space | O(1) | O(1) |
| Number of Swaps | Can be high — many adjacent swaps | At most n−1 — one swap per pass |
| Stable? | Yes — equal elements preserve order | No — swaps can change relative order |
| Best Used When | Data is nearly sorted; small datasets | Minimising writes/swaps is important; small datasets |
Summary
- Both algorithms are simple to implement but inefficient for large datasets — O(n²) becomes impractical for n > 10,000
- For real-world large-scale sorting, prefer merge sort or quicksort — both O(n log n)
- These algorithms are valuable for learning and understanding how sorting works at a low level
B2.4.4 Explain recursion and its applications (AO2) HL
B2.4.4_1 Fundamentals — advantages and limitations
What is Recursion?
- A function that calls itself to solve a smaller version of the same problem
- Every recursive solution has two essential components:
- Base case: a condition that stops the recursion (returns a direct answer)
- Recursive case: reduces the problem towards the base case and calls the function again
- Each call is placed on the call stack and resolved when the base case is reached
Advantages
- Elegant and concise — recursive solutions are often far shorter than iterative equivalents
- Natural fit for problems with a recursive structure (trees, fractals, divide-and-conquer)
- Improves code readability when the problem itself is recursive in nature
- Simplifies complex algorithms like quicksort and tree traversal
Limitations
- Memory usage: each recursive call adds a stack frame — deep recursion can cause a stack overflow
- Performance: function call overhead makes recursion slower than iteration for simple repetitions
- Complexity: tracing and debugging recursive code can be harder to follow
- Risk of infinite recursion if base case is missing or unreachable
B2.4.4_2 Applications — problems suited to recursion
Quicksort
- Selects a pivot element and partitions the list into elements smaller and larger than the pivot
- Recursively sorts each partition
- Average time: O(n log n); worst case O(n²)
def quicksort(lst):
if len(lst) <= 1: return lst # Base case
pivot = lst[len(lst) // 2]
left = [x for x in lst if x < pivot]
mid = [x for x in lst if x == pivot]
right = [x for x in lst if x > pivot]
return quicksort(left) + mid + quicksort(right)
if len(lst) <= 1: return lst # Base case
pivot = lst[len(lst) // 2]
left = [x for x in lst if x < pivot]
mid = [x for x in lst if x == pivot]
right = [x for x in lst if x > pivot]
return quicksort(left) + mid + quicksort(right)
Binary Tree Traversal
- Trees are naturally recursive: each node has left and right subtrees
- In-order traversal visits left → root → right to produce sorted output
- Recursion visits every node exactly once: O(n)
def inorder(node):
if node is None: return # Base case
inorder(node.left) # Visit left subtree
print(node.value) # Visit root
inorder(node.right) # Visit right subtree
if node is None: return # Base case
inorder(node.left) # Visit left subtree
print(node.value) # Visit root
inorder(node.right) # Visit right subtree
Fractal Image Creation
- Fractals are shapes that contain smaller copies of themselves
- Each recursive call draws a smaller version of the same pattern
- Example: The Sierpiński triangle — a triangle made of three smaller triangles, each containing three smaller triangles, and so on
- Recursion depth controls level of detail
When to Choose Recursion
- Use recursion when the problem naturally decomposes into identical sub-problems (trees, divide-and-conquer, fractals)
- Avoid recursion when depth could be large (risk of stack overflow) or when a simple loop will do
- Use memoisation (caching results) to avoid recalculating the same sub-problems in recursive algorithms
B2.4.4_3 Limitations — complexity and memory usage
Stack Overflow Example
def countdown(n):
# Missing base case → infinite recursion → RecursionError
print(n)
return countdown(n - 1) # Never stops
def countdown_fixed(n):
if n < 0: return # Base case prevents overflow
print(n)
countdown_fixed(n - 1)
# Missing base case → infinite recursion → RecursionError
print(n)
return countdown(n - 1) # Never stops
def countdown_fixed(n):
if n < 0: return # Base case prevents overflow
print(n)
countdown_fixed(n - 1)
Naive Recursion vs Memoisation
# Naive Fibonacci: O(2ⁿ) — recalculates the same values repeatedly
def fib(n):
if n <= 1: return n
return fib(n - 1) + fib(n - 2) # Exponential time!
# With memoisation: O(n) — each value computed only once
memo = {}
def fib_memo(n):
if n <= 1: return n
if n in memo: return memo[n] # Use cached result
memo[n] = fib_memo(n-1) + fib_memo(n-2)
return memo[n]
def fib(n):
if n <= 1: return n
return fib(n - 1) + fib(n - 2) # Exponential time!
# With memoisation: O(n) — each value computed only once
memo = {}
def fib_memo(n):
if n <= 1: return n
if n in memo: return memo[n] # Use cached result
memo[n] = fib_memo(n-1) + fib_memo(n-2)
return memo[n]
- fib(50) with naive recursion: over 10¹⁰ calls; with memoisation: just 50 unique calls
B2.4.5 Construct, trace recursive algorithms (AO3) HL
B2.4.5_1 Simple, non-branching recursive algorithms
Constructing Recursive Algorithms
- Solve problems by calling themselves with smaller inputs
- Consist of:
- Base case: Terminates recursion
- Recursive case: Reduces problem size
- Non-branching: Makes a single recursive call per iteration
Factorial Example
def factorial(n):
# Base case
if n == 0 or n == 1:
return 1
# Recursive case
return n * factorial(n - 1)
# Computes n! (e.g., 5! = 5 × 4 × 3 × 2 × 1 = 120)
# Base case
if n == 0 or n == 1:
return 1
# Recursive case
return n * factorial(n - 1)
# Computes n! (e.g., 5! = 5 × 4 × 3 × 2 × 1 = 120)
Tracing Recursive Algorithms
- Follow each recursive call, tracking values until base case
- Then compute final output by unwinding the call stack
Trace for factorial(4):
- Call 1: factorial(4) → 4 × factorial(3)
- Call 2: factorial(3) → 3 × factorial(2)
- Call 3: factorial(2) → 2 × factorial(1)
- Call 4: factorial(1) → Base case, returns 1
- Unwind:
- factorial(2) → 2 × 1 = 2
- factorial(3) → 3 × 2 = 6
- factorial(4) → 4 × 6 = 24
- Result: factorial(4) returns 24
Sum of Numbers Example
def sum_to_n(n):
# Base case
if n <= 1:
return n
# Recursive case
return n + sum_to_n(n - 1)
# Computes sum from 1 to n (e.g., sum_to_n(4) = 1+2+3+4 = 10)
# Base case
if n <= 1:
return n
# Recursive case
return n + sum_to_n(n - 1)
# Computes sum from 1 to n (e.g., sum_to_n(4) = 1+2+3+4 = 10)
Trace for sum_to_n(4):
- Call 1: sum_to_n(4) → 4 + sum_to_n(3)
- Call 2: sum_to_n(3) → 3 + sum_to_n(2)
- Call 3: sum_to_n(2) → 2 + sum_to_n(1)
- Call 4: sum_to_n(1) → Base case, returns 1
- Unwind:
- sum_to_n(2) → 2 + 1 = 3
- sum_to_n(3) → 3 + 3 = 6
- sum_to_n(4) → 4 + 6 = 10
- Result: sum_to_n(4) returns 10
Key Considerations
- Ensure a clear base case to prevent infinite recursion
- Non-branching algorithms are simpler to trace and construct
- Example Use Case: Calculating power of a number (x^n)
def power(x, n):
if n == 0: # Base case
return 1
return x * power(x, n - 1) # Recursive case
# Trace for power(2, 3):
# power(2, 3) → 2 * power(2, 2)
# power(2, 2) → 2 * power(2, 1)
# power(2, 1) → 2 * power(2, 0)
# power(2, 0) → 1
# Unwind: 2 * 1 = 2, 2 * 2 = 4, 2 * 4 = 8
# Result: 8
if n == 0: # Base case
return 1
return x * power(x, n - 1) # Recursive case
# Trace for power(2, 3):
# power(2, 3) → 2 * power(2, 2)
# power(2, 2) → 2 * power(2, 1)
# power(2, 1) → 2 * power(2, 0)
# power(2, 0) → 1
# Unwind: 2 * 1 = 2, 2 * 2 = 4, 2 * 4 = 8
# Result: 8
B2.5.1 Construct file-processing code (AO3)
B2.5.1_1 Manipulate text files
Description
- Writing code to read from, write to, or modify text files
- Enables data persistence and processing
- Purpose: Store, retrieve, or update data for tasks like logging, configuration, or analysis
- Example: Reading student names from a file, updating grades, and saving results
B2.5.1_2 Open files in read, write, append modes
Read Mode ('r')
- Opens file for reading
- Raises error if file doesn't exist
- Example: Reading student grades from text file
Write Mode ('w')
- Opens file for writing
- Overwrites if exists or creates new file
- Example: Writing processed data to a file
Append Mode ('a')
- Opens file to add data at the end
- Preserves existing content
- Example: Adding new log entries without overwriting
Python Code Example
# Example: Manipulating a text file with different modes
# Write mode: Create or overwrite a file
with open('students.txt', 'w') as file:
file.write("Alice,85\nBob,90\n") # Write initial data
# Append mode: Add new data
with open('students.txt', 'a') as file:
file.write("Charlie,78\n") # Append new student
# Read mode: Read and print file contents
with open('students.txt', 'r') as file:
content = file.read()
print("File Contents:", content)
# Write mode: Create or overwrite a file
with open('students.txt', 'w') as file:
file.write("Alice,85\nBob,90\n") # Write initial data
# Append mode: Add new data
with open('students.txt', 'a') as file:
file.write("Charlie,78\n") # Append new student
# Read mode: Read and print file contents
with open('students.txt', 'r') as file:
content = file.read()
print("File Contents:", content)
B2.5.1_3 Read, write, append, close files
Read
- Retrieves data using methods like read(), readline(), or readlines()
- Example: file.read() reads entire file as string
- file.readlines() reads lines into a list
Write
- Writes data to file in 'w' mode
- Overwrites existing content
- Example: file.write("data") writes a string
Append
- Adds data to end in 'a' mode
- Preserves existing content
- Example: file.write("new data\n") appends a line
Close
- Closes file to free system resources
- Automatically handled using 'with' statement
- Example: with open('file.txt', 'r') as file: ensures closure
Python Code Example
# Read, write, append example
# Write initial data
with open('grades.txt', 'w') as file:
file.write("Alice: 85\n")
# Append additional data
with open('grades.txt', 'a') as file:
file.write("Bob: 90\n")
# Read and process file
with open('grades.txt', 'r') as file:
lines = file.readlines() # Read all lines into a list
for line in lines:
name, grade = line.strip().split(': ')
grade = int(grade) + 5 # Increase grade by 5
print(f"Updated: {name}, {grade}")
# Write initial data
with open('grades.txt', 'w') as file:
file.write("Alice: 85\n")
# Append additional data
with open('grades.txt', 'a') as file:
file.write("Bob: 90\n")
# Read and process file
with open('grades.txt', 'r') as file:
lines = file.readlines() # Read all lines into a list
for line in lines:
name, grade = line.strip().split(': ')
grade = int(grade) + 5 # Increase grade by 5
print(f"Updated: {name}, {grade}")
B2.5.1_4 Java classes: Scanner, FileWriter, BufferedReader
Scanner
- Java class (java.util.Scanner) for reading data
- Parses input into tokens
import java.util.Scanner;
import java.io.File;
File file = new File("students.txt");
Scanner scanner = new Scanner(file);
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
scanner.close();
import java.io.File;
File file = new File("students.txt");
Scanner scanner = new Scanner(file);
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
scanner.close();
FileWriter
- Java class (java.io.FileWriter) for writing
- Writes character data to files
import java.io.FileWriter;
FileWriter writer = new FileWriter("students.txt");
writer.write("Alice,85\n");
writer.close(); // Explicitly close
FileWriter writer = new FileWriter("students.txt");
writer.write("Alice,85\n");
writer.close(); // Explicitly close
BufferedReader
- Java class for efficient reading
- Buffers input for better performance
import java.io.BufferedReader;
import java.io.FileReader;
BufferedReader reader = new BufferedReader(
new FileReader("students.txt"));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
import java.io.FileReader;
BufferedReader reader = new BufferedReader(
new FileReader("students.txt"));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
B2.5.1_5 Python functions: open(), read(), readline(), write(), close()
open()
- Opens file in specified mode
- Returns a file object
- Example: file = open('data.txt', 'r')
read()
- Reads entire file as string
- Example: content = file.read()
readline()
- Reads a single line
- Returns empty string at end
- Example: line = file.readline()
write()
- Writes string to file
- Example: file.write("Hello\n")
close()
- Closes file, freeing resources
- Used without 'with' statement
- Example: file.close()
Example Program
# Process a CSV file with student data
# Write student data
with open('students.csv', 'w') as file:
file.write("Name,Grade\nAlice,85\nBob,90\n")
# Read and manipulate data
with open('students.csv', 'r') as file:
lines = file.readlines()[1:] # Skip header
for line in lines:
name, grade = line.strip().split(',')
grade = int(grade) + 5 # Increase grade by 5
print(f"Updated: {name}, {grade}")
# Append new student
with open('students.csv', 'a') as file:
file.write("Charlie,78\n")
# Write student data
with open('students.csv', 'w') as file:
file.write("Name,Grade\nAlice,85\nBob,90\n")
# Read and manipulate data
with open('students.csv', 'r') as file:
lines = file.readlines()[1:] # Skip header
for line in lines:
name, grade = line.strip().split(',')
grade = int(grade) + 5 # Increase grade by 5
print(f"Updated: {name}, {grade}")
# Append new student
with open('students.csv', 'a') as file:
file.write("Charlie,78\n")
Use Case
- A program managing a log file:
- Writes new entries (write, append mode)
- Reads and analyzes past entries (readlines)
- Ensures proper closure (with or close)
- Enables reliable file processing for tasks like tracking user activity or storing configuration data