B2.4 Programming Algorithms

Big O, searching and sorting. Code in Python.

Contents

B2.4.4 and B2.4.5 (recursion) are HL and will get their own page.

B2.4.1: Big O Notation

Big O notation describes how the number of operations (time) or memory (space) an algorithm needs grows as the input size n grows. It gives the upper bound, so it describes the worst case. It measures scalability, not actual seconds.

Big ONameOperations grow...Example
O(1)Constantnot at allAccess by index, stack push/pop
O(log n)Logarithmicby 1 each time n doubles (problem halves each step)Binary search
O(n)Linearin proportion to nLinear search, one loop through a list
O(n log n)Log lineara bit faster than nMerge sort, quicksort (average)
O(n²)Quadraticwith n squaredNested loops: bubble sort, selection sort
O(n³)Cubicwith n cubedThree nested loops
O(2n)Exponentialdoubles every time n increases by 1Brute forcing every password combination, naive recursive Fibonacci
O(n!)Factorialby multiplying by n each timeTrying every route in the travelling salesman problem

Why it matters

nlog nnn log nn²2n
10310331001,024
1,000101,000~10,0001,000,000impossible
1,000,000201,000,000~20,000,0001012impossible

Fastest to slowest: O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2n) < O(n!)

Working out Big O

  1. Find the operation that repeats most (usually inside the loops).
  2. Count how many times it runs in terms of n.
  3. Keep only the biggest term, drop constants. 3n + 5 → O(n). n² + n → O(n²).
Code shapeBig O
No loop, fixed number of stepsO(1)
One loop over n itemsO(n)
Two loops one after the otherO(n + n) = O(n)
A loop inside a loop, both over nO(n × n) = O(n²)
Loop where the range halves each timeO(log n)
def percent(x, y):          # O(1): 2 operations, whatever the input
    return x / y * 100

def find_max(values):       # O(n): the loop body runs n times
    highest = values[0]
    for v in values:
        if v > highest:
            highest = v
    return highest

def has_duplicate(values):  # O(n²): n passes of the inner loop for each of n outer passes
    for i in range(len(values)):
        for j in range(i + 1, len(values)):
            if values[i] == values[j]:
                return True
    return False

Time complexity = how the number of operations grows. Space complexity = how much extra memory is needed. Sorting in place (swapping inside the same list) is O(1) space.

B2.4.3: Bubble Sort and Selection Sort

Bubble sort

Compare each adjacent pair and swap if they are in the wrong order. After each pass the largest unsorted value has "bubbled" to the end, so each pass checks one fewer element.

def bubble_sort(a):
    n = len(a)
    for i in range(n - 1):              # n - 1 passes
        swapped = False
        for j in range(n - 1 - i):      # last i items are already sorted
            if a[j] > a[j + 1]:
                a[j], a[j + 1] = a[j + 1], a[j]
                swapped = True
        if not swapped:                 # no swaps: already sorted, stop early
            break
    return a

print(bubble_sort([10, 8, 349, 3, 39, 1]))   # [1, 3, 8, 10, 39, 349]

Trace (state at the end of each pass, green = in its final place):

PassListSwaps
start1083493391
181033913494
283101393492
338110393492
431810393491
513810393491

Pass 1 in detail: 10>8 swap, 10<349 no swap, 349>3 swap, 349>39 swap, 349>1 swap.

Selection sort

Find the smallest value in the unsorted part and swap it into the first unsorted position. The sorted part grows from the left by one each pass.

def selection_sort(a):
    n = len(a)
    for i in range(n - 1):
        min_index = i
        for j in range(i + 1, n):       # find the smallest in the unsorted part
            if a[j] < a[min_index]:
                min_index = j
        a[i], a[min_index] = a[min_index], a[i]   # one swap per pass
    return a

print(selection_sort([10, 8, 349, 3, 39, 1]))   # [1, 3, 8, 10, 39, 349]
PassSmallest foundList after swap
start1083493391
11 (index 5)1834933910
23 (index 3)1334983910
38 (index 3)1383493910
410 (index 5)1381039349
539 (index 4)1381039349

Selection sort can also find the largest each pass and place it at the end. Same idea, same Big O.

Comparison

Bubble sortSelection sort
Time: worst / averageO(n²)O(n²)
Time: best (already sorted)O(n) with the swapped checkO(n²): always scans everything
SpaceO(1), sorts in placeO(1), sorts in place
SwapsMany, up to one per comparisonAt most n − 1 (one per pass)
AdvantagesSimple. Stops early on nearly sorted data.Simple. Few swaps, good when swapping is expensive.
DisadvantagesBoth are too slow for large data sets. Real programs use O(n log n) sorts like merge sort.

Standard Algorithms

All O(n): one pass through the list.

data = [1, 20, 39, 4, 12, 20, 4, 34, 2, 29, 20, 5, 66]

total = 0
count = 0
max_value = data[0]
max_index = 0
min_value = data[0]

for i in range(len(data)):
    total = total + data[i]              # sum
    if data[i] == 20:
        count = count + 1                # count occurrences
    if data[i] > max_value:
        max_value = data[i]              # maximum (and where it is)
        max_index = i
    if data[i] < min_value:
        min_value = data[i]              # minimum

average = total / len(data)
print(total, round(average, 2), count, max_value, max_index, min_value)
# 256 19.69 3 66 12 1

Start max and min at data[0], not 0. If every value were negative, a max starting at 0 would be wrong. Built in shortcuts: sum(), max(), min(), list.count(), but in exams write the loop.

Practice

  1. Print the first element of a list.
  2. Print every element of a list, then print every element again.
  3. For every student, compare their score with every other student's score.
  4. A guessing game where each guess halves the possible range.
a. O(1)   b. O(n), since 2n drops the constant   c. O(n²)   d. O(log n)

[10, 11, 12, 15, 20, 25, 45, 84, 129, 139, 483, 586, 685]

lowhighmiditems[mid]Action
01264545 < 84, low = 7
7129139139 > 84, high = 8
78784Found, return 7
3 comparisons.
5>1 swap → 1,5,4,2,8. 5>4 swap → 1,4,5,2,8. 5>2 swap → 1,4,2,5,8. 5<8 no swap. After pass 1: 1, 4, 2, 5, 8 (8 is in place).
Pass 1: smallest is 10, swap with 29 → 10, 29, 14, 37, 13. Pass 2: smallest of the rest is 13, swap with 29 → 10, 13, 14, 37, 29.
Linear search checks each element in order, while binary search repeatedly checks the middle and halves the data [1]. Binary search requires the data to be sorted, linear search does not [1]. Linear search is O(n) and binary search is O(log n) [1], so binary search is much faster on large data sets, while linear search is fine for small or unsorted ones [1].
Heads up: textbook errors.
Ninja Notes: