Sorting Comparison
You've now coded five sorting algorithms by hand: Bubble, Selection, Insertion, Merge,
and Quick sort. Interviewers rarely ask you to write a sixth from scratch — what they
actually want is for you to pick the right one and justify the pick with time complexity,
space complexity, stability, and whether it sorts in-place. This lesson puts all five side by side in
one table, then looks at what Python's own sorted() and list.sort() do under
the hood — a single, carefully engineered algorithm called Timsort — and how the
key= parameter lets you sort by anything without writing a custom comparison function.
The Full Comparison Table
| Algorithm | Best | Average | Worst | Space | Stable? | In-place? | When to use |
|---|---|---|---|---|---|---|---|
| Bubble Sort | O(n) | O(n2) | O(n2) | O(1) | Yes | Yes | Teaching only — never in production code. |
| Selection Sort | O(n2) | O(n2) | O(n2) | O(1) | No | Yes | When the cost of swapping is very high and comparisons are cheap — it does the fewest swaps of any O(n2) sort (at most n−1). |
| Insertion Sort | O(n) | O(n2) | O(n2) | O(1) | Yes | Yes | Small lists, or nearly-sorted/streaming data — this is exactly the algorithm Timsort falls back to for small runs, see below. |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes | No | When you need a guaranteed O(n log n) with no worst-case cliff, or stability matters, or you're sorting a linked structure. |
| Quick Sort | O(n log n) | O(n log n) | O(n2) | O(log n) | No | Yes | General-purpose in languages without a great built-in sort — fast in practice, but Python already gives you something better tuned (see below). |
sorted() / list.sort() |
O(n) | O(n log n) | O(n log n) | O(n) | Yes | list.sort(): yes. sorted(): no (returns a new list) |
The default choice for essentially everything in Python — see the rest of this lesson. |
Python's Built-In sorted() and list.sort() — Timsort
Every Python list already knows how to sort itself, and you should reach for the built-in before ever hand-rolling a sort in real code. Both entry points run the same algorithm, Timsort (named after Tim Peters, who wrote it for CPython in 2002 — it's since been adopted by Java for object arrays, and by Android and V8 as well):
sorted(iterable)— works on any iterable (list, tuple, string, dict keys, generator...) and always returns a brand-new list, leaving the original untouched.some_list.sort()— a method that exists only on list, sorts it in-place, and returns None (a common beginner bug: writingmy_list = my_list.sort()silently setsmy_listto None).
numbers = [5, 2, 9, 1, 5, 6]
# sorted() -- returns a new list, original is untouched
result = sorted(numbers)
print(result) # [1, 2, 5, 5, 6, 9]
print(numbers) # [5, 2, 9, 1, 5, 6] -- unchanged
# list.sort() -- sorts in-place, returns None
numbers.sort()
print(numbers) # [1, 2, 5, 5, 6, 9] -- the original list is now sorted
# Both accept reverse=True for descending order
print(sorted(numbers, reverse=True)) # [9, 6, 5, 5, 2, 1]
Time complexity: O(n log n) worst case, O(n) best case on already-sorted or nearly-sorted input (Timsort detects existing order and exploits it — this is what "adaptive" means below). Space complexity: O(n) for the temporary merge buffer Timsort allocates.
Timsort's Three Defining Properties
| Property | What it means in practice |
|---|---|
| Hybrid | Insertion sort handles small runs, merge sort combines them — you get the strengths of both without the weaknesses of either alone. |
| Stable | Two elements considered equal by the comparison keep their original relative order. This matters whenever you sort by one field and need ties broken by an earlier sort's order. |
| Adaptive | The more pre-existing order in the input, the less work Timsort does — O(n) on fully sorted or fully reverse-sorted input, degrading gracefully toward O(n log n) as the input gets more random. |
The key= Parameter — Sorting By Anything
Rather than writing a custom two-argument comparison function (Python 3 removed the old
cmp= parameter entirely), both sorted() and list.sort() take a
key= function: it's called once per element, and the results are what actually
get compared — the original elements are then arranged to match that comparison order.
# Sort strings by length instead of alphabetically
words = ["banana", "kiwi", "fig", "apple"]
print(sorted(words, key=len))
# ['fig', 'kiwi', 'apple', 'banana'] -- 3, 4, 5, 6 letters
# Sort a list of tuples by their second element
scores = [("Alice", 72), ("Bob", 95), ("Cara", 88)]
print(sorted(scores, key=lambda pair: pair[1], reverse=True))
# [('Bob', 95), ('Cara', 88), ('Alice', 72)] -- highest score first
# Sort case-insensitively without changing the original casing
names = ["bob", "Alice", "cara"]
print(sorted(names, key=str.lower))
# ['Alice', 'bob', 'cara']
# Sort dicts/objects with operator.itemgetter / attrgetter (faster than a lambda)
from operator import itemgetter
people = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
print(sorted(people, key=itemgetter("age")))
# [{'name': 'Bob', 'age': 25}, {'name': 'Alice', 'age': 30}]
Time complexity: O(n log n) — the key function itself runs in O(1) per call in all four examples above (each is called exactly once per element, not once per comparison, which is why key= is efficient even for an expensive-to-compute key). Space complexity: O(n) — one cached key value per element, plus Timsort's own O(n) merge buffer.
key= is asymptotically cheaper, not just more convenient to write.A Decision Checklist For Interviews
| Question to ask yourself | Points toward |
|---|---|
| Is n small, or is the data already nearly sorted? | Insertion sort by hand, or just trust Timsort — it detects this automatically and runs close to O(n). |
| Do I need a worst-case guarantee? | Merge sort / Timsort — no O(n2) cliff, unlike Quick Sort. |
| Am I sorting objects and must preserve tie order? | Merge sort / Timsort — stability required. |
| Is this an interview question specifically about implementing a sort? | Write the one being asked for — but say out loud that in real code you'd use sorted(). |
| Is this just "sort this data" with no special constraint? | sorted() / list.sort() — don't hand-roll what CPython already tuned for two decades. |