Skip to content
ExamSpine

Markscheme: Practice Paper 2, HL, set 1

Mark your own answers point by point. The number in brackets after each point is what it is worth. Coding parts are checked by running them against tests.

Answer all questions

Question 1[14 marks]

A school library looks up books by their ID numbers.

1.1

Complete the trace table for this Python code. A school library keeps its book IDs in ids, sorted from smallest to largest. This code is a binary search for the book with ID 150. // divides and keeps only the whole number part, and found stays -1 until the book is found. Then answer the question below the table. [4]

ids = [104, 117, 123, 138, 150, 162, 171]
target = 150
low = 0
high = len(ids) - 1
found = -1
while low <= high and found == -1:
    mid = (low + high) // 2
    if ids[mid] == target:
        found = mid
    elif ids[mid] < target:
        low = mid + 1
    else:
        high = mid - 1

How many positions of ids did the loop look at in all, including the one where it found 150?

The table, made by running the code:

lowhighmidfound
0
6
-1
3
4
5
4
4
4
  • p1 [1]

    The mid column shows 3, then 5, then 4.

  • p2 [1]

    The low column starts at 0 and changes once, to 4, and the high column starts at 6 and changes once, to 4.

  • p3 [1]

    The found column starts at -1 and changes once, to 4, as the last change in the table.

  • p4 [1]

    States 3 positions (3, 5 and 4).

1.2

A school library gives every book a whole-number ID. The IDs are kept in a list (an array in Java) called ids, sorted from smallest to largest, with no ID appearing twice.

Write a function that uses a binary search to return the position (the index) of target in ids, or -1 if no book has that ID. Positions start at 0.

  1. Keep two positions, low and high, for the part of the list that could still hold target. They start at the first and the last position.
  2. Look at the middle position, (low + high) divided by 2, rounded down. If the ID there is target, return that position. If it is smaller than target, search only the part after it; if it is larger, search only the part before it.
  3. When low has passed high, nothing is left to search, so return -1.
  4. ids can be empty, and holds at most 10000 IDs, each between 1 and 1000000.
  5. Write the search yourself. The shortcuts listed with this question are not allowed.

The tests check only the number your function returns, so they cannot tell a binary search from a linear one. In the exam, a linear search would not earn the marks for this part.

def find_book(ids: list[int], target: int) -> int
public static int findBook(int[] ids, int target)

A Paper 2 question that asks for a binary search wants to see the search written out, so the built-in searches are not allowed.

Marked by running your code against tests. Open it in the runner to check your answer. The hidden tests check:

  • an empty list (rule 4)
  • the first ID in the list
  • the last ID in the list
  • an even number of IDs, so the middle is rounded down (rule 2)
  • a missing ID smaller than every ID (rule 3)
  • a missing ID larger than every ID (rule 3)
  • a longer list, wanted ID at the very end
  • one book, not the one wanted

Run it against the tests

[6]

1.3

A school library keeps the IDs of its 1000 books in a list (an array in Java), and a librarian looks up one ID at a time.

(a) Explain why a binary search only works if the IDs are sorted. [2]

(b) State the largest number of IDs a linear search would have to look at to find one ID, and the largest number a binary search would have to look at, for the 1000 books. [2]

[4]

  • p1 [1]

    (a) Says that a binary search looks at the middle ID and throws away (does not search) one half of what is left.

  • p2 [1]

    (a) Says that throwing away a half is only safe when the IDs are sorted, because only then are all the IDs in that half known to be smaller (or larger) than the target; in an unsorted list the target could be in the half thrown away.

  • p3 [1]

    (b) States 1000 for a linear search.

  • p4 [1]

    (b) States 10 for a binary search.

Also accept

  • For p2, an example of an unsorted list where the search goes the wrong way and misses an ID that is there earns the mark.
  • For p4, "about log2(1000), which is 10" earns the mark, and so does "10, because 2 to the power 10 is 1024, which is at least 1000".

No marks for

  • For (a), "because binary search needs sorted data". This repeats the question without saying why.
  • For (a), "because it is faster". Speed does not explain why an unsorted list gives wrong answers.
  • For p4, 500. Halving once is not the whole search; the remaining part is halved again on every look.
  • For p4, 9 or 9.97. The search must keep looking until one ID is left, which takes 10 looks for 1000 IDs.

A full-marks answer

