AS & A Level Computer Science · Area 2 · A Level content

Advanced algorithms and recursion

Exam questions on this topic are rarely about knowing a definition and almost always about writing or tracing the algorithm, so the marks sit in the loop bounds, the pointer changes and the base case rather than in the prose. Students lose marks by using a binary search on unsorted data, by writing a sort whose inner loop never actually moves an item, by confusing a stack with a queue, and by writing a recursive routine with no base case or one whose argument never gets closer to it. When a question asks you to compare two algorithms it expects Big O notation and a reason for the growth, not a claim that one is better.

Not started

Syllabus coverage

  • Show understanding of linear and binary searching methods, and write an algorithm to implement each
  • The conditions necessary for the use of a binary search, and how the performance of a binary search varies according to the number of data items
  • Show understanding of insertion sort and bubble sort methods, and write an algorithm to implement each
  • The performance of a sorting routine may depend on the initial order of the data and the number of data items
  • Show understanding of and use abstract data types (ADTs)
  • Write algorithms to find an item in a linked list and in a binary tree
  • Write algorithms to insert an item into each of: stack, queue, linked list, binary tree
  • Write algorithms to delete an item from each of: stack, queue, linked list
  • Show how it is possible for ADTs to be implemented from another ADT: describe the stack, queue, linked list, dictionary and binary tree and demonstrate how each can be implemented from appropriate built-in types or other ADTs
  • A graph is an example of an ADT: describe its key features and justify its use for a given situation (no code is required for a graph structure)
  • Show understanding that different algorithms which perform the same task can be compared using criteria such as the time taken to complete the task and the memory used
  • Use Big O notation to specify time and space complexity
  • Show understanding of recursion: its essential features, and how recursion is expressed in a programming language
  • Show awareness of what a compiler has to do to translate recursive code; write and trace recursive algorithms; when the use of recursion is beneficial; the use of stacks and unwinding

Part of the AS & A Level Computer Science syllabus 9618.

Linear search and binary search

A linear search starts at the first item and compares each item with the search value in turn, stopping when it finds a match or reaches the end of the list. It is the only option when the data is in no particular order, and it is perfectly adequate for a short list. Its weakness is that the work grows in step with the data: for 1 000 items the worst case is 1 000 comparisons and the average is about 500.

A binary search works only on data that is already sorted. It looks at the middle item: if that is the search value the search is over, if the search value is larger then it cannot be in the lower half, and if it is smaller it cannot be in the upper half, so half of the remaining data is discarded at every comparison. Searching 1 000 items this way takes at most 10 comparisons and searching 1 000 000 items takes at most 20, because each comparison halves what is left.

Three conditions must be met before a binary search can be used. The data must be sorted into the order used by the comparisons; it must be held in a structure where the middle item can be reached directly, such as an array or a list with an index, because a linked list can only be walked from one end; and the items must be comparable, so that a decision can be made about which half to keep.

A binary search also assumes that the data does not change while it is running. Inserting or deleting an item moves the positions of the middle items, so the halving no longer works; this is why a binary search is used on a static sorted array, or on a sorted file that is read into an array first.

  • Linear search: set Found <- FALSE and Index <- 1; while Index <= NumberOfItems and Found = FALSE, compare DataArray[Index] with SearchValue and add 1 to Index; then report the position if Found, otherwise report that the value is not in the list.
  • Binary search: set First <- 1 and Last <- NumberOfItems; while First <= Last and Found = FALSE, set Middle <- (First + Last) DIV 2; if DataArray[Middle] equals SearchValue stop, if it is less than SearchValue set First <- Middle + 1, otherwise set Last <- Middle - 1.
  • Conditions for a binary search: the data must be sorted, the middle item must be directly accessible (an array rather than a linked list), and the items must be comparable so that a half can be discarded.
  • Performance: for n items a linear search needs up to n comparisons and a binary search up to log2(n) + 1; doubling the data adds one comparison to a binary search but doubles the work of a linear search.
  • Both algorithms must cope with the value not being present: a linear search ends when the index passes the last item, a binary search ends when Last becomes smaller than First.
  • A linear search is still the right choice for a short list, for unsorted data, or where the data changes so often that keeping it sorted would cost more than the search saves.
Number of items (n)Worst case: linear searchWorst case: binary searchComparison
1010 comparisons4 comparisonsTwo and a half times fewer
100100 comparisons7 comparisonsAbout fourteen times fewer
1 0001 000 comparisons10 comparisonsA hundred times fewer
10 00010 000 comparisons14 comparisonsAbout seven hundred times fewer
1 000 0001 000 000 comparisons20 comparisonsFifty thousand times fewer
A binary search on unsorted data is not a slow search, it is a wrong one. Once the half that supposedly cannot contain the value has been discarded, a correct answer may already be gone, and no number of further comparisons will bring it back.

Bubble sort and insertion sort

A bubble sort makes repeated passes through the array, comparing each item with the one after it and swapping the pair if they are in the wrong order. After the first pass the largest item has bubbled to the last position, after the second the second largest is in place, so each pass can be one comparison shorter. With n items it needs at most n - 1 passes, which is why its worst case is about n(n - 1)/2 comparisons, or O(n^2).

A swap flag makes the bubble sort stop as soon as a pass makes no swaps, because no swaps means the data is in order. That one addition takes the best case from n - 1 passes down to a single pass of n - 1 comparisons, so an already sorted array is handled in O(n).

An insertion sort takes the items one at a time from position 2 onwards, holds the current item, shifts every larger item in the sorted part one place to the right, and drops the current item into the gap. The left-hand part of the array is always a sorted list, and each new item is inserted into its correct place in that list, which is why the sort is called insertion.

The two behave differently on the same data. An insertion sort does the minimum of movement when the data is already nearly in order, because each item travels only a short way, and it is the faster of the two on small arrays and on data that arrives almost sorted. A bubble sort moves an item only one place per pass, so on data in reverse order it does the largest number of swaps of any of the simple sorts, while an insertion sort also does its maximum work but with one move per shift instead of three assignments per swap.

  • Bubble sort: for Pass <- 1 TO NumberOfItems - 1, set Swap <- FALSE; for Index <- 1 TO NumberOfItems - Pass, compare DataArray[Index] with DataArray[Index + 1] and swap them if they are out of order, setting Swap <- TRUE; if Swap is still FALSE after the pass, stop.
  • Swap: Temp <- DataArray[j]; DataArray[j] <- DataArray[j + 1]; DataArray[j + 1] <- Temp.
  • Insertion sort: for Index <- 2 TO NumberOfItems, set Current <- DataArray[Index] and Position <- Index - 1; while Position >= 1 and DataArray[Position] > Current, move DataArray[Position] to Position + 1 and subtract 1 from Position; then store Current at Position + 1.
  • Both algorithms work in place: they rearrange the original array and need only one or two extra variables, so their space complexity is O(1).
  • Best case for both is data that is already sorted: O(n), the bubble sort because of the swap flag and the insertion sort because each item needs one comparison and no shifts.
  • Worst case for both is data in reverse order: O(n^2).
Bubble sortInsertion sort
MechanismRepeated passes; compare and swap each adjacent pair; the largest item reaches the end of each passTake each item in turn, shift the larger items in the sorted part to the right, and insert the item into the gap
Best caseAlready sorted, with a swap flag: one pass, n - 1 comparisons, O(n)Already sorted: one comparison per item, no shifts, O(n)
Worst caseReverse order: n - 1 passes, about n(n - 1)/2 comparisons and the same number of swaps, O(n^2)Reverse order: every item travels the length of the sorted part, about n(n - 1)/2 shifts, O(n^2)
Nearly sorted dataAlmost as slow as the worst case until the last pass proves no swaps are needed; each swap moves an item only one placeClose to O(n), because each item moves only a short distance - the reason it suits data that is almost in order
Extra memoryO(1): rearranged in place, one temporary variable per swapO(1): rearranged in place, one temporary variable for the item being inserted
Both sorts are O(n^2), so neither is the better algorithm in general. The answer depends on the number of items and on how far the data already is from its sorted order, and an answer that gives only one of those two factors is incomplete.

Abstract data types: stacks and queues

An abstract data type (ADT) is a description of a data structure in terms of the data it holds and the operations that can be performed on it, without saying how those operations are implemented. A programmer using a stack needs to know what push and pop do, not whether the data is held in an array, in a linked list or on disk. That separation is the point: the implementation can be changed, or written by somebody else, without any of the code that uses the ADT being rewritten.

A stack is a last in, first out (LIFO) structure: the item most recently pushed is the first one popped. Its operations are push (add to the top), pop (remove and return the top item), peek (look at the top item without removing it), and tests for empty and full. A stack is the right structure whenever the most recent thing is the next thing needed - reversing a list, checking that brackets are nested correctly, or the undo history of an editor.

A queue is a first in, first out (FIFO) structure: items join at the rear and leave from the front. Its operations are enqueue (add at the rear), dequeue (remove and return the item at the front), and tests for empty and full. A queue is the right structure whenever things must be handled in the order they arrived - jobs sent to a printer, keystrokes in a buffer, or processes waiting for the processor.

Both can be implemented from a one-dimensional array plus integer variables that mark the positions, or from a linked list. With an array, a stack needs a Top pointer that starts at 0 and is incremented on push and decremented on pop; a queue needs Front and Rear pointers. A plain array queue wastes the spaces at the front as items are dequeued, so the pointers are wrapped round with MOD to make a circular queue, and a count of the number of items held is the simplest way to tell a full queue from an empty one.

  • Push(Item): if Top = MaxSize then report overflow, otherwise add 1 to Top and store Item at Stack[Top].
  • Pop: if Top = 0 then report underflow, otherwise read Stack[Top], subtract 1 from Top, and return the item that was read.
  • Enqueue(Item): if the queue is full then report overflow, otherwise move Rear on one place (Rear <- Rear MOD MaxSize + 1) and store Item at Queue[Rear].
  • Dequeue: if the queue is empty then report underflow, otherwise read Queue[Front], move Front on one place (Front <- Front MOD MaxSize + 1), and return the item.
  • Circular queue: Front and Rear wrap round to 1 after the last position, so the spaces freed at the front are reused instead of being lost.
  • Underflow and overflow checks are part of the algorithm, not an optional extra: a pop from an empty stack is a run-time error, and the check is a mark in its own right.
  • Stack from a linked list: push is an insert at the head and pop is a removal of the head node - the same LIFO behaviour with no fixed size.
LIFO or FIFO is decided by the problem, not by preference. If the last item in must come out first it is a stack; if the first item in must come out first it is a queue; name which end the algorithm adds to and removes from, or the mark is not earned.

Linked lists, binary trees and dictionaries

A linked list stores each item in a node with a pointer to the next node, and a separate head pointer marks the first node. Nothing is stored in consecutive positions, so an item can be inserted or deleted by changing pointers instead of shifting every item after it. The price is that there is no direct access to the nth item: a search must follow the pointers from the head, which makes it O(n).

