Python 实现的 DSA 归并排序

归并排序

归并排序(Merge Sort)算法是一种分治算法,它先将数组拆分成更小的数组,然后以正确的方式重新组合数组,从而实现排序。


{{ msgDone }}

分解:该算法首先将数组不断拆分成更小的部分,直到子数组仅包含一个元素。

合并:该算法通过将最小值放在前面,将数组的小部分重新组合在一起,从而得到一个已排序的数组。

归并排序通过递归的方式对数组进行拆分和重组。

在上面的动画中,每次条形图下移都代表一次递归调用,将数组拆分成更小的部分。当条形图上移时,表示两个子数组合并完成。

归并排序算法的描述如下:

工作原理:

  1. 将未排序的数组分成两个子数组,每个子数组的大小为原数组的一半。
  2. 只要当前数组部分包含多个元素,就继续拆分子数组。
  3. 通过始终将最小值放在前面来合并两个子数组。
  4. 持续合并,直到没有子数组剩下。

请查看下面的图示,从不同的角度了解归并排序的工作原理。如图所示,数组被拆分成越来越小的部分,直到重新合并在一起。在合并过程中,会对每个子数组的值进行比较,以确保最小值排在最前面。

归并排序

手动运行示例

在实际使用 Python 程序实现归并排序之前,让我们先手动进行排序,以便更好地理解归并排序的工作原理。

第 1 步:我们从一个未排序的数组开始,我们知道它会一直拆分,直到子数组仅包含一个元素。归并排序函数会调用自身两次,每次处理数组的一半。这意味着第一个子数组将首先拆分成最小的部分。

[ 12, 8, 9, 3, 11, 5, 4]
[ 12, 8, 9] [ 3, 11, 5, 4]
[ 12] [ 8, 9] [ 3, 11, 5, 4]
[ 12] [ 8] [ 9] [ 3, 11, 5, 4]

第 2 步:第一个子数组的拆分已完成,现在是时候进行合并了。8 和 9 是要合并的前两个元素。8 是最小值,因此在第一个合并的子数组中,8 排在 9 前面。

[ 12] [ 8, 9] [ 3, 11, 5, 4]

第 3 步:接下来要合并的子数组是 [ 12] 和 [ 8, 9]。从开头开始比较两个数组中的值。8 小于 12,因此 8 排在前面,9 也小于 12。

[ 8, 9, 12] [ 3, 11, 5, 4]

第 4 步:现在递归拆分第二个大的子数组。

[ 8, 9, 12] [ 3, 11, 5, 4]
[ 8, 9, 12] [ 3, 11] [ 5, 4]
[ 8, 9, 12] [ 3] [ 11] [ 5, 4]

第 5 步:3 和 11 按照所示顺序合并在一起,因为 3 小于 11。

[ 8, 9, 12] [ 3, 11] [ 5, 4]

第 6 步:包含值 5 和 4 的子数组被拆分,然后合并,使 4 排在 5 前面。

[ 8, 9, 12] [ 3, 11] [ 5] [ 4]
[ 8, 9, 12] [ 3, 11] [ 4, 5]

第 7 步:合并右侧的两个子数组。通过比较创建新合并数组中的元素:

  1. 3 小于 4
  2. 4 小于 11
  3. 5 小于 11
  4. 11 是最后剩下的值
[ 8, 9, 12] [ 3, 4, 5, 11]

第 8 步:合并最后剩下的两个子数组。让我们更详细地了解如何进行比较,以创建新的合并数组并完成排序:

3 小于 8:

之前: [ 8, 9, 12] [ 3, 4, 5, 11]
之后: [ 3, 8, 9, 12] [ 4, 5, 11]

第 9 步:4 小于 8:

之前: [ 3, 8, 9, 12] [ 4, 5, 11]
之后: [ 3, 4, 8, 9, 12] [ 5, 11]

第 10 步:5 小于 8:

之前: [ 3, 4, 8, 9, 12] [ 5, 11]
之后: [ 3, 4, 5, 8, 9, 12] [ 11]

第 11 步:8 和 9 小于 11:

之前: [ 3, 4, 5, 8, 9, 12] [ 11]
之后: [ 3, 4, 5, 8, 9, 12] [ 11]

第 12 步:11 小于 12:

之前: [ 3, 4, 5, 8, 9, 12] [ 11]
之后: [ 3, 4, 5, 8, 9, 11, 12]

排序完成!

运行下面的模拟程序,查看上述步骤的动画演示:

{{ msgDone }}
{{ x.dieNmbr }}

在 Python 中实现归并排序

要实现归并排序算法,我们需要:

  1. 包含待排序值的数组。
  2. 一个函数,该函数接受一个数组,将其拆分成两部分,并分别对每一部分调用自身,以便不断递归拆分数组,直到子数组仅包含一个值。
  3. 另一个函数,该函数以排序的方式重新合并子数组。

生成的代码如下所示:

实例

在 Python 中实现归并排序算法:

def mergeSort(arr):
  if len(arr) <= 1:
    return arr

  mid = len(arr) // 2
  leftHalf = arr[:mid]
  rightHalf = arr[mid:]

  sortedLeft = mergeSort(leftHalf)
  sortedRight = mergeSort(rightHalf)

  return merge(sortedLeft, sortedRight)

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

mylist = [3, 7, 6, -10, 15, 23.5, 55, -13]
mysortedlist = mergeSort(mylist)
print("Sorted array:", mysortedlist)

亲自试一试

在第 6 行arr[:mid] 获取数组中直到但不包括索引 'mid' 处的值的所有值。

在第 7 行arr[mid:] 获取数组中从索引 'mid' 处的值开始的所有值以及接下来的所有值。

在第 26-27 行,完成了合并的第一部分。此时,比较了两个子数组的值,并且左子数组或右子数组为空,因此结果数组可以直接用左子数组或右子数组中剩余的值填充。这些行可以互换,结果将保持不变。

非递归归并排序

由于归并排序是一种分治算法,因此递归是实现该算法最直观的代码。递归实现的归并排序可能也更容易理解,并且通常使用的代码行数更少。

但是,归并排序也可以在不使用递归的情况下实现,即没有函数调用自身的情况。

请查看下面不使用递归的归并排序实现:

实例

不使用递归的归并排序:

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

def mergeSort(arr):
  step = 1 # 从长度为 1 的子数组开始
  length = len(arr)

  while step < length:
    for i in range(0, length, 2 * step):
      left = arr[i:i + step]
      right = arr[i + step:i + 2 * step]

    merged = merge(left, right)

    # 将合并后的数组放回原数组
    for j, val in enumerate(merged):
      arr[i + j] = val

    step *= 2 # 将子数组长度加倍,以便进行下一次迭代

  return arr

mylist = [3, 7, 6, -10, 15, 23.5, 55, -13]
mysortedlist = mergeSort(mylist)
print(mysortedlist)

亲自试一试

您可能会注意到,在上述两个归并排序实现中,merge 函数完全相同,但在上面的实现中,mergeSort 函数中的 while 循环用于替代递归。while 循环在原地对数组进行拆分和合并,这使得代码稍微难以理解。

简而言之,mergeSort 函数中的 while 循环使用短步长,利用 merge 函数对初始数组的微小部分(子数组)进行排序。然后,增加步长以合并和排序数组的较大部分,直到整个数组排序完成。

归并排序的时间复杂度

归并排序的时间复杂度为:\( O( n \cdot \log n ) \)。

对于不同类型的数组,时间复杂度几乎相同。无论数组是否已经排序或完全打乱,该算法都需要拆分数组并重新合并。

下图显示了归并排序的时间复杂度。

时间复杂度

无论数组是否已经排序,归并排序的性能几乎都相同,因为无论何种情况,都需要拆分数组并使用比较进行合并。