如何找到递推关系,并计算归并排序码的主定理?

Posted

技术标签:

【中文标题】如何找到递推关系,并计算归并排序码的主定理?【英文标题】:How to find the recurrence relation, and calculate Master Theorem of a Merge Sort Code? 【发布时间】:2022-01-01 09:06:06 【问题描述】:

我试图找到这个合并排序代码的主定理,但首先我需要找到它的递归关系,但我很难做到并理解两者。我已经在这里看到了一些类似的问题,但无法理解解释,例如,首先我需要找出代码有多少操作?有人可以帮我吗?


def mergeSort(alist):
    print("Splitting ",alist)
    if len(alist)>1:
        mid = len(alist)//2
        lefthalf = alist[:mid]
        righthalf = alist[mid:]

        mergeSort(lefthalf)
        mergeSort(righthalf)

        i=0
        j=0
        k=0
        while i < len(lefthalf) and j < len(righthalf):
            if lefthalf[i] < righthalf[j]:
                alist[k]=lefthalf[i]
                i=i+1
            else:
                alist[k]=righthalf[j]
                j=j+1
            k=k+1

        while i < len(lefthalf):
            alist[k]=lefthalf[i]
            i=i+1
            k=k+1

        while j < len(righthalf):
            alist[k]=righthalf[j]
            j=j+1
            k=k+1
    print("Merging ",alist)

alist = [54,26,93,17,77,31,44,55,20]
mergeSort(alist)
print(alist)

【问题讨论】:

【参考方案1】:

要使用主定理确定分治算法的运行时间,您需要将算法的运行时间表示为输入大小的递归函数,格式如下:

T(n) = aT(n/b) + f(n)

T(n) 是我们在输入大小 n 上表达算法总运行时间的方式。

a 代表算法进行的递归调用次数。

T(n/b) 表示递归调用:n/b 表示递归调用的输入大小是原始输入大小的某个特定部分(分治法的 divide 部分) .

f(n) 表示您在算法主体中需要做的工作量,通常只是将递归调用的解决方案组合成一个整体解决方案(您可以说这是征服 部分)。

下面是对 mergeSort 的稍微重构的定义:

def mergeSort(arr):
  if len(arr) <= 1: return # array size 1 or 0 is already sorted
  
  # split the array in half
  mid = len(arr)//2
  L = arr[:mid]
  R = arr[mid:]

  mergeSort(L) # sort left half
  mergeSort(R) # sort right half
  merge(L, R, arr) # merge sorted halves

我们需要确定an/bf(n)

因为mergeSort的每次调用都会进行两次递归调用:mergeSort(L)mergeSort(R)a=2

T(n) = 2T(n/b) + f(n)

