CSC148H1/Unit 10
Unit 10 · Introduction to Computer Science

Sorting & Inheritance

Two closing threads. Divide-and-conquer sorting is recursion paying off in speed — $O(n\log n)$ where the obvious approaches give $O(n^2)$. Inheritance finishes the object-oriented thread by letting one piece of code work on several types.

By the end you can
  • Implement mergesort and quicksort recursively.
  • Explain where the $n\log n$ comes from.
  • Write a subclass that extends or overrides a parent.
  • Say what polymorphism buys and what it demands.
Why it matters & where it comes from
Why it exists

Sorting is the clearest case where recursion is not just tidier but asymptotically faster. Inheritance is how you avoid writing the same method five times for five related types.

In the real world

Python's sort is Timsort, a hybrid of mergesort and insertion sort tuned for partly ordered data. Every GUI framework is a class hierarchy where a button and a text box are both widgets.

Story & open questions

Von Neumann wrote mergesort in 1945, among the first algorithms designed for a stored-program computer. Hoare invented quicksort in 1959 while trying to sort words for a translation project.

Mergesort

Rule Divide, sort, merge
Split the list in half.
Sort each half — recursively.
Merge two sorted halves into one sorted list.
Example Implementation
python
def mergesort(xs: list) -> list:
    """Return a new sorted list containing the items of xs.

    >>> mergesort([3, 1, 2])
    [1, 2, 3]
    """
    if len(xs) < 2:
        return xs[:]
    mid = len(xs) // 2
    return _merge(mergesort(xs[:mid]), mergesort(xs[mid:]))


def _merge(left: list, right: list) -> list:
    """Return one sorted list from two sorted lists.

    >>> _merge([1, 3], [2, 4])
    [1, 2, 3, 4]
    """
    out = []
    i = 0
    j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            out.append(left[i])
            i = i + 1
        else:
            out.append(right[j])
            j = j + 1
    return out + left[i:] + right[j:]
The base case is a list of length $0$ or $1$, which is already sorted.
The leap of faith again: assume both recursive calls return sorted lists, and merging is then straightforward.
The final line appends whatever remains. Exactly one of the two slices is non-empty, so this is not a special case.
Rule Where $n\log n$ comes from
Halving repeatedly gives about $\log_2 n$ levels of recursion.
At each level, merging touches every element once — $O(n)$ work per level.
Total: $O(n\log n)$. Compare selection sort's $n$ passes over $n$ elements, which is $O(n^2)$.

At a million items that is roughly $2 \times 10^7$ operations against $10^{12}$ — the difference between instant and hopeless.

Quicksort

Example Partition, then recurse
python
def quicksort(xs: list) -> list:
    """Return a new sorted list containing the items of xs.

    >>> quicksort([3, 1, 2])
    [1, 2, 3]
    """
    if len(xs) < 2:
        return xs[:]
    pivot = xs[0]
    smaller = [x for x in xs[1:] if x < pivot]
    larger = [x for x in xs[1:] if x >= pivot]
    return quicksort(smaller) + [pivot] + quicksort(larger)
Mergesort splits by position and does its work when combining. Quicksort splits by value and does its work before recursing, so the combine step is plain concatenation.
Average case $O(n\log n)$, when the pivot lands near the middle and the halves are balanced.
Quicksort's worst case is sorted input

Taking the first element as pivot on an already-sorted list puts everything in one partition. The recursion depth becomes $n$ and the cost $O(n^2)$ — the same failure mode as the unbalanced BST in Unit 8, and for the same reason.

Choosing a random pivot, or the median of three, makes the bad case vanishingly unlikely.
Mergesort has no such weakness: it is $O(n\log n)$ on every input. Its cost is the extra memory for the merging.

Inheritance

Definition

A subclass inherits the attributes and methods of its parent, and may add new ones or replace existing ones. Written class Child(Parent):.

Example A small hierarchy
python
class Shape:
    """A geometric shape."""

    def area(self) -> float:
        """Return the area of this shape.

        Subclasses must override this.
        """
        raise NotImplementedError

    def describe(self) -> str:
        """Return a description of this shape and its area.

        >>> Square(3).describe()
        'Square with area 9'
        """
        return type(self).__name__ + ' with area ' + str(self.area())


class Square(Shape):
    """A square with a given side length."""

    def __init__(self, side: float) -> None:
        """Initialize a square with the given side length."""
        self.side = side

    def area(self) -> float:
        """Return the area of this square."""
        return self.side ** 2


class Circle(Shape):
    """A circle with a given radius."""

    def __init__(self, radius: float) -> None:
        """Initialize a circle with the given radius."""
        self.radius = radius

    def area(self) -> float:
        """Return the area of this circle."""
        return 3.14159 * self.radius ** 2
describe is written once in Shape and works for every subclass, because it calls self.area() and Python looks that up on the actual object.
raise NotImplementedError in the parent says "every subclass must supply this". Instantiating Shape directly and calling area then fails loudly rather than returning nonsense.
Rule Polymorphism