(a) A binary search looks at the middle ID and, if it is not the target, keeps only one half of the list and never looks at the other half again (p1). That is only safe when the IDs are sorted, because then every ID in the discarded half is known to be on the wrong side of the target. If the list were not sorted, the target could be in the half that was thrown away, so the search would say it is missing when it is not (p2).

(b) A linear search may have to look at all 1000 IDs (p3). A binary search looks at no more than 10, because each look halves what is left and 2 to the power 10 is 1024, which is at least 1000 (p4).

Question 2[20 marks]

A school library program keeps track of its books with a Book class.

2.1

A school library program has a Book class. Here it is in Python and in Java.

class Book:
    def __init__(self, title, copies):
        self.title = title
        self.__available = copies

    def get_available(self):
        return self.__available

    def borrow(self):
        if self.__available > 0:
            self.__available -= 1
            return True
        return False
public class Book {
    private String title;
    private int available;

    public Book(String title, int copies) {
        this.title = title;
        this.available = copies;
    }

    public int getAvailable() {
        return available;
    }

    public boolean borrow() {
        if (available > 0) {
            available--;
            return true;
        }
        return false;
    }
}

(a) Identify the method in Book that is an accessor. [1]

(b) The number of available copies is private (in Python, its name starts with two underscores). Explain why it is private, and why code outside the class changes it only by calling borrow. [3]

(c) Another part of the program runs Book("Dune", 2) (in Java, new Book("Dune", 2)) and Book("Emma", 1). Distinguish between the class Book and the objects these lines create. [2]

[6]

  • p1 [1]

    (a) Identifies get_available (in Java, getAvailable) as the accessor.

  • p2 [1]

    (b) Says making it private hides it, so code outside the class cannot read or set it directly (encapsulation, or information hiding).

  • p3 [1]

    (b) Says borrow checks that a copy is available before taking one away, so the count can never be set to an impossible value such as a negative number.

  • p4 [1]

    (b) Says that because every change goes through the class's own methods, the class controls its data, so the way the count is stored can change, or a bug in it be found, in one place without changing the code that uses the class.

  • p5 [1]

    (c) Says the class is the template (blueprint) that defines what attributes and methods every book has, and exists once.

  • p6 [1]

    (c) Says each object is one instance made from the class, with its own values, so the Dune object has its own title and count of 2, and the Emma object its own title and count of 1.

Also accept

  • For p2, "other classes cannot access the variable directly; it can only be accessed through methods" earns the mark.
  • For p3, "borrow validates the change, so available cannot go below 0" earns the mark.
  • For p4, "it reduces the chance of other code corrupting the data by mistake, and keeps the rule for borrowing in one place" earns the mark.
  • For p6, "an object is an instantiation of the class, created by calling the constructor, and holds its own state" earns the mark.

No marks for

  • For (a), naming borrow. It changes the count, so it is a mutator, not an accessor.
  • For (a), naming __init__ or the constructor. It creates the object; it is not an accessor.
  • For (b), saying private makes the data secret from the user, or encrypted. Private controls which code can reach it, not who can see the screen.
  • For (b), saying private makes the program run faster.
  • For (c), saying the class and the object are the same thing, or that the second line changes the first object.

A full-marks answer

(a) get_available (getAvailable in Java) [p1].

(b) Making the count private hides it, so code outside the class cannot read or set it directly [p2]. The only way to change it is to call borrow, and borrow checks that a copy is available first, so the count can never become an impossible value such as -1 [p3]. Because every change goes through the class's own methods, the class keeps control of its data: the rule for borrowing is in one place, and the way the count is stored can change without changing any code that uses Book [p4].

(c) The class Book is the template that says every book has a title and a count of available copies, and the methods get_available and borrow; there is one class [p5]. Each line creates an object, an instance of the class with its own values: one object has the title "Dune" and 2 copies, the other has "Emma" and 1 copy [p6].

2.2

The school library's Book class now also counts how many books have been created. Here it is in Python and in Java, with code that uses it. In Java the last part runs inside a main method.

class Book:
    count = 0

    def __init__(self, title, copies):
        self.title = title
        self.__available = copies
        Book.count += 1

    def get_available(self):
        return self.__available

    def borrow(self):
        if self.__available > 0:
            self.__available -= 1
            return True
        return False

b1 = Book("Dune", 1)
b2 = Book("Emma", 2)
print(b1.borrow())
print(b1.borrow())
print(b2.borrow())
print(b2.get_available())
print(Book.count)
public class Book {
    private static int count = 0;
    private String title;
    private int available;

    public Book(String title, int copies) {
        this.title = title;
        this.available = copies;
        count++;
    }

    public int getAvailable() {
        return available;
    }

    public boolean borrow() {
        if (available > 0) {
            available--;
            return true;
        }
        return false;
    }

    public static int getCount() {
        return count;
    }
}

// in main
Book b1 = new Book("Dune", 1);
Book b2 = new Book("Emma", 2);
System.out.println(b1.borrow());
System.out.println(b1.borrow());
System.out.println(b2.borrow());
System.out.println(b2.getAvailable());
System.out.println(Book.getCount());

(a) State the five lines of output, in order. [3]

(b) Explain why the last line of output is 2 and not 1. [2]

[5]

  • p1 [1]

    (a) The first two lines are True then False (true then false in Java), because Dune has only one copy.

  • p2 [1]

    (a) The third and fourth lines are True (true in Java) then 1, because Emma's borrow takes its count from 2 to 1.

  • p3 [1]

    (a) The fifth line is 2.

  • p4 [1]

    (b) Says count is a static (class) variable, so there is one copy of it shared by the class, not one per object.

  • p5 [1]

    (b) Says the constructor adds 1 to that one shared count each time an object is created, and two objects were created, so it ends at 2.

Also accept

  • For p1 and p2, writing true and false in lower case for the Python version, or True and False in title case for the Java version, earns the marks; the case is not what is being tested.
  • For p4, "count belongs to the class Book, while title and available belong to each object" earns the mark.

No marks for

  • For (a), 0 as the fourth line. Only Emma's own count changed, from 2 to 1; Dune's borrow does not affect Emma.
  • For (a), True as the second line. Dune's one copy was taken by the first borrow.
  • For (b), saying count is 2 because two borrows succeeded. count changes only in the constructor.
  • For (b), saying each object has its own count of 1. A static variable has one value for the whole class.

A full-marks answer

(a) True, False [p1], True, 1 [p2], 2 [p3]. In Java the first three are written true, false, true.

(b) count is a static (class) variable, so there is only one of it, shared by the class Book rather than stored in each object [p4]. The constructor adds 1 to it every time a Book is created. b1 and b2 were created, so it went from 0 to 1 to 2 [p5].

2.3

The school library's Book class has a constructor, an accessor get_available (in Java, getAvailable) and a method borrow that takes one copy if one is available and returns whether it did. Reference books, such as atlases, must stay in the library, so a programmer adds this subclass. Here it is in Python and in Java.

class ReferenceBook(Book):
    def __init__(self, title, copies, room):
        super().__init__(title, copies)
        self.room = room

    def borrow(self):
        return False

shelf = [Book("Dune", 1),
         ReferenceBook("Atlas", 3, "R2")]
for b in shelf:
    print(b.borrow())
public class ReferenceBook extends Book {
    private String room;

    public ReferenceBook(String title,
            int copies, String room) {
        super(title, copies);
        this.room = room;
    }

    @Override
    public boolean borrow() {
        return false;
    }
}

// in main
ArrayList<Book> shelf = new ArrayList<>();
shelf.add(new Book("Dune", 1));
shelf.add(new ReferenceBook("Atlas", 3, "R2"));
for (Book b : shelf) {
    System.out.println(b.borrow());
}

(a) Outline what ReferenceBook inherits from Book, and what it adds. [2]

(b) The loop prints True then False (in Java, true then false). Explain how polymorphism produces this output, although the loop makes the same call each time. [3]

[5]

  • p1 [1]

    (a) Says each ReferenceBook object has Book's state (the title and the count of available copies, set by calling Book's constructor through super) and inherits get_available (and borrow, which it overrides), without rewriting them.

  • p2 [1]

    (a) Says it adds a room attribute of its own (and replaces borrow with its own version).

  • p3 [1]

    (b) Says ReferenceBook overrides borrow, giving its own version with the same name and parameters as the one in Book.

  • p4 [1]

    (b) Says the version of borrow that runs is chosen by the type of the object at run time, not by how the variable or list is declared.

  • p5 [1]

    (b) Applies it to the loop. The Dune object is a Book, so Book's borrow runs and takes its one copy (True); the Atlas object is a ReferenceBook, so its own borrow runs and returns False without changing its count.

