【问题标题】:Swift running sum快速运行总和
【发布时间】:2016-02-02 18:13:45
【问题描述】:

我想要一个函数 runningSum 在数字数组 a(或任何可添加事物的有序集合)上,它返回一个长度相同的数组,其中每个元素 i 是 A 中所有元素的总和 最多包括i

例子:

runningSum([1,1,1,1,1,1]) -> [1,2,3,4,5,6]
runningSum([2,2,2,2,2,2]) -> [2,4,6,8,10,12]
runningSum([1,0,1,0,1,0]) -> [1,1,2,2,3,3]
runningSum([0,1,0,1,0,1]) -> [0,1,1,2,2,3]

我可以使用 for 循环或其他方式来做到这一点。有没有更实用的选择?它有点像reduce,只是它构建了一个包含所有中间值的结果数组。

更一般的做法是拥有一个函数,它接受任何序列并提供一个序列,该序列是输入序列的总和。

【问题讨论】:

    标签: swift


    【解决方案1】:

    您要查找的通用组合子通常称为 scan,并且可以根据 reduce 定义(与列表中的所有高阶函数一样):

    extension Array {
        func scan<T>(initial: T, _ f: (T, Element) -> T) -> [T] {
            return self.reduce([initial], combine: { (listSoFar: [T], next: Element) -> [T] in
                // because we seeded it with a non-empty
                // list, it's easy to prove inductively
                // that this unwrapping can't fail
                let lastElement = listSoFar.last!
                return listSoFar + [f(lastElement, next)]
            })
        }
    }
    

    (但我认为这不是一个很好的实现。)

    这是一个非常有用的通用函数,可惜没有包含在标准库中。

    然后您可以通过专门化起始值和操作来生成累积和:

    let cumSum = els.scan(0, +)
    

    您可以简单地省略零长度的情况:

    let cumSumTail = els.scan(0, +).dropFirst()
    

    【讨论】:

    • 我喜欢这个。是否有可能(并且有利?)将其定义为序列上的通用函数?
    • extension Array 更改为extension SequenceType 并将Element 的两个实例更改为Generator.Element 以使其适用于任何SequenceType
    • 为了好玩,也许有用,我添加了一个answer,在Sequence 上实现了scan,它返回一个Sequence
    【解决方案2】:

    斯威夫特 4

    一般序列案例

    引用 OP:

    更一般的方法是有一个可以接受任何序列的函数 并提供一个序列,它是输入的总和 顺序。

    考虑一些任意序列(符合Sequence),比如说

    var seq = 1... // 1, 2, 3, ... (CountablePartialRangeFrom)
    

    要创建另一个序列,它是 seq 上的(惰性)运行总和,您可以使用全局 sequence(state:next:) 函数:

    var runningSumSequence =
        sequence(state: (sum: 0, it: seq.makeIterator())) { state -> Int? in
        if let val = state.it.next() {
            defer { state.sum += val }
            return val + state.sum
        }
        else { return nil }
    }
    
    // Consume and print accumulated values less than 100
    while let accumulatedSum = runningSumSequence.next(),
        accumulatedSum < 100 { print(accumulatedSum) }
    // 1 3 6 10 15 21 28 36 45 55 66 78 91
    
    // Consume and print next
    print(runningSumSequence.next() ?? -1) // 120
    
    // ...
    

    如果我们愿意(为了它的乐趣),我们可以将闭包压缩为上面的sequence(state:next:)

    var runningSumSequence =
        sequence(state: (sum: 0, it: seq.makeIterator())) {
            (state: inout (sum: Int, it: AnyIterator<Int>)) -> Int? in
            state.it.next().map { (state.sum + $0, state.sum += $0).0 }
    }
    

    但是,对于sequence(state:next:) 的这些单行返回,类型推断往往会中断(也许还有一些未解决的错误?),迫使我们显式指定state 的类型,因此... in 在关闭。

    或者:自定义序列累加器

    protocol Accumulatable {
        static func +(lhs: Self, rhs: Self) -> Self
    }
    extension Int : Accumulatable {}
    
    struct AccumulateSequence<T: Sequence>: Sequence, IteratorProtocol
    where T.Element: Accumulatable {
        var iterator: T.Iterator
        var accumulatedValue: T.Element?
    
        init(_ sequence: T) {
            self.iterator = sequence.makeIterator()
        }
    
        mutating func next() -> T.Element? {
            if let val = iterator.next() {
                if accumulatedValue == nil {
                    accumulatedValue = val
                }
                else { defer { accumulatedValue = accumulatedValue! + val } }
                return accumulatedValue
    
            }
            return nil
        }
    }
    
    var accumulator = AccumulateSequence(1...)
    
    // Consume and print accumulated values less than 100
    while let accumulatedSum = accumulator.next(),
        accumulatedSum < 100 { print(accumulatedSum) }
    // 1 3 6 10 15 21 28 36 45 55 66 78 91
    

    具体数组情况:使用reduce(into:_:)

    从 Swift 4 开始,我们可以使用 reduce(into:_:) 将运行总和累加到一个数组中。

    let runningSum = arr
        .reduce(into: []) { $0.append(($0.last ?? 0) + $1) }
        // [2, 4, 6, 8, 10, 12]
    

    通过使用reduce(into:_:)[Int] 累加器将不会在后续的reduce迭代中被复制;引用Language reference:

    此方法优于reduce(_:_:) 结果是写时复制类型,例如 ArrayDictionary.

    另请参阅implementation of reduce(into:_:),注意累加器作为inout 参数提供给提供的闭包。

    但是,每次迭代仍然会导致对累加器数组的append(_:) 调用; amortized O(1) 是多次调用的平均值,但这里仍然可以说是不必要的开销,因为我们知道累加器的最终大小。

    因为数组使用指数增加其分配的容量 策略,将单个元素附加到数组是 O(1) 操作 当对append(_:) 方法的多次调用进行平均时。当一个数组 具有额外容量并且不与其他人共享其存储空间 例如,附加一个元素是O(1)。当一个数组需要 在追加之前重新分配存储或其存储共享 另一个副本,追加的是O(n),其中n是数组的长度。

    因此,知道了累加器的最终大小,我们可以使用reserveCapacity(_:) 为它显式保留这样的容量(就像the native implementation of map(_:) 所做的那样)

    let runningSum = arr
        .reduce(into: [Int]()) { (sums, element) in
            if let sum = sums.last {
                sums.append(sum + element)
            }
            else {
                sums.reserveCapacity(arr.count)
                sums.append(element)
            }
    } // [2, 4, 6, 8, 10, 12]
    

    为了它的喜悦,浓缩:

    let runningSum = arr
        .reduce(into: []) {
            $0.append(($0.last ?? ($0.reserveCapacity(arr.count), 0).1) + $1)
    } // [2, 4, 6, 8, 10, 12]
    

    Swift 3:使用enumerated() 进行后续调用reduce

    另一个 Swift 3 替代方案(有开销...)是在每个元素映射中结合使用 enumerated().mapreduce

    func runningSum(_ arr: [Int]) -> [Int] {
        return arr.enumerated().map { arr.prefix($0).reduce($1, +) }
    } /* thanks @Hamish for improvement! */
    
    let arr = [2, 2, 2, 2, 2, 2]
    print(runningSum(arr)) // [2, 4, 6, 8, 10, 12]
    

    好处是您不必在单个 reduce 中使用数组作为收集器(而不是重复调用 reduce)。

    【讨论】:

    • @Benjohn 我在实际项目而不是操场上重新进行了测试,在这种情况下,Swift 编译器显示了它的优势并修复了我们在任何在操场上分析它们时的方法。因此,删除了我糟糕的基准测试,只留下了enumeration() 解决方案(本问答中各种解决方案的另一种选择)。
    • 我现在理解代码了。除非编译器管理一些魔法,否则它是 O(n^2),对吧?您是否查看了生成的程序集并发现它派生了 O(n) 解决方案 - 如果是这样,那似乎令人印象深刻!
    • @Benjohn 是的,这个在O(n^2) 中运行(除非有一些编译器魔法将它减少到更少),但是,在分析器告诉你之前,这应该不是问题。如果性能成为问题,那么 JAL:s O(n) 使用“外部”变量作为累积和的解决方案应该是最快的解决方案。
    • @Benjohn 啊,我错过了这条评论 :) 是的,我也有科学计算的背景,但我强迫自己重新教导自己,对于成熟的应用程序,我最擅长简单地编写整洁的应用程序和可读的代码,担心后期的优化(但是,要记住复杂性总是一件好事!);因此不必担心较小的O(n^2) 算法,但自然会避免任何散发着指数时间的气味!
    • @dfri 略有改进,您可以使用元素作为 reduce 的起始值:arr.enumerated().map { arr.prefix($0).reduce($1, +) } :)
    【解决方案3】:

    只是为了好玩:作为单行的运行总和:

    let arr = [1, 2, 3, 4]
    
    let rs = arr.map({ () -> (Int) -> Int in var s = 0; return { (s += $0, s).1 } }())
    
    print(rs) // [1, 3, 6, 10]
    

    它与JAL's answer 中的(更新的)代码相同,特别是, 不生成中间数组。 sum 变量在返回转换的立即求值闭包中被捕获。

    【讨论】:

      【解决方案4】:

      如果你只是想让它为 Int 工作,你可以使用这个:

      func runningSum(array: [Int]) -> [Int] {
          return array.reduce([], combine: { (sums, element) in
              return sums + [element + (sums.last ?? 0)]
          })
      }
      

      如果您希望它对元素类型具有通用性,则必须做很多额外的工作来声明各种数字类型以符合提供零元素的自定义协议,并且(如果您希望它对两者都通用)浮点和整数类型)加法运算,因为 Swift 还没有这样做。 (未来版本的 Swift 可能会解决这个问题。)

      【讨论】:

      • 很好,谢谢 - 我绝对更喜欢这个,而不是基于 map 的带有外部变量的方法。您能否评论以这种方式构建阵列的效率?我还要看看scan 方法。
      • 我不确定效率。它是 O(N) 或 O(N^2)。在分析器告诉您这是一个瓶颈之前,不要担心它。
      【解决方案5】:

      假设Ints 的数组,听起来您可以使用map 来操作输入:

      let arr = [0,1,0,1,0,1]
      
      var sum = 0
      let val = arr.map { (sum += $0, sum).1 }
      
      print(val) // "[0, 1, 1, 2, 2, 3]\n"
      

      我将继续研究不使用外部变量的解决方案。

      【讨论】:

      • :-) 没有外部总和就是这样。
      • 你不能在没有外部变量的情况下使用map 来完成它,但你可以使用reduce 和一个数组作为收集器
      • @Kevin 我想知道这一点,但认为它可能非常低效?
      • @Sulthan 您可以使用arr.enumerate().map({}) 与索引进行映射。
      • @JAL:您可以将地图缩短为let var = arr.map { (sum += $0, sum).1 } :)
      【解决方案6】:

      我想我会很酷地扩展 Sequence 并使用通用 scan 函数,正如伟大的 first answer 中所建议的那样。

      有了这个扩展,你可以像这样得到一个数组的运行总和:[1,2,3].scan(0, +)

      但你也可以得到其他有趣的东西……

      • 运行产品:array.scan(1, *)
      • 运行最大值:array.scan(Int.min, max)
      • 运行最小值:array.scan(Int.max, min)

      由于实现是Sequence 上的函数并返回Sequence,因此您可以将其与其他序列函数链接在一起。它是高效的,具有线性运行时间。

      这是扩展名……

      extension Sequence {
      
          func scan<Result>(_ initialResult: Result, _ nextPartialResult: @escaping (Result, Self.Element) -> Result) -> ScanSequence<Self, Result> {
              return ScanSequence(initialResult: initialResult, underlying: self, combine: nextPartialResult)
          }
      }
      
      struct ScanSequence<Underlying: Sequence, Result>: Sequence {
      
          let initialResult: Result
          let underlying: Underlying
          let combine: (Result, Underlying.Element) -> Result
      
          typealias Iterator = ScanIterator<Underlying.Iterator, Result>
      
          func makeIterator() -> Iterator {
              return ScanIterator(previousResult: initialResult, underlying: underlying.makeIterator(), combine: combine)
          }
      
          var underestimatedCount: Int {
              return underlying.underestimatedCount
          }
      }
      
      struct ScanIterator<Underlying: IteratorProtocol, Result>: IteratorProtocol {
      
          var previousResult: Result
          var underlying: Underlying
          let combine: (Result, Underlying.Element) -> Result
      
          mutating func next() -> Result? {
              guard let nextUnderlying = underlying.next() else {
                  return nil
              }
      
              previousResult = combine(previousResult, nextUnderlying)
              return previousResult
          }
      }
      

      【讨论】:

      • 我觉得这将是一个很棒的功能,可以添加到 Swift 中。有资格的人可以向 Swift Evolution 提出 scan 功能吗?
      【解决方案7】:

      一个使用reduce的解决方案:

      func runningSum(array: [Int]) -> [Int] {
          return array.reduce([], combine: { (result: [Int], item: Int) -> [Int] in
              if result.isEmpty {
                  return [item] //first item, just take the value
              }
      
              // otherwise take the previous value and append the new item
              return result + [result.last! + item]
          })
      }
      

      【讨论】:

      • 内部可以像if let last = result.last { return result + [last + item] } else { return [item] }一样干净一点
      • @Kevin 我不认为这有什么不同。 rob-mayoff 的 solution 可能是最干净的。
      【解决方案8】:

      我参加这个聚会很晚了。其他答案有很好的解释。但是他们都没有以通用的方式提供初始结果。这个实现对我很有用。

      public extension Sequence {
        /// A sequence of the partial results that `reduce` would employ.
        func scan<Result>(
          _ initialResult: Result,
          _ nextPartialResult: @escaping (Result, Element) -> Result
        ) -> AnySequence<Result> {
          var iterator = makeIterator()
          return .init(
            sequence(first: initialResult) { partialResult in
              iterator.next().map {
                nextPartialResult(partialResult, $0)
              }
            }
          )
        }
      }
      
      extension Sequence where Element: AdditiveArithmetic & ExpressibleByIntegerLiteral {
        var runningSum: AnySequence<Element> { scan(0, +).dropFirst() }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-12-05
        • 1970-01-01
        • 2010-12-31
        • 2016-02-23
        • 1970-01-01
        • 1970-01-01
        • 2017-10-02
        相关资源
        最近更新 更多