Expert · Algorithms, data structures and testing · topic 46 of 50 · 35 min
Recursion and sorting algorithms
Explanation
A function that calls itself
A recursive function solves a problem by solving a smaller version of the same problem. Every recursive function needs two parts. The base case is the smallest problem, answered directly without another call. The recursive case breaks the problem into a smaller one and calls the function again.
def factorial(n):
if n <= 1: # base case
return 1
return n * factorial(n - 1) # recursive case: a smaller problem
print(factorial(5)) # 5 * 4 * 3 * 2 * 1 = 120def sum_digits(n):
if n < 10: # one digit left
return n
return n % 10 + sum_digits(n // 10)
print(sum_digits(2024)) # 2 + 0 + 2 + 4 = 8Forgetting the base case: RecursionError
Each call waits in memory on the call stack (the list of functions that are still running) until the call it made returns. Without a base case the calls never stop. Python protects itself with a limit of about 1000 nested calls and raises RecursionError.
def countdown(n):
return countdown(n - 1) # no base case!
try:
countdown(3)
except RecursionError as err:
print("Stopped:", err)Selection sort: simple but slow
Selection sort finds the smallest item and swaps it to the front, then finds the smallest of the rest and puts it second, and so on. For each of the n positions it scans the remaining items, so it does about n x n / 2 comparisons: O(n^2).
def selection_sort(items):
items = list(items) # work on a copy
for i in range(len(items)):
smallest = i
for j in range(i + 1, len(items)):
if items[j] < items[smallest]:
smallest = j
items[i], items[smallest] = items[smallest], items[i]
return items
print(selection_sort([64, 25, 12, 22, 11]))Merge sort: divide and conquer
- Base case: a list with 0 or 1 items is already sorted.
- Split the list into two halves.
- Sort each half by calling merge sort on it (recursion).
- Merge the two sorted halves: repeatedly take the smaller front item of the two.
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:]) # whatever is left over
result.extend(right[j:])
return result
def merge_sort(items):
if len(items) <= 1:
return list(items)
mid = len(items) // 2
return merge(merge_sort(items[:mid]), merge_sort(items[mid:]))
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))Why O(n log n)? Halving the list again and again gives about log n levels (20 levels for a million items). At each level, merging touches all n items once. So the total is n times log n, far less than n x n for big lists.
| Algorithm | Big-O | Notes |
|---|---|---|
| Bubble sort / selection sort | O(n^2) | Good for learning, too slow for real data |
| Merge sort | O(n log n) | Always fast, needs extra memory for merging |
| Timsort (Python's sorted) | O(n log n) | Very fast on real data, even faster on nearly sorted lists |
orders = [("Riya", 450), ("Kabir", 120), ("Sana", 450), ("Dev", 300)]
by_amount = sorted(orders, key=lambda order: order[1], reverse=True)
print(by_amount) # Riya stays before Sana: Timsort is stableWatch the lesson
Recursion and sorting algorithms · 1/7
In this lesson you will learn recursion, and use it to understand how fast sorting algorithms work.
Prefer a video by a teacher? Find free videos on “python recursion and merge sort explained” on YouTube ↗
In real life: Sorting delivery orders and walking folders
A grocery delivery app sorts thousands of orders by delivery time before assigning riders; it simply calls sorted() with a key, because Timsort is fast and reliable. Recursion shows up elsewhere in the same app: the product catalogue has categories inside categories (Food > Snacks > Namkeen), and a recursive function counts every product in a category and all its sub-categories.
catalogue = {
"name": "Food",
"products": 2,
"children": [
{"name": "Snacks", "products": 5, "children": [
{"name": "Namkeen", "products": 12, "children": []},
]},
{"name": "Drinks", "products": 7, "children": []},
],
}
def count_products(category):
total = category["products"]
for child in category["children"]:
total += count_products(child) # same problem, smaller piece
return total
print(count_products(catalogue)) # 2 + 5 + 12 + 7 = 26Practice (0/3 done)
Write a RECURSIVE function sum_digits(n) that returns the sum of the digits of a non-negative whole number. sum_digits(2024) is 8. Base case: a number below 10 is its own digit sum.
Write merge(left, right). Both lists are already sorted. Return ONE new sorted list with all items from both. Do not call sorted() or .sort(): walk both lists with two positions.
Write merge_sort(items) that returns a NEW sorted list using merge sort: base case for 0 or 1 items, split in half, sort each half recursively, then merge. Do not change the input list. A merge(left, right) helper is already written for you.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.