【问题标题】:Merge Sort algorithm efficiency归并排序算法效率
【发布时间】:2017-07-05 01:24:26
【问题描述】:

我目前正在学习一个在线算法课程,其中老师不提供解决算法的代码,而是提供粗略的伪代码。因此,在上网寻找答案之前,我决定自己尝试一下。

在这种情况下,我们正在研究的算法是归并排序算法。在获得伪代码后,我们还深入分析了针对数组中 n 个项目的运行时间算法。经过快速分析,老师到达6nlog(base2)(n) + 6n作为算法的大致运行时间。

给出的伪代码仅用于算法的合并部分,如下所示:

C = output [length = n]
A = 1st sorted array [n/2] 
B = 2nd sorted array [n/2] 
i = 1
j = 1
for k = 1 to n
    if A(i) < B(j)
        C(k) = A(i)
        i++
    else [B(j) < A(i)]
        C(k) = B(j) 
        j++
    end
end 

他基本上以4n+22 用于声明ij4 用于执行的操作数量——forif ,数组位置分配和迭代)。我相信为了课堂,他将其简化为6n
这一切对我来说都很有意义,我的问题来自我正在执行的实现以及它如何影响算法以及它可能增加的一些权衡/低效率。

以下是我使用游乐场快速编写的代码:

func mergeSort<T:Comparable>(_ array:[T]) -> [T] {
    guard array.count > 1 else { return array }

    let lowerHalfArray = array[0..<array.count / 2]
    let upperHalfArray = array[array.count / 2..<array.count]

    let lowerSortedArray = mergeSort(array: Array(lowerHalfArray))
    let upperSortedArray = mergeSort(array: Array(upperHalfArray))

    return merge(lhs:lowerSortedArray, rhs:upperSortedArray)
}

func merge<T:Comparable>(lhs:[T], rhs:[T]) -> [T] {
    guard lhs.count > 0 else { return rhs }
    guard rhs.count > 0 else { return lhs }

    var i = 0
    var j = 0

    var mergedArray = [T]()
    let loopCount = (lhs.count + rhs.count)
    for _ in 0..<loopCount {
        if j == rhs.count || (i < lhs.count && lhs[i] < rhs[j]) {
            mergedArray.append(lhs[i])
            i += 1
        } else {
            mergedArray.append(rhs[j])
            j += 1
        }
    }

    return mergedArray
}

let values = [5,4,8,7,6,3,1,2,9]
let sortedValues = mergeSort(values)

我的问题如下:

  1. merge&lt;T:Comparable&gt; 函数开头的guard 语句实际上是否使其效率更低?考虑到我们总是将数组减半,它唯一成立的时间是基本情况以及数组中有奇数个项目时。
    在我看来,这实际上会增加更多的处理并提供最小的回报,因为它发生的时间是我们将数组减半到没有项目的程度。

  2. 关于我在合并中的if 语句。由于它检查多个条件,这会影响我编写的算法的整体效率吗?如果是这样,对我的影响似乎会因它何时会突破 if 语句而有所不同(例如,在第一个条件或第二个条件下)。
    这是在分析算法时需要重点考虑的事情吗?如果是的话,当它从算法中爆发出来时,你如何解释方差?

您可以就我所写的内容提供任何其他分析/提示,我们将不胜感激。

【问题讨论】:

  • 我想知道 for 循环中的 6n 部分是否认为 A[i] 或 B[j] 在合并期间被读取了两次,一次在 if 期间,一次在移动期间。

标签: swift algorithm sorting mergesort


【解决方案1】:

您很快就会了解 Big-O 和 Big-Theta,而您并不关心确切的运行时间(相信我,当我说 非常 很快,就像在一两次讲座中一样)。在那之前,这是你需要知道的:

  1. 是的,守卫需要一些时间,但每次迭代的时间都是相同的。因此,如果每次迭代都需要 X 没有保护的时间量并且您执行 n 函数调用,那么总共需要 X*n 时间量。现在添加在每次通话中花费Y 时间的守卫。您现在总共需要(X+Y)*n 时间。这是一个常数因子,当n 变得非常大时,与n 因子相比,(X+Y) 因子变得可以忽略不计。也就是说,如果您可以将函数 X*n 减少到 (X+Y)*(log n),那么添加 Y 的工作量是值得的,因为您总共执行的迭代次数更少。

  2. 同样的推理也适用于您的第二个问题。是的,检查“如果 X 或 Y”比检查“如果 X”需要更多时间,但它是一个常数因素。额外时间不随n的大小而变化。

在某些语言中,如果第一个条件失败,您只检查第二个条件。我们如何解释这一点?最简单的解决方案是实现比较次数的上限为 3,而迭代次数可能为数百万,n 很大。但是 3 是一个常数,所以每次迭代最多增加一个常数的工作量。您可以深入了解细节并尝试推理第一个、第二个和第三个条件为真或假的频率分布,但通常您并不想走那条路。假装你总是做所有的比较。

所以是的,如果您执行与以前相同数量的迭代,添加防护可能对您的运行时不利。但有时在每次迭代中添加额外的工作可以减少所需的迭代次数。

【讨论】:

  • 非常感谢您帮助我们更好地理解这一点。事实上,我们确实学习了 Big-O 和 Big-Theta。抑制常量感觉很尴尬,因为即使守卫没有提供任何帮助,它也几乎让守卫感到“合理”。同时,编写高效算法的真正魔力是真正能够去除绒毛并以最有效的方式执行任务。在这种情况下,你能给我一个意见,看守是否有道理?根据我对 n=even 的想法,它们不是,但对于 n=odd,它们将我们带到 n-1(尽管仍然是一个被抑制的常数)
  • 守卫增加处理时间,正确。将它们放在那里的原因是为了便于阅读。守卫检查基本情况,一开始就这样做是个好主意,所以你不要忘记。同时,不同层次的优化将使其在一般情况下(当它们为真时)不会花费太多时间。如果不需要,最好不要调用该函数,在调用站点添加警卫。它归结为可读性以及在您选择的语言中函数调用的成本。
  • 就我个人而言,我总是喜欢在函数的开头检查基本情况。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-11-27
  • 1970-01-01
  • 1970-01-01
  • 2013-03-17
  • 1970-01-01
  • 1970-01-01
  • 2015-01-21
相关资源
最近更新 更多