In an exam the linked list is usually held in two parallel arrays, one for the data and one for the next pointer, with a null pointer such as -1 marking the end of the list and a free list of unused nodes. Following the pointers is the whole of a linked list algorithm: search, insert and delete all reduce to deciding which pointer is changed.

A binary tree is made of nodes that each hold an item and two pointers, to a left child and a right child, with a root pointer to the first node. In a binary search tree every item in the left subtree is smaller than the node and every item in the right subtree is larger, so one comparison at each node discards a whole subtree. A search and an insert follow the same path: compare with the current node, go left if the new item is smaller, go right if it is larger, and stop when the item is found or the pointer is null.

A dictionary is an ADT that stores pairs of a key and a value, and looks a value up by its key rather than by its position, such as a table of student IDs and names. It can be implemented from two parallel arrays (one of keys and one of values), from a list of records searched linearly, or from a binary tree keyed on the key.

An ADT can be built from another ADT, not only from a built-in type, and the syllabus asks for exactly that. A stack can be made from a linked list by inserting and removing at the head; a queue from a linked list by inserting at the tail and removing from the head; a binary tree from a linked list whose nodes hold two pointers; and a dictionary from a binary tree or from a sorted array searched with a binary search. The operations offered to the user stay the same - only the code underneath them changes.

  • Search a linked list: start at the head pointer, compare the data in each node, follow the next pointer, and stop when the item is found or the pointer is -1.
  • Insert into a linked list: take a node from the free list, store the item, set its pointer to the node it must come before, and set the previous node's pointer to the new node.
  • Delete from a linked list: set the previous node's pointer to the deleted node's pointer, then return the freed node to the free list.
  • Search a binary tree: compare the search value with the current node, go left if it is smaller and right if it is larger, and repeat until it is found or a null pointer is reached.
  • Insert into a binary tree: follow the same comparison path until a null pointer is found, then attach the new node at that point.
  • Stack from a linked list: push = insert at the head, pop = remove the head node. Queue from a linked list: enqueue = insert at the tail, dequeue = remove the head node.
  • Dictionary operations: add a key and value, find the value for a key, delete a key, and report a missing key - the position of a pair is never used by the program that uses the dictionary.
ADTOrder of accessMain operationsHow it can be implemented
StackLIFO - last in, first outPush, pop, peek, isEmpty, isFullA one-dimensional array with a Top pointer, or a linked list with insertion and removal at the head
QueueFIFO - first in, first outEnqueue, dequeue, isEmpty, isFullA circular array with Front, Rear and a count, or a linked list with head and tail pointers
Linked listSequence, reached only by following pointersTraverse, search, insert, deleteParallel arrays of data and next pointers with a free list, or a chain of records each holding a pointer
Binary treeBinary search tree order: left smaller, right largerSearch, insert, traverseNodes holding data plus left and right pointers, held in arrays or in a linked list of records
DictionaryLookup by key, not by positionAdd a pair, find the value for a key, delete a keyTwo parallel arrays of keys and values, a list of records, or a binary tree keyed on the key
Insertion and deletion are cheap in a linked list; finding the place is not. Say which of the two the question is about before you claim a linked list is more efficient than an array, and remember that a search of a linked list is still O(n).

Graphs as an abstract data type

A graph is a set of vertices (nodes) together with a set of edges (arcs) that join pairs of vertices. It is an ADT because what matters is the set of relationships it records and the operations on them, not the way those relationships happen to be stored in memory.

Edges may be undirected, meaning the relationship works both ways, as with two towns joined by a road, or directed, meaning it works one way only, as with a one-way street or the statement that one task must be finished before another. They may be weighted, carrying a number such as a distance, a cost or a time, or unweighted. A vertex's degree is the number of edges that meet it, and a path is a sequence of edges leading from one vertex to another.

A graph is stored either as an adjacency matrix, a two-dimensional array in which a cell records whether an edge exists and with what weight, or as an adjacency list, in which each vertex keeps a list of the vertices it is joined to. A matrix answers the question 'is there an edge between these two vertices' immediately but uses n x n cells even when there are very few edges; an adjacency list uses memory in proportion to the number of edges but can need a search of a list to answer the same question.

A graph is chosen when the problem is about connections between items rather than about the items in a sequence: a road network for a route planner, a set of flights between airports, a network of friendships, or the tasks in a project and which of them must be completed before which. Storing the same information in an array or a table either loses the connections or repeats them so heavily that a question such as 'what is the shortest route from here to there' becomes very hard to ask. The graph is also the structure that graph searches such as Dijkstra's algorithm and A* need, which is why it returns in the artificial intelligence topic.

  • Vertex (node): an object in the graph. Edge (arc): a link between two vertices.
  • Undirected / directed: the link works both ways / the link works one way only.
  • Weighted / unweighted: each edge carries a value such as a distance, cost or time / the edges carry no value.
  • Adjacency matrix: an n x n two-dimensional array; O(1) to test one pair, but O(n^2) memory even when the graph is sparse.
  • Adjacency list: one list of neighbours for each vertex; memory in proportion to the number of edges, but testing one particular edge may need a search of a list.
  • Justifying a graph: name the vertices, name the edges, say whether the edges are directed or weighted, and say which question the graph makes easy to answer.
  • No code is required for a graph structure at this level - you are asked to describe and justify it, not to implement it.
