Calculate the Big O efficiency of algorithms, write and trace linear search, binary search, bubble sort and selection sort without built-ins, and (HL) explain recursion and quicksort and write simple recursive subprograms.
Each is in the Computer Science glossary, with every other term in the subject.
A measure of the amount of computing resources, mainly time and memory, that an algorithm uses as the size of its input increases.
The expected number of steps an algorithm performs over all possible inputs of size , assuming each input is equally likely, and the complexity this gives.
The condition in a recursive subprogram under which it returns a result directly, without calling itself again.
The input of a given size for which an algorithm performs the fewest steps, and the complexity on that input.
A way of describing the time or space complexity of an algorithm by the term that grows fastest as the input size
Describe the time and space complexity of an algorithm by calculating its Big O notation.
Describe how Big O notation is used to analyse scalability and to choose between algorithms.
Construct and trace a linear search and a binary search.
Compare linear search and binary search and choose the right one for a given data set.
Construct and trace bubble sort and selection sort.
Evaluate bubble sort and selection sort in terms of their time and space complexity on different data sets.
Explain the concept of recursion, the call stack, and the advantages and limitations of recursive solutions (HL only).
Explain how quicksort, binary tree traversal and fractals use recursion (HL only).
Construct and trace simple non-branching recursive algorithms (HL only).
Searching and sorting are among the most common jobs a computer does. A phone looks up a contact by name, a shop's website lists products from cheapest to most expensive, and a database finds one customer among millions. Several different algorithms can do each of these jobs, and they all give the same answer, but some finish in a fraction of a second while others would take days on the same data. The difference comes from how the number of steps grows as the amount of data grows.
Computer scientists measure that growth with Big O notation, which turns "this algorithm is faster" into a precise statement that can be calculated from the code. Linear search, binary search, bubble sort and selection sort are the standard algorithms for learning to reason this way: each is short enough to write and trace by hand, and together they show the main patterns of growth. Recursion (HL), in which a
181 exam-style questions on this subtopic, written in the style of the IB papers. The first has its markscheme open. Counted on
A fitness app stores a user's daily step counts in a list (Python) or array (Java) called steps, holding n values. The app uses the four functions/methods below.
def first_day(steps):
return steps[0]
def total_steps(steps):
total = 0
for s in steps:
total = total + s
return total
def count_beaten(steps):
count = 0
for i in range(len(steps)):
for j in range(len(steps)):
if steps[i] > steps[j]:
count = count + 1
count
():
k =
count =
k < (steps):
k = k *
count = count +
countOne of 75 short multiple-choice questions on B2.4. Pick an answer to see why it's right or wrong.
What does the Python code print?

