Sorting#
Understanding how sorting works is essential even if you always use the built-in methods in practice.
Python’s Built-in Sort#
list.sort() sorts in place (modifies the list, returns None).
sorted(iterable) returns a new sorted list without modifying the
original. Try it live and watch sort modify the list in place:
>>> nums = [3, 1, 4, 1, 5, 9, 2]
>>> nums.sort()
>>> print(nums)
[1, 1, 2, 3, 4, 5, 9]
The key and reverse arguments control how sorted() orders
items. Try it live: sort by len, in reverse, or with your own key:
>>> words = ["banana", "apple", "cherry", "fig"]
>>> print(sorted(words)) # alphabetical
['apple', 'banana', 'cherry', 'fig']
>>> print(sorted(words, key=len)) # by length
['fig', 'apple', 'banana', 'cherry']
>>> print(sorted(words, reverse=True)) # reverse alphabetical
['fig', 'cherry', 'banana', 'apple']
Functions as Values#
Look again at sorted(words, key=len). You did not call len —
you handed the function itself to sorted and let it do the calling.
In Python a function is a value like any other: you can store it in a
variable, pass it to another function, or return it. A function that
takes another function as an argument (the way sorted takes key)
is called a higher-order function.
When the function you want to pass is small, naming it with def is
more trouble than it is worth. A lambda is a short, unnamed function
written inline. You write the keyword lambda, then its parameters, a
colon, and a single expression. So lambda w: w[-1] reads as “given
w, give back w[-1], its last letter” — the same as a one-line
def that returns w[-1], but with no name and no return
keyword. A lambda is a single expression, and its value is whatever that
expression produces. Lambdas earn their place when there is no built-in
that does what you need:
>>> words = ["banana", "apple", "cherry", "fig"]
>>> print(sorted(words, key=lambda w: w[-1])) # by last letter
['banana', 'apple', 'fig', 'cherry']
>>> print(sorted(words, key=lambda w: (len(w), w))) # length, then alphabetical
['fig', 'apple', 'banana', 'cherry']
The second key, lambda w: (len(w), w), returns a tuple, so the words
sort by length first and then alphabetically among ties.
Passing small functions around like this is your first taste of
functional thinking, and it pairs with a second idea you have already
met. sorted returns a new list; list.sort changes the list in
place. sorted is a pure function: it computes a result from its
arguments and does nothing else, so the same input always gives the same
output. list.sort has a side effect: it modifies something and
returns None.
>>> nums = [3, 1, 2]
>>> result = sorted(nums) # pure: builds a new list
>>> print(result, nums) # original untouched
[1, 2, 3] [3, 1, 2]
>>> nums.sort() # side effect: changes nums
>>> print(nums)
[1, 2, 3]
Neither style is wrong. Pure functions are easier to test and reason
about, because you only have to think about what goes in and what comes
out. Side effects are how a program changes the world — printing,
writing files, updating a list in place. The skill is knowing which one
you are using. We come back to functional thinking — map,
filter, and writing your own higher-order functions — in the
Functional Programming chapter.
Selection Sort#
Selection sort finds the minimum of the unsorted portion and swaps it into place. It always makes exactly N-1 swaps, but O(N²) comparisons:
def selection_sort(data: list) -> None:
"""Sort data in place using selection sort — O(N²)."""
n = len(data)
for i in range(n):
min_pos = i
for j in range(i + 1, n):
if data[j] < data[min_pos]:
min_pos = j
data[i], data[min_pos] = data[min_pos], data[i]
nums = [12, 8, -5, 22, 9, 2]
selection_sort(nums)
print(nums)
Output:
[-5, 2, 8, 9, 12, 22]
Python’s tuple-assignment swap a, b = b, a eliminates the need for
a temporary variable.
Bubble Sort#
Bubble sort repeatedly compares adjacent elements and swaps them if they are out of order. After each pass the largest unsorted element “bubbles” to its correct position. Both swaps and comparisons are O(N²):
def bubble_sort(data: list) -> None:
"""Sort data in place using bubble sort — O(N²)."""
n = len(data)
for i in range(n - 1):
for j in range(n - 1 - i):
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
nums = [12, 8, -5, 22, 9, 2]
bubble_sort(nums)
print(nums)
Output:
[-5, 2, 8, 9, 12, 22]
Insertion Sort#
Insertion sort builds a sorted sublist from the left, inserting each new element into its correct position. It is efficient for nearly-sorted data:
def insertion_sort(data: list) -> None:
"""Sort data in place using insertion sort — O(N²), efficient for nearly-sorted data."""
for i in range(1, len(data)):
key = data[i]
j = i - 1
while j >= 0 and data[j] > key:
data[j + 1] = data[j]
j -= 1
data[j + 1] = key
nums = [12, 8, -5, 22, 9, 2]
insertion_sort(nums)
print(nums)
Output:
[-5, 2, 8, 9, 12, 22]
Algorithm Comparison#
Algorithm |
Comparisons |
Swaps |
|---|---|---|
Bubble sort |
O(N²) |
O(N²) |
Selection sort |
O(N²) |
O(N) |
Insertion sort |
O(N²) avg |
O(N²) avg |
Python |
O(N log N) |
O(N log N) |
For any real application use list.sort() or sorted(). Study
the O(N²) algorithms to understand the problem; use the built-in to
solve it.