算法设计与分析——排序算法(二):归并排序

分类目录:《算法设计与分析》总目录
相关文章:
· 排序算法(一):插入排序
· 排序算法(二):归并排序
· 排序算法(三):堆排序
· 排序算法(四):选择排序
· 排序算法(五):冒泡排序
· 排序算法(六):希尔排序
· 排序算法(七):快速排序
\qquad · ①基础知识
\qquad · ②快速排序的性能
\qquad · ③快速排序的随机化
\qquad · ④快速排序的分析
· 排序算法(八):计数排序
· 排序算法(九):基数排序
· 排序算法(十):桶排序
· 排序算法:比较排序算法的下界
· 排序算法:十大排序算法总结


归并排序算法完全遵循分治模式。直观上其操作如下:

  1. 分解:分解待排序的 n n n个元素的序列成各具 n 2 \frac{n}{2} 2n个元素的两个子序列。
  2. 解决:使用归并排序递归地排序两个子序列。
  3. 合并:合并两个已排序的子序列以产生已排序的答案。

当待排序的序列长度为1时,递归“开始回升”,在这种情况下不要做任何工作,因为长度为1的每个序列都已排好序。归并排序算法的关键操作是“合并”步骤中两个已排序序列的合并。我们通过调用一个辅助过程merge(arr, l, m, r)来完成合并,其中 A A A是一个数组, l l l m m m r r r是数组下标,满足 l ≤ m < r l \leq m < r lm<r。该过程假设子数组 A [ l ⋯ m ] A[l \cdots m] A[lm] A [ m + 1 ⋯ r ] A[m+1 \cdots r] A[m+1r]都已排好序。它合并这两个子数组形成单一的已排好序的子数组并代替当前的子数组 A [ p ⋯ r ] A[p \cdots r] A[pr]

过程merge(arr, l, m, r)需要 Θ ( n ) \Theta(n) Θ(n)的时间,其中 n = r − l + 1 n = r - l + 1 n=rl+1是待合并元素的总数。它按以下方式工作。回到《排序算法:插入排序》中玩扑克牌的例子,假设桌上有两堆牌面朝上的牌,每堆都已排序,最小的牌在顶上。我们希望把这两堆牌合并成单一的排好序的输出堆,牌面朝下地放在桌上。我们的基本步骤包括在牌面朝上的两堆牌的顶上两张牌中选取较小的一张,将该牌从其堆中移开(该堆的顶上将显露一张新牌)并牌面朝下地将该牌放置到输出堆。重复这个步骤,直到一个输入堆为空,这时,我们只是拿起剩余的输入堆并牌面朝下地将该堆放置到输出堆。因为我们只是比较顶上的两张牌,所以计算上每个基本步骤需要常量时间。因为我们最多执行 n n n个基本步骤,所以合并需要 Θ ( n ) \Theta(n) Θ(n)的时间。

下面的代码实现了上面的思想,我们还可以有一个额外的变化,以避免在每个基本步骤必须检查是否有堆为空。在每个堆的底部放置一张哨兵牌,它包含一个特殊的值,用于简化代码。这里,我们使用 ∞ \infin 作为哨兵值,结果每当显露一张值为 ∞ \infin 的牌,它不可能为较小的牌,除非两个堆都已显露出其哨兵牌。但是,一旦发生这种情况,所有非哨兵牌都已被放置到输出堆。因为我们事先知道刚好 r − l + 1 r - l + 1 rl+1张牌将被放置到输出堆,所以一旦已执行 r − l + 1 r - l + 1 rl+1个基本步骤,算法就可以停止。

def merge(arr, l, m, r): 
    n1 = m - l + 1
    n2 = r - m 
  
    # 创建临时数组
    L = [0] * (n1)
    R = [0] * (n2)
  
    # 拷贝数据到临时数组 arrays L[] 和 R[] 
    for i in range(0 , n1): 
        L[i] = arr[l + i] 
  
    for j in range(0 , n2): 
        R[j] = arr[m + 1 + j] 
  
    # 归并临时数组到 arr[l..r] 
    i = 0     # 初始化第一个子数组的索引
    j = 0     # 初始化第二个子数组的索引
    k = l     # 初始归并子数组的索引
  
    while i < n1 and j < n2 : 
        if L[i] <= R[j]: 
            arr[k] = L[i] 
            i += 1
        else: 
            arr[k] = R[j] 
            j += 1
        k += 1
  
    # 拷贝 L[] 的保留元素
    while i < n1: 
        arr[k] = L[i] 
        i += 1
        k += 1
  
    # 拷贝 R[] 的保留元素
    while j < n2: 
        arr[k] = R[j] 
        j += 1
        k += 1

def merge_sort(arr,l,r): 
    if l < r: 
        m = int((l+(r-1))/2)
        merge_sort(arr, l, m) 
        merge_sort(arr, m+1, r) 
        merge(arr, l, m, r) 

过程MERGE的详细工作过程如下:第2行计算子数组 A [ m ⋯ l ] A[m \cdots l] A[ml]的长度 n 1 n_1 n1,第3行计算子数组 A [ l + 1 ⋯ r ] A[l + 1 \cdots r] A[l+1r]的长度 n 2 n_2 n2。在第4行,我们创建长度分别为 n 1 n_1 n1 n 2 n_2 n2的数组 L L L R R R。第10、11行的for循环将子数组 A [ m ⋯ l ] A[m \cdots l] A[ml]复制到 L [ 1 ⋯ n 1 ] L[1 \cdots n_1] L[1n1],第13、14行的for循环将子数组 A [ l + 1 ⋯ r ] A[l + 1 \cdots r] A[l+1r]复制到 R [ 1 ⋯ n 2 ] R[1 \cdots n_2] R[1n2]。后面的代码图示如下图,通过维持以下循环不变式,执行 r − l + 1 r - l + 1 rl+1个基本步骤:在开始循环的每次迭代时,子数组 A [ p ⋯ k − 1 ] A[p \cdots k - 1] A[pk1]按从小到大的顺序包含 L [ 1 ⋯ n 1 ] L[1 \cdots n1] L[1n1] R [ 1 ⋯ n 2 ] R[1 \cdots n2] R[1n2]中的 k − p k - p kp个最小元素。进而, L [ i ] L[i] L[i] R [ j ] R[j] R[j]是各自所在数组中未被复制回数组A的最小元素。
归并排序图示
我们必须证明循环的第一次迭代之前该循环不变式成立,该循环的每次迭代保持该不变式,并且循环终止时,该不变式提供了一种有用的性质来证明正确性。

  • 初始化:循环的第一次迭代之前,有 k = p k = p k=p,所以子数组 A [ p ⋯ k − 1 ] A[p \cdots k - 1] A[pk1]为空。这个空的子数组包含 L L L R R R k − p = 0 k - p = 0 kp=0个最小元素。又因为 i = j = 1 i = j = 1 i=j=1,所以 L [ i ] L[i] L[i] R [ j ] R[j] R[j]都是各自所在数组中未被复制回数组 A A A的最小元素。
  • 保持:为了理解每次迭代都维持循环不变式,首先假设 L [ i ] ≤ R [ j ] L[i] \leq R[j] L[i]R[j]。这时, L [ i ] L[i] L[i]是未被复制回数组 A A A的最小元素。因为 A [ p ⋯ k − 1 ] A[p \cdots k - 1] A[pk1]包含 k − p k - p kp个最小元素,所以在第15行将 L [ i ] L[i] L[i]复制到 A [ k ] A[k] A[k]之后,子数组 A [ p ⋯ k ] A[p \cdots k] A[pk]将包含 k − p + 1 k - p + 1 kp+1个最小元素。增加 k k k的值(在for循环中更新)和i的值(在第16行中)后,为下次迭代重新建立了该循环不变式。反之,若 L [ i ] ≥ R [ j ] L[i] \geq R[j] L[i]R[j],则执行适当的操作来维持该循环不变式。
  • 终止:终止时 k = r + 1 k = r + 1 k=r+1。根据循环不变式,子数组 A [ p ⋯ k − 1 ] A[p \cdots k - 1] A[pk1]就是 A [ p ⋯ r ] A[p \cdots r] A[pr]且按从小到大的顺序包含 L L L R R R中的 k − p = r − p + 1 k - p = r - p + 1 kp=rp+1个最小元素。数组 L L L R R R一起包含 n 1 + n 2 + 2 = r − p + 3 n1 + n2 + 2 = r - p + 3 n1+n2+2=rp+3个元素。

下面是上述归并排序的简化版本,更易于理解,其中merge(a, b)实现将已排好序的列表ab合并的功能,而merge_sort(arr)则是归并排序的核心代码:

def merge(a, b):
    ret = []
    while len(a) > 0 and len(b) > 0:
        if a[0] <= b[0]:
            ret.append(a[0])
            a.remove(a[0])    
        else:
            ret.append(b[0])
            b.remove(b[0])
    if len(a) == 0:
        ret += b
    if len(b) == 0:
        ret += a
    return ret

def merge_sort(arr):
    length = len(arr)
    if length <= 1:
        return arr
    else:
        mid = length // 2
        left = merge_sort(arr[:mid])
        right = merge_sort(arr[mid:])
        return merge(left,right)

最后,我们用动图演示一下归并排序的全过程:
归并排序

  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

von Neumann

您的赞赏是我创作最大的动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值