Skip to content
Blog

Bubble sort and insertion sort in Python, step by step

By Published A short read

Bubble sort and insertion sort on 7, 3, 9, 2, 5: bubble sort makes 10 comparisons, insertion sort makes 8, and doubling the data makes four times the work

Bubble sort walks along a list comparing neighbours and swapping any pair that's out of order, and each pass carries the biggest remaining item to the end. Insertion sort takes the items one at a time and slides each one left until it fits, the way most people sort a hand of cards. Both are six lines of Python, and both are O(n²) on average: on a random list, double the data and the work goes up four times. In this post we trace both by hand on five numbers, write them in Python, catch the classic mistakes, and count every comparison to see why a full spreadsheet would take bubble sort 549,755,289,600 of them.

Key takeaways

  • Bubble sort compares neighbours and swaps them if the left one is bigger. Pass 1 puts the biggest item in its final place, pass 2 the second biggest, and so on.
  • Insertion sort keeps the left part of the list sorted and slides each new item left past every bigger item.
  • On 7, 3, 9, 2, 5, bubble sort makes 10 comparisons and insertion sort makes 8. Both make the same 6 swaps: one for every pair that starts out of order.
  • Bubble sort (as usually written) always makes n(n − 1)/2 comparisons. Insertion sort makes n − 1 on a sorted list and n(n − 1)/2 on a reversed one. 1,000 items: 499,500. 2,000 items: 1,999,000, four times as many.
  • In real code, use sorted() or list.sort(). On 1,048,576 random numbers, sorted() made 19,651,111 comparisons, about 28,000 times fewer than bubble sort. And it still uses insertion sort for small pieces.

This post goes with letsBug Code, episode 2. Watch it first, or read on: everything in the video is here, plus the counting code, a side-by-side table and a challenge.

Sorting a million rows

Open a spreadsheet, pick a column and click Sort A to Z. Your contacts, a game's leaderboard, your emails by date: all of them are sorted by a program. A full Excel sheet holds 1,048,576 rows. That's 220, just over a million: the number behind the binary search post, where a million sorted items took at most 20 checks to search. But that list had to be sorted first. How?

Before we start, a prediction. Suppose a simple sort takes 1 second to sort 1,000 numbers. How long does it take for 2,000? The tempting answer is 2 seconds: double the data, double the work. Hold on to your answer; we come back to it below.

Bubble sort: the idea

Here are five numbers: 7, 3, 9, 2, 5. Bubble sort walks along the list comparing neighbours. If the left one is bigger, they swap. Then it moves one step right and compares the next pair. One full walk along the list is called a pass.

On every pass, the biggest number not yet in place gets carried to the end, like a bubble rising to the top. That's where the name comes from.

Tracing pass 1

Let's trace the first pass, one comparison at a time. i is the position of the left number in each pair:

iCompareSwap?List after
07 > 3?yes3, 7, 9, 2, 5
17 > 9?no3, 7, 9, 2, 5
29 > 2?yes3, 7, 2, 9, 5
39 > 5?yes3, 7, 2, 5, 9

That's 4 comparisons and 3 swaps. The 9 has reached the end, its final place, and it never moves again. Notice row 1: nothing changed, but it's still a row in the trace. Skipping the "no change" rows is an easy way to lose track.

The other passes: 10 comparisons

Here's the classic slip: writing the list after one pass as if it's sorted. It isn't. After pass 1 we have 3, 7, 2, 5, 9, and only the 9 is in its final place. So we keep going, and each pass is one comparison shorter, because the end of the list is already done:

Bubble sort on 7, 3, 9, 2, 5: pass 1 compares four pairs and swaps three of them, then passes 2, 3 and 4 make 3, 2 and 1 comparisons; 10 comparisons and 6 swaps in total
Every comparison bubble sort makes on our five numbers. Green numbers are in their final place.
PassComparisonsSwapsList after
1433, 7, 2, 5, 9
2323, 2, 5, 7, 9
3212, 3, 5, 7, 9
4102, 3, 5, 7, 9

After pass 3 the list is already sorted. But the code can't see that, so pass 4 still makes one more comparison. Add them up: 4 + 3 + 2 + 1 = 10 comparisons for five numbers, and 3 + 2 + 1 = 6 swaps.

Bubble sort in Python

def bubble_sort(items):
    n = len(items)
    for end in range(n - 1, 0, -1):
        for i in range(end):
            if items[i] > items[i + 1]:
                items[i], items[i + 1] = items[i + 1], items[i]

numbers = [7, 3, 9, 2, 5]
bubble_sort(numbers)
print(numbers)  # [2, 3, 5, 7, 9]

Line by line:

  • end is the last position each pass reaches: 4, then 3, 2 and 1 for our five numbers. range(n - 1, 0, -1) counts down and stops before 0.
  • The inner loop walks i from 0 up to end - 1, so it compares items[i] with items[i + 1], ending at the pair items[end - 1] and items[end].
  • If the pair is out of order, a, b = b, a swaps them in one line. Python builds the right-hand side first, so no temporary variable is needed.
  • It sorts the list in place: it changes numbers itself and returns nothing.

The classic bug: range(n)

Let the inner loop run over the whole list and it breaks on the very first pass:

def bubble_sort_buggy(items):
    n = len(items)
    for end in range(n - 1, 0, -1):
        for i in range(n):  # the bug: should be range(end)
            if items[i] > items[i + 1]:
                items[i], items[i + 1] = items[i + 1], items[i]

bubble_sort_buggy([7, 3, 9, 2, 5])
# IndexError: list index out of range

On the last step i is 4, so i + 1 is 5, one past the end of a five-item list. That's why the inner range stops one short. range(n - 1) would also avoid the crash, but it keeps re-checking the finished end of the list on every pass. range(end) skips it.

Insertion sort: like sorting a hand of cards

Pick up playing cards one at a time and you probably slide each new card left until it sits in the right place. That's insertion sort. The left part of the list is always sorted. Take the next item and slide it left past every bigger item until it fits.

Same five numbers, 7, 3, 9, 2, 5. The 7 on its own is our sorted part to start with:

Insertion sort on 7, 3, 9, 2, 5: 3 slides past 7 (1 comparison), 9 stays (1), 2 slides past 9, 7 and 3 (3), 5 slides past 9 and 7 and stops at 3 (3); 8 comparisons in total
Each step takes the next item and slides it left. The green part is sorted.
StepItemSlides pastComparisonsList after
13713, 7, 9, 2, 5
29nothing: 7 is smaller13, 7, 9, 2, 5
329, 7, 332, 3, 7, 9, 5
459, 7, then stops at 332, 3, 5, 7, 9

That's 1 + 1 + 3 + 3 = 8 comparisons, two fewer than bubble sort. Count the comparison that stops a slide too: in step 4, "is 3 bigger than 5?" is a real comparison, and the answer "no" is what tells the 5 to stop. The swaps are 1 + 0 + 3 + 2 = 6, exactly the same as bubble sort's.

Insertion sort in Python

def insertion_sort(items):
    for i in range(1, len(items)):
        j = i
        while j > 0 and items[j - 1] > items[j]:
            items[j - 1], items[j] = items[j], items[j - 1]
            j -= 1

numbers = [7, 3, 9, 2, 5]
insertion_sort(numbers)
print(numbers)  # [2, 3, 5, 7, 9]
  • i is the item we're placing. It starts at 1, because one item on its own is already sorted.
  • j is where that item is right now. It starts at i and moves left as the item slides.
  • The while keeps going while there's something to the left (j > 0) and that something is bigger.
  • Each step swaps the item one place left and moves j with it.
  • As soon as the item to the left is smaller or equal, the loop stops early. That's why a list that's already sorted takes only one comparison per item: n − 1 in total.
Don't drop j > 0. Without it, when an item reaches the front, j becomes 0 and Python reads items[-1], which is the last item, not an error. The loop wraps round, swaps the wrong items, and ends with IndexError. On our five numbers it crashes. In fact it only survives when the smallest item happens to be first. Keep j > 0 first, too: and stops at the first False, so we never read items[-1] at all.

Bubble sort vs insertion sort

Both sorts swap only neighbours that are out of order, and every such swap fixes exactly one out-of-order pair. So they always make the same number of swaps: one per pair that starts in the wrong order (7, 3, 9, 2, 5 has six such pairs). The difference is how many comparisons they waste finding them:

Bubble sortInsertion sort
Comparisons, list already sortedn(n − 1)/2 (499,500 for 1,000 items)n − 1 (999 for 1,000 items)
Comparisons, list in reverse ordern(n − 1)/2n(n − 1)/2
Comparisons, random list of 1,000499,500about 250,000
Swapsone per out-of-order pairthe same
Extra memorynone: it sorts in placenone: it sorts in place
Keeps equal items in their original order (stable)yesyes

So insertion sort usually wins: on random data it makes about half the comparisons, and on a list that's nearly sorted it's close to one pass. That's why insertion sort, not bubble sort, is the simple sort that real libraries, Python's included, still use on small pieces. Both stay stable only because they swap on >. Change it to >= and equal items start swapping past each other.

Counting comparisons: why it's O(n²)

On n items, bubble sort's first pass makes n − 1 comparisons, the next n − 2, and so on down to 1. Stack those rows up and you get a triangle, about half of an n by n square:

(n − 1) + (n − 2) + … + 2 + 1 = n(n − 1)/2

For our five numbers that's 5 × 4 / 2 = 10, just like the trace. Insertion sort on a reversed list hits the same total, because every item has to slide all the way to the front.

Comparisons grow like n squared: the 4 + 3 + 2 + 1 triangle for 5 items, then bars for bubble sort on 1,000, 2,000 and 4,000 items: 499,500, 1,999,000 and 7,998,000 comparisons, four times more each time the list doubles
Double the list and the triangle gets twice as wide and twice as tall: four times the work.
Items (n)Bubble sort, any list; insertion sort, reversedInsertion sort, sorted list
5104
1,000499,500999
2,0001,999,0001,999
1,048,576549,755,289,6001,048,575

Now back to your prediction. Double the list, and the triangle gets twice as wide and twice as tall: four times the work, not two. So if 1,000 numbers take 1 second, 2,000 take about 4 seconds. And a million numbers have a thousand times more items, so about a million times more work: a million seconds, or about 11.6 days. Programmers write this growth as O(n²).

Count them yourself

You don't have to trust the table. Wrap each number in a small class that counts every time it's compared, then run the real sort functions on it. Run this after the two functions above:

class Counted:
    """A number that counts every comparison made on it."""
    comparisons = 0

    def __init__(self, value):
        self.value = value

    def __gt__(self, other):
        Counted.comparisons += 1
        return self.value > other.value

    def __lt__(self, other):
        Counted.comparisons += 1
        return self.value < other.value


def count(sort, values):
    Counted.comparisons = 0
    sort([Counted(v) for v in values])
    return Counted.comparisons


print(count(bubble_sort, [7, 3, 9, 2, 5]))        # 10
print(count(insertion_sort, [7, 3, 9, 2, 5]))     # 8
print(count(insertion_sort, range(1000)))         # 999: already sorted
print(count(insertion_sort, range(1000, 0, -1)))  # 499500: reversed
print(count(bubble_sort, range(1000)))            # 499500: even when sorted

Python calls __gt__ whenever it evaluates a > b, so every comparison inside the sort goes through our counter. Our two sorts only use >; __lt__ is there for Python's own sort, which uses <.

How real sorts are fast

So how does a spreadsheet sort a million rows without waiting days? Not like this. Let's count Python's built-in sort on a full column of random numbers with the same count function:

import random

rng = random.Random(1)
rows = [rng.random() for _ in range(1_048_576)]
print(count(sorted, rows))       # 19651111
print(1_048_576 * 1_048_575 // 2)  # 549755289600: bubble sort, the same rows

About 20 million comparisons against 550 billion: roughly 28,000 times fewer. (We ran it on Python 3.13; another version may give a slightly different count.) That's close to n × log2(n) = 1,048,576 × 20 = 20,971,520. Practical sorts grow like this, written O(n log n): double the data and the work only a little more than doubles.

Python's sort, called Timsort, is built on merging. It walks the list looking for stretches that are already in order, called runs, and merges them together, which is much cheaper than comparing everything with everything. We'll build merge sort ourselves later in the series.

But the slow sorts aren't useless. When a run is too short, Python's sort lengthens it with binary insertion sort: insertion sort that finds each item's place with a binary search instead of sliding one step at a time. And if the whole list has fewer than 64 items, it finds the first run and binary-insertion-sorts the rest into it. On a few items, insertion sort is hard to beat.

Bubble sort has no such job. Wikipedia sums up why: other simple sorts like insertion sort "generally run faster than bubble sort, and are no more complex". Donald Knuth wrote that it "seems to have nothing to recommend it, except a catchy name and the fact that it leads to some interesting theoretical problems". And in 2007, when Google's then CEO Eric Schmidt asked presidential candidate Barack Obama the best way to sort a million integers, Obama answered: "I think the bubble sort would be the wrong way to go."

1) Bubble-sort 12, 5, 18, 9, 3, 15 into ascending order. Write the list after the first two passes. How many comparisons does pass 1 make?

2) Four runners' race times are 14, 11, 15 and 12 seconds. Sort them fastest first with insertion sort, writing the list after each step. How many comparisons in total?

3) Change one thing: make bubble sort stop after a pass with no swaps. How many comparisons does it make on 1,000 numbers that are already sorted?

Show the answers

1) Pass 1: 5, 12, 9, 3, 15, 18 (5 comparisons). Pass 2: 5, 9, 3, 12, 15, 18 (4 comparisons).
2) 11, 14, 15, 12 (1 comparison), then 11, 14, 15, 12 again (1: 15 stays, but you still write the row), then 11, 12, 14, 15 (3). That's 5 comparisons.
3) 999: one pass of 999 comparisons, no swaps, stop. Here's the version with the flag:

def bubble_sort_early_exit(items):
    n = len(items)
    for end in range(n - 1, 0, -1):
        swapped = False
        for i in range(end):
            if items[i] > items[i + 1]:
                items[i], items[i + 1] = items[i + 1], items[i]
                swapped = True
        if not swapped:
            break  # a pass with no swaps: the list is sorted

print(count(bubble_sort_early_exit, range(1000)))      # 999
print(count(bubble_sort_early_exit, [7, 3, 9, 2, 5]))  # 10

On our five numbers the flag saves nothing: pass 3 still swaps, so pass 4 has to run to find out the list is sorted.

Questions people ask

Which is faster, bubble sort or insertion sort?

Insertion sort, almost always. Both make the same swaps, but insertion sort stops each slide as soon as the item fits, so it makes about half the comparisons on random data (about 250,000 against 499,500 for 1,000 items) and only n − 1 on a sorted list. On a reversed list they tie at n(n − 1)/2.

What is the time complexity of bubble sort and insertion sort?

Both are O(n²) in the worst and the average case. In the best case, an already sorted list, insertion sort is O(n). Bubble sort as written above is still O(n²) there, because it always makes n(n − 1)/2 comparisons; with the early-exit flag from the challenge it drops to O(n), one pass of n − 1.

Why does bubble sort's inner loop use range(end) and not range(n)?

The loop compares items[i] with items[i + 1], so i must stop one short of the last position or items[i + 1] runs off the end with IndexError. And everything after end is already in its final place, so there's no need to look at it again.

Does Python use insertion sort?

Yes, for small pieces. Python's sort (Timsort) merges runs, and it uses binary insertion sort to build short runs up to a minimum length. A list with fewer than 64 items is sorted with just that run check and binary insertion sort.

Are bubble sort and insertion sort stable?

Yes, as written here. Stable means equal items keep their original order. Both only swap when the left item is strictly bigger (>), so two equal items never pass each other. Python's own sorted() is guaranteed stable too.

What's the difference between sort() and sorted() in Python?

list.sort() sorts the list in place and returns None, like our two functions. sorted() builds a new sorted list and leaves the original alone. Sorting in place has a surprise: every name that points at the list sees the change.

a = [3, 1, 2]
b = a
a.sort()
print(b)  # [1, 2, 3]: b is the same list as a

c = [3, 1, 2]
d = sorted(c)
print(c, d)  # [3, 1, 2] [1, 2, 3]

Keep going

Sources

Every code sample in this post was run in Python 3.13 before publishing.