The official deck for this part of the syllabus.
Define algorithmic efficiency.
Why is an algorithm's efficiency not measured by timing it with a stopwatch?
With a free account, Augment remembers where you stopped and what you have practiced.
A search algorithm for a sorted list that compares the target with the middle element of the part still being searched and discards the half that cannot contain it, repeating until the target is found or no elements remain.
Recursion in which a call of the subprogram can make two or more recursive calls, so the calls form a tree.
A sorting algorithm that makes repeated passes through a list, comparing each pair of adjacent elements and swapping them if they are in the wrong order, until a pass makes no swaps.
A stack maintained by the running program that holds one entry for each subprogram call that has started but not yet returned, with the most recent call on top.
A complexity in which the number of steps stays the same however large the input becomes.
An algorithm design strategy that divides a problem into smaller sub-problems of the same type, solves each sub-problem recursively, and combines their solutions to solve the original problem.
A complexity in which the number of steps roughly doubles each time the input size increases by one.
A shape or pattern that contains smaller copies of itself at every scale, so that each part resembles the whole.
An algorithm that transforms its data within the memory the data already occupies, using only a constant amount of extra memory, .
A search algorithm that checks each element of a list in order, from the first, until it finds the target or reaches the end of the list.
A complexity in which the number of steps grows in direct proportion to the input size, so that doubling the input doubles the work.
A complexity in which the number of steps grows in proportion to multiplied by .
The power to which a base must be raised to give a number, so that means ; in computer science, is the number of times can be halved before reaching 1.
A complexity in which the number of steps grows in proportion to the logarithm of the input size, so that doubling the input adds only a constant amount of work.
Recursion in which each call of the subprogram makes at most one recursive call, so the calls form a single chain from the first call to the base case.
In quicksort, the step that rearranges part of a list so that all elements smaller than the pivot come before it and all others come after it, leaving the pivot in its final sorted position.
One complete traversal of the unsorted part of a list by a sorting algorithm, after which at least one more element is in its final position.
The element chosen in each step of quicksort against which every other element in the current part of the list is compared.
A complexity in which the number of steps grows in proportion to the square of the input size, so that doubling the input multiplies the work by four.
A divide-and-conquer sorting algorithm that chooses a pivot element, partitions the list so that smaller elements come before the pivot and the others after it, and then sorts the two parts recursively.
A technique in which a subprogram solves a problem by calling itself on a smaller or simpler instance of the same problem, until it reaches an instance simple enough to solve directly.
The part of a recursive subprogram that calls the subprogram again with an argument that is closer to the base case, and uses the result of that call to build its own result.
A sorting algorithm that repeatedly finds the smallest element in the unsorted part of a list and swaps it with the first element of the unsorted part, so that the sorted part grows by one element per pass.
Arranging the elements of a list into a defined order, such as ascending or descending numerical or alphabetical order, according to a key.
A description of how the amount of extra memory an algorithm needs, beyond the memory holding its input, grows as the size of its input, , increases.
A sorting algorithm that keeps elements with equal keys in the same relative order as they had before sorting.
The entry on the call stack for one subprogram call, holding its parameters, its local variables and the return address where execution continues when the call finishes.
The exchange of the values held in two positions of a list or array, done with a temporary variable so that neither value is lost.
A description of how the number of basic steps an algorithm performs grows as the size of its input, , increases.
The phase of recursion after the base case is reached, in which each call returns its result to the call below it and its stack frame is popped, until the original call returns.
The input of a given size for which an algorithm performs the most steps, and the complexity on that input.
YouTubeTwo programs that solve the same problem correctly can still differ greatly in how much time and memory they use. Measuring that difference with a stopwatch is unreliable: the same program runs faster on a new laptop than on an old phone, slower when other programs are busy, and at different speeds in Python and Java. A fairer measure counts the basic steps an algorithm performs, such as comparisons, assignments and swaps, and asks how that count changes as the input gets bigger.
The size of the input is written . For a search, is the number of items in the list; for a sort, the number of items to be sorted; for a program that reads a file, the number of lines. Efficiency is then described as a relationship between and the work done, in two separate dimensions.
Time complexity ignores the actual seconds taken. If one algorithm does comparisons and another does , the second slows down far more as grows, whatever the hardware. Memory use is described the same way.
A sort that rearranges a list inside the list it was given needs only a few extra variables, however long the list is. A sort that copies the data into a second list of the same length needs extra memory proportional to . Both may take the same time, but the second uses far more memory on a large data set.
The function count_comparisons contains a nested loop in which the inner loop starts one place after the outer loop's current position. It counts how many times the inner loop body runs for three sizes of , the same pattern of work that comparing every item in a list with every later item produces.
def count_comparisons(n):
comparisons = 0
for i in range(n):
for j in range(i + 1, n):
comparisons = comparisons + 1
return comparisons
for n in [10, 100, 1000]:
print(n, count_comparisons(n), n * n)public class CountSteps {
static int countComparisons(int n) {
int comparisons = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
comparisons = comparisons + 1;
}
}
return comparisons;
}
public static void main(String[] args) {
int[] sizes = {10, 100, 1000};
for (int n : sizes) {
System.out.println(n + " " + countComparisons(n) + " " + (n * n));
}
}
}10 45 100
100 4950 10000
1000 499500 1000000The output shows two things. When is multiplied by 10, the count is multiplied by about 100, so the work grows with the square of . The exact count is , always a little less than half of , because the inner loop runs times, then , down to 0:
For that is , matching the program.
An exact formula such as is more detail than is needed to compare algorithms. As becomes large, the term dominates: at the term contributes only 500 out of 499,500. The constant does not change the shape of the growth either, since doubling still multiplies the work by four. Big O notation keeps only the part that decides the shape.
Big O is an upper bound on growth: it says the work grows no faster than the stated function once is large. It does not give a time in seconds, and it does not say that an algorithm is faster than an one for every input; for very small , an algorithm with a worse Big O but less work per step can win. What Big O guarantees is that, as keeps growing, the algorithm with the slower-growing function eventually wins and keeps winning by more.
Three rules turn a step count into Big O notation:
Keep only the fastest-growing term. becomes , because the other terms become negligible as grows.
Drop constant multipliers. becomes , and becomes . The constant depends on the language and the machine; the shape does not.
A fixed number of steps, however many, is . Five assignments or five hundred are both constant, because neither number depends on .
For many algorithms the number of steps depends on the particular data as well as on . A search through a list of 1,000 names might find its target at the first position or not at all. Three cases describe this spread.
The best case is rarely used to choose an algorithm, because a program cannot rely on always receiving its most convenient input. It does show when an algorithm can finish early, which matters for data that is often already in order.
The average case describes typical behaviour on random data. For many algorithms it has the same Big O as the worst case, only with a smaller constant: a linear search for an item that is present checks about half the list on average, items, which is still .
The worst case is the usual basis for comparison because it is a guarantee: whatever data arrives, the algorithm will not do more work than this. A system that must always respond quickly, such as a cash machine checking a card or a game redrawing the screen sixty times a second, is designed around its worst case.
A small number of growth patterns appear again and again. Each has a name, and each comes from a recognisable shape of code.
Reading the element at a known index of an array is : the address is calculated directly from the index, so element 5 and element 5,000,000 take the same time. Adding two numbers, swapping two elements and returning a value are also .
The next class needs one piece of mathematics. A logarithm answers the question "how many times must a number be halved to get down to 1?" For , the halvings are 16, 8, 4, 2, 1: four halvings, so .
When is not a power of 2 the logarithm is not a whole number: and . Logarithms grow extremely slowly. Doubling adds only 1 to , so a million items need only about 20 halvings and a billion about 30.
In Big O notation the base of the logarithm is not written, because changing the base only multiplies the value by a constant. The base is still 2 whenever the data is halved.
A single loop that visits every element once, such as a traversal that totals a list or finds its largest value, is . Linear time is the best possible for any task that must look at every item at least once.
appears when an algorithm does about steps of work at each of about levels, for example by repeatedly splitting a list in half and doing a linear amount of work on every level. It grows only a little faster than linear time.
Two nested loops that each run up to times give , as count_comparisons showed. Bubble sort and selection sort are quadratic. Ten times more data means a hundred times more work, which is why quadratic algorithms become impractical for large data sets.
Exponential algorithms are only usable for very small inputs. An algorithm that tries every possible subset of items does steps: is fine, but steps at a billion steps per second would take about 37 years. A recursive function that calls itself twice for each value, such as the naive Fibonacci function in the HL section on recursion, grows in this way.
The growth curves show the classes together. For small they are close; as grows they separate quickly.
The table gives the step counts for three sizes of input. The last column assumes, for illustration, a computer that performs one step per microsecond (a million steps per second).
Big O | Name | Typical code | Time at | |||
|---|---|---|---|---|---|---|
Constant | Access by index | 1 | 1 | 1 | 1 µs | |
Logarithmic | Halving loop (binary search) | 3.3 | 10 | 20 | 20 µs | |
Linear | One loop over the data (linear search) | 10 | 1000 | 1,000,000 | 1 second | |
Linearithmic | Efficient sorts (quicksort on average) | 33 | 9,966 | about 20 million | about 20 seconds | |
Quadratic | Two nested loops (bubble sort) | 100 | 1,000,000 | about 11.6 days | ||
Exponential | Trying every subset | 1,024 | a number with 302 digits | far larger | far longer than the age of the universe |
Big O can be read from the structure of the code without counting every step. The rules work from the inside out.
Code structure | Steps | Big O |
|---|---|---|
A fixed sequence of statements with no loop over the data | a constant | |
One loop that runs times (or , or ) with work inside | about | |
A loop whose variable is halved (or doubled) each time | about | |
A loop inside a loop, each running up to times | about | |
A loop that runs times, containing a halving loop | about | |
Two loops one after the other, each | ||
An loop followed by an nested loop |
Loops in sequence add their costs, and the larger term wins. Loops that are nested multiply their costs. A loop that calls a subprogram multiplies its own count by the cost of that call: calling an linear search inside a loop that runs times gives .
The halving rule needs a closer look, because the loop is easy to misread as linear. The loop in halvings divides n by 2 with integer division until it reaches 1 and counts the passes.
def halvings(n):
count = 0
while n > 1:
n = n // 2
count = count + 1
return count
print(halvings(16), halvings(1000), halvings(1000000))public class Halvings {
static int halvings(int n) {
int count = 0;
while (n > 1) {
n = n / 2;
count = count + 1;
}
return count;
}
public static void main(String[] args) {
System.out.println(halvings(16) + " " + halvings(1000) + " " + halvings(1000000));
}
}The program prints 4 9 19: the loop runs times, so it is . Java's / on two int values already performs integer division, which Python writes as //.
Space complexity counts only the extra memory an algorithm needs while it runs, not the memory already holding its input. The rules are the same as for time: a fixed number of extra variables is ; an extra list that grows with the input is ; an extra two-dimensional grid of by values is .
Algorithm | Extra memory | Space complexity |
|---|---|---|
Find the largest value in a list with a loop | one variable for the largest so far, one loop counter | |
Reverse a list by building a new reversed list | a second list of elements | |
Reverse a list in place by swapping ends inwards | one temporary variable and two indexes | |
Bubble sort or selection sort | one temporary variable and loop counters | |
A recursive subprogram that calls itself deep (HL) | one stack frame per active call |
Time and space often trade against each other. Storing extra data, such as an index or a copy sorted in a different order, can make searches faster at the cost of memory. On a device with little memory, such as a sensor or a smartwatch, an -space algorithm can be the right choice even when a faster one exists.
Scalability is the ability of a system to keep working well as its load grows. For an algorithm, the load is , and Big O predicts what happens when increases: an algorithm handles ten times more data with ten times more work, an algorithm needs a hundred times more, and an algorithm needs only a few more steps. Choosing an algorithm therefore means asking how large will become, not only how fast the program runs on today's test data.
Several factors decide the choice besides the Big O of the worst case:
The size of the data. For a list of 20 items any algorithm finishes instantly, so the simplest correct one is the best choice. For millions of items the growth rate dominates.
The state of the data. Some algorithms need sorted data (binary search) and some run much faster on data that is already nearly in order (bubble sort with an early exit).
How often the task runs. Sorting a list once so that it can then be searched thousands of times is worth the cost; sorting it to perform a single search is not.
Memory limits. An algorithm that needs extra memory may not fit on a small device.
Simplicity. A simple algorithm is easier to write correctly, test and maintain, which matters when its speed is good enough.
A search algorithm looks for a target value in a collection and reports where it is, usually by returning its index, or reports that it is absent. By convention a search that fails returns -1, a value that can never be a valid index. Both search algorithms work on a one-dimensional array in Java or a list in Python, and both are written without the built-in tools that would do the job in one line: Python's in operator and index method, and Java's contains, indexOf and Arrays.binarySearch.
The simplest way to find something is to look at every item in turn, starting from the first, until the target turns up or the items run out.
Linear search makes no assumptions about the data. The list can be in any order, can contain repeated values, and can hold any data type that can be compared for equality. The diagram of a linear search for the value 5 shows each comparison in turn.