FeatureMeaningWhy it matters
Vertex (node)An item in the graphThe things being modelled: junctions, airports, people or tasks
Edge (arc)A link between two verticesRecords that a relationship exists between the two items
Directed edgeThe link can only be used from one vertex to the otherModels one-way streets, precedence and one-directional relationships
Weighted edgeThe edge carries a number, such as distance, time or costAllows the graph to answer 'shortest' or 'cheapest', not just 'connected'
Adjacency matrixA two-dimensional array of edgesFast to test a pair, but wastes memory on a graph with few edges
Adjacency listA list of neighbours for each vertexCompact for a sparse graph, but slower to test one particular edge
Justifying a graph means naming the vertices, the edges, whether they are directed and weighted, and the question the graph makes easy to answer. Saying that a graph is 'nodes joined by lines' earns nothing on its own.

Comparing algorithms with Big O notation

Two algorithms can perform the same task and still be very different in the resources they need. The syllabus criteria are the time taken to complete the task and the memory used, and Big O notation is how those are specified without depending on one computer, one compiler or one particular set of data.

Big O describes how the work or the memory grows as the number of data items, n, grows. Constants and lower-order terms are dropped, because they do not change the shape of the growth: an algorithm that makes 3n + 5 comparisons is O(n). The figure usually quoted is the worst case, because that is the case that has to be planned for, though a best case may be quoted when the question is about data that is already in a favourable order.

The classes worth knowing are small in number. O(1) constant: the work does not depend on n, as in reading Array[5]. O(log n) logarithmic: one more step of work each time n doubles, as in a binary search. O(n) linear: the work grows in step with n, as in a linear search. O(n log n) means a logarithmically growing number of levels with O(n) work at each level. O(n^2) quadratic: the work grows with the square of n, which is what a loop inside a loop produces, as in a bubble or insertion sort.

Space complexity is counted in the same way. Bubble sort and insertion sort rearrange the array in place and need only a few extra variables, so their space complexity is O(1); a sort that copies the whole array into a second array would need O(n). A binary search implemented with a loop needs O(1) space, but a recursive version needs O(log n) stack frames, and a recursive algorithm of depth n needs O(n) space for the stack even when it holds no data structure at all.

Big O does not tell you which algorithm is faster on a particular small set of data, because it ignores constants: an O(n^2) insertion sort with a small constant can beat an O(n log n) algorithm on twenty items, which is why real libraries fall back to a simple sort for small sections. What Big O does tell you is which algorithm wins as n becomes large, and that is the comparison an exam question is asking for.

  • Criteria for comparing algorithms: the time taken to complete the task and the memory used - say which one your comparison is about.
  • O(1) constant: array access by index, push and pop on a stack stored in an array.
  • O(log n) logarithmic: binary search - each comparison halves the remaining items.
  • O(n) linear: linear search, or one pass through an array.
  • O(n^2) quadratic: bubble sort and insertion sort, because of the loop inside a loop.
  • Space: bubble sort and insertion sort are O(1) extra memory; a recursive routine costs one stack frame per level.
  • Drop the constants and the lower-order terms: 3n + 100 is O(n), 2n^2 + 5n is O(n^2). Write the class, not the formula.
ComplexityNameWork as n growsWhere it appears in this topic
O(1)ConstantDoes not change with nReading an array element by index; push and pop on an array-based stack
O(log n)LogarithmicOne more step each time n doublesBinary search: about 20 comparisons for 1 000 000 items
O(n)LinearGrows in step with nLinear search; a single pass through an array or a linked list
O(n log n)LinearithmicLogarithmically many levels of O(n) workAlgorithms that split the data in half and then do O(n) work at each level
O(n^2)QuadraticGrows with the square of nBubble sort and insertion sort, because of the loop inside a loop
Big O compares growth, not seconds. A question that asks you to compare two algorithms wants the class, the reason for that class from the loop structure, and the effect as n becomes large.

Recursion: the essential features

Recursion is a technique in which a procedure or function calls itself to solve a smaller version of the same problem. The problem is reduced at every call until it reaches a version simple enough to answer directly, and the answers are then combined on the way back out.

Every correct recursive routine has two parts. The base case is the stopping condition: a test that returns a result without making another call, such as 'if n = 1 then return 1'. The general case, also called the recursive case, does one step of the work and then calls the routine again with a smaller or simpler argument, for example 'return n * Factorial(n - 1)'. Without a base case the routine never stops; without a call that moves towards the base case it never reaches it.

In a programming language recursion is expressed by calling the routine by its own name inside its own body, passing changed arguments, and using the returned value. The base case is written as an IF statement that returns before the recursive call can be reached. A function that returns a value, such as Factorial or a search of a tree, and a procedure that produces output or changes a structure, such as printing a folder tree or drawing a fractal, can both be recursive.

Nothing special is needed in the language for this: a call is a call, whether it is to another routine or to the routine that is already running. Each call gets its own copy of the parameters and local variables, which is why the n in Factorial(4) and the n in Factorial(3) do not interfere with one another.

  • Base case: the condition that stops the recursion and returns a value directly, without another call.
  • General case: the step that performs part of the work and calls the routine again on a smaller version of the problem.
  • Progress: every call must move the argument closer to the base case, usually by subtracting 1 or by halving it.
  • Own copy of variables: each call has its own parameters and local variables, held in its own stack frame.
  • Factorial(n): IF n = 1 THEN RETURN 1 ELSE RETURN n * Factorial(n - 1) ENDIF - one base case, one general case, one step towards the base.
  • Recursion and iteration are interchangeable: any recursive algorithm can be rewritten with a loop and an explicit stack, and any loop can be written recursively. Recursion is chosen for clarity, not because it can do something a loop cannot.
Two things are marked separately: a base case, and a call that gets closer to it. 'It calls itself' is not a definition of recursion, and a routine with a base case but no progress towards it is still infinite.

