Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Recursion, Searching & Sorting  ›  Lesson

Elementary Sorts & the Play Chart

Elementary Sorts 25 minBubble, selection and insertion sort — what they cost and what they do to ties
You're building a piece ofTunebox — search & charts
This piece — rank_tracks(): Orders tracks by play count, the simple way.
Scenario The Play Chart lists every track in your library by play count. Two tracks on 12 plays should stay in the order you added them, so the ranking has to be a stable sort.
Your task
Build rank_tracks(tracks). Each track is a dict {"title", "plays"}. Return the titles ordered by plays, most-played first, and keep tracks with equal plays in the order they arrived — a stable sort. Write the sort yourself (insertion sort fits) instead of calling sorted() or .sort(). Example: rank_tracks([{"title": "Dawn", "plays": 12}, {"title": "Aria", "plays": 40}, {"title": "Blue", "plays": 25}]) → ["Aria", "Blue", "Dawn"].

Elementary Sorts & the Play Chart

Tunebox's Play Chart ranks every track by how often it's been played. In production that's one call to sorted() — but which order you get for ties, how long it takes on 50,000 tracks, and why an almost-sorted chart re-sorts instantly all come from how sorting works. Three simple sorts show all of it.

Bubble sort — swap neighbours until nothing moves

Walk the list comparing each pair of neighbours; swap any pair that's out of order. Each pass carries the largest remaining value to the end, like a bubble rising.

python
What does this print?
nums = [5, 1, 4, 2]
for j in range(len(nums) - 1):
    if nums[j] > nums[j + 1]:
        nums[j], nums[j + 1] = nums[j + 1], nums[j]
print(nums)

Selection sort — pick the smallest, put it next

Find the smallest value in the unsorted part and swap it to the front of that part. Repeat with one fewer value each time.

python

Selection sort never gets lucky: it scans the whole unsorted part every time, so it makes n(n−1)/2 comparisons even on a list that was already sorted.

Insertion sort — grow a sorted prefix

Keep the left part sorted. Take the next value (the key), shift every bigger value in the sorted part one place right, and drop the key into the gap. It's how most people sort a hand of cards.

step through it
1items = [3, 1, 2]
2for i in range(1, len(items)):
3 key = items[i]
4 j = i - 1
5 while j >= 0 and items[j] > key:
6 items[j + 1] = items[j]
7 j -= 1
8 items[j + 1] = key
9print(items)

Watch j walk left while it finds bigger values, and notice where the key lands: one place right of where j stopped. Getting that index wrong is the usual insertion-sort bug:

broken — fix it

This should print [10, 20, 30]. It prints [30, 30, 10] — the key is written over the wrong slot, and at j == -1 it even lands at the END of the list.

Why they're all O(n²)

Each sort has a loop inside a loop over the same data. Double the tracks and the work roughly quadruples:

python

4,950 → 19,900 → 79,800. At 50,000 tracks that's over a billion comparisons — which is why the next two lessons exist.

❓ Cross-question — "If they're all O(n²), why learn three?" Because they differ exactly where it matters in practice: whether ties survive, and how they behave on data that's almost sorted. That's the rest of this lesson.

Stability — what happens to ties

A sort is stable if items that compare equal keep their original relative order. The chart sorts by plays; two tracks with 12 plays are "equal", and a stable sort leaves them in library order.

What does this print?
tracks = [("Echo", 12), ("Aria", 40), ("Dawn", 12)]
ranked = sorted(tracks, key=lambda t: t[1])
print([title for title, plays in ranked])

Bubble and insertion sort are stable: they only ever move a value past a strictly bigger neighbour. Selection sort is not — its long-distance swap can jump one tie over another:

python

Why it matters: stability lets you sort by several keys in passes. Sort by title first, then by plays — ties on plays stay in title order.

❓ Cross-question — "Can I make selection sort stable?" Yes — instead of swapping, remove the smallest and insert it at position i, shifting the rest right. That shift is really insertion sort's move, and it costs more writes.

Insertion sort on nearly-sorted data

Insertion sort does work only for values that are out of place. On a sorted list the while stops immediately every time — O(n). A chart where two tracks just swapped places is nearly sorted, and insertion sort fixes it in a single pass.

What does this print?
def shifts(nums):
    items, moved = list(nums), 0
    for i in range(1, len(items)):
        key, j = items[i], i - 1
        while j >= 0 and items[j] > key:
            items[j + 1] = items[j]
            j -= 1
            moved += 1
        items[j + 1] = key
    return moved

print(shifts([1, 2, 3, 4, 5]), shifts([5, 4, 3, 2, 1]))
sort best average worst extra space stable
bubble (with early exit) O(n) O(n²) O(n²) O(1) yes
selection O(n²) O(n²) O(n²) O(1) no
insertion O(n) O(n²) O(n²) O(1) yes
sorted() / list.sort() O(n) O(n log n) O(n log n) O(n) yes

Idioms & real-world patterns

In production: key= and reverse=

You'll almost never hand-write these sorts. You will need to say what order you want — and Python's sort keeps ties stable even with reverse=True:

python

list.sort() sorts in place and returns Nonechart = chart.sort() throws the chart away. sorted() returns a new list.

Coming from Java/JS: Java's Collections.sort and Arrays.sort on objects are stable (TimSort); Arrays.sort on primitives isn't, but equal ints can't be told apart anyway. JS's sort has been stable since ES2019 — but with no comparator it compares as strings: [10, 9, 1].sort() is [1, 10, 9].

Insertion sort lives inside Timsort

Python's own sort, Timsort, finds runs that are already in order and uses insertion sort to extend short runs before merging them. It's O(n) on sorted input for the same reason insertion sort is.

⚡ Advanced — counting sort, when keys are small integers

Comparison sorts can't beat O(n log n). But star ratings are only 1–5, so you can skip comparing: count how many of each, then write them out. O(n + k) for k possible values:

python

🎯 Your turn

Write rank_tracks(tracks). Each track is {"title": ..., "plays": ...}. Return the titles ordered by plays, most-played first; tracks with equal plays keep the order they had in tracks. Sort them yourself — insertion sort fits — rather than calling sorted() or .sort():

  • rank_tracks([{"title": "Dawn", "plays": 12}, {"title": "Aria", "plays": 40}, {"title": "Blue", "plays": 25}])["Aria", "Blue", "Dawn"]
  • rank_tracks([{"title": "Echo", "plays": 12}, {"title": "Dawn", "plays": 12}])["Echo", "Dawn"]

Hint — insertion sort, but shift a track right only while the one to its left has fewer plays than the key — strictly fewer, so ties never swap.

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of Tunebox is built. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upBubble sort

Write bubble_sort(nums) returning a new list sorted low→high using bubble sort: repeatedly swap neighbouring values that are out of order. Don't use sorted() or .sort().

bubble-sortswap
DrillSelection sort, biggest first

Write selection_sort_desc(plays) returning a new list sorted high→low using selection sort: find the largest value in the unsorted part and swap it to the front of that part. Don't use sorted() or .sort().

selection-sortswap
BuildHow unsorted is the chart?

Write count_shifts(nums) returning how many single-place shifts insertion sort makes while sorting nums low→high. Equal values are never shifted past each other. count_shifts([2, 1, 3])1; a sorted list → 0.

insertion-sortcomplexity
BossGenre charts

Each track is {"title", "genre", "plays"}. Write chart_by_genre(tracks) returning titles ordered by genre A→Z, then within a genre by plays high→low; tracks with the same genre and plays keep their original order. Write the sort by hand — no sorted() or .sort().

insertion-sortstable-sortmulti-key-sort
CapstoneRe-rank after one update

chart is already ranked by plays high→low (a list of {"title", "plays"}). One track's count changes. Write rerank(chart, title, plays) returning the new ranked titles by moving only that track — no full re-sort. The result must match a stable sort of the updated chart: moving up it stops behind tracks it now ties with; moving down it stays ahead of them. An unknown title changes nothing. Don't modify the input.

insertion-sortstable-sortlinear-search
rank_tracks.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
rank_tracks(tracks) → listTitles ranked by plays, most first, with ties kept in their original order.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.