n/b 表示进行递归调用的当前输入的一部分。因为我们要找到中点并将输入分成两半,将当前数组的一半传递给每个递归调用n/b = n/2b=2。 (如果每个递归调用得到原始数组的 1/4,b 将是 4

T(n) = 2T(n/2) + f(n)

f(n) 表示算法除了进行递归调用之外所做的所有工作。每次调用 mergeSort 时,我们都会计算 O(1) 时间的中点。 我们还将数组拆分为LR,从技术上讲,创建这两个子数组副本是 O(n)。然后,假设mergeSort(L) 对数组的左半部分进行了排序,mergeSort(R) 对右半部分进行了排序,我们仍然需要将已排序的子数组合并在一起,以便使用merge 函数对整个数组进行排序。总之,这使得f(n) = O(1) + O(n) + complexity of merge。现在我们来看看merge

def merge(L, R, arr):
  i = j = k = 0    # 3 assignments
  while i < len(L) and j < len(R): # 2 comparisons
    if L[i] < R[j]: # 1 comparison, 2 array idx
      arr[k] = L[i] # 1 assignment, 2 array idx
      i += 1        # 1 assignment
    else:
      arr[k] = R[j] # 1 assignment, 2 array idx
      j += 1        # 1 assignment
    k += 1          # 1 assignment

  while i < len(L): # 1 comparison
    arr[k] = L[i]   # 1 assignment, 2 array idx
    i += 1          # 1 assignment
    k += 1          # 1 assignment

  while j < len(R): # 1 comparison
    arr[k] = R[j]   # 1 assignment, 2 array idx
    j += 1          # 1 assignment
    k += 1          # 1 assignment

这个函数还有更多工作要做,但我们只需要得到它的整体复杂度等级就可以准确地应用主定理。我们可以计算每一个操作,即每次比较、数组索引和赋值,或者只是更一般地对其进行推理。一般来说,您可以说,在三个 while 循环中,我们将遍历 L 和 R 的每个成员,并将它们分配给输出数组 arr,为每个元素执行恒定数量的工作。请注意,我们正在处理 L 和 R 的每个元素(总共 n 个元素)并为每个元素做恒定量的工作就足以说明合并在 O(n) 中。

但是,如果您愿意,您可以对计数操作进行更具体的操作。对于第一个 while 循环,每次迭代我们进行 3 次比较、5 次数组索引和 2 次赋值(常数),循环一直运行直到 L 和 R 中的一个被完全处理。然后,接下来的两个 while 循环中的一个可能会运行以处理来自另一个数组的任何剩余元素,执行 1 个比较、2 个数组索引和 3 个变量分配(持续工作)。因此,因为 L 和 R 的 n 个总元素中的每一个都导致在 while 循环中执行最多恒定数量的操作(根据我的计数,10 或 6 个,所以最多 10 个)和 i=j=k=0 语句只有 3 个常量赋值,合并在 O(3 + 10*n) = O(n) 中。回到整体问题,这意味着:

f(n) = O(1) + O(n) + complexity of merge
     = O(1) + O(n) + O(n)
     = O(2n + 1)
     = O(n)

T(n) = 2T(n/2) + n

在我们应用主定理之前的最后一步:我们希望 f(n) 写为 n^c。对于 f(n) = n = n^1,c=1。 (注意:如果 f(n) = n^c*log^k(n) 而不是简单的 n^c,则情况会发生轻微的变化,但我们在这里不必担心)

您现在可以应用主定理,它最基本的形式是比较a(递归调用的数量增长速度)与b^c(每个递归调用的工作量减少的速度)。有 3 种可能的情况,我试图解释其中的逻辑,但如果括号中的解释没有帮助,您可以忽略它们:

    a &gt; b^c, T(n) = O(n^log_b(a))。 (递归调用总数的增长速度快于每次调用的工作量减少的速度,所以总工作量由递归树最底层的调用次数决定。调用次数从 1 开始乘以@ 987654356@log_b(n)次,因为log_b(n)是递归树的深度,所以总工作量=a^log_b(n)=n^log_b(a))

    a = b^c, T(n) = O(f(n)*log(n))。 (调用次数的增长与每次调用工作量的减少相平衡。因此,递归树每一级的工作量是恒定的,所以总工作量就是 f(n)*(depth of tree) = f(n) *log_b(n) = O(f(n)*log(n))

    a &lt; b^c, T(n) = O(f(n))。 (每次调用的工作量减少的速度快于调用次数的增加量。因此,总工作量由递归树顶层的工作量主导,即 f(n))

对于 mergeSort 的情况,我们已经看到 a = 2、b = 2 和 c = 1。作为 a = b^c,我们应用第二种情况:

T(n) = O(f(n)*log(n)) = O(n*log(n))

你就完成了。这似乎需要做很多工作,但是为 T(n) 提出一个递归式会变得越容易,而且一旦你有一个递归式,它很快就会检查它属于哪种情况,这使得主定理相当解决更复杂的分/治重复的有用工具。

【讨论】:

以上是关于如何找到递推关系,并计算归并排序码的主定理?的主要内容,如果未能解决你的问题,请参考以下文章

归并排序

20171105主定理的基本解释整合

二路归并排序java实现

归并排序 快速排序

JavaScript 数据结构与算法之美 - 归并排序快速排序希尔排序堆排序

动画 | 什么是归并排序?