Tracing recursion, the call stack and unwinding

When a recursive call is made, the computer must remember where to come back to. Each call gets a stack frame, also called an activation record, holding its parameters, its local variables and the return address, and the frames pile up as the calls go deeper. This is the stack that the syllabus means by the use of stacks and unwinding.

Unwinding is what happens as the calls return. The innermost call reaches the base case and returns its value, its frame is popped, the value is handed back to the call below, that call completes its own step and returns, and so on back to the original call. A trace of Factorial(4) shows the descent 4 -> 3 -> 2 -> 1 and then the unwind 1 -> 2 -> 6 -> 24.

A trace table for a recursive routine needs a column for each parameter and one row per call, with the returns filled in from the bottom upwards, because a return value cannot be known until the call beneath it has returned. The commonest tracing error is to work down the table writing returns as you go, which produces a wrong answer even when the logic of the routine is right.

The compiler needs no special instruction for recursion, but it must generate the code that makes it work. At each call it saves the return address and the current values on the stack, passes the new arguments, jumps to the start of the routine, and on return restores the saved values and continues after the call. Because each call consumes memory, the depth of the recursion is limited by the size of the stack: a routine that never reaches its base case, or one that is simply too deep, exhausts the stack - a stack overflow.

Recursion is beneficial when the problem is defined in terms of itself, because the code then matches the definition and is short and easy to check: searching or traversing a tree, walking a folder and its subfolders, the Towers of Hanoi puzzle, and algorithms that split the data in half and solve each half. It is a poor choice when the recursion is deep, because a list of 100 000 items handled one item per call will overflow the stack, and a very poor choice when the same subproblem is recalculated many times, as in the naive recursive calculation of Fibonacci numbers, where the number of calls grows exponentially.

  • Benefit: short, readable code for problems that are naturally recursive, such as trees, nested structures and divide and conquer.
  • Benefit: no separate data structure is needed to remember what to do next - the call stack holds it.
  • Cost: every call uses memory for a stack frame and time for the call and the return.
  • Cost: deep recursion can overflow the stack, so the depth is limited by the stack size rather than by the problem.
  • Cost: recursion is harder to trace and debug, and a missing or unreachable base case ends in a crash rather than an obvious wrong answer.
  • Cost: a naive recursive solution can recalculate the same subproblem many times, making it far slower than the loop version.
  • Tracing: list the calls downwards to the base case, then substitute the returns upwards - the return value of each call depends on the one beneath it.
Recursion is not faster than iteration. It is usually slower and uses more memory; it is chosen because the code is clearer for certain problems, and that is the reason to give in an exam rather than claiming recursion is more efficient.

Key terms you must be able to define

Linear search
A search that compares the search value with each item in turn from the start of the list until it is found or the list ends. It works on unsorted data and needs up to n comparisons for n items, so its time complexity is O(n).
Binary search
A search that compares the search value with the middle item of a sorted list and discards the half that cannot contain it, repeating until the value is found or no items remain. It needs at most log2(n) + 1 comparisons, so its time complexity is O(log n), and it requires sorted data held in a structure with direct access to the middle item.
Bubble sort
A sort that makes repeated passes through the data, comparing each item with the next and swapping them if they are out of order, so that the largest remaining item reaches the end of each pass. Its worst case is O(n^2), and with a swap flag that stops the passes early its best case on sorted data is O(n).
Insertion sort
A sort that takes each item in turn, shifts the larger items in the already sorted part of the list one place to the right, and inserts the item into the gap. Its worst case is O(n^2), but it is close to O(n) on data that is already nearly in order, which makes it the better of the two simple sorts for small or almost sorted lists.
Abstract data type (ADT)
A description of a data structure in terms of the data it holds and the operations that can be performed on it, with no reference to how those operations are implemented. An ADT can be implemented from a built-in type such as an array, or from another ADT such as a linked list.
Stack
A last in, first out (LIFO) ADT in which items are added and removed at the same end, called the top. Its operations are push, pop, peek and tests for empty and full, and it can be implemented from an array with a Top pointer or from a linked list with insertion and removal at the head.
Queue
A first in, first out (FIFO) ADT in which items are added at the rear and removed from the front. Its operations are enqueue, dequeue and tests for empty and full; a circular queue wraps the Front and Rear pointers round the array so that the spaces freed at the front are reused.
Linked list
An ADT in which each item is held in a node with a pointer to the next node, and a head pointer marks the first. Insertion and deletion need only pointer changes, but there is no direct access to the nth item, so a search is O(n). It is often implemented from two parallel arrays: one of data, one of next pointers, with a free list.
Binary tree
An ADT of nodes each holding an item and two pointers, to a left child and a right child, reached from a root pointer. In a binary search tree the left subtree holds smaller items and the right subtree larger ones, so a search or an insert follows one comparison path from the root and discards a subtree at every step.
Dictionary
An ADT that stores pairs of a key and a value and looks a value up by its key rather than by its position. It can be implemented from two parallel arrays of keys and values, from a list of records, or from a binary tree keyed on the key.
Graph
An ADT made of vertices (nodes) and edges (arcs) joining pairs of them, used to model relationships. Its edges may be directed or undirected and weighted or unweighted, and it is stored as an adjacency matrix or as an adjacency list.
Big O notation
A notation that specifies how the time taken or the memory used by an algorithm grows as the number of data items n grows, ignoring constants and lower-order terms. Common classes are O(1), O(log n), O(n), O(n log n) and O(n^2).
Recursion
A technique in which a procedure or function calls itself on a smaller version of the same problem. A correct recursive routine has a base case that stops the calls and a general case that performs one step and calls itself with an argument that moves towards the base case.
Call stack and unwinding
Each recursive call is given a stack frame holding its parameters, local variables and return address. Unwinding is the process of returning back up the stack: the innermost call reaches the base case, its frame is popped, and its value is passed out to the call below until the original call receives the answer.