Also accept

  • For p1, "it is a Book, so it has everything a Book has, including get_available and the copy count" earns the mark.
  • For p1, "in Java the private fields are not inherited and cannot be used by name in ReferenceBook, but every ReferenceBook object still holds them and uses them through Book's public methods" earns the mark.
  • For p4, "dynamic binding" or "the method is looked up on the actual object" earns the mark.
  • For p5, saying the Atlas's count stays at 3 earns the mark along with saying it returns False.

No marks for

  • For (a), saying ReferenceBook inherits room. room belongs only to the subclass.
  • For (a), saying in Java that the subclass cannot have the available count because it is private. Every ReferenceBook object has it; the subclass code just cannot use the name directly.
  • For (b), calling it overloading. Overloading is two methods with the same name but different parameters; here borrow has the same parameters and the subclass replaces it.
  • For (b), saying the loop checks the type with an if statement. There is no if; the object's own method runs.

A full-marks answer

(a) Each ReferenceBook object has Book's state, the title and the count of available copies, which are set by calling Book's constructor through super. It inherits get_available, and borrow, which it then overrides, so none of that is written again [p1]. (In Java the private fields are not inherited by name: ReferenceBook's own code cannot use them, but each object still holds them and reaches them through Book's public methods.) It adds its own room attribute, and its own version of borrow [p2].

(b) ReferenceBook overrides borrow: it defines a method with the same name and parameters as Book's [p3]. When b.borrow() runs, the version used is chosen by the actual type of the object at run time, not by how the list or the variable b is declared [p4]. On the first pass b is the Dune Book, so Book's borrow runs, takes its one copy and returns True. On the second pass b is the Atlas ReferenceBook, so its own borrow runs and returns False, and its count stays at 3 [p5].

2.4

A school library keeps a Book object for each title. Its mutator methods refuse a change that would make the number of copies on the shelf impossible. Write a function that works out the same thing from simple data: it is given how many copies of a book the library owns, all on the shelf at the start, and a list (an array in Java) of actions, carried out in order. It returns how many copies are on the shelf after the last action.

  1. "borrow" takes one copy off the shelf, but only if at least one copy is there. Otherwise it is refused and changes nothing.
  2. "return" puts one copy back, but only if fewer than total copies are on the shelf. Otherwise it is refused and changes nothing.
  3. Any other text, such as "renew", changes nothing. Capital letters matter: "Borrow" is not "borrow".
  4. total is a whole number from 0 to 50. There are at most 50 actions, and the list can be empty.

Only the number your function returns is checked.

def copies_left(total: int, actions: list[str]) -> int
public static int copiesLeft(int total, String[] actions)

Marked by running your code against tests. Open it in the runner to check your answer. The hidden tests check:

  • no actions, so every copy is still on the shelf (rule 4)
  • a refused borrow, then a return (rules 1 and 2)
  • returns when the shelf is already full are refused (rule 2)
  • more borrows than copies (rule 1)
  • Borrow with a capital letter changes nothing (rule 3)
  • a library with no copies refuses both (rules 1 and 2)
  • the copy goes out, comes back, and goes out again (rules 1 and 2)

Run it against the tests

[4]

Question 3[20 marks]

A school code editor checks brackets and keeps an undo history.

3.1

A school's online code editor sends students' programs to one shared printer in the computer room, and it has an undo button that takes back the student's most recent edits, one at a time.

(a) Identify whether a stack or a queue should hold the print jobs waiting for the printer. [1]

(b) Explain why that ADT suits the print jobs. [2]

(c) Identify and explain which ADT suits the undo button. [2]

[5]

  • p1 [1]

    (a) Identifies a queue.

  • p2 [1]

    (b) Says a queue is first in, first out: jobs are added at the back (enqueue) and taken from the front (dequeue).

  • p3 [1]

    (b) Applies it, so the job sent first is printed first and every student's job is printed in the order it was sent, and no job is left waiting while later ones go ahead of it.

  • p4 [1]

    (c) Identifies a stack for the undo button.

  • p5 [1]

    (c) Explains that a stack is last in, first out: each edit is pushed on top and undo pops the top, so the most recent edit is taken back first, then the one before it.

Also accept

  • For p2, "FIFO" with a correct description of where jobs join and leave earns the mark.
  • For p3, "it is fair, because jobs are printed in the order they arrived" earns the mark.
  • For p5, "LIFO" with a correct description of push and pop, linked to the edits, earns the mark.

