已發布·持續改進
排序 指南 · 2/6
本章目前僅提供英文版。
This chapter traces the classic sorting algorithms by hand on small arrays. After a quick look at three simple sorts, it walks through merge sort, quicksort and heapsort, the ones that matter most in practice and in interviews, and then counting sort, which never compares elements.
n - 1 swaps, but always about n²/2 comparisons.All three are O(n²) on average, but insertion sort is close to O(n) on nearly sorted input and very fast on tiny arrays, so real library sorts use it for short ranges. Here is insertion sort on [6, 3, 8, 2, 5]:
| Step | Inserted value | Result |
|---|---|---|
| i = 1 | 3 | [3, 6, 8, 2, 5] |
| i = 2 | 8 | [3, 6, 8, 2, 5] |
| i = 3 | 2 | [2, 3, 6, 8, 5] |
| i = 4 | 5 | [2, 3, 5, 6, 8] |
def insertion_sort(a):
for i in range(1, len(a)):
key = a[i]
j = i - 1
while j >= 0 and a[j] > key: # shift only strictly larger values: stable
a[j + 1] = a[j]
j -= 1
a[j + 1] = key
return a
print(insertion_sort([6, 3, 8, 2, 5])) # [2, 3, 5, 6, 8]Merge sort is the textbook divide-and-conquer algorithm. Split the array in half, sort each half recursively, then merge the two sorted halves. Merging repeatedly takes the smaller of the two front elements, so it costs time proportional to the total length. Sorting [38, 27, 43, 3, 9, 82, 10]:
| Phase | State |
|---|---|
| Split | [38, 27, 43] / [3, 9, 82, 10] |
| Split again | [38] / [27, 43] , [3, 9] / [82, 10] |
| Merge small pieces | [27, 38, 43] , [3, 9, 10, 82] |
| Final merge | [3, 9, 10, 27, 38, 43, 82] |
In the final merge, 27 is compared with 3 and 3 is taken; then 27 against 9, 27 against 10, 27 against 82, and so on. Taking the left element on ties keeps the sort stable. The recursion is levels deep and each level merges elements in total, giving .
log nnO(n log n)Quicksort picks a pivot, partitions the array so that smaller values go left and larger values go right, then sorts both sides recursively. There is no merge step, and the work happens in place. With the simple Lomuto partition and pivot 3 (the last element), [5, 2, 8, 1, 9, 3] is processed like this, where i is the next slot for a small value:
| j | a[j] | Less than 3? | Action | Array | i |
|---|---|---|---|---|---|
| 0 | 5 | no | none | [5, 2, 8, 1, 9, 3] | 0 |
| 1 | 2 | yes | swap with a[0] | [2, 5, 8, 1, 9, 3] | 1 |
| 2 | 8 | no | none | [2, 5, 8, 1, 9, 3] | 1 |
| 3 | 1 | yes | swap with a[1] | [2, 1, 8, 5, 9, 3] | 2 |
| 4 | 9 | no | none | [2, 1, 8, 5, 9, 3] | 2 |
| end | - | - | move pivot to a[2] | [2, 1, 3, 5, 9, 8] | 2 |
The pivot 3 now sits at its final index 2. Sorting [2, 1] and [5, 9, 8] the same way finishes the job.
def partition(a, lo, hi):
pivot = a[hi]
i = lo
for j in range(lo, hi):
if a[j] < pivot:
a[i], a[j] = a[j], a[i]
i += 1
a[i], a[hi] = a[hi], a[i]
return i
data = [5, 2, 8, 1, 9, 3]
print(partition(data, 0, len(data) - 1), data) # 2 [2, 1, 3, 5, 9, 8]If the pivot is always an extreme value (for example, the last element of an already sorted array), the split is n - 1 versus 0 and the running time becomes O(n²). Real implementations pick a random pivot or the median of three.
Heapsort turns the array into a max-heap (a complete binary tree where each parent is at least as large as its children), then repeatedly swaps the root, the maximum, to the end and repairs the heap. The children of index i are 2i + 1 and 2i + 2. [4, 10, 3, 5, 1] becomes the heap [10, 5, 3, 4, 1], and then:
| Extracted | After swap and sift down |
|---|---|
| 10 | [5, 4, 3, 1, 10] |
| 5 | [4, 1, 3, 5, 10] |
| 4 | [3, 1, 4, 5, 10] |
| 3 | [1, 3, 4, 5, 10] |
Heapsort is O(n log n) in the worst case with O(1) extra memory, but it is not stable and its scattered memory access usually makes it slower than quicksort in practice.
If the values are integers from 0 to k - 1, you can sort without comparisons. Count each value, turn the counts into prefix sums that mark where each value ends, then walk the input backwards and drop each element into place. Walking backwards makes it stable, which is what lets radix sort use it as a building block.
def counting_sort(a, k):
count = [0] * k
for x in a:
count[x] += 1
for v in range(1, k):
count[v] += count[v - 1] # prefix sum: one past the last slot of value v
out = [0] * len(a)
for x in reversed(a):
count[x] -= 1
out[count[x]] = x
return out
print(counting_sort([3, 1, 2, 3, 0, 1], 4)) # [0, 1, 1, 2, 3, 3]Radix sort applies this stable counting sort digit by digit, from the least significant to the most significant. Because each pass is stable, the order built by earlier passes survives.
O(n log n) worst-case sort; counting sort sorts integers by counting instead of comparing.
0 則留言
登入 · 登入後即可留言。
來留下第一則留言吧。