Sorting Algorithms in TheAlgorithms/Python
Seven sorting strategies side by side: from the classroom bubble loop to radix buckets.
What you will learn
- How bubble sort's swapped flag terminates early on nearly-sorted input
- How insertion sort shifts rather than swaps, reducing memory writes
- How selection sort minimizes swaps at the cost of comparisons
- How merge sort's nested merge function combines two sorted halves in linear time
- How quicksort's random pivot avoids the sorted-input worst case
- How heapify enforces the max-heap property by sinking a node to its correct position
- How radix sort bypasses comparison entirely by distributing digits into buckets
Prerequisites
- Comfort reading Python for-loops and list indexing
- Familiarity with the concept of O(n) and O(n log n) complexity
- No prior knowledge of any specific sort required
Bubble sort: the iterative loop with an early-exit flag
sorts/bubble_sort.py:51Two nested loops push the maximum unsorted element rightward each pass; a swap flag enables early termination.
Bubble sort is the standard first-sort example because its mechanism is visible: adjacent elements that are out of order get swapped, and the largest unsorted element rises to its correct position after each outer pass. Without an optimization the algorithm always does O(n squared) comparisons. The swapped flag on line 53 fixes that: if the inner loop completes without a single swap, the collection was already sorted and the outer loop breaks immediately. That turns the best case into O(n) for already-sorted input. For a deeper look at bubble sort's two implementations in this file, see the deeper tour. The outer loop iterates in reverse so the effective boundary shrinks by one after each pass.
The swapped flag is the only practical optimization bubble sort offers; it converts best-case performance from O(n squared) to O(n).
length = len(collection)
for i in reversed(range(length)):
swapped = False
for j in range(i):
if collection[j] > collection[j + 1]:
swapped = True
collection[j], collection[j + 1] = collection[j + 1], collection[j]
if not swapped:
break # Stop iteration if the collection is sorted.
return collectionInsertion sort: shift-and-insert rather than swap
sorts/insertion_sort.py:53Each new element walks left past larger values via overwrites until it reaches its sorted position.
Insertion sort imagines the list as two regions: a sorted prefix on the left and an unsorted suffix on the right. For each element in the unsorted region, the algorithm saves the value in insert_value and shifts all larger sorted-region elements one position right, creating a gap. When the correct gap is found the value drops in. The key distinction from bubble sort is that this is a shift operation, not a swap: only one element moves per iteration of the while loop. That halves the number of write operations on lists that are already mostly sorted. See the deeper tour for full doctest analysis and comparisons with Python's built-in timsort, which uses insertion sort for small subranges.
Insertion sort writes fewer values than bubble sort on nearly-sorted input because shifting overwrites in one direction rather than swapping pairs.
for insert_index in range(1, len(collection)):
insert_value = collection[insert_index]
while insert_index > 0 and insert_value < collection[insert_index - 1]:
collection[insert_index] = collection[insert_index - 1]
insert_index -= 1
collection[insert_index] = insert_value
return collectionSelection sort: find the minimum, then swap once per pass
sorts/selection_sort.py:19Each pass scans for the minimum index in the unsorted region, then places that element at the front with one swap.
Selection sort's trade-off runs in the opposite direction from insertion sort: it does the maximum number of comparisons but the minimum number of writes. The inner loop on lines 22-24 scans the entire unsorted region to find the index of the smallest remaining element. After the scan, exactly one swap moves that element to position i. The guard on line 25 skips the swap when the element is already in place, saving one write. In O(n squared) comparison count, selection sort matches bubble sort, but it never does more than O(n) swaps total across the full sort. That matters when writes are expensive, such as in flash memory. The deeper tour covers the full file.
Selection sort does at most O(n) swaps across the entire sort, making it useful when write operations are more costly than reads.
length = len(collection)
for i in range(length - 1):
min_index = i
for k in range(i + 1, length):
if collection[k] < collection[min_index]:
min_index = k
if min_index != i:
collection[i], collection[min_index] = collection[min_index], collection[i]
return collectionMerge sort: a nested merge function combines sorted halves
sorts/merge_sort.py:32A nested merge function walks two sorted halves and appends the smaller leading element each step until both are empty.
Merge sort separates the concern of splitting from the concern of combining. The outer merge_sort handles splitting: if the collection has more than one element it computes a midpoint and recurses on both halves. The inner merge function handles combining: two already-sorted lists are walked in lockstep, and the smaller of the two leading elements is popped and appended to result each iteration. When one list empties, extend drains the other in order. The pop(0) call is O(n) on Python lists, which makes this implementation educational rather than production-ready. A production merge sort uses index pointers instead. For a full breakdown including complexity analysis, see the deeper tour.
The nested merge function is where the O(n log n) guarantee is enforced: each level of recursion does O(n) merge work across O(log n) levels.
def merge(left: list, right: list) -> list:
"""
Merge two sorted lists into a single sorted list.
:param left: Left collection
:param right: Right collection
:return: Merged result
"""
result = []
while left and right:
result.append(left.pop(0) if left[0] <= right[0] else right.pop(0))
result.extend(left)
result.extend(right)
return result
if len(collection) <= 1:
return collection
mid_index = len(collection) // 2
return merge(merge_sort(collection[:mid_index]), merge_sort(collection[mid_index:]))Quicksort: random pivot, two list comprehensions, one recursive return
sorts/quick_sort.py:30A random pivot prevents worst-case behavior; two comprehensions partition the remainder in one pass each.
Quicksort's worst case occurs when the pivot is always the largest or smallest element, which degrades the recursion tree to O(n squared) depth. Choosing the pivot with randrange makes that worst case statistically unlikely across all input distributions. After the pivot is removed with pop, two list comprehensions divide the remaining elements: lesser collects everything at most the pivot and greater collects everything strictly above it. The final return uses list-spread syntax to concatenate the two recursively sorted halves around the pivot in one readable expression. For a full walkthrough of every line in the file, see the deeper tour.
Randomizing the pivot turns quicksort's O(n squared) worst case from a structural certainty on sorted input into a probabilistic rarity.
# Base case: if the collection has 0 or 1 elements, it is already sorted
if len(collection) < 2:
return collection
# Randomly select a pivot index and remove the pivot element from the collection
pivot_index = randrange(len(collection))
pivot = collection.pop(pivot_index)
# Partition the remaining elements into two groups: lesser or equal, and greater
lesser = [item for item in collection if item <= pivot]
greater = [item for item in collection if item > pivot]
# Recursively sort the lesser and greater groups, and combine with the pivot
return [*quick_sort(lesser), pivot, *quick_sort(greater)]Heap sort: heapify sinks a node to its correct position
sorts/heap_sort.py:6Heapify enforces the max-heap property by finding the largest among a parent and its two children and swapping if needed.
Heap sort runs in two phases: first it rearranges the input list into a max-heap, then it repeatedly extracts the maximum element from the heap root and places it at the end of the sorted region. The heapify function is the atomic operation both phases share. Given an index and a heap boundary, it computes the left child at index 2i + 1 and the right child at 2i + 2, finds which of the three is largest, and if a child is larger it swaps and recurses downward. The doctest proves the behavior: after one call on [1, 4, 3, 5, 2] at index 0, the root becomes 4 as the largest child; a second call promotes 5. For the full heap_sort driver that calls heapify in two phases, see the deeper tour.
heapify is the atomic building block of heap sort: it sinks one node to its correct depth in O(log n) time by comparing against children.
def heapify(unsorted: list[int], index: int, heap_size: int) -> None:
"""
:param unsorted: unsorted list containing integers numbers
:param index: index
:param heap_size: size of the heap
:return: None
>>> unsorted = [1, 4, 3, 5, 2]
>>> heapify(unsorted, 0, len(unsorted))
>>> unsorted
[4, 5, 3, 1, 2]
>>> heapify(unsorted, 0, len(unsorted))
>>> unsorted
[5, 4, 3, 1, 2]
"""
largest = index
left_index = 2 * index + 1
right_index = 2 * index + 2
if left_index < heap_size and unsorted[left_index] > unsorted[largest]:
largest = left_index
if right_index < heap_size and unsorted[right_index] > unsorted[largest]:
largest = right_index
if largest != index:
unsorted[largest], unsorted[index] = (unsorted[index], unsorted[largest])
heapify(unsorted, largest, heap_size)Radix sort: distribute by digit, not by comparison
sorts/radix_sort.py:25Radix sort avoids comparisons by distributing integers into ten digit-value buckets per pass, repeating for each digit position.
Every algorithm seen so far sorts by comparing elements. Radix sort does not: it distributes integers into buckets based on the value of a single digit, then reads the buckets back in bucket order. One pass handles one digit position. The outer while loop repeats for each position, from the ones place up through the highest digit of the maximum element. Inside, int((i / placement) % RADIX) extracts the current digit for each integer and routes it to bucket 0 through 9. The bucket-drain loop then writes the integers back into the list in digit order, preserving the relative order of elements that share the same digit. After all digit positions are processed, the list is sorted in O(n * k) time where k is the number of digits, which beats O(n log n) comparison sorts for large integer sets with bounded digit counts.
Radix sort achieves O(n * k) by routing integers into digit-value buckets rather than comparing elements, bypassing the O(n log n) lower bound for comparison sorts.
placement = 1
max_digit = max(list_of_ints)
while placement <= max_digit:
# declare and initialize empty buckets
buckets: list[list] = [[] for _ in range(RADIX)]
# split list_of_ints between the buckets
for i in list_of_ints:
tmp = int((i / placement) % RADIX)
buckets[tmp].append(i)
# put each buckets' contents into list_of_ints
a = 0
for b in range(RADIX):
for i in buckets[b]:
list_of_ints[a] = i
a += 1
# move to next
placement *= RADIX
return list_of_intsYou've walked through 7 key areas of the The Algorithms - Python codebase.
Continue: Search Algorithms in TheAlgorithms/Python → Browse all projectsCreate code tours for your project
Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.
Install Intraview Free