Applied to real systems

9618 sets its questions inside systems you already use - a phone, a school network, a supermarket till, a website, a bank. These show how to apply the theory to a context you have not seen before, then you write your own.

Python's built-in sort: insertion sort inside a hybrid

Python's list.sort() and sorted() use Timsort, which finds short runs of items that are already in order and uses insertion sort within each run before merging the runs together. Insertion sort is chosen for those small runs because it does almost no work when the items are nearly in order, and it has a small constant cost on short data. It shows the point of this topic in one real system: the best algorithm depends on the number of items and on how much order the data already holds, which is why a general-purpose library does not use a single sort for everything. A list that is already sorted is handled in a single pass, while a list in reverse order is the slowest case.

The undo buffer, the back button and the printer queue

A word processor's undo history is a stack: each edit is pushed, and undo pops the most recent one, then the one before that, which is exactly the behaviour LIFO gives. A web browser's back button works the same way, and so does the call stack of a running program. A printer spooler is a queue instead: documents are printed in the order they were sent, even when a later one is much shorter, because that is the fair order for jobs arriving from different users. The same underlying array could hold either structure - the difference is only which end items are added to and removed from.

A route planner: a graph with weighted edges

A satellite navigation system stores the road network as a graph in which every junction is a vertex and every stretch of road between two junctions is an edge. The edge is weighted with the time or distance it takes to drive, so the graph can answer questions about the quickest route rather than only about whether two places are connected. Most roads can be driven in both directions, so most edges are undirected, but a one-way street makes an edge directed. Dijkstra's algorithm or A* then searches this graph, which is why the graph is the natural ADT here: the data is a set of connections, not a list of items in sequence.

Walking a folder tree, and the stack overflow that limits it

A folder on a disk contains files and other folders, and those folders contain files and folders again, so a routine that lists everything is naturally written to call itself on each subfolder. The base case is a folder with no subfolders, where the routine simply lists the files and returns. This is the structure that makes recursion worth using, and it is also where the limit shows: every level of nesting needs another stack frame, which is why Python raises RecursionError and Java throws StackOverflowError when the depth grows too large. The recursive solution is short and correct, but it is bounded by the size of the stack rather than by the size of the disk.

Flashcards

Click a card to flip it. Mark honestly — the tracker uses this.

Q1What conditions are necessary before a binary search can be used?
The data must be sorted into the order used by the comparisons; the middle item must be directly accessible, so an array or an indexed list rather than a linked list; and the items must be comparable so that a half can be discarded. If the data is not sorted, the half that is thrown away may contain the search value, so the search gives a wrong answer rather than a slow one.
Q2How many comparisons do a linear search and a binary search need in the worst case for 1 000 000 items, and when is the linear search the better choice?
A linear search needs up to 1 000 000 comparisons, O(n). A binary search needs about 20, because log2(1 000 000) is about 20 and each comparison halves what remains, so it is O(log n). A linear search is the better choice when the data is unsorted, when the list is very short, or when items are inserted and deleted so often that keeping the data sorted costs more than the search saves.
Q3How does a bubble sort work, and what does a swap flag add?
It makes repeated passes through the array, comparing each item with the next and swapping the pair when they are out of order, so the largest remaining item reaches the end of each pass and each pass can be one comparison shorter. Without a flag it always makes n - 1 passes, about n(n - 1)/2 comparisons, O(n^2). With a swap flag it stops after the first pass that makes no swaps, so sorted data is handled in one pass, O(n).
Q4How does an insertion sort work, and when is it the better of the two simple sorts?
It takes each item from position 2 onwards, holds it, shifts the larger items in the sorted left-hand part one place to the right, and inserts the item into the gap. It is the better choice for small arrays and for data that is already nearly in order, because each item then moves only a short distance and the work is close to O(n); a bubble sort moves an item only one place per pass. Both are O(n^2) in the worst case (reverse-ordered data).
Q5What is an abstract data type, and why does the separation matter?
An ADT is a description of a data structure in terms of the data it holds and the operations that can be performed on it, without saying how those operations are implemented. The user of a stack only needs to know what push and pop do, not whether the data is held in an array or a linked list, so the implementation can be changed or replaced without rewriting any of the code that uses it. An ADT can be implemented from a built-in type such as an array, or from another ADT such as a linked list.
Q6Describe a stack, its operations, and how it is implemented from an array.
A stack is LIFO: the most recently pushed item is the first popped. Operations: push, pop, peek, isEmpty, isFull. Implementation from an array: a one-dimensional array plus a Top pointer starting at 0; push checks Top = MaxSize, adds 1 to Top and stores the item; pop checks Top = 0, reads Stack[Top], subtracts 1 from Top and returns the item. A pop from an empty stack is an underflow error.
Q7Describe a queue, its operations, and why a circular queue is used.
A queue is FIFO: items are enqueued at the rear and dequeued from the front. Operations: enqueue, dequeue, isEmpty, isFull. In a plain array implementation the spaces at the front are never reused once Front moves on, so the queue seems full while there is still room at the start; a circular queue wraps Rear and Front round with MOD (Rear <- Rear MOD MaxSize + 1) so those spaces are reused, and a count of the items held distinguishes full from empty.
Q8Give the algorithms to search a linked list, to insert into it and to delete from it.
Search: start at the head pointer, compare each node's data, follow the next pointer, stop when the item is found or the pointer is -1; O(n). Insert: take a node from the free list, store the item, set its pointer to the node it must precede, and set the previous node's pointer to the new node. Delete: set the previous node's pointer to the deleted node's pointer and return the node to the free list. Insertion and deletion need no shifting, but finding the position still needs a traversal.
Q9How is an item searched for and inserted in a binary tree?
In a binary search tree every item in the left subtree is smaller than the node and every item in the right subtree is larger. Search: compare with the current node, go left if the value is smaller and right if it is larger, until it is found or a null pointer is reached. Insert: follow the same comparison path from the root until a null pointer is found, then attach the new node there. One comparison at each node discards a whole subtree.
Q10What are the key features of a graph ADT, how is a graph stored, and when is it justified?
Vertices (nodes) and edges (arcs); edges may be directed or undirected and weighted or unweighted; a vertex's degree is the number of edges meeting it and a path is a sequence of edges between two vertices. Storage: an adjacency matrix (an n x n array, fast for one pair but O(n^2) memory) or an adjacency list (compact for a sparse graph, slower for one particular pair). Justified when the problem is about relationships between items - road networks, flights, friendships, task dependencies - rather than items in sequence. No code is required for a graph structure.
Q11Put these in order of growth, O(n^2), O(1), O(n), O(log n), and state the time and space complexity of a bubble sort.
O(1) constant, then O(log n) logarithmic (binary search), then O(n) linear (linear search), then O(n^2) quadratic (bubble and insertion sort); O(n log n) sits between O(n) and O(n^2). A bubble sort is O(n^2) in the worst case, about n(n - 1)/2 comparisons and swaps, and O(n) in the best case with a swap flag on sorted data. Its space complexity is O(1) because it rearranges the array in place and needs only a temporary variable.
Q12What are the essential features of recursion, and what happens on the stack when a recursive routine runs?
A base case that returns a value without another call, and a general case that performs one step and calls the routine again with an argument that moves towards the base case. Each call gets a stack frame holding its parameters, local variables and return address; the frames pile up as the calls go deeper and are popped as the returns unwind, with each value passed back to the call below until the original call receives the answer. Too deep, or no base case, gives a stack overflow.
0 of 12 marked as known