Code written against the parent works on any subclass:

python
shapes = [Square(2), Circle(1), Square(5)]
total = sum(s.area() for s in shapes)

The loop knows nothing about squares or circles. Adding a Triangle later requires no change to it at all — that is the entire value of the arrangement.

A subclass must honour the parent's contract

If Shape.area promises a non-negative number, no subclass may return a negative one. Code written against the parent relies on that promise. This is the Liskov substitution principle from Unit 1: a subclass must be usable anywhere the parent is expected.

Example Calling the parent
python
class ColouredSquare(Square):
    """A square with a colour."""

    def __init__(self, side: float, colour: str) -> None:
        """Initialize a coloured square."""
        Square.__init__(self, side)      # reuse the parent's setup
        self.colour = colour

Reusing the parent's __init__ rather than repeating it means a change there propagates automatically. Repeating the assignment would work today and drift later.

Common pitfalls

• Forgetting the base case in a recursive sort and recursing forever.
• Quoting quicksort as $O(n\log n)$ without the average-case qualifier.
• Overriding a method with one that breaks the parent's promise.
• Forgetting to call the parent's __init__ from a subclass.
• Building a deep hierarchy where composition would be simpler. Inherit only when the subclass genuinely is a kind of the parent.

Practice

Write out each answer before opening it. Original exercises written for this guide in the style of the course's weekly work.

Practice 1 · Base case

What is mergesort's base case?

Answer
A list of length $0$ or $1$, which is already sorted.
Practice 2 · Where the log comes from

Why is mergesort $O(n\log n)$?

Answer
About $\log_2 n$ levels of halving, with $O(n)$ merging work at each level.
Practice 3 · Quicksort worst case

What input makes quicksort $O(n^2)$ with a first-element pivot?

Answer
Already-sorted input. Every element lands in one partition and the recursion becomes $n$ deep.
Practice 4 · Merge two sorted lists

Merge $[1, 4, 6]$ and $[2, 3, 7]$ by hand.

Answer
$[1, 2, 3, 4, 6, 7]$. Compare the fronts, take the smaller, repeat, then append whatever remains.
Practice 5 · Which sort

Data arrives nearly sorted. Which algorithm, and why?

Answer
Mergesort, or Python's built-in. Quicksort with a naive pivot degrades exactly here, while mergesort is $O(n\log n)$ regardless. Timsort actively exploits existing order and does even better.
Practice 6 · Write a subclass

Add a Rectangle to the Shape hierarchy.

Solution
python
class Rectangle(Shape):
    """A rectangle with a width and a height."""

    def __init__(self, width: float, height: float) -> None:
        """Initialize a rectangle of the given width and height."""
        self.width = width
        self.height = height

    def area(self) -> float:
        """Return the area of this rectangle."""
        return self.width * self.height
describe works immediately, with no change to Shape.
Practice 7 · Polymorphism

Why does describe work for every subclass?

Answer
It calls self.area(), and Python resolves that on the actual object at run time — so each subclass supplies its own.
Practice 8 · Substitution

A Penguin subclasses Bird, which has fly(). What is wrong?

Answer
A penguin cannot fly, so it breaks the parent's contract and any code calling fly() on a Bird. The hierarchy is wrong: flying should belong to a narrower class than Bird. This is the standard illustration of the Liskov principle.

Expanded topic checklist

Course coverage. This unit is part of the complete CSC148H1 topic map. It should cover the following items before you move on:

  • Mergesort. Divide, recurse, merge, and why the cost is $n\log n$.
  • Quicksort. Partitioning by value, average case, and the sorted-input worst case.
  • Inheritance. Subclasses, overriding, and reusing the parent's initialiser.
  • Polymorphism. Writing code against the parent, and the substitution obligation.
Textbook/source alignment

The calendar names "object-oriented programming" and "analyzing the efficiency of programs", both of which close here. Check Quercus for the assigned exercises and for whether sorting is examined by code or by analysis.

Core idea map

Recursion applied to sorting buys a better growth rate. Inheritance applied to classes buys reuse. Both are the same instinct as the rest of the course: find the structure in the problem and let the code follow it.

Study routine. Trace mergesort once on eight elements, drawing the recursion tree. Seeing the $\log n$ levels and the $n$ work per level makes the complexity argument permanent.

Deep practice extensions

Use these after the main practice set. They push on definitions and edge cases rather than routine calculation.

Extension 1 · The two sorts

How do mergesort and quicksort differ structurally?

Guide
Mergesort splits by position and works when combining; quicksort splits by value and works before recursing.
Extension 2 · Guarantees

Which sort has no bad input?

Guide
Mergesort. It is $O(n\log n)$ always, at the cost of extra memory. Quicksort's worst case is quadratic.
Extension 3 · Override

What does a subclass owe the parent's contract?

Guide
It must keep every promise the parent made, so that code written against the parent still works. That is the Liskov substitution principle.
Extension 4 · When not to inherit

When is composition better?

Guide
When the relationship is "has a" rather than "is a". A car is not a kind of engine; it contains one.