我们来系统地讲解一下排序算法。排序算法是计算机科学中最基础且重要的主题之一,它的目标是将一组数据(如数组或列表)按照某种顺序(升序或降序)进行重新排列。
我将从简单到复杂,介绍几种经典和常用的排序算法,并附上它们的核心思想、步骤、优缺点和代码示例(使用Python)。
1. 概述与分类
排序算法通常可以从几个维度进行分类:
- 时间复杂度:衡量算法执行时间随数据规模增长的趋势。
- 空间复杂度:衡量算法执行所需额外内存空间的大小。
- 稳定性:如果相等的元素在排序后相对次序保持不变,则该算法是稳定的。
- 内部排序 vs. 外部排序:内部排序所有操作都在内存中完成;外部排序因数据量太大,需要在内存和磁盘之间进行。
我们今天主要讨论内部排序算法。
为了直观比较,我们先看一个总结表:
| 算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 | 核心思想 |
|---|---|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 | 两两比较,大的下沉 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 | 找最小元素,放到前面 |
| 插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 | 构建有序序列,未排序数据插入 |
| 希尔排序 | O(n log n) ~ O(n²) | O(n log² n) | O(n²) | O(1) | 不稳定 | 分组插入排序,缩小增量 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 | 分治法,先分后合 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) ~ O(n) | 不稳定 | 分治法,找基准分区 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 | 利用堆结构选择极值 |
2. 简单排序算法 (O(n²))
(1) 冒泡排序 (Bubble Sort)
思想:重复遍历列表,比较相邻元素,如果顺序错误就交换它们。每次遍历都会将当前未排序部分的最大元素“冒泡”到正确位置。
步骤:
- 比较相邻元素。如果第一个比第二个大,就交换它们。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个(已经排序好的)。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
优化:可以设置一个标志,如果某次遍历没有发生交换,说明列表已有序,提前终止。
Python实现:
def bubble_sort(arr):
n = len(arr)
for i in range(n):
# 提前退出标志
swapped = False
# 最后i个元素已经排好序,无需再比较
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j] # 交换
swapped = True
# 如果没有发生交换,说明数组已经有序
if not swapped:
break
return arr
(2) 选择排序 (Selection Sort)
思想:每次从未排序的部分中找到最小(或最大)的元素,将其存放到已排序序列的末尾。
步骤:
- 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
Python实现:
def selection_sort(arr):
n = len(arr)
for i in range(n):
min_idx = i # 假设当前索引i的元素是最小的
for j in range(i + 1, n): # 在i之后的部分寻找更小的
if arr[j] < arr[min_idx]:
min_idx = j
# 将找到的最小元素与第i个位置的元素交换
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
注意:选择排序是不稳定的,例如 [5, 8, 5, 2],第一个5会和2交换,跑到另一个5后面。
(3) 插入排序 (Insertion Sort)
思想:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
步骤:
- 从第一个元素开始,该元素可以认为已经被排序。
- 取出下一个元素,在已经排序的元素序列中从后向前扫描。
- 如果该元素(已排序)大于新元素,将该元素移到下一位置。
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
- 将新元素插入到该位置后。
- 重复步骤2~5。
Python实现:
def insertion_sort(arr):
n = len(arr)
# 从第二个元素开始(索引1)
for i in range(1, n):
key = arr[i] # 当前需要插入的元素
j = i - 1 # 从i的前一个元素开始比较
# 将比key大的元素都向后移动一位
while j >= 0 and key < arr[j]:
arr[j + 1] = arr[j]
j -= 1
# 找到位置,插入key
arr[j + 1] = key
return arr
插入排序对近乎有序的数组效率非常高,可以达到近乎O(n)的时间复杂度。
3. 高效排序算法 (O(n log n))
(1) 归并排序 (Merge Sort)
思想:采用分治法。
- 分:递归地将当前数组平均分成两半。
- 治:将两个已经排序的子序列合并成一个有序序列。
步骤:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列。
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置。
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置。
- 重复步骤3直到某一指针到达序列尾。
- 将另一序列剩下的所有元素直接复制到合并序列尾。
Python实现:
def merge_sort(arr):
if len(arr) <= 1:
return arr
# 1. 分 (Divide)
mid = len(arr) // 2
left_half = arr[:mid]
right_half = arr[mid:]
# 递归调用
left_sorted = merge_sort(left_half)
right_sorted = merge_sort(right_half)
# 2. 治 (Conquer) - 合并两个有序数组
return merge(left_sorted, right_sorted)
def merge(left, right):
result = []
i = j = 0
# 比较两个子数组的元素,将较小的放入结果数组
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
# 将剩余的元素添加到结果数组中
result.extend(left[i:])
result.extend(right[j:])
return result
(2) 快速排序 (Quick Sort)
思想:也采用分治法,但比归并排序更常用。
- 从数列中挑出一个元素,称为“基准”。
- 分区操作:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区操作。
- 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
步骤(使用 Lomuto 分区方案):
- 选择最右边的元素作为基准。
- 设置一个指针
i,初始指向最左边减一(i = low - 1)。 - 遍历从
low到high-1的元素j。 - 如果当前元素
arr[j]小于等于基准,则i++,并交换arr[i]和arr[j]。 - 遍历结束后,将基准(
arr[high])与arr[i+1]交换,此时基准位于正确位置。 - 返回基准的索引
i+1。
Python实现:
def quick_sort(arr, low=0, high=None):
if high is None:
high = len(arr) - 1
if low < high:
# pi 是分区索引,arr[pi]现在在正确的位置
pi = partition(arr, low, high)
# 递归排序分区前后的元素
quick_sort(arr, low, pi - 1)
quick_sort(arr, pi + 1, high)
return arr
def partition(arr, low, high):
# 选择最右边的元素作为基准
pivot = arr[high]
i = low - 1 # 较小元素的索引
for j in range(low, high):
# 如果当前元素小于或等于基准
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i] # 交换
# 将基准元素放到正确的位置
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
快速排序的效率高度依赖于基准的选择。随机选择基准或三数取中可以有效避免最坏情况O(n²)的发生。
(3) 堆排序 (Heap Sort)
思想:利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即父节点的键值总是大于等于(或小于等于)任何一个子节点的键值。
步骤:
- 建堆:将待排序序列构建成一个大顶堆(升序排序用大顶堆)。
- 此时,整个序列的最大值就是堆顶的根节点。
- 将其与末尾元素进行交换,此时末尾就为最大值。
- 然后将剩余
n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
Python实现:
def heapify(arr, n, i):
largest = i # 初始化最大值为根
left = 2 * i + 1
right = 2 * i + 2
# 如果左子节点存在且大于根
if left < n and arr[left] > arr[largest]:
largest = left
# 如果右子节点存在且大于当前最大值
if right < n and arr[right] > arr[largest]:
largest = right
# 如果最大值不是根,则需要交换
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i] # 交换
heapify(arr, n, largest) # 递归地调整受影响的子堆
def heap_sort(arr):
n = len(arr)
# 构建最大堆(从最后一个非叶子节点开始)
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
# 一个个从堆顶取出元素
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i] # 交换堆顶和当前末尾元素
heapify(arr, i, 0) # 对缩小后的堆进行调整
return arr
4. 如何选择排序算法?
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 小规模数据 (n < 50) | 插入排序 | 常数项小,实际运行速度快于O(n log n)的算法。对于近乎有序的数据,效率极高。 |
| 大规模数据,追求效率 | 快速排序 | 平均情况下最快,缓存 locality 好。但需要小心处理最坏情况(如已排序数组)。 |
| 大规模数据,要求稳定 | 归并排序 | 稳定且保证O(n log n)时间复杂度。缺点是需O(n)额外空间。 |
| 大规模数据,要求最坏情况稳定 | 堆排序 | 最坏情况也是O(n log n),且只需O(1)额外空间。但不稳定,且缓存不友好。 |
| 数据范围有限(如整数) | 非比较排序(如计数排序、桶排序) | 这些算法可以突破O(n log n)的下限,达到O(n)的时间复杂度,但有特定适用条件。 |
总结一下:
sorted()和list.sort():在Python中,内置的排序函数sorted()和列表的sort()方法使用的是 Timsort 算法,它是一种混合排序算法,结合了归并排序和插入排序的优点,高效且稳定。在绝大多数情况下,你应该直接使用它们,而不是自己实现。- 学习目的:理解这些经典排序算法的思想对于培养算法思维和解决更复杂的问题至关重要。
希望这个详细的讲解能帮助你更好地理解排序算法!

23万+

被折叠的 条评论
为什么被折叠?