Exam-style questions

7 questions, 23 marks. Write your answer, then reveal the mark scheme and tick each point you actually made. Answering honestly here is what makes the rest of the site useful.

StateAO11 mark
State one condition that must be satisfied by the data before a binary search can be used.

Mark scheme — tick each point you actually made

Examiner tip: One mark, one condition. Sorted data is the answer the examiner is looking for; do not add 'it must be in an array' as well and leave the sorted condition out.
CompleteAO22 marks
An incomplete binary search of the array DataArray[1:1000], which is sorted in ascending order, sets First <- 1 and Last <- 1000 and then loops while Found = FALSE AND First <= Last. Inside the loop the variable Middle is not yet assigned, and the branch that runs when DataArray[Middle] < SearchValue is empty. Complete the two missing parts of the algorithm.

Mark scheme — tick each point you actually made

Examiner tip: Two details earn and lose these marks: the integer division (DIV, or integer division in your language) so that Middle is a valid index, and adding 1 so that the item just compared is not compared again.
ExplainAO23 marks
Explain why a binary search is considerably faster than a linear search on a very large set of sorted data, and state one situation in which a linear search is nevertheless the better choice.

Mark scheme — tick each point you actually made

Examiner tip: A numerical comparison of the two counts is what makes the difference concrete. Saying 'binary search is faster because it is more efficient' without the halving, or without a case where the linear search wins, is worth at most one mark.
DescribeAO13 marks
Describe how a stack ADT can be implemented using a one-dimensional array, including the algorithm to push an item and the algorithm to pop an item.

Mark scheme — tick each point you actually made

Examiner tip: Both algorithms need the check before the pointer change, and the pop must decrement Top as well as returning the item. Writing only 'Top <- Top + 1' and 'Top <- Top - 1' without the checks and the stored item loses two of the three marks.
ExplainAO24 marks
Explain how the performance of a bubble sort and of an insertion sort depends on the initial order of the data and on the number of data items.

Mark scheme — tick each point you actually made

Examiner tip: The question names two variables, so answer with both: a point about the number of items and points about sorted, reverse-ordered and nearly ordered data. A single paragraph saying 'both are O(n^2)' scores one mark at most.
WriteAO24 marks
Write a recursive function, Total(N), that returns the sum of all the integers from 1 to N. State the value returned by Total(4).

Mark scheme — tick each point you actually made

Examiner tip: The base case must be written before the recursive call, and the call must use N - 1 rather than N. A function that calls Total(N) inside itself has no progress towards the base case and will overflow the stack however correct the rest looks.
DiscussAO26 marks
A school stores 5 000 student records in an array, sorted by student ID, and adds a batch of new records at the end of each term before re-sorting the array. Discuss the algorithms that should be used to search the array and to keep it in order, using Big O notation to support your answer.

Mark scheme — tick each point you actually made

Examiner tip: A discuss answer needs both algorithms, the Big O class for each, and a judgement that says what the choice depends on - the size of the data and the fact that the batch arrives nearly in order. Marks are lost by naming the algorithms and stopping, or by claiming a bubble sort is O(n) because the data is sorted.