No marks for

  • For (a), "a list" or "an array". The question asks which of the two ADTs.
  • For (b), saying a stack, so the newest job prints first. The first student to send a job could wait for ever.
  • For (c), saying a queue, so the first edit is undone first. Undo takes back the most recent change.
  • For (b), naming FIFO with no link to the print jobs earns only p2. For (c), naming LIFO with no link to the edits does not earn p5.

A full-marks answer

(a) A queue [p1].

(b) A queue is first in, first out: a new job joins at the back and the printer takes the job at the front [p2]. So the job sent first is printed first, and every student's job is printed in the order it was sent, with no job overtaken by later ones [p3].

(c) A stack [p4]. A stack is last in, first out: each edit is pushed on top, and undo pops the top one, so the most recent edit is taken back first, then the one before it, which is exactly the order undo must work in [p5].

3.2

Complete the trace table for this Python code. A school code editor checks brackets using stack as a stack: append pushes onto the top (the right-hand end) and pop() removes the top item and gives it back. The stack column holds the whole stack in square brackets, like ["("], after each change. Then answer the question below the table. [4]

text = "[(a)]"
stack = []
for ch in text:
    if ch == "(" or ch == "[":
        stack.append(ch)
    elif ch == ")" or ch == "]":
        top = stack.pop()

The same code is run with text = "(a))". What happens when the loop reaches the last character, and why?

The table, made by running the code:

chstacktop
[]
'['
['[']
'('
['[', '(']
'a'
')'
['[']'('
']'
[]'['
  • p1 [1]

    The ch column shows [, (, a, ), then ], one character per pass.

  • p2 [1]

    The stack column shows [] at the start, then ['['], then ['[', '('], then ['['] after the first pop, then [] after the second, and does not change when ch is a.

  • p3 [1]

    The top column shows ( first and then [, the most recent bracket still open each time.

  • p4 [1]

    Says the program stops with an error (an IndexError) because the second ) tries to pop from an empty stack, since nothing is left open.

3.3

A school's online code editor warns students when the brackets in a line of code do not match up. Write a function that returns whether the brackets in a piece of text are balanced. Use a list (an ArrayList in Java) as a stack.

  1. The brackets are ( with ), [ with ], and { with }. Every other character is ignored.
  2. The brackets are balanced when every closing bracket closes the most recent bracket still open, and that bracket is of the same kind. So ([]) is balanced, and ([)] is not.
  3. A closing bracket with no bracket open makes the text unbalanced.
  4. A bracket still open at the end makes the text unbalanced.
  5. Text with no brackets at all, including empty text, is balanced.
  6. The text has at most 100 characters, all of them printable ASCII. Write the stack yourself: the shortcuts listed with this question are not allowed.

Only the True or False (in Java, true or false) your function returns is checked.

def balanced(text: str) -> bool
public static boolean balanced(String text)

An exam question like this one tests whether you can use a stack. Removing pairs of brackets with replace, or matching them with a regular expression, skips the stack, so those shortcuts are not allowed.

Marked by running your code against tests. Open it in the runner to check your answer. The hidden tests check:

  • empty text (rule 5)
  • text with no brackets at all (rule 5)
  • a closing bracket before any is open, even though the counts match (rule 3)
  • three kinds, nested and side by side (rules 1 and 2)
  • only opening brackets (rule 4)
  • a single closing bracket (rule 3)
  • the } closes the ( on top, not the { below it (rule 2)
  • each kind opened and closed one after another (rule 2)
  • one ( still open at the end (rule 4)
  • a { closed by a ] (rule 2)
  • a ( closed by a } (rule 2)
  • a [ closed by a } (rule 2)

Run it against the tests

[6]

3.4

The school code editor's bracket checker keeps its stack in a Python list (in Java, an ArrayList). The checker only ever uses the stack through three operations: push, pop, and a test for whether the stack is empty. A developer wants to change the stack so that it is stored as a singly linked list instead, with the head of the list as the top of the stack.

(a) Describe how push and pop would work on this singly linked list. [3]

(b) Explain why the bracket-checking code would not need to change, as long as push, pop and the empty test still do the same job. [2]

[5]

  • p1 [1]

    (a) Says each item is held in a node, which stores the item and a reference (pointer) to the next node, and the stack keeps a reference to the head node.

  • p2 [1]

    (a) Says push makes a new node, sets its next reference to the current head, and makes the new node the head.

  • p3 [1]

    (a) Says pop takes the item from the head node and makes the head refer to the next node (and the stack is empty when the head refers to nothing, null or None).

  • p4 [1]

    (b) Says the stack is an abstract data type: the checker depends only on what the operations do (the interface), not on how the data is stored (the implementation).

  • p5 [1]

    (b) Says that because the operations keep the same names and behaviour, the storage can be swapped behind them and the checker gives the same results, so only the stack's own code changes.

Also accept

  • For p1, a labelled description or sketch of nodes, each with a data part and a next part, earns the mark.
  • For p2, "insert at the head" with the new node pointing to the old head earns the mark.
  • For p4, "the implementation is hidden behind the interface (encapsulation)" earns the mark.

No marks for

  • For p2, saying push walks to the end of the list and adds the node there. The question puts the top at the head, and walking to the end is not needed.
  • For p3, saying pop deletes the last node of the list. With the head as the top, the item comes off the head.
  • For (b), saying the code would not change because linked lists are faster. Speed is not the reason the checker is unaffected.
  • For (b), saying a linked list and a Python list are the same thing.

A full-marks answer

(a) Each item is held in a node that stores the item and a reference to the next node, and the stack keeps a reference to the head node [p1]. To push, make a new node holding the item, set its next reference to the current head, then make the new node the head [p2]. To pop, take the item from the head node and move the head on to the next node; the stack is empty when the head refers to nothing (None or null) [p3].

(b) The stack is an abstract data type: the checker uses only what push, pop and the empty test do, which is the interface, and never how the items are stored, which is the implementation [p4]. If the new version keeps the same operations with the same behaviour, the storage can be swapped behind them, the checker gets the same results, and only the stack's own code has to change [p5].

Question 4[19 marks]

A game keeps a leaderboard of player scores.

4.1

An online puzzle game keeps its players' scores in a list (an array in Java) called scores. This recursive function adds up the first n scores.

def total(scores, n):
    if n == 0:
        return 0
    return scores[n - 1] + total(scores, n - 1)
static int total(int[] scores, int n) {
    if (n == 0) {
        return 0;
    }
    return scores[n - 1]
        + total(scores, n - 1);
}

(a) Identify the base case of the function. [1]

(b) The function is called as total with scores [40, 25, 10, 5] and n = 3. State the value it returns. [1]

(c) State how many times total is called in (b), counting the first call. [1]

[3]

  • p1 [1]

    (a) Identifies n == 0 (returning 0) as the base case.

  • p2 [1]

    (b) States that the call returns 75 (10 + 25 + 40 + 0).

  • p3 [1]

    (c) States that total is called 4 times in all, with n = 3, 2, 1 and 0.

Also accept

  • For p1, "when n is 0 it returns 0 without calling itself" or quoting the if line earns the mark.
  • For p3, listing the calls with n = 3, 2, 1 and 0 earns the mark.

No marks for

  • For (a), "return scores[n - 1] + total(scores, n - 1)". That is the recursive case, because it calls the function again.
  • For (b), 80, which adds all four scores. Only the first 3 are added.
  • For (b), 40, which is scores[0] alone.
  • For (c), 3, which leaves out the last call with n = 0.

A full-marks answer

(a) The base case is n == 0, where the function returns 0 without calling itself (p1).

(b) total(scores, 3) = 10 + total(scores, 2) = 10 + 25 + total(scores, 1) = 10 + 25 + 40 + total(scores, 0) = 10 + 25 + 40 + 0, so it returns 75 (p2).

(c) total is called 4 times, with n = 3, 2, 1 and 0 (p3).

4.2

An online puzzle game keeps the scores of its players in a list (an array in Java) called scores. The leaderboard shows them from highest to lowest.

Write a function that uses a selection sort to put the scores in order from highest to lowest, and returns the sorted list.

  1. On each pass, find the position of the highest score in the part of the list not yet sorted, then swap it with the first score of that part.
  2. Equal scores stay in the list as many times as they appeared.
  3. scores can be empty. Scores are whole numbers between -1000 and 100000 (a score can be below 0 after penalties).
  4. You may change scores and return it, or return a new list.
  5. Write the sort yourself. The shortcuts listed with this question are not allowed.

The tests check only the list your function returns, so they cannot tell a selection sort from another sort. In the exam, a different sort would not earn the marks for this part.

def rank_scores(scores: list[int]) -> list[int]
public static int[] rankScores(int[] scores)

Paper 2 asks you to write a sorting algorithm yourself, so the built-in sorts and the shortcuts for finding the largest value are not allowed.

Marked by running your code against tests. Open it in the runner to check your answer. The hidden tests check:

  • an empty list (rule 3)
  • one score
  • lowest first to begin with, so every pass moves something
  • already highest first
  • scores below 0 (rule 3)
  • every score the same (rule 2)
  • the largest and smallest allowed scores, and a repeat (rules 2 and 3)
  • the highest is last and a swap must keep both values (rule 1)

Run it against the tests

[6]

4.3

The puzzle game keeps the scores of its players in a list (an array in Java) called scores, in the order the games were played.

Write a recursive function that returns the highest score among the first n scores, that is scores[0] to scores[n - 1].

  1. Base case: when n is 1, the highest of the first score is scores[0].
  2. Recursive case: find the highest of the first n - 1 scores by calling the function itself, then compare that with scores[n - 1] and return the larger.
  3. n is always between 1 and the length of scores, and scores holds at most 500 scores. Scores after position n - 1 are not looked at.
  4. Scores are whole numbers between -1000 and 100000, so a score can be below 0.
  5. Write the comparison yourself. The shortcuts listed with this question are not allowed.

The tests check only the number your function returns, so they cannot tell a recursive answer from one that uses a loop. In the exam, a loop would not earn the marks for this part.

def best_score(scores: list[int], n: int) -> int
public static int bestScore(int[] scores, int n)

The question tests whether you can write the recursion and the comparison yourself, so the built-ins that find the largest value are not allowed.

Marked by running your code against tests. Open it in the runner to check your answer. The hidden tests check:

  • one score in the list
  • the highest score appears twice
  • the highest score is the last one looked at
  • the highest score is in the middle, neither first nor last
  • the lowest scores allowed (rule 4)
  • the highest score allowed, first in the list
  • a larger score after position n - 1 is not looked at (rule 3)

Run it against the tests

[6]

4.4

The puzzle game finds the highest score among the first n scores with a recursive function, best_score(scores, n), which calls itself with n - 1 until n is 1. A programmer suggests replacing it with a loop that walks through the scores once and remembers the highest seen so far.

Compare the recursive best_score with the loop version, referring to time, memory and clarity. [4]

  • p1 [1]

    States a similarity in time: both look at each of the n scores once, so both take linear time, O(n).

  • p2 [1]

    States that the recursive version uses more memory, because every call that has not yet returned keeps a stack frame, so it uses O(n) extra memory, while the loop needs only a few variables, O(1).

  • p3 [1]

    Explains a consequence of that memory use, such as that a very long list of scores can make the program stop with a stack overflow (StackOverflowError in Java, RecursionError in Python), which the loop never does; or that each call adds a small time overhead.

  • p4 [1]

    Makes a point about clarity in either direction, such as that the recursive version follows the definition "the best of n scores is the larger of the best of n - 1 and the last one", or that the loop is easier for many programmers to trace and debug.

Also accept

  • For p1, "they make the same number of comparisons, n - 1" earns the mark.
  • For p3, a stated limit, such as Python's default limit of about 1000 nested calls, earns the mark.

No marks for

  • For p1, "recursion is slower" with no reason. Both are O(n); the difference is a constant overhead per call, which only p3 credits.
  • For p2, "recursion uses more memory" with no reason. The reason, one stack frame per call still waiting, is what earns the mark.
  • Saying the recursive version is O(n squared) or O(log n). It makes one call per score.

A full-marks answer

Both versions take linear time, O(n), because each looks at each of the first n scores once and makes n - 1 comparisons (p1). They differ in memory: every recursive call waits for the next one to return, so up to n stack frames are held at once, O(n) extra memory, while the loop only needs a position and the best so far, O(1) (p2). Because of this, a very long list of scores could make the recursive version stop with a stack overflow (RecursionError in Python, StackOverflowError in Java), which the loop never does (p3). On clarity, the recursive version reads like the definition, the best of n scores is the larger of the best of n - 1 scores and the last one, but many programmers find the loop easier to trace and debug (p4).

Back to the paper