Python数据结构(十五):归并排序详解
本文是Python数据结构系列的第十五篇,我们将深入探讨高级排序算法中的归并排序。归并排序是一种基于分治思想的稳定排序算法,具有可预测的时间复杂度O(n log n),适合处理大规模数据和外部排序场景。
一、归并排序的基本概念
归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法,由约翰·冯·诺伊曼于1945年发明。该算法采用分治策略,将已有序的子序列合并,得到完全有序的序列。
归并排序的名称由来
该算法因其核心操作"归并"(将两个有序序列合并为一个有序序列)而得名。归并排序是典型的分治算法应用,体现了"分而治之"的算法设计思想。
归并排序的基本思想
归并排序的基本思想可以概括为:将待排序数组递归地分成两半,分别对两半进行排序,然后将两个有序的子数组合并成一个有序数组。
归并排序的直观理解:
想象一下合并两叠已经按从小到大排列的扑克牌:我们总是比较两叠牌最上面的那张,取出较小的一张放在新牌堆中,重复这个过程直到所有牌都放入新牌堆。
二、归并排序的算法原理
2.1 算法步骤
归并排序的算法步骤可以详细描述如下:
- 分割阶段:递归地将当前数组平均分割成两个子数组
- 递归排序:对两个子数组递归地进行归并排序
- 合并阶段:将两个已排序的子数组合并成一个有序数组
- 递归终止:当子数组的长度为1时,认为它已经有序
2.2 排序过程演示
以下是一个具体的排序过程示例:
初始数组: [64, 34, 25, 12, 22, 11, 55]
分割阶段:
- 第一次分割:[64, 34, 25] 和 [12, 22, 11, 55]
- 继续分割左半部分:[64, 34] 和 [25]
- 继续分割:[64] 和 [34]
- 右半部分:[12, 22] 和 [11, 55]
- 继续分割:[12] 和 [22],[11] 和 [55]
合并阶段(从最底层开始):
- 合并[64]和[34] → [34, 64]
- 合并[34, 64]和[25] → [25, 34, 64]
- 合并[12]和[22] → [12, 22]
- 合并[11]和[55] → [11, 55]
- 合并[12, 22]和[11, 55] → [11, 12, 22, 55]
- 最后合并[25, 34, 64]和[11, 12, 22, 55] → [11, 12, 22, 25, 34, 55, 64]
三、Python实现归并排序
3.1 基础实现
python
def merge_sort(arr):
"""
归并排序基础实现
:param arr: 待排序的列表
:return: 排序后的列表
"""
# 递归终止条件:数组长度为0或1
if len(arr) <= 1:
return arr
# 将数组平分成两半
mid = len(arr) // 2
left_half = arr[:mid]
right_half = arr[mid:]
# 递归排序左右两半
left_half = merge_sort(left_half)
right_half = merge_sort(right_half)
# 合并两个有序数组
return merge(left_half, right_half)
def merge(left, right):
"""
合并两个有序数组
:param left: 左有序数组
:param right: 右有序数组
:return: 合并后的有序数组
"""
merged = []
left_index = 0
right_index = 0
# 比较两个数组的元素,将较小的加入结果
while left_index < len(left) and right_index < len(right):
if left[left_index] < right[right_index]:
merged.append(left[left_index])
left_index += 1
else:
merged.append(right[right_index])
right_index += 1
# 将剩余元素添加到结果中
merged.extend(left[left_index:])
merged.extend(right[right_index:])
return merged
# 测试归并排序
arr = [64, 34, 25, 12, 22, 11, 55]
print(f'原始数组: {arr}')
print(f'归并排序后: {merge_sort(arr.copy())}')
3.2 实现细节分析
递归终止条件
python
if len(arr) <= 1:
return arr
- 数组长度为0或1时已经有序
- 这是递归的基本情况
- 确保算法能够正常结束
数组分割
python
mid = len(arr) // 2
left_half = arr[:mid]
right_half = arr[mid:]
- 计算中间位置
- 使用切片操作分割数组
- 分割是递归进行的
递归调用
python
left_half = merge_sort(left_half)
right_half = merge_sort(right_half)
- 递归排序左右子数组
- 每次递归处理规模更小的子问题
- 递归深度为log₂n
合并函数
python
while left_index < len(left) and right_index < len(right):
if left[left_index] < right[right_index]:
merged.append(left[left_index])
left_index += 1
else:
merged.append(right[right_index])
right_index += 1
- 比较两个数组当前元素
- 将较小的元素加入结果
- 移动相应数组的指针
剩余元素处理
python
merged.extend(left[left_index:])
merged.extend(right[right_index:])
- 将未遍历完的数组剩余元素加入结果
- 使用extend方法高效添加
- 保证所有元素都被包含
3.3 原地归并实现
上面的实现创建了多个新列表,内存开销较大。下面是原地归并的实现思路:
python
def merge_sort_inplace(arr, left=0, right=None):
"""
原地归并排序实现
"""
if right is None:
right = len(arr) - 1
if left < right:
# 计算中间位置
mid = (left + right) // 2
# 递归排序左右两部分
merge_sort_inplace(arr, left, mid)
merge_sort_inplace(arr, mid + 1, right)
# 合并两个有序部分
merge_inplace(arr, left, mid, right)
return arr
def merge_inplace(arr, left, mid, right):
"""
原地合并函数
"""
# 创建临时数组存放合并结果
temp = []
i = left
j = mid + 1
# 比较并合并
while i <= mid and j <= right:
if arr[i] <= arr[j]:
temp.append(arr[i])
i += 1
else:
temp.append(arr[j])
j += 1
# 添加剩余元素
while i <= mid:
temp.append(arr[i])
i += 1
while j <= right:
temp.append(arr[j])
j += 1
# 将临时数组复制回原数组
for k in range(len(temp)):
arr[left + k] = temp[k]
四、归并排序的时间复杂度分析
4.1 时间复杂度计算
分割阶段
- 每次都将数组分成两半
- 分割次数:log₂n
- 每次分割需要常数时间
合并阶段
- 每层需要合并n个元素
- 合并操作需要比较n-1次
- 合并总时间复杂度:O(n log n)
时间复杂度总结
- 最好情况:O(n log n)
- 最坏情况:O(n log n)
- 平均情况:O(n log n)
归并排序的时间复杂度非常稳定,无论输入数据的初始状态如何,都需要O(n log n)的时间。
4.2 空间复杂度
基础实现
- 需要额外的O(n)空间存储临时数组
- 递归调用栈深度:O(log n)
- 总空间复杂度:O(n)
原地实现
- 需要O(n)的临时空间进行合并
- 递归调用栈深度:O(log n)
- 总空间复杂度:O(n)
4.3 性能特点
- 时间复杂度稳定:始终为O(n log n)
- 空间开销较大:需要额外的O(n)空间
- 递归深度合理:递归深度为log₂n
- 适合外部排序:可以处理无法全部放入内存的大数据
五、归并排序的特点
5.1 优点
- 稳定排序:归并排序是稳定的排序算法
- 时间复杂度稳定:始终为O(n log n),可预测性好
- 适合大数据:可以处理无法全部放入内存的数据
- 并行性好:容易实现并行化处理
5.2 缺点
- 空间复杂度高:需要额外的O(n)存储空间
- 不适合小数据:对于小规模数据,常数因子较大
- 实现较复杂:相比快速排序实现稍复杂
- 非原地排序:需要额外的存储空间
5.3 适用场景
- 大数据排序:适合处理无法全部放入内存的数据
- 稳定排序需求:需要保持相等元素的相对顺序
- 链表排序:特别适合链表数据结构的排序
- 外部排序:用于数据库等需要处理磁盘数据的场景
六、归并排序与其他排序算法的比较
6.1 与快速排序比较
| 特性 | 归并排序 | 快速排序 |
|---|---|---|
| 时间复杂度 | 稳定O(n log n) | 平均O(n log n),最坏O(n²) |
| 空间复杂度 | O(n) | O(log n) |
| 稳定性 | 稳定 | 不稳定 |
| 适合场景 | 大数据、外部排序 | 通用排序、内存排序 |
| 实现难度 | 中等 | 中等 |
6.2 与堆排序比较
| 特性 | 归并排序 | 堆排序 |
|---|---|---|
| 时间复杂度 | O(n log n) | O(n log n) |
| 空间复杂度 | O(n) | O(1) |
| 稳定性 | 稳定 | 不稳定 |
| 缓存友好性 | 较好 | 较差 |
| 适合场景 | 大数据排序 | 内存受限环境 |
6.3 与希尔排序比较
| 特性 | 归并排序 | 希尔排序 |
|---|---|---|
| 时间复杂度 | O(n log n) | O(n log n) ~ O(n²) |
| 空间复杂度 | O(n) | O(1) |
| 稳定性 | 稳定 | 不稳定 |
| 时间复杂度稳定性 | 稳定 | 不稳定 |
| 实现方式 | 分治 | 插入排序改进 |
总结
归并排序是一种高效、稳定的排序算法,基于分治思想设计,具有可预测的时间复杂度,适合处理大规模数据和需要稳定排序的场景。
核心要点回顾:
- 基本思想:递归分割数组,然后合并有序子数组
- 算法复杂度:时间复杂度稳定为O(n log n),空间复杂度O(n)
- 稳定性:归并排序是稳定排序算法
- 分治策略:典型的分治算法设计范例
算法特点总结:
- 稳定性:保持相等元素的相对顺序
- 可预测性:时间复杂度稳定,不受输入数据影响
- 适合大数据:可以处理外部存储的大数据
- 并行潜力:容易实现并行化处理
在实际应用中,归并排序常用于需要稳定排序的场景,如数据库排序、文件排序等。虽然空间开销较大,但其稳定性和可预测性使其在许多场景下具有不可替代的价值。
注意:本文是Python数据结构系列的第十五篇,重点讲解归并排序的基本概念和实现。归并排序是算法设计和分析的重要案例,理解它的原理和特性对于掌握算法思想和解决实际问题都有重要价值。