The function linear_search returns the index of the first element equal to target, or -1 if there is none. The Python version keeps its own index counter instead of using len, so it works without that built-in.
def linear_search(items, target):
index = 0
for item in items:
if item == target:
return index
index = index + 1
return -1
scores = [12, 7, 31, 5, 19]
print(linear_search(scores, 5))
print(linear_search(scores, 8))public class LinearSearch {
static int linearSearch(int[] items, int target) {
for (int i = 0; i < items.length; i++) {
if (items[i] == target) {
return i;
}
}
return -1;
}
public static void main(String[] args) {
int[] scores = {12, 7, 31, 5, 19};
System.out.println(linearSearch(scores, 5));
System.out.println(linearSearch(scores, 8));
}
}The program prints 3 (the index of 5) and then -1 (8 is not in the list). The return -1 statement sits after the loop: it is reached only when every element has been checked without a match. In Java, items.length is a field of the array that stores its length, not a method that counts the elements.
A trace table records the search step by step, with one row for each pass of the loop. For linear_search(scores, 5):
|
|
| Returned |
|---|---|---|---|
0 | 12 | False | |
1 | 7 | False | |
2 | 31 | False | |
3 | 5 | True | 3 |
Some programmers prefer a search with a single exit point at the end of the subprogram. That version uses a conditional loop that stops as soon as a Boolean flag called found becomes true, or when the index passes the last element.
def linear_search(items, target):
index = 0
found = False
while not found and index < len(items):
if items[index] == target:
found = True
else:
index = index + 1
if found:
return index
return -1static int linearSearch(int[] items, int target) {
int index = 0;
boolean found = false;
while (!found && index < items.length) {
if (items[index] == target) {
found = true;
} else {
index = index + 1;
}
}
if (found) {
return index;
}
return -1;
}Both versions give the same results and have the same efficiency. The test index < items.length in the while condition is what stops a failed search: without it, the body would go on to read items[5] in a five-element array, and Java would throw an ArrayIndexOutOfBoundsException exception (Python an IndexError).
The work done by a linear search is the number of comparisons with the target.
Best case: the target is the first element. One comparison, whatever the length of the list: .
Worst case: the target is the last element or is not in the list at all. Every one of the elements is compared: .
Average case: if the target is present and equally likely to be at any position, the search checks about elements on average. Dropping the constant gives .
The space complexity is : the search needs only the index variable and, in the second version, the flag, however long the list is. Because the work grows in direct proportion to , a list ten times longer takes ten times longer to search in the worst case.
Finding a name in a printed phone book by linear search would mean reading from "Aaron" onwards. Nobody does this, because the book is in alphabetical order: opening it near the middle shows at once which half holds the name, and the other half can be ignored. Repeating the step on the remaining half finds any name in a few dozen openings at most.
The algorithm keeps two indexes, low and high, marking the first and last positions of the part of the list that might still contain the target. At each step it calculates the middle position mid and compares the element there with the target:
Set low to 0 and high to the last index, .
While low is less than or equal to high:
Set mid to divided by 2, rounded down.
If the element at mid equals the target, return mid.
If the element at mid is less than the target, the target can only be to its right: set low to mid + 1.
Otherwise the target can only be to its left: set high to mid - 1.
If the loop ends because low has passed high, the target is not in the list: return -1.
The flowchart of binary search shows the same steps with the standard symbols from subtopic B1.1, Approaches to computational thinking. A flowchart describes an algorithm without tying it to either programming language.
![Flowchart: Start; low = 0, high = n − 1; decision low ≤ high? No leads to return −1. Yes: mid = (low + high) // 2; decision a[mid] = target? Yes leads to return mid. No: decision a[mid] < target? Yes: low = mid + 1; no: high = mid − 1; both loop back to low ≤ high?](/_next/image?url=https%3A%2F%2Fmedia.augment-education.com%2Fnotes%2F2026%2F10%2F152e703a-bbea-4eff-be5c-2299f61b1360.webp&w=3840&q=75)
The diagram of a search for 42 in a list of 11 sorted codes shows the part still being searched shrinking at each step.

The function binary_search follows the steps exactly. It assumes items is sorted in ascending order.
def binary_search(items, target):
low = 0
high = len(items) - 1
while low <= high:
mid = (low + high) // 2
if items[mid] == target:
return mid
elif items[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
codes = [3, 8, 14, 21, 27, 35, 42, 50, 61, 77, 89]
print(binary_search(codes, 42))
print(binary_search(codes, 30))public class BinarySearch {
static int binarySearch(int[] items, int target) {
int low = 0;
int high = items.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2; // same as (low + high) / 2, but cannot overflow
if (items[mid] == target) {
return mid;
} else if (items[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
public static void main(String[] args) {
int[] codes = {3, 8, 14, 21, 27, 35, 42, 50, 61, 77, 89};
System.out.println(binarySearch(codes, 42));
System.out.println(binarySearch(codes, 30));
}
}The program prints 6 and then -1. Python's // rounds the midpoint down; Java's / on two integers does the same. The Java version calculates the midpoint as low + (high - low) / 2, which always gives the same value as (low + high) / 2 for valid indexes but avoids an overflow bug that affected real libraries for years.
The trace table for binary_search(codes, 42) has one row per pass of the loop:
|
|
|
| Comparison | Action |
|---|---|---|---|---|---|
0 | 10 | 5 | 35 | 35 < 42 |
|
6 | 10 | 8 | 61 | 61 > 42 |
|
6 | 7 | 6 | 42 | 42 = 42 | return 6 |
Three comparisons were needed; a linear search would have needed seven.
Binary search is short, but small changes break it. Each detail has a reason:
low <= high, not low < high. When low equals high, one element is left and it still has to be checked. With <, a search for the last remaining candidate (such as 89 in codes) would stop one comparison early and wrongly return -1.
low = mid + 1 and high = mid - 1, not low = mid or high = mid. The element at mid has already been checked, so it is excluded. Setting low = mid can leave low and mid unchanged forever when high = low + 1, an infinite loop.
high = len(items) - 1, not len(items). Indexes run from 0 to . Starting high at lets mid reach and read past the end of the list.
Integer division for mid. An index must be a whole number. In Python, / gives a float such as 5.0, which cannot be used as an index.
Every pass of the loop either finds the target or halves the number of candidates. Starting from elements, after one comparison about remain, after two about , and after comparisons about . The search must end once fewer than one element remains, so the largest number of comparisons is the number of halvings that take down to 1, plus one for the final check:
Best case: the target is at the first middle position. One comparison: .
Worst case: the target is found on the last possible comparison or is absent: comparisons, .
Average case: also , only about one comparison fewer than the worst case, because half of all positions are reached only on the last comparison.
Space: for the loop version (three index variables). The recursive version for HL uses memory for its stack frames.
Number of sorted items | Linear search, worst case | Binary search, worst case |
|---|---|---|
11 | 11 | 4 |
1,000 | 1,000 | 10 |
1,000,000 | 1,000,000 | 20 |
1,000,000,000 | 1,000,000,000 | 30 |
8,000,000,000 | 8,000,000,000 | 33 |
Searching a sorted list with an entry for every person on Earth would need at most 33 comparisons. The graph compares the worst-case comparisons of the two searches.
YouTubeBinary search looks so simple that a bug in it lay unnoticed in famous code for decades. Jon Bentley's book Programming Pearls (1986) presented a binary search that he had proved correct and tested. Joshua Bloch, who wrote the binary search in Java's standard library (java.util.Arrays) using the same approach, reported in 2006 that both contained the same bug: the Java version had been "lying in wait for nine years or so" until it broke a real program.
The fault was the line int mid = (low + high) / 2;. A Java int holds values only up to . In an array of more than about a billion () elements, low and high can both be large enough for their sum to exceed that limit. The sum then suffers integer overflow and wraps round to a negative number, so mid becomes negative and reading items[mid] throws an ArrayIndexOutOfBoundsException. The program Midpoint shows the effect with two large values.
public class Midpoint {
public static void main(String[] args) {
int low = 1500000000;
int high = 2000000000;
int badMid = (low + high) / 2;
int safeMid = low + (high - low) / 2;
System.out.println(badMid);
System.out.println(safeMid);
}
}low = 1500000000
high = 2000000000
print((low + high) // 2)
print(low + (high - low) // 2)The Java program prints -397483648 and then 1750000000; the Python version prints 1750000000 twice. The two languages differ because a Python int grows to whatever size a value needs and never overflows, while a Java int has a fixed 32 bits. The usual fix, low + (high - low) / 2, never forms the large sum: high - low is at most the length of the array, so the result always fits.
Source: Joshua Bloch, “Extra, Extra – Read All About It: Nearly All Binary Searches and Mergesorts are Broken”, Google Research Blog, 2 June 2006.
The case shows three things. An algorithm can be correct while its implementation is wrong, because the code runs on real hardware with fixed-size integers that the mathematical version ignores. Bugs that appear only at extreme sizes are hard to find by ordinary testing, which uses small inputs; this one needed arrays far larger than any computer of 1986 could hold. Choosing data types is part of choosing an algorithm: the same line is safe in Python and dangerous in Java.
Binary search is far faster on large data, but it is not always the right choice. Its requirement for sorted data has a cost, and for some data it is impossible.
Feature | Linear search | Binary search |
|---|---|---|
Data must be sorted | No | Yes, in the order being searched |
Best case | : target first | : target at the first middle |
Worst case | ||
Extra memory (loop version) | ||
Works on a structure without direct access by index, such as a linked list | Yes | No: reaching the middle element takes steps |
Simplicity | Very simple | Easy to get the bounds wrong |
Finds the first of several equal values | Yes | Not necessarily: it finds one of them |
Sorting data first so that it can be searched costs with an efficient sort, more than a single linear search's . Sorting is worth it only when the data will be searched many times, or when it is kept sorted anyway, as a database index is.
A printed phone book shows both sides of the choice. Its entries are sorted by surname, so finding a person's number from their name is a binary-search problem. Finding whose number 555 0193 is means a linear search through every entry, because the book is not sorted by number. A program in the same position has two options: search linearly, or keep a second copy of the data sorted by number (more memory and more work on every update) so that binary search can be used for both questions.
Sorted data is easier for both people and programs to use. A sorted list can be searched with binary search, its smallest and largest values are at the ends, duplicates sit next to each other, and a report in order of date or score is easier to read.
Bubble sort and selection sort are built from two operations: comparing two elements, and exchanging them when they are in the wrong order.
A swap needs three assignments. Writing items[0] = items[1] first would overwrite the value at index 0 before it had been saved, so the old value is copied into temp first.
items = [6, 3, 8]
temp = items[0]
items[0] = items[1]
items[1] = temp
print(items)int[] items = {6, 3, 8}; // inside main
int temp = items[0];
items[0] = items[1];
items[1] = temp;
System.out.println(java.util.Arrays.toString(items));The output is [3, 6, 8]. Python also allows items[0], items[1] = items[1], items[0], which swaps in one line; the three-line version works in both languages and shows exactly what happens.
Both sorts also share two properties that describe how a sorting algorithm uses memory and treats equal values.
Bubble sort and selection sort are both in place: they rearrange the list they are given using one temporary variable and a few counters. The built-in sorted function in Python is not, because it builds and returns a new list.
Stability matters when records are sorted by one field after another. If a class list is first sorted by name and then sorted by grade with a stable sort, students with the same grade stay in alphabetical order. An unstable sort may mix them up.
Bubble sort repeatedly walks along the list comparing each pair of neighbouring elements and swapping any pair that is in the wrong order. On each walk, the largest unsorted value is carried along by successive swaps until it reaches its final place at the end, like a bubble rising to the surface.
Each walk along the list has its own name.
After the first pass of bubble sort, the largest value is at the end. After the second pass, the two largest are in place, and so on. A list of elements therefore needs at most passes, and each pass can stop one position earlier than the one before, because the end of the list is already sorted. The diagram of bubble sort on [6, 3, 8, 2, 5] shows the list after each pass.

The basic version uses a counted loop for the passes and a nested counted loop for the comparisons. The inner loop runs to n - 1 - p, so it never compares into the sorted part and never reads past the end.
def bubble_sort(items):
n = len(items)
for p in range(n - 1):
for j in range(n - 1 - p):
if items[j] > items[j + 1]:
temp = items[j]
items[j] = items[j + 1]
items[j + 1] = temp
heights = [6, 3, 8, 2, 5]
bubble_sort(heights)
print(heights)import java.util.Arrays;
public class BubbleBasic {
static void bubbleSort(int[] items) {
int n = items.length;
for (int p = 0; p < n - 1; p++) {
for (int j = 0; j < n - 1 - p; j++) {
if (items[j] > items[j + 1]) {
int temp = items[j];
items[j] = items[j + 1];
items[j + 1] = temp;
}
}
}
}
public static void main(String[] args) {
int[] heights = {6, 3, 8, 2, 5};
bubbleSort(heights);
System.out.println(Arrays.toString(heights));
}
}The program prints [2, 3, 5, 6, 8]. The function returns nothing: it changes the list it was given. In both languages a list or array is passed to a subprogram as a reference, so the caller's heights is the list that gets sorted.
The basic version always makes all passes, even when the list became sorted early. If a whole pass makes no swaps, every adjacent pair is in order and the list is sorted, so the algorithm can stop. The improved version records this with a flag called swapped and uses a conditional loop for the passes.
def bubble_sort(items):
n = len(items)
passes = 0
swapped = True
while swapped:
swapped = False
for j in range(n - 1 - passes):
if items[j] > items[j + 1]:
temp = items[j]
items[j] = items[j + 1]
items[j + 1] = temp
swapped = True
passes = passes + 1
heights = [6, 3, 8, 2, 5]
bubble_sort(heights)
print(heights)import java.util.Arrays;
public class BubbleSort {
static void bubbleSort(int[] items) {
int n = items.length;
int passes = 0;
boolean swapped = true;
while (swapped) {
swapped = false;
for (int j = 0; j < n - 1 - passes; j++) {
if (items[j] > items[j + 1]) {
int temp = items[j];
items[j] = items[j + 1];
items[j + 1] = temp;
swapped = true;
}
}
passes = passes + 1;
}
}
public static void main(String[] args) {
int[] heights = {6, 3, 8, 2, 5};
bubbleSort(heights);
System.out.println(Arrays.toString(heights));
}
}A full trace of a sort records every comparison. For pass 1 of the improved version on [6, 3, 8, 2, 5]:
Pass |
|
|
| Swap? | List after the step |
|
|---|---|---|---|---|---|---|
1 | 0 | 6 | 3 | Yes | [3, 6, 8, 2, 5] | True |
1 | 1 | 6 | 8 | No | [3, 6, 8, 2, 5] | True |
1 | 2 | 8 | 2 | Yes | [3, 6, 2, 8, 5] | True |
1 | 3 | 8 | 5 | Yes | [3, 6, 2, 5, 8] | True |
2 | 0 | 3 | 6 | No | [3, 6, 2, 5, 8] | False |
2 | 1 | 6 | 2 | Yes | [3, 2, 6, 5, 8] | True |
2 | 2 | 6 | 5 | Yes | [3, 2, 5, 6, 8] | True |
3 | 0 | 3 | 2 | Yes | [2, 3, 5, 6, 8] | True |
3 | 1 | 3 | 5 | No | [2, 3, 5, 6, 8] | True |
4 | 0 | 2 | 3 | No | [2, 3, 5, 6, 8] | False |
At the start of each pass swapped is reset to False; it becomes True at the first swap of the pass. Pass 4 makes no swap, so swapped stays False and the while loop ends. In total the sort made 10 comparisons and 6 swaps. A shorter pass-by-pass table, with the list after each pass, is often enough when a question asks only for the state of the list:
After pass | List | Comparisons | Swaps |
|---|---|---|---|
1 | [3, 6, 2, 5, 8] | 4 | 3 |
2 | [3, 2, 5, 6, 8] | 3 | 2 |
3 | [2, 3, 5, 6, 8] | 2 | 1 |
4 | [2, 3, 5, 6, 8] | 1 | 0 |
The number of comparisons in pass 1 is , in pass 2 , down to 1 in the last pass. In the worst case, a list in reverse order, every pass is needed and every comparison leads to a swap:
Worst case: reverse order. comparisons and the same number of swaps: .
Average case: random order. About the same number of comparisons and about half as many swaps: still .
Best case (improved version): already sorted. The first pass makes comparisons and no swaps, so the loop stops: . The basic version without the flag makes all
Space: ; bubble sort is in place.
Stability: stable, because it swaps only when items[j] > items[j + 1], never when two values are equal, so equal values never pass each other.
Bubble sort's reputation is poor. Donald Knuth wrote in The Art of Computer Programming that it "seems to have nothing to recommend it, except a catchy name and the fact that it leads to some interesting theoretical problems". When Google's chief executive Eric Schmidt asked the presidential candidate Barack Obama in a 2007 interview how to sort a million integers, Obama answered: "I think the bubble sort would be the wrong way to go." For a million integers in random order, bubble sort needs about comparisons.
Selection sort builds the sorted list from the left. On each pass it searches the unsorted part for the smallest value and swaps it into the first unsorted position. After pass 1 the smallest value is at index 0, after pass 2 the second smallest is at index 1, and so on.
The diagram of selection sort on the same list, [6, 3, 8, 2, 5], shows the sorted part growing from the left.

Finding the minimum is itself a small algorithm: assume the first unsorted element is the smallest, then compare it with each later element and remember the index of any smaller one. Selection sort repeats this for each position. The code finds the minimum itself instead of calling Python's min or Java's Collections.min.
def selection_sort(items):
n = len(items)
for i in range(n - 1):
min_index = i
for j in range(i + 1, n):
if items[j] < items[min_index]:
min_index = j
if min_index != i:
temp = items[i]
items[i] = items[min_index]
items[min_index] = temp
heights = [6, 3, 8, 2, 5]
selection_sort(heights)
print(heights)import java.util.Arrays;
public class SelectionSort {
static void selectionSort(int[] items) {
int n = items.length;
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (items[j] < items[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
int temp = items[i];
items[i] = items[minIndex];
items[minIndex] = temp;
}
}
}
public static void main(String[] args) {
int[] heights = {6, 3, 8, 2, 5};
selectionSort(heights);
System.out.println(Arrays.toString(heights));
}
}The program prints [2, 3, 5, 6, 8]. The test if min_index != i skips a pointless swap of an element with itself; the sort is still correct without it, but it would then always make exactly swaps.
The trace table records min_index as the inner loop moves through the unsorted part. For [6, 3, 8, 2, 5]:
|
|
|
|
| Swap at end of pass | List at end of pass |
|---|---|---|---|---|---|---|
0 | 1 | 3 | 6 | 1 | ||
0 | 2 | 8 | 3 | 1 | ||
0 | 3 | 2 | 3 | 3 | ||
0 | 4 | 5 | 2 | 3 | index 0 with 3 | [2, 3, 8, 6, 5] |
1 | 2 | 8 | 3 | 1 | ||
1 | 3 | 6 | 3 | 1 | ||
1 | 4 | 5 | 3 | 1 | none | [2, 3, 8, 6, 5] |
2 | 3 | 6 | 8 | 3 | ||
2 | 4 | 5 | 6 | 4 | index 2 with 4 | [2, 3, 5, 6, 8] |
3 | 4 | 8 | 6 | 3 | none | [2, 3, 5, 6, 8] |
The sort made 10 comparisons and 2 swaps: the same number of comparisons as bubble sort on this list, but a third of the swaps.
Pass 1 compares the first element's candidate with the other elements, pass 2 makes comparisons, and so on. Unlike bubble sort, selection sort has no way to finish early: it cannot know that the rest of the list is in order without scanning it for the minimum. Every input of size therefore costs exactly
Best, average and worst case: comparisons, , even on a list that is already sorted.
Swaps: at most one per pass, so at most : . On sorted data it makes none.
Space: ; selection sort is in place.
Stability: not stable in this array form. A swap can jump an element over others with the same key: sorting the records (Ana, 4), (Ben, 4), (Cy, 1) by grade swaps (Cy, 1) with (Ana, 4) on the first pass, giving (Cy, 1), (Ben, 4), (Ana, 4), so Ana and Ben change order.
The small number of swaps is selection sort's one real advantage. When writing to memory is much more expensive than reading, as with flash memory, where every write wears the cells a little, or when each element is a large record that is slow to move, swaps instead of up to can matter more than the comparisons.
Both algorithms are quadratic in the worst case and use constant extra memory, so on large data sets both are slow. They differ in how they respond to the starting order of the data and in how many writes they make.
Property | Bubble sort (with swapped flag) | Selection sort |
|---|---|---|
Idea | Swap adjacent pairs; the largest value moves to the end each pass | Find the minimum; swap it to the front each pass |
Comparisons, best case (sorted) | : | : |
Comparisons, average case | about : | |
Comparisons, worst case (reverse order) | : | |
Swaps, worst case | : | |
Extra memory | , in place | , in place |
Stable | Yes | No (array version) |
Can stop early | Yes, after a pass with no swaps | No |
Counting the operations on real runs of the two Python functions shows how the starting order changes the picture. The "nearly sorted" lists have one pair of neighbouring values exchanged.
Data () | Bubble sort comparisons | Bubble sort swaps | Selection sort comparisons | Selection sort swaps |
|---|---|---|---|---|
Sorted (10) | 9 | 0 | 45 | 0 |
Nearly sorted (10) | 17 | 1 | 45 | 1 |
Random (10) | 42 | 25 | 45 | 9 |
Reverse order (10) | 45 | 45 | 45 | 5 |
Sorted (1,000) | 999 | 0 | 499,500 | 0 |
Random (1,000) | 498,069 | 247,734 | 499,500 | 994 |
Reverse order (1,000) | 499,500 | 499,500 | 499,500 | 500 |
Counts from the two Python functions with counters added for comparisons and swaps; each random list is one run with a fixed seed.
The counts lead to a clear evaluation. On sorted or nearly sorted data, bubble sort with a flag is far faster, finishing in one or two passes, while selection sort still makes every comparison. On random or reverse-ordered data both make about the same number of comparisons, but bubble sort makes hundreds of times more swaps on the large lists, so selection sort usually runs faster. On a reverse-ordered list selection sort makes only swaps, because each swap puts two values in their final places. For lists of a few dozen items the difference is unimportant, and both are easy to write correctly.
The graph compares the comparison counts as grows. Selection sort follows the quadratic curve for every input; bubble sort follows it for random and reversed data but drops to the straight line on sorted data.
Python's sorted and list.sort, and Java's Arrays.sort and Collections.sort, are not bubble or selection sorts. Python used Timsort, written by Tim Peters in 2002, from version 2.3 until version 3.11 replaced it with a closely related algorithm called Powersort; Java SE 7 adopted Timsort for sorting arrays of objects, and Android uses it too. Timsort is a hybrid of insertion sort and merge sort that finds runs of data already in order and merges them, giving in the worst case and close to on nearly sorted data. For arrays of primitive values such as int[], Java's Arrays.sort uses a dual-pivot quicksort.
Sources: Wikipedia, “Timsort”; Oracle, Java SE 21 API documentation for java.util.Arrays.
The built-ins are the right choice in real programs. Bubble sort and selection sort are studied because they are simple enough to write, trace and analyse by hand, and because they show clearly what makes an algorithm quadratic.
Some problems contain smaller versions of themselves. The factorial of 4, written , is , but it can also be described as : four times the factorial of a smaller number. In the same way , , and needs no further work. A subprogram can follow this description directly by calling itself.
Every correct recursive subprogram has two parts. The first stops the recursion.
For factorial, the base case is , where the answer is 1. Without a base case, or with one the calls never reach, the subprogram would call itself forever. The second part does the self-reference.
The function factorial contains both parts, and factorial_loop computes the same value with a counted loop, for comparison.
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
def factorial_loop(n):
result = 1
for k in range(2, n + 1):
result = result * k
return result
print(factorial(4), factorial_loop(4))
print(factorial(20), factorial_loop(20))public class Factorial {
static long factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
static long factorialLoop(int n) {
long result = 1;
for (int k = 2; k <= n; k++) {
result = result * k;
}
return result;
}
public static void main(String[] args) {
System.out.println(factorial(4) + " " + factorialLoop(4));
System.out.println(factorial(20) + " " + factorialLoop(20));
}
}Both lines of output show equal results: 24 24, then 2432902008176640000 2432902008176640000. The Java version returns a long, because is far larger than the largest int; would overflow even a long, while Python's integers simply grow.
A correct recursive subprogram needs a base case, recursive calls that move closer to it (here n - 1 is smaller than n), and a recursive case that uses the returned value (n * the result). Designing one is an example of algorithmic design: the programmer trusts that the call on the smaller problem returns the right answer and only has to decide how to combine it.
YouTubeWhen factorial(4) calls factorial(3), the first call has not finished: it is waiting to multiply 4 by whatever comes back. The computer must remember that n was 4 in the first call while n is 3 in the second, and where to continue when the second call returns. It does this with a stack, the last-in, first-out structure from subtopic B2.2, Data structures.
The call stack is a different thing from a stack data structure that a program creates for itself, although both work in the same last-in, first-out way. Each entry on it has its own name.
Each call gets its own frame, so the n in factorial(4) and the n in factorial(3) are separate variables that do not interfere. The diagram of the call stack for factorial(4) shows the frames at the moment the base case is reached.

The process has two phases. In the winding phase, each call pushes a new frame, until the base case is reached with four factorial frames on the stack. Nothing has been multiplied yet. In the second phase the calls finish in reverse order.
During unwinding, factorial(1) returns 1 to factorial(2), which returns to factorial(3), which returns to factorial(4), which returns to the main program. All the multiplications happen on the way back.
The call stack has a limited size. A recursion that is too deep, or has no reachable base case, pushes frames until the space runs out: a stack overflow, the same idea as pushing onto a full stack applied to subprogram calls. The function count_down has no base case.
def count_down(n):
print(n)
count_down(n - 1)
count_down(3)public class NoBase {
static int calls = 0;
static void countDown(int n) {
calls = calls + 1;
countDown(n - 1);
}
public static void main(String[] args) {
try {
countDown(3);
} catch (StackOverflowError e) {
System.out.println("StackOverflowError after " + calls + " calls");
}
}
}The Python program prints 3, 2, 1, 0, −1 and onwards down to −995, 999 lines in all, then stops with an error:
RecursionError: maximum recursion depth exceededCPython limits the depth of calls to 1,000 by default (sys.getrecursionlimit() returns 1000) and raises a RecursionError rather than letting the real stack overflow and crash the interpreter. Java has no fixed count: the JVM throws a StackOverflowError when the thread's stack memory is full. In test runs of NoBase with the default stack size, this happened after between about 42,000 and 46,000 calls; the depth depends on the stack size and on how much each frame holds, and varies between runs.
Source: Python documentation, sys.getrecursionlimit; runs with CPython 3.13 and OpenJDK 25.
The recursive subprograms so far make at most one recursive call each time they run, so the calls form a single chain.
A non-branching recursion that reduces by 1 each time makes calls and has frames on the stack at its deepest: time and space. Some problems split into two or more smaller problems instead.
The Fibonacci sequence 0, 1, 1, 2, 3, 5, 8, … is defined by , with and . Written directly as code, each call makes two more calls. The function counts how many calls it makes; in Python the global statement is needed to change the variable calls from inside the function.
calls = 0
def fib(n):
global calls
calls = calls + 1
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
for n in [5, 10, 20, 30]:
calls = 0
print(n, fib(n), calls)public class FibCalls {
static long calls = 0;
static long fib(int n) {
calls = calls + 1;
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
public static void main(String[] args) {
int[] sizes = {5, 10, 20, 30};
for (int n : sizes) {
calls = 0;
long result = fib(n);
System.out.println(n + " " + result + " " + calls);
}
}
}5 5 15
10 55 177
20 6765 21891
30 832040 2692537Computing takes more than 2.6 million calls, because the same values are calculated again and again: fib(30) calls fib(28) once directly and again inside fib(29), and the repetition multiplies at every level. Each increase of by one multiplies the number of calls by about 1.6, so this function runs in exponential time, usually written as an upper bound. A loop that keeps the last two values computes in 29 additions. The graph compares the calls made by this branching function with the calls of a non-branching one such as factorial.
Anything that can be written recursively can also be written with loops, sometimes with an explicit stack data structure to replace the call stack. The choice is about clarity, efficiency and the shape of the problem.
Recursion | Iteration (loops) | |
|---|---|---|
Fits problems that | contain smaller copies of themselves: trees, nested structures, divide and conquer, fractals | repeat a step a known or conditional number of times |
Code length and clarity | often shorter and closer to the mathematical definition | can be longer for tree-shaped problems, which need a manual stack |
Memory | one stack frame per active call: extra space | usually extra space |
Speed | each call has overhead (creating and removing a frame), so usually slower | no call overhead |
Failure when too deep | stack overflow ( | none from depth |
Ease of tracing and debugging | harder: many frames with the same variable names | easier: one set of variables |
Risk of hidden inefficiency | branching recursion can repeat work, as in | repetition is usually visible in the loop |
The advantages are strongest for problems that branch. Visiting every node of a tree, drawing a fractal, or sorting by splitting a list into parts are short and natural with recursion and awkward with loops. For a simple chain such as factorial, a sum or a countdown, a loop is usually the better engineering choice: it is at least as clear, uses constant memory and cannot overflow the stack.
Several of the fastest algorithms use the same strategy: split a problem into smaller independent problems of the same kind, solve each one recursively, then combine the results.
Binary search is a simple case: it divides the list, then continues in only one half, so it needs only non-branching recursion. Quicksort, developed by Tony Hoare in 1959 while he was a visiting student at Moscow State University and needed to sort Russian words for a machine translation project, divides a list and then sorts both parts. It was published in 1961.
The algorithm needs two further terms. The first is the value that everything is compared with.
The pivot can be any element: the first, the last, the middle, or a random one. Simple versions use the last element of the part being sorted. The rearrangement around it is the core of the algorithm.
After one partition the pivot never moves again, and no element ever needs to cross it. Quicksort then calls itself on the part to the left of the pivot and on the part to the right. A part with zero or one element is already sorted: that is the base case. The diagram of quicksort on [7, 2, 9, 4, 1, 8, 5] shows each level of the recursion.

The partition scheme in partition, often credited to Nico Lomuto, takes the last element as the pivot. Index i marks where the next element smaller than the pivot should go; index j scans the part from left to right. Hoare's original scheme scans inwards from both ends and makes fewer swaps, but is harder to follow. Reading the code is optional: the trace of partition and the diagram show everything the algorithm does, and quicksort is explained rather than written by hand.
def partition(items, low, high):
pivot = items[high]
i = low
for j in range(low, high):
if items[j] < pivot:
temp = items[i]
items[i] = items[j]
items[j] = temp
i = i + 1
temp = items[i]
items[i] = items[high]
items[high] = temp
return i
def quicksort(items, low, high):
if low < high:
p = partition(items, low, high)
quicksort(items, low, p - 1)
quicksort(items, p + 1, high)
marks = [7, 2, 9, 4, 1, 8, 5]
quicksort(marks, 0, len(marks) - 1)
print(marks)Java:
import java.util.Arrays;
public class QuickSort {
static int partition(int[] items, int low, int high) {
int pivot = items[high];
int i = low;
for (int j = low; j < high; j++) {
if (items[j] < pivot) {
int temp = items[i];
items[i] = items[j];
items[j] = temp;
i = i + 1;
}
}
int temp = items[i];
items[i] = items[high];
items[high] = temp;
return i;
}
static void quicksort(int[] items, int low, int high) {
if (low < high) {
int p = partition(items, low, high);
quicksort(items, low, p - 1);
quicksort(items, p + 1, high);
}
}
public static void main(String[] args) {
int[] marks = {7, , , , , , };
quicksort(marks, , marks.length - );
System.out.println(Arrays.toString(marks));
}
}Both programs print [1, 2, 4, 5, 7, 8, 9].
The trace of the first call, partition(marks, 0, 6) on marks = [7, 2, 9, 4, 1, 8, 5] with pivot 5, shows how the small values collect at the left:
|
|
| Action |
| List after the step |
|---|---|---|---|---|---|
0 | 7 | No | none | 0 | [7, 2, 9, 4, 1, 8, 5] |
1 | 2 | Yes | swap index 0 and 1 | 1 | [2, 7, 9, 4, 1, 8, 5] |
2 | 9 | No | none | 1 | [2, 7, 9, 4, 1, 8, 5] |
3 | 4 | Yes | swap index 1 and 3 | 2 | [2, 4, 9, 7, 1, 8, 5] |
4 | 1 | Yes | swap index 2 and 4 | 3 | [2, 4, 1, 7, 9, 8, 5] |
5 | 8 | No | none | 3 | [2, 4, 1, 7, 9, 8, 5] |
end | swap pivot (index 6) with index 3 | [2, 4, 1, 5, 9, 8, 7] |
partition returns 3. Then quicksort(marks, 0, 2) sorts [2, 4, 1] and quicksort(marks, 4, 6) sorts [9, 8, 7], each by the same steps. Quicksort is branching recursion: each call that partitions can make two further calls, so the calls form a tree, as the quicksort diagram shows level by level.
Each level of the recursion partitions all the elements still unsorted, which takes about comparisons per level. The number of levels depends on how evenly the pivots split the lists.
Best and average case: the pivots split each list into two parts of roughly equal size, so the list is halved at each level and there are about levels. About comparisons on each of levels gives .
Worst case: each pivot is the smallest or largest element, so one part is empty and the other holds everything else. With the last element as pivot this happens on data that is already sorted or in reverse order. The levels then number , with , , … comparisons:
Space: quicksort partitions in place, but its recursive calls use the call stack: frames deep on average, in the worst case.
Stability: not stable, because partitioning swaps elements over long distances.
Runs of the optional Python code, with counters added, show both behaviours. On 1,000 values in random order, quicksort made 11,508 comparisons with a deepest recursion of 22 calls, close to . On 1,000 values already in order it made 499,500 comparisons, exactly , and its recursion went 1,000 calls deep; that run needed a raised recursion limit, because with CPython's default limit the same call stops with a RecursionError. Library versions avoid the worst case by choosing the pivot more carefully, for example as the median of the first, middle and last elements, or at random.
A binary tree is a recursive structure: each node holds a value and up to two smaller binary trees, its left and right subtrees. Processing the whole tree therefore follows the same pattern at every node. An in-order traversal of a binary search tree visits the left subtree, then the node itself, then the right subtree, and so produces the values in sorted order. The base case is an empty subtree, where there is nothing to visit.
Each node makes two recursive calls, one per subtree, so tree traversal is branching recursion. It shows recursion at its most useful: the recursive version is three lines long, while a loop version must manage its own stack of nodes still to visit. Subtopic B4.1 describes trees, their traversals and binary search trees in full.
Recursion also produces images. A shape drawn by replacing each part of it with smaller copies of the whole, repeated at smaller and smaller scales, has the same structure as a recursive subprogram: the drawing step calls itself on each smaller piece, and a minimum size or a depth counter is the base case.
The Koch curve, described by the Swedish mathematician Helge von Koch in 1904, starts from a straight line segment. Each iteration divides every segment into three equal parts and replaces the middle third with two sides of an equilateral triangle pointing outwards, so each segment becomes four segments, each one-third as long. Three Koch curves on the sides of an equilateral triangle form the Koch snowflake.

The recursive structure leads to an unusual property. Every iteration multiplies the perimeter by , so it grows without limit, while the added triangles become so small that the area approaches a fixed value, of the original triangle's area. A recursive drawing subprogram takes a segment and a depth: if the depth is 0 (the base case) it draws the segment; otherwise it calls itself four times on the four smaller segments with the depth reduced by 1. This is branching recursion, and a depth of produces segments, so the work grows exponentially with the depth.
Source: Wikipedia, “Koch snowflake”.
Writing a recursive subprogram is easier with a fixed sequence of four decisions:
Find the smallest version of the problem and its answer. This gives the base case: an empty list sums to 0; any number to the power 0 is 1; a string of length 0 or 1 is its own reverse.
Describe the problem using a smaller version of itself. The sum of a list is its first element plus the sum of the rest; ; the reverse of a string is the reverse of everything after its first character, followed by that first character.
Make sure every call moves towards the base case. The index moves one place along the list; the exponent falls by 1; the string loses a character.
Return or use the result of the recursive call. The recursive case must return the combined value.
Three subprograms built this way are sum_list, power and reverse_text. To avoid copying the list at each step, sum_list takes the index of the first element still to add as a second parameter.
def sum_list(items, index):
if index == len(items):
return 0
return items[index] + sum_list(items, index + 1)
def power(base, exponent):
if exponent == 0:
return 1
return base * power(base, exponent - 1)
def reverse_text(text):
if len(text) <= 1:
return text
return reverse_text(text[1:]) + text[0]
print(sum_list([4, 9, 2, 7], 0))
print(power(3, 4))
print(reverse_text("loop"))public class RecursionSet {
static int sumList(int[] items, int index) {
if (index == items.length) {
return 0;
}
return items[index] + sumList(items, index + 1);
}
static int power(int base, int exponent) {
if (exponent == 0) {
return 1;
}
return base * power(base, exponent - 1);
}
static String reverseText(String text) {
if (text.length() <= 1) {
return text;
}
return reverseText(text.substring(1)) + text.charAt(0);
}
public static void main(String[] args) {
int[] values = {4, 9, 2, 7};
System.out.println(sumList(values, 0));
System.out.println(power(3, 4));
System.out.println(reverseText("loop"));
}
}The output is 22, 81 and pool. Python's text[1:] and Java's text.substring(1) both give the string without its first character.
A recursive trace records each call with its parameters, what it is waiting for, and what it finally returns. The calls are listed in the order they start (winding), and the return values are filled in from the bottom up (unwinding). For reverse_text("loop"):
Call |
| Base case? | Waits for | Returns |
|---|---|---|---|---|
1 | "loop" | No |
| "poo" + "l" = "pool" |
2 | "oop" | No |
| "po" + "o" = "poo" |
3 | "op" | No |
| "p" + "o" = "po" |
4 | "p" | Yes | (nothing) | "p" |
Call 4 returns first, then call 3, then 2, then 1, the reverse of the order in which they started. The original call returns "pool". A call table with the "Returns" column filled in from the bottom row upwards is the clearest way to show unwinding.
Both searches can be written recursively. Binary search suits it well, because each step continues with only one half of the list: one recursive call, so it is non-branching. The part still being searched is passed in through the parameters low and high, and the base case low > high means that no elements remain.
def binary_search(items, target, low, high):
if low > high:
return -1
mid = (low + high) // 2
if items[mid] == target:
return mid
elif items[mid] < target:
return binary_search(items, target, mid + 1, high)
else:
return binary_search(items, target, low, mid - 1)
codes = [3, 8, 14, 21, 27, 35, 42, 50, 61, 77, 89]
print(binary_search(codes, 42, 0, len(codes) - 1))
print(binary_search(codes, 30, 0, len(codes) - 1))public class BinarySearchRec {
static int binarySearch(int[] items, int target, int low, int high) {
if (low > high) {
return -1;
}
int mid = low + (high - low) / 2;
if (items[mid] == target) {
return mid;
} else if (items[mid] < target) {
return binarySearch(items, target, mid + 1, high);
} else {
return binarySearch(items, target, low, mid - 1);
}
}
public static void main(String[] args) {
int[] codes = {3, 8, 14, 21, 27, 35, 42, 50, 61, 77, 89};
System.out.println(binarySearch(codes, 42, 0, codes.length - 1));
System.out.println(binarySearch(codes, 30, 0, codes.length - 1));
}
}The output is 6 and -1, the same as the loop version. The call table for the search for 42 has three calls, and the answer is passed back unchanged through each one:
Call |
|
|
|
| Next step | Returns |
|---|---|---|---|---|---|---|
1 | 0 | 10 | 5 | 35 | 35 < 42: call with | 6 |
2 | 6 | 10 | 8 | 61 | 61 > 42: call with | 6 |
3 | 6 | 7 | 6 | 42 | found | 6 |
The recursive version has the same time as the loop, but uses space for its stack frames instead of . For linear search, a recursive version (check items[index], otherwise search from index + 1) would need one frame per element, space, and would fail with a RecursionError on any Python list of more than about 1,000 items: a clear case where the loop is the better design.
The function sum_to is meant to add the whole numbers from 1 to n, but its recursive case has no return.
def sum_to(n):
if n == 0:
return 0
n + sum_to(n - 1)static int sumTo(int n) { // does not compile
if (n == 0) {
return 0;
}
int total = n + sumTo(n - 1);
} // error: missing return statementThe function mystery builds a string as the calls unwind. Its structure, a recursive call made before the current call adds its own part, means the results are assembled in reverse order.
def mystery(n):
if n == 0:
return ""
return mystery(n // 2) + str(n % 2)
print(mystery(13))public class Mystery {
static String mystery(int n) {
if (n == 0) {
return "";
}
return mystery(n / 2) + (n % 2);
}
public static void main(String[] args) {
System.out.println(mystery(13));
}
}The greatest common divisor (GCD) of two numbers has a short recursive definition found by Euclid: the GCD of a and b equals the GCD of b and the remainder of a divided by b, and the GCD of a and 0 is a.
def gcd(a, b):
if b == 0:
return a
return gcd(b, a % b)
print(gcd(84, 36))public class Gcd {
static int gcd(int a, int b) {
if (b == 0) {
return a;
}
return gcd(b, a % b);
}
public static void main(String[] args) {
System.out.println(gcd(84, 36));
}
}The program prints 12. The calls are gcd(84, 36), gcd(36, 12) (because ) and gcd(12, 0) (because ), which is the base case and returns 12; the value is passed back unchanged through the other two calls. When the recursive call is the very last action and its result is returned unchanged, as here and in recursive binary search, the recursion can be rewritten as a simple while loop that updates the parameters.
Programming algorithms are examined on Paper 2, in the version of the paper for your language, Python or Java. Questions set the algorithm in a context (a list of scores, readings or product codes) and move through parts of rising demand:
State a Big O value or the output of a search (1 mark each).
Describe or calculate efficiency: count comparisons, read the Big O from loop structure, or explain why one algorithm scales better (2–4 marks).
Trace a search or a sort in a trace table: low, high and mid for binary search; the list after each pass for a sort (3–5 marks). Marks go to correct rows in order, so show every change.
Construct a search or sort, often "without using built-in functions" such as sort, sorted, max, min, index or in (4–6 marks). Marks are awarded for features: initialisation, loop bounds, the comparison, the swap or update, and the return value.
Evaluate or compare algorithms for given data, such as bubble sort against selection sort on nearly sorted data, or linear against binary search (4–6 marks), with a justified conclusion.
The compulsory algorithmic-thinking question has no code: a search or sort may appear as a flowchart, as numbered steps, or as a process with cards or objects, to be traced or described in words. HL papers add recursion: construct a short non-branching recursive subprogram, trace a recursive call with a call table, explain limitations of recursion, or explain how quicksort sorts a given list.
The command term decides the shape of a "compare" or "evaluate" answer. Each point should name both algorithms and link a property to a consequence for the data in the question.
Binary search questions often hinge on a single row, the one where the loop ends.
The HL questions on recursion have their own routine.
State the worst-case time complexity of binary search and the best-case time complexity of selection sort. [2]
Explain why a binary search of a sorted list of 1,000,000 items needs at most 20 comparisons. [2]
Determine the time complexity of this Python fragment, where n is the size of the input, and justify your answer: for i in range(n): containing j = n and while j > 1: j = j // 2. [2]
Trace the basic bubble sort (no swapped flag) on [9, 4, 7, 1], giving the list after each pass and the number of swaps in each pass. [3]
Construct a function count_occurrences(items, target) (Java: countOccurrences(int[] items, int target)) that returns how many times target appears in items, without using built-in functions such as count. [3]
Compare linear search and binary search. [4]
Explain, using an example, why selection sort is not a stable sort. [3]
(HL only) Construct a recursive function count_char(text, ch) (Java: countChar(String text, char ch)) that returns the number of times the character ch appears in the string text. [4]
(HL only) The function show(n) is defined as: if n > 0, print "down" and n, call show(n - 1), then print "up" and n. State the output of show(3) and explain the order in terms of the call stack. [4]
(HL only) Explain why quicksort with the last element as the pivot takes time on a list that is already sorted, and suggest one way to avoid this. [4]
Binary search worst case [1]; selection sort best case [1].
Each comparison discards half of the remaining items [1]; 1,000,000 can be halved about 20 times before one item remains: [1].
[1]: the outer loop runs times and the inner loop halves j each time, running about times; nested loops multiply [1].
After pass 1: [4, 7, 1, 9], 3 swaps; after pass 2: [4, 1, 7, 9], 1 swap; after pass 3: [1, 4, 7, 9], 1 swap. [1] per correct pass.
Award [1] for initialising a counter to 0, [1] for a loop comparing each element with target and adding 1 on a match, [1] for returning the counter after the loop. In Python:
def count_occurrences(items, target):
count = 0
for item in items:
if item == target:
count = count + 1
return countJava:
static int countOccurrences(int[] items, int target) {
int count = 0;
for (int i = 0; i < items.length; i++) {
if (items[i] == target) {
count = count + 1;
}
}
return count;
}count_occurrences([3, 7, 3, 1, 3], 3) returns 3.
Both find the position of a target or report it absent and both use extra memory (loop versions) [1]. Linear search works on unsorted data, binary search needs data sorted by the search key [1]. Linear search's worst case is , binary search's , so binary search is much faster on large lists [1]. Linear search is simpler and better for small or unsorted lists searched once, since sorting first costs
def count_char(text, ch):
if len(text) == 0:
return 0
if text[0] == ch:
return 1 + count_char(text[1:], ch)
return count_char(text[1:], ch)Java:
static int countChar(String text, char ch) {
if (text.length() == 0) {
return 0;
}
if (text.charAt(0) == ch) {
return 1 + countChar(text.substring(1), ch);
}
return countChar(text.substring(1), ch);
}count_char("banana", "a") returns 3. Only one of the two recursive calls runs each time, so this is non-branching recursion.
Output: down 3, down 2, down 1, up 1, up 2, up 3 [2]. Each call prints "down" before its recursive call, while winding [1]; it prints "up" only after the deeper call has returned, so the "up" lines appear as frames are popped, in reverse order (unwinding) [1].
The last element of a sorted list is its largest value, so every other element is smaller and goes to one side [1]; one part is empty and the other has elements, so each level removes only the pivot [1]; there are levels with
Time complexity describes how the number of steps grows with input size ; space complexity describes how extra memory grows. Big O keeps the fastest-growing term and drops constants, usually for the worst case.
The common classes in increasing order of growth are . Loops in sequence add; nested loops multiply; a halving loop is .
Linear search checks every element in turn: works on unsorted data, best case, worst case, space.
Binary search needs data sorted by the search key and halves the search range with low, high and mid: worst case, at most comparisons.
Bubble sort swaps adjacent out-of-order pairs; with a swapped flag it is on sorted data and otherwise; it is in place and stable.
Selection sort swaps the minimum of the unsorted part into place: always comparisons, , but at most
Built-in sorts use algorithms (Timsort and its successors, dual-pivot quicksort); hand-written searches and sorts are needed when built-ins are prohibited.
(HL) A recursive subprogram has a base case and a recursive case that moves towards it; each call gets a stack frame on the call stack, and results are combined as the stack unwinds. Too deep a recursion causes a stack overflow (RecursionError at depth 1,000 in CPython, StackOverflowError in Java).
(HL) Recursion suits self-similar problems (divide and conquer, tree traversal, fractals) but uses more memory and call overhead than loops, and branching recursion can repeat work exponentially.
(HL) Quicksort partitions around a pivot and sorts both parts recursively: on average, when pivots split badly, as with sorted data and a last-element pivot.
I can define time complexity, space complexity and Big O notation.
I can work out the Big O of code by reading its loops: sequence, nesting and halving.
I can put , , , , and in order and give an example of each.
I can explain the best, average and worst case of linear search, binary search, bubble sort and selection sort.
I can write a linear search that returns an index or -1 without in, index or contains.
I can write a binary search and trace low, high and mid, including an unsuccessful search.
I can explain why binary search needs sorted data and when linear search is the better choice.
I can write bubble sort with a swapped flag and trace it pass by pass.
I can write selection sort and trace min_index through each pass, and say whether each sort is in place and stable.
I can evaluate bubble sort and selection sort for sorted, nearly sorted, reversed and random data.
I can explain base case, recursive case, stack frame, call stack and unwinding with factorial (HL only).
I can explain how quicksort partitions a list around a pivot, and its average and worst cases (HL only).
I can explain how tree traversal and fractals such as the Koch snowflake use branching recursion (HL only).
I can explain the advantages and limitations of recursion, including stack overflow (HL only).
I can write and trace a non-branching recursive function with a call table (HL only).
public static int firstDay(int[] steps) {
return steps[0];
}
public static int totalSteps(int[] steps) {
int total = 0;
for (int s : steps) {
total = total + s;
}
return total;
}
public static int countBeaten(int[] steps) {
int count = 0;
for (int i = 0; i < steps.length; i++) {
for (int j = 0; j < steps.length; j++) {
if (steps[i] > steps[j]) {
count = count + 1;
}
}
}
return count;
}
public static int doublings(int[] steps) {
int k = 1;
int count = 0;
while (k < steps.length) {
k = k * 2;
count = count + 1;
}
return count;
}State the time complexity, in Big O notation, of each of the four functions/methods.
4 marks
Award [1] for each correct complexity, up to [4 max].first_day / firstDay: ;total_steps / totalSteps: ;count_beaten / countBeaten: ;doublings: ;
Note: Accept for doublings. Accept "constant", "linear", "quadratic", "logarithmic" if clearly matched to each function.
Identify which function/method would scale least well as the user keeps recording steps for several years, and outline why.
2 marks
Award [1] for identifying the function and [1] for the outline.count_beaten / countBeaten;
its nested loops each run n times, so the number of comparisons grows with / doubling the number of days multiplies the work by about four;
Note: Allow FT from part (a) only if the function identified has the highest complexity given in (a).
A school's timetabling program uses an algorithm with time complexity , where n is the number of students. Timetabling 200 students takes 3 seconds.
Calculate the approximate time needed to timetable 800 students, showing your working.
2 marks
A rival program uses an algorithm that also takes 3 seconds for 200 students. State its approximate time for 800 students.
1 mark
Explain why the times calculated in parts (a) and (b) are only estimates.
2 marks
A weather buoy stores the last n water temperatures in a list (Python) or array (Java) called temps. Two functions/methods have been written to put the readings in reverse order.
def reversed_copy(temps):
result = []
for i in range(len(temps) - 1, -1, -1):
result.append(temps[i])
return result
def reverse_in_place(temps):
left = 0
right = len(temps) - 1
while left < right:
hold = temps[left]
temps[left] = temps[right]
temps[right] = hold
left = left + 1
right = right - 1public static int[] reversedCopy(int[] temps) {
int[] result = new int[temps.length];
for (int i = 0; i < temps.length; i++) {
result[i] = temps[temps.length - 1 - i];
}
return result;
}
public static void reverseInPlace(int[] temps) {
int left = 0;
int right = temps.length - 1;
while (left < right) {
int hold = temps[left];
temps[left] = temps[right];
temps[right] = hold;
left = left + 1;
right = right - 1;
}
}Outline what is meant by the space complexity of an algorithm.
2 marks
State the space complexity of reversed_copy / reversedCopy and of reverse_in_place / reverseInPlace.
2 marks
Both functions/methods have a time complexity of . Explain why reverse_in_place / reverseInPlace is more suitable for the buoy, which has very little memory and stores a million readings.
2 marks
selection sort makes at most swaps, an advantage only if writes are expensive;
both are in place / extra memory, so memory does not separate them;
conclusion: bubble sort (with the flag) is the better choice for this data.
Selection sort swaps the minimum with the first unsorted element, which can jump that element past others with an equal key [1]. Example: sorting (Ana, 4), (Ben, 4), (Cy, 1) by grade swaps Cy with Ana [1], giving (Cy, 1), (Ben, 4), (Ana, 4), so Ana and Ben, equal on the key, have changed order [1].
Award [1] base case (empty string returns 0), [1] checking the first character, [1] recursive call on the rest of the string, [1] adding 1 only on a match and returning the result: