From bubble to quick sort with side-by-side animations: how each algorithm moves data, when O(n²) is fine, why O(n log n) is the wall, and how counting sort tunnels under it.
0 of 7 lessons checked off
Introduction
What it is
Sorting rearranges data into order. The comparison sorts (bubble, selection, insertion, merge, quick, heap) work by comparing pairs; the counting family (counting, radix, bucket) exploits structure in the keys to skip comparisons entirely.
Two labels matter beyond speed: stable (equal elements keep their original relative order) and in-place (O(1) or O(log n) extra memory).
Why it matters
Sortedness is a precondition machine: binary search, two pointers, merging intervals, greedy scheduling, and deduplication all switch on after one O(n log n) sort.
Interviewers rarely ask you to implement quicksort cold — they ask WHICH sort fits a scenario, whether stability matters, and why your 'faster' idea can't beat Ω(n log n) comparisons.
How it works
O(n²) sorts grow a sorted region one element per pass (selection picks the min; insertion slides the next element home; bubble floats the max up).
Merge sort splits and re-zips; quick sort partitions around a pivot so each element reaches its final home; heap sort repeatedly extracts the max from a heap.
Counting sort tallies occurrences of each key value; radix applies it digit by digit — O(n + k), no comparisons.
Where it's used
Python's built-in sort (Timsort) is a merge/insertion hybrid tuned for real data with pre-sorted runs — the practical answer to 'which sort should I use?' is almost always 'the built-in'.
Databases external-merge-sort data too big for memory; graphics pipelines radix-sort draw calls; log processors bucket by time.
In interviews
Sort-then-sweep problems (merge intervals, meeting rooms), k-th element via quickselect, 'sort colors' (Dutch flag partitioning), custom comparator questions, and stability scenarios.
Analogy: Sorting a hand of cards: insertion sort is how humans actually do it (slide each new card into place). Merge sort is two friends each sorting half the deck and zipping the halves. Quick sort is announcing 'everything smaller than this card to my left' and repeating inside each side.
Interactive diagram
Halves are sorted independently, then zipped by repeatedly taking the smaller front element.
38
12
45
7
29
18
41
Unsorted input
Merge sort splits the array in half until pieces have one element, then merges sorted halves back together.
1 / 14
Lessons in this topic
Check off lessons as you go — your progress is saved in this browser.
Bubble, selection, insertion sort
The O(n²) trio: mechanics, best cases, and when they're actually the right tool.
Partitioning, pivot choice, average vs worst case, quickselect.
30 min
Heap sort (overview)
Heapify + repeated extract-max: O(n log n), in-place, not stable.
15 min
Counting, radix, bucket sort
Beating the comparison bound when keys are small integers or uniform.
30 min
Stability and in-place-ness
Which sorts preserve ties, which need buffers, and when each property matters.
15 min
What Python actually does (Timsort)
Runs, galloping, and why sorted(data) is the right production answer.
15 min
Operations
Bubble sort
Compare neighbours, swap when out of order; the largest value bubbles to the end each pass. Early-exit when a pass makes no swaps.
38
12
45
7
29
Unsorted input
Bubble sort repeatedly compares neighbours and swaps them when out of order. The largest value 'bubbles' to the end of each pass.
1 / 12
1defbubble_sort(nums:list[int])->None:2"""In place, stable. O(n^2) worst, O(n) best (already sorted)."""3n=len(nums)4forendinrange(n-1,0,-1):5swapped=False6foriinrange(end):7ifnums[i]>nums[i+1]:8nums[i],nums[i+1]=nums[i+1],nums[i]9swapped=True10ifnotswapped:# a clean pass means fully sorted11break
Time: Best O(n) · average/worst O(n²)Space: O(1), stable
Edge cases
Already-sorted input: the swapped flag exits after one pass.
All-equal elements: no swaps ever happen (stability preserved).
Single element or empty: the outer range is empty — safe.
Common mistakes
Omitting the early-exit flag, turning the best case back into O(n²).
Looping i to n−1 instead of end, re-scanning the already-settled suffix.
Selection sort
Scan the unsorted region for its minimum and swap it to the front. Exactly n−1 swaps — minimal writes, maximal comparisons.
38
12
45
7
29
Unsorted input
Selection sort finds the minimum of the unsorted region and swaps it to the front — one guaranteed placement per pass.
1 / 10
1defselection_sort(nums:list[int])->None:2"""In place, NOT stable. Always Θ(n^2) comparisons, ≤ n-1 swaps."""3n=len(nums)4foriinrange(n-1):5smallest=i6forjinrange(i+1,n):7ifnums[j]<nums[smallest]:8smallest=j9ifsmallest!=i:10nums[i],nums[smallest]=nums[smallest],nums[i]
Time: Θ(n²) in every case — no adaptive best caseSpace: O(1), not stable
Edge cases
Sorted input still costs Θ(n²) comparisons — selection can't detect it.
The long-range swap is what breaks stability: equal elements can leapfrog.
Duplicate minimums: the first one found is taken.
Common mistakes
Swapping inside the inner loop (that's bubble-ish and wrong for the swap-count guarantee).
Claiming selection sort is stable — the swap teleports elements over equals.
Insertion sort
Grow a sorted prefix; each new element walks left to its slot. Nearly-sorted input walks almost nowhere — the adaptive O(n) best case.
38
12
45
7
29
Unsorted input
Insertion sort grows a sorted prefix. Each new value walks left until it finds its slot — like sorting cards in your hand.
Leftover run after one side empties — the two extend calls.
<= vs < during merge decides stability.
Linked-list merge sort achieves O(1) extra space — a classic follow-up.
Common mistakes
Forgetting the leftovers, silently dropping elements.
Re-slicing inside the merge loop (hidden O(n) per step).
Quick sort
Partition around a pivot — smaller left, larger right — placing the pivot at its final index; recurse on both sides. Fast constants, fragile worst case.
38
12
45
7
29
18
41
Unsorted input
Quick sort picks a pivot, partitions smaller values to its left and larger to its right, then recurses on each side.
1 / 15
1importrandom234defquick_sort(nums:list[int],lo:int=0,hi:int|None=None)->None:5"""In place, average O(n log n), worst O(n^2). Not stable."""6ifhiisNone:7hi=len(nums)-18iflo>=hi:9return10# random pivot defends against sorted/adversarial input11r=random.randint(lo,hi)12nums[r],nums[hi]=nums[hi],nums[r]1314pivot=nums[hi]15i=lo-116forjinrange(lo,hi):# Lomuto partition17ifnums[j]<=pivot:18i+=119nums[i],nums[j]=nums[j],nums[i]20nums[i+1],nums[hi]=nums[hi],nums[i+1]2122quick_sort(nums,lo,i)# left of pivot23quick_sort(nums,i+2,hi)# right of pivot
Time: Average O(n log n) · worst O(n²) without pivot randomisationSpace: O(log n) average stack, in place
Edge cases
Sorted input + last-element pivot = the O(n²) worst case; randomisation fixes it.
Many duplicates degrade Lomuto — 3-way (Dutch flag) partitioning is the cure.
Recursing on [lo, i] and [i+2, hi] skips the settled pivot.
Common mistakes
Always picking the first/last pivot and testing only random arrays — the worst case hides until production.
Recursing on ranges that include the pivot, causing infinite recursion on duplicates.
Counting sort (beating the comparison bound)
When keys are small integers, tally each value and rebuild — no comparisons, O(n + k). The prefix-sum placement keeps it stable for radix sort.
Counting sort (beating the comparison bound)
1defcounting_sort(nums:list[int],max_value:int)->list[int]:2"""Stable counting sort for 0..max_value keys. O(n + k)."""3counts=[0]*(max_value+1)4forxinnums:# tally5counts[x]+=167forvinrange(1,max_value+1):# prefix sums: end position of each value8counts[v]+=counts[v-1]910out=[0]*len(nums)11forxinreversed(nums):# backwards pass keeps ties stable12counts[x]-=113out[counts[x]]=x14returnout
k much larger than n (e.g. keys up to 10⁹) makes the counts array the bottleneck — counting sort is wrong there.
Negative keys need an offset shift first.
The reversed pass is what makes radix sort possible on top.
Common mistakes
Using it as a general-purpose sort regardless of key range.
Forward placement pass, which silently breaks stability (and thus radix).
Complexity analysis
Operation
Best
Average
Worst
Space
Bubble sort
O(n)
O(n²)
O(n²)
O(1) · stable
Selection sort
O(n²)
O(n²)
O(n²)
O(1) · unstable
Insertion sort
O(n)
O(n²)
O(n²)
O(1) · stable
Merge sort
O(n log n)
O(n log n)
O(n log n)
O(n) · stable
Quick sort
O(n log n)
O(n log n)
O(n²)
O(log n) · unstable
Heap sort
O(n log n)
O(n log n)
O(n log n)
O(1) · unstable
Counting sort
O(n + k)
O(n + k)
O(n + k)
O(n + k) · stable
Radix sort (d digits)
O(d·(n + b))
O(d·(n + b))
O(d·(n + b))
O(n + b) · stable
Timsort (Python's sorted)
O(n)
O(n log n)
O(n log n)
O(n) · stable
The table interviewers expect you to reproduce from memory. Stability and space are as quotable as time.
Python implementation
Production-quality code with type hints, validation, and docstrings.
Quickselect: k-th smallest in average O(n) (sorting's best spin-off)
1importrandom234defquickselect(nums:list[int],k:int)->int:5"""Returnthek-thsmallest(1-indexed)element.6AverageO(n):eachroundkeepsONEside,unlikequicksort'stwo."""7ifnot1<=k<=len(nums):8raiseValueError("k out of range")910lo,hi=0,len(nums)-111target=k-1# index in sorted order12whileTrue:13iflo==hi:14returnnums[lo]15# randomised Lomuto partition16r=random.randint(lo,hi)17nums[r],nums[hi]=nums[hi],nums[r]18pivot=nums[hi]
What interviewers expect you to know
What interviewers expect you to know
The full complexity/stability table above, cold.
Comparison sorts cannot beat Ω(n log n): n! orderings need log₂(n!) ≈ n log n bits of comparisons — cite it when asked 'can you do better?'.
Stability's practical meaning: sort by amount, then stably by date → grouped by date with amounts still ordered inside.
Quicksort vs merge sort trade-off: cache-friendly speed and O(log n) space vs guaranteed bound and stability.
Scenario questions to rehearse
"1M records, nearly sorted?" — insertion sort or Timsort (which detects runs): near O(n).
Commonly associated with: Google, Amazon, Microsoft
O(log n) per addNum, O(k) per getIntervals time · O(n) space
Topic quiz
6 questions across concepts, code output, complexity, and scenarios. Answer all, then submit for explanations.
Frequently asked questions
Which sorting algorithm should I say Python uses?
Timsort: a stable merge/insertion hybrid that finds pre-sorted runs, merges them adaptively, and hits O(n) on sorted data, O(n log n) worst case. It's used by Python and (for objects) Java.
Do I ever need to hand-write quicksort in an interview?
Occasionally at the 'implement partition' level — and quickselect (built on partition) is genuinely common. Practice the Lomuto partition until it's mechanical; the rest is recursion.
When is an O(n²) sort actually the right choice?
Tiny inputs (n ≲ 32, where constants beat asymptotics — Timsort itself switches to insertion sort), nearly-sorted data (insertion is ~O(n)), or write-limited hardware (selection sort's n−1 swaps).
Summary & cheat sheet
Key takeaways
Learn the table: time × space × stability for all nine algorithms.
Ω(n log n) binds comparison sorts; counting/radix tunnel under it when keys are structured.
Insertion = adaptive and stable; merge = guaranteed and stable; quick = fastest average, needs randomisation; heap = guaranteed and in-place.
Partition once → pivot settled → quickselect finds k-th in O(n) average.
In practice: sorted() — and know why.
Formulas & cheat sheet
Comparison lower bound: log₂(n!) = Θ(n log n)
Merge sort: T(n) = 2T(n/2) + Θ(n) = Θ(n log n)
Quickselect: n + n/2 + n/4 + … = O(n) expected
Radix: O(d · (n + b)) for d digits in base b
Interview checklist
I can animate each algorithm on paper for a 5-element array.
I can reproduce the complexity/stability table from memory.
I can implement Lomuto partition and quickselect.
I can match five scenario prompts to the right algorithm.