【问题标题】:Swift - speed and efficiency of higher order functions (reduce)Swift - 高阶函数的速度和效率(减少)
【发布时间】:2016-07-29 09:36:19
【问题描述】:

关于大输入数据的高阶 swift 函数的效率的快速问题。在最近的一次测试中,我有一个关于在数组中查找“平衡索引”的问题——即数组的索引,其中索引下方所有元素的总和等于索引上方所有元素的总和

这个数组的平衡索引是任意整数 P 使得 0 ≤ P

          A[0] + A[1] + ... + A[P−1] = A[P+1] + ... + A[N−2] + A[N−1].

挑战是编写一个简短的函数来计算第一个(或任何)被认为是“平衡”的索引。 我整理了一个简单的 sn-p,它得分很高,但在一些使用大型输入数据(数组大小约为 100,000)的“性能”测试中失败了。

这是代码

public func solution(inout A : [Int]) -> Int {
   var index = 0;
        for _ in A {
            let sumBefore = A[0...index].reduce(0) { $0 + $1 }
            let sumAfter = A[index...A.count-1].reduce(0) { $0 + $1 }
            if (sumBefore == sumAfter) { return index; }
            index += 1;
        }
        return -1;
}

谁能解释一下为什么代码在处理大量数据或任何推荐的替代方案时表现如此糟糕?

例如,这里是对性能测试失败的描述:

大型性能测试,O(n^2) 解决方案应该失败。

✘ 超时错误 运行时间:>6.00 秒,时限:0.22 秒。

【问题讨论】:

    标签: arrays swift algorithm performance


    【解决方案1】:

    看来挑战失败了,因为您的解决方案是 O(n^2)

    您的 for 循环,以及 2 个连续的 reduces 内部,构成您的解决方案 ~ O(2*n^2),因为 reduce 再次遍历所有元素。

    一个更简单的解决方案是首先计算总和,然后遍历元素一次,从总和中逐个减去每个值,从而可以访问左右和以进行比较。

    使用 Swift 3.0、Xcode 8:

    func findEquilibriumIndex(in array: [Int]) -> Int? {
      var leftSum = 0
      var rightSum = array.reduce(0, combine: +)
    
    
      for (index, value) in array.enumerated() {
        rightSum -= value
    
        if leftSum == rightSum {
            return index
        }
    
        leftSum += value
      }
    
      return nil
    }
    
    let sampleArray = [-7, 1, 5, 2, -4, 3, 0]
    
    findEquilibriumIndex(in: sampleArray)
    

    【讨论】:

      【解决方案2】:

      问题不在于“内置函数的性能如此糟糕”。 您的解决方案很慢,因为在每次迭代中,N 元素都是 添加(N 是数组的长度)。会更有效率 计算总和一次并更新“前总和” 和遍历数组时的“求和后”。这减少了 从O(N^2)O(N) 的复杂度:

      public func solution(A : [Int]) -> Int {
      
          var sumBefore = 0
          var sumAfter = A.reduce(0, combine: +)
      
          for (idx, elem) in A.enumerate() {
              sumAfter -= elem
              if sumBefore == sumAfter {
                  return idx
              }
              sumBefore += elem
          }
          return -1
      }
      

      (斯威夫特 2.2,Xcode 7.3.1)

      备注:

      • 没有理由将数组作为inout 参数传递。
      • 可以将运算符(在本例中为 +)作为参数传递给 reduce() 函数。
      • enumerate() 返回一系列数组索引以及 对应的元素,这样可以节省另一个数组访问。

      另请注意,更“Swifty”的设计是使返回类型 一个可选的Int?,如果没有找到解决方案,则为nil

      【讨论】:

        【解决方案3】:

        incrementalSums 扩展

        如果你定义了这个扩展

        extension Array where Element : SignedInteger {
            var incrementalSums: [Element] {
                return Array(reduce([0]) { $0.0 + [$0.0.last! + $0.1] }.dropLast())
            }
        }
        

        给定一个Int(s) 数组,您可以构建一个数组,其中n-th 位置处的Int 表示原始数组中从0(n-1) 的值的总和。

        例子

        [1, 2, 3, 10, 2].incrementalSums // [0, 1, 3, 6, 16]
        

        balanceIndex 函数

        现在你可以构建一个这样的函数

        func equilibriumIndex(nums: [Int]) -> Int? {
            let leftSums = nums.incrementalSums
            let rightSums = nums.reversed().incrementalSums.reversed()
            return Array(zip(leftSums, rightSums)).index { $0 == $1 }
        }
        

        【讨论】:

        • 我不确定在闭包中使用副作用是否是个好主意。最有可能的是,它将从第一个数组元素开始按顺序执行,但我不会依赖,除非该行为被明确记录。
        • @MartinR:感谢您的提示。那我再试试别的办法。
        • @MartinR:我找到了另一个 "functional" 解决方案 :) 任何评论将不胜感激。
        • 它看起来是正确的(没有测试它)。我假设它比其他建议的解决方案慢,因为构建了两个数组,但为了得到明确的答案,必须测量和比较。
        • @MartinR:我同意。如果存在解决方案,您的代码通常需要n + n/2 步骤。如果不是,您将需要2n 步骤。如果解决方案存在,我的代码通常需要2n + n/2 步骤,如果不存在则需要3n 步骤。您的代码也可能更易于阅读。
        【解决方案4】:

        这是 Swift 3 中解决方案的功能版本

          let total = sampleArray.reduce(0,+) 
          var sum    = 0
          let index  = sampleArray.index{ v in defer {sum += v}; return sum * 2 == total - v } 
        

        如果我理解正确,结果索引处的元素将从每一侧的总和中排除(我不确定其他解决方案是否能实现)

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-08-22
          • 1970-01-01
          • 2015-11-24
          • 2012-06-30
          • 2013-03-27
          • 1970-01-01
          相关资源
          最近更新 更多