Exam technique for this topic

  • For an algorithm-writing question, earn the structural marks before the logic: initialise the variables before the loop, get the loop bounds right, and make the middle calculation integer - Middle <- (First + Last) DIV 2. An algorithm with no initialisation, or one that calculates Middle to a decimal place, loses marks even when the idea is correct.
  • When writing an ADT operation, name the operation, state the underflow or overflow check, and then the pointer change. A push without a full check, or a pop that forgets to decrement Top, is incomplete for the mark scheme even though it works on the happy path.
  • Write the base case first in any recursive routine. It guarantees the marks for the stopping condition, proves the recursion terminates, and forces you to make the recursive call use a smaller argument.
  • For a tracing question, draw the calls downwards to the base case, then fill in the returns upwards. The value of a return cannot be written until the call beneath it has returned, and marking often awards a mark for the final value alone.
  • Never answer a comparison question with 'it is faster'. Give the Big O class, say what in the algorithm produces that class - a loop, a loop inside a loop, or a halving - and say how it behaves as n becomes large, adding the initial order of the data when a sort is involved.

Why students lose marks here

A student wroteA student wrote: 'A binary search can be used on any list of data, because even if the list is not sorted it will still find the item if it is there.'
Why it loses marksA binary search decides which half to throw away by comparing the middle item with the search value. If the data is not sorted, that comparison says nothing about where the value can be, so the half being discarded may well contain it. The search then reports that the item is not in the list while it is sitting in the discarded half, so the algorithm is not slow, it is incorrect.
The answer that scoresA binary search requires the data to be sorted into the order used by the comparisons, because it discards half of the remaining items at every comparison on the assumption that everything before the middle is smaller and everything after it is larger. It also requires the middle item to be reachable directly, which is true of an array but not of a linked list that must be walked from one end, and the items must be comparable. When those conditions hold, a search of 1 000 000 items takes at most 20 comparisons; when they do not, the only safe option is a linear search, or sorting the data first, which costs far more than the search saves on a single pass.
A student wroteA student wrote: 'A bubble sort always makes n - 1 passes, so its best case is O(n^2) and it is always slower than an insertion sort.'
Why it loses marksWith a swap flag the bubble sort stops after the first pass that makes no swaps, so an already sorted array costs one pass of n - 1 comparisons, which is O(n), not O(n^2). Both sorts are O(n^2) in the worst case, and which is faster in practice depends on the initial order of the data and on the number of items, so neither is always faster.
The answer that scoresA bubble sort without a swap flag always makes n - 1 passes and is O(n^2) whatever the data, which is one reason the flag is added. With the flag the sort ends as soon as a pass makes no swaps, so sorted data costs one pass of n - 1 comparisons, O(n), and data in reverse order still costs about n(n - 1)/2 comparisons and swaps, O(n^2). An insertion sort is also O(n) on sorted data and O(n^2) in reverse order, but it moves each item only as far as it needs to go, so on data that is nearly in order it does much less work than a bubble sort, which moves an item only one place per pass. On small arrays with a small constant cost per comparison, an insertion sort is usually the faster of the two, but the honest answer is that it depends on the initial order and on n.
A student wroteA student wrote: 'To delete an item from a queue you remove the item at the rear, and to delete an item from a stack you remove the item at the bottom.'
Why it loses marksThis reverses both definitions. A queue is first in, first out, so items are removed from the front, which is the end they have been waiting at longest; a stack is last in, first out, so items are removed from the top, which is the end they were most recently added to. The error is not just a definition slip - it produces algorithms that remove the wrong item, and it usually comes with a missing underflow check as well.
The answer that scoresA queue is a first in, first out structure: enqueue adds an item at the rear and dequeue removes and returns the item at the front, moving the Front pointer on one place (with MOD in a circular queue). A stack is a last in, first out structure: push adds an item at the top and pop removes and returns the item at the top, decrementing the Top pointer. Both operations begin with a check: dequeue and pop must test for an empty structure and report underflow, and enqueue and push must test for a full one and report overflow.
A student wroteA student wrote: 'My recursive function keeps calling itself until it finds the answer, so it does not need a base case.'
Why it loses marksWithout a base case there is nothing to stop the calls, every call adds another stack frame, and the program ends in a stack overflow instead of producing an answer. A base case is one of the two essential features of recursion, and a routine whose argument does not move towards the base case behaves in exactly the same way.
The answer that scoresEvery recursive routine needs a base case - a condition that returns a result without making another call - and a general case that calls the routine with an argument that has moved closer to that base case. For example, Factorial(N) returns 1 when N = 1, and otherwise returns N * Factorial(N - 1), so the argument falls by one at every call until the base case is reached. Each call is given a stack frame holding its parameter and return address, and the frames are popped as the returns unwind back up to the original call; with no base case the frames are never popped and the stack is exhausted.
A student wroteA student wrote: 'An algorithm that is O(n) is always faster than an algorithm that is O(n^2), whatever the data.'
Why it loses marksBig O describes how the work grows as n grows and deliberately ignores constants and lower-order terms. For small values of n an O(n^2) algorithm with a small constant can finish before an O(n) algorithm with a large one, so Big O guarantees the ordering only as n becomes large. It also says nothing about a particular implementation, compiler or machine.
The answer that scoresBig O specifies the growth of the time taken or the memory used as the number of data items n increases, with constants and lower-order terms dropped. An O(n) algorithm's work grows in step with n, while an O(n^2) algorithm's work grows with the square of n, so as n becomes large the O(n) algorithm wins clearly - a linear search of 1 000 000 items may need 1 000 000 comparisons where a binary search needs about 20. For a small n, however, the constant cost of each step matters more than the growth: a bubble sort on twenty items can be quicker than a more elaborate algorithm with a large constant per item, which is why general-purpose libraries switch to a simple sort for small sections.