【问题标题】:How to merge two sorted arrays in Swift?如何在 Swift 中合并两个排序数组?
【发布时间】:2018-07-18 14:35:44
【问题描述】:

考虑以下两个排序数组:

let arr1 = [1, 7, 17, 25, 38]
let arr2 = [2, 5, 17, 29, 31]

简单地说,预期的结果应该是:

[1, 2, 5, 7, 17, 17, 25, 29, 31, 38]


事实上,如果我们尝试对这个问题做一个简单的研究,我们会发现很多资源都提供了以下“典型”的方法:

func mergedArrays(_ array1: [Int], _ array2: [Int]) -> [Int] {
    var result = [Int]()
    var i = 0
    var j = 0

    while i < array1.count && j < array2.count {
        if array1[i] < array2[j] {
            result.append(array1[i])
            i += 1
        } else {
            result.append(array2[j])
            j += 1
        }
    }

    while i < array1.count {
        result.append(array1[i])
        i += 1
    }

    while j < array2.count {
        result.append(array2[j])
        j += 1
    }

    return result
}

因此:

let merged = mergedArrays(arr1, arr2) // [1, 2, 5, 7, 17, 17, 25, 29, 31, 38]

这是完全可行的。

但是,我的问题是:

如果我们尝试用更“Swifty”的速写解决方案来实现它会怎样?


注意这样做是:

let merged = Array(arr1 + arr2).sorted()

不会这么聪明,因为它应该是O(n)

【问题讨论】:

  • sortedstd lib 最佳排序

标签: arrays swift algorithm sorting


【解决方案1】:

我尝试在函数式编程无变量中解决您的问题。

给定 2 个数组

let nums0 = [1, 7, 17, 25, 38]
let nums1 = [2, 5, 17, 29, 31]

我们将第一个与第二个的反转版本连接起来

let all = nums0 + nums1.reversed()

结果会是这种金字塔。

[1, 7, 17, 25, 38, 31, 29, 17, 5, 2]

理论

现在,如果我们一个接一个地选择边缘(左或右)的最小元素,我们保证会按升序选择所有元素。

[1, 7, 17, 25, 38, 31, 29, 17, 5, 2] -> we pick 1 (left edge)
[7, 17, 25, 38, 31, 29, 17, 5, 2] -> we pick 2 (right edge)
[7, 17, 25, 38, 31, 29, 17, 5] -> we pick 5 (right edge)
[7, 17, 25, 38, 31, 29, 17] -> we pick 7 (left edge)
[17, 25, 38, 31, 29, 17] -> we pick 17 (right edge)
[17, 25, 38, 31, 29] -> we pick 17 (left edge)
[25, 38, 31, 29] -> we pick 25 (left edge)
[38, 31, 29] -> we pick 29 (right edge)
[38, 31] -> we pick 31 (right edge)
[38] -> we pick 38 (both edges)

现在让我们看看我们构建的数组,挑选所有这些元素。

We selected 1: [1]
We selected 2: [1, 2]
We selected 5: [1, 2, 5]
We selected 7: [1, 2, 5, 7]
We selected 17: [1, 2, 5, 7, 17]
We selected 17: [1, 2, 5, 7, 17, 17]
We selected 25: [1, 2, 5, 7, 17, 17, 25]
We selected 29: [1, 2, 5, 7, 17, 17, 25, 29]
We selected 31: [1, 2, 5, 7, 17, 17, 25, 29, 31]
We selected 38: [1, 2, 5, 7, 17, 17, 25, 29, 31, 38]

这看起来是我们想要达到的结果吧?

现在是时候编写一些 Swifty 代码了。

代码!

好的,我们如何在函数式编程中做到这一点?

这是代码

let merged = all.reduce((all, [Int]())) { (result, elm) -> ([Int], [Int]) in

    let input = result.0
    let output = result.1

    let first = input.first!
    let last = input.last!
    // I know these ☝️ force unwraps are scary but input will never be empty

    if first < last {
        return (Array(input.dropFirst()), output + [first])
    } else {
        return (Array(input.dropLast()), output + [last])
    }

}.1

它是如何工作的?

1. 我们将一个包含all 数组和一个空数组的元组传递给reduce。

all.reduce((all, [Int]()))

我们将调用第一个数组input 和第二个数组output。 逐步减少将删除input 边缘的最小元素,并将其附加到output

2. 然后,在闭包内部,我们为 out 元组的 2 个元素命名

let input = result.0
let output = result.1

3.我们选择输入的第一个和最后一个元素

let first = input.first!
let last = input.last!

是的,我也不喜欢强制展开,但由于input 永远不会为空,因此这些强制展开永远不会产生致命错误。

4. 现在如果first &lt; last 我们需要:

  • 返回输入减去第一个元素
  • 返回输出 + 输入的第一个元素

否则我们会做相反的事情。

if first < last {
    return (Array(input.dropFirst()), output + [first])
} else {
    return (Array(input.dropLast()), output + [last])
}

5.最后我们选择reduce返回的元组的第二个元素,因为它是我们存储结果的地方。

}.1  

时间复杂度

计算时间为 O(n + m),其中 n 是 nums0.count,m 是 nums1.count,因为:

nums1.reversed()

这个☝️是O(1)

all.reduce(...) { ... }

这个☝️是O(n + m),因为闭包是针对all的每个元素执行的

时间复杂度为 O(n) ^ 2。请参阅下面来自 @dfri 的有价值的 cmets。

版本 2

这个版本应该真的有 O(n) 时间复杂度。

let merged = all.reduce(into: (all, [Int]())) { (result, elm) in
    let first = result.0.first!
    let last = result.0.last!

    if first < last {
        result.0.removeFirst()
        result.1.append(first)
    } else {
        result.0.removeLast()
        result.1.append(last)
    }
}.1

【讨论】:

  • 另一种方法很好,但是正如您提到的(渐近)时间复杂度,应该注意的是,reduce 闭包中重复的数组实例化、复制和附加可能非常昂贵。在O(n+m) 的计算中,您将渐近分析的基本操作 隐式定义为闭包的一个单一覆盖(reduce),但每个这样的传递都包含两个 new 的实例化 数组:input 的大小之一和output 的大小之一,以及可能将这些(可能被省略)复制为return。这笔费用将是巨大的(乘以m +n)。
  • ...如果我们不严格地将时间复杂度分析的基本操作描述为“查看和执行all数组成员的操作 复制这样的成员”,那么时间复杂度将是 O(n^2) 而不是 O(n),因为对于每个 reduce 迭代,我们两次复制(/实例化)一个数组,平均而言,大小(n+m)/2.
  • @dfri 很好的观察。我真的害怕时间会更糟。闭包需要复制 2 个数组(输入和输出),它们的长度之和始终为 n+m。所以总时间是O(n+m)^2。你怎么看?
  • 抱歉,回答迟了。 Imo、O(n)^2O(m+n)^2 是相同的——我什至认为后者是对 Big-O 表示法的(常见)误用。我们可以不失一般性地假设n &gt; m,然后对大小为2n 的输入数组进行渐近分析,在这种情况下,2 只是一个常数,对 Big- 方面的渐近增长没有影响O 分析(因为我们从最终符号中省略了任何常量:例如,不写(O(3n),而只是O(n))。所以总结一下:由于复制,我认为时间复杂度为O(n)^2。跨度>
  • Version 2 确实纠正了第一个版本中存在的开销。但是,我首先不确定是否存在任何问题,将selfall;与removeFirst()removeLast() 一起变异)作为inout 变量的(一部分)传递给@ 987654323@,但我意识到最初的(all, [Int]()) 元组包含all 的副本,用于以下突变,所以没有问题:) 很好!
【解决方案2】:

我不确定您所说的“更 'Swifty'”是什么意思,但这里是这样。

我会编写如下函数。它不是更短,而是更通用:你可以合并任意两个Sequences,只要它们具有相同的Element 类型并且ElementComparable

/// Merges two sequences into one where the elements are ordered according to `Comparable`.
///
/// - Precondition: the input sequences must be sorted according to `Comparable`.
func merged<S1, S2>(_ left: S1, _ right: S2) -> [S1.Element]
    where S1: Sequence, S2: Sequence, S1.Element == S2.Element, S1.Element: Comparable
{
    var leftIterator = left.makeIterator()
    var rightIterator = right.makeIterator()

    var merged: [S1.Element] = []
    merged.reserveCapacity(left.underestimatedCount + right.underestimatedCount)

    var leftElement = leftIterator.next()
    var rightElement = rightIterator.next()
    loop: while true {
        switch (leftElement, rightElement) {
        case (let l?, let r?) where l <= r:
            merged.append(l)
            leftElement = leftIterator.next()
        case (let l?, nil):
            merged.append(l)
            leftElement = leftIterator.next()
        case (_, let r?):
            merged.append(r)
            rightElement = rightIterator.next()
        case (nil, nil):
            break loop
        }
    }
    return merged
}

另一个有趣的增强是使序列变得惰性,即定义一个 MergedSequence 和随附的迭代器结构,用于存储基本序列并按需生成下一个元素。这将类似于标准库中的许多函数所做的事情,例如zipSequence.joined。 (如果不想定义新类型,也可以返回AnySequence&lt;S1.Element&gt;。)

【讨论】:

  • let prevTime = Date().timeIntervalSince1970 print(merged(arr1, arr2)) let nowTime = Date().timeIntervalSince1970 print("Difference--",nowTime - prevTime)//0.00197887420654297let prev = Date().timeIntervalSince1970 print(addedArray.sorted { $0 &lt; $1 }) let now = Date().timeIntervalSince1970 print("Difference--",now - prev)//给出 0.000524044036865234 。添加的数组是 arr1+arr2
【解决方案3】:

也不确定您的定义,但您可能会将其解释为更快捷:

func mergeOrdered<T: Comparable>(orderedArray1: [T], orderedArray2: [T]) -> [T] {

    // Create mutable copies of the ordered arrays:
    var array1 = orderedArray1
    var array2 = orderedArray2

    // The merged array that we'll fill up:
    var mergedArray: [T] = []

    while !array1.isEmpty {

        guard !array2.isEmpty else {
            // there is no more item in array2,
            // so we can just add the remaining elements from array1:
            mergedArray += array1
            return mergedArray
        }

        var nextValue: T
        if array1.first! < array2.first! {
            nextValue = array1.first!
            array1.removeFirst()
        } else {
            nextValue = array2.first!
            array2.removeFirst()
        }
        mergedArray.append(nextValue)
    }

    // Add the remaining elements from array2 if any:
    return mergedArray + array2
}

然后:

let merged = mergeOrdered(orderedArray1: arr1, orderedArray2: arr2)
print(merged) // prints [1, 2, 5, 7, 17, 17, 25, 29, 31, 38]

这是一个类似的想法,代码并没有那么短,但在我看来“更快捷”的是你不需要以这种方式跟踪两个索引。

虽然这个和你的实现给你 O(n),但它有点不安全,因为它假设两个输入数组都已经排序。人们可能很容易监督这一先决条件。所以,我个人还是更喜欢

let merged = (arr1 + arr2).sorted()

当然,这取决于用例。

【讨论】:

    【解决方案4】:

    引用@OleBegemann's answer

    另一个有趣的增强是使序列变得惰性, 即定义一个 MergedSequence 和随附的迭代器结构 存储基本序列并按需生成下一个元素。

    如果我们想使用一些“更 Swifty”的方法,并且还想实现交错的惰性交错序列(基于用于元素比较的 &lt; 谓词),而不是像您的示例中那样,一个数组,我们可以使用sequence(state:next:) 和一个助手enum,并重新使用Ole Begemann 的回答中的一些左/右switch 逻辑:

    enum QueuedElement {
        case none
        case left(Int)
        case right(Int)
    }
    
    var lazyInterleavedSeq = sequence(
        state: (queued: QueuedElement.none,
                leftIterator: arr1.makeIterator(),
                rightIterator: arr2.makeIterator()),
        next: { state -> Int? in
            let leftElement: Int?
            if case .left(let l) = state.queued { leftElement = l }
            else { leftElement = state.leftIterator.next() }
    
            let rightElement: Int?
            if case .right(let r) = state.queued { rightElement = r }
            else { rightElement = state.rightIterator.next() }
    
            switch (leftElement, rightElement) {
            case (let l?, let r?) where l <= r:
                state.queued = .right(r)
                return l
            case (let l?, nil):
                state.queued = .none
                return l
            case (let l, let r?):
                state.queued = l.map { .left($0) } ?? .none
                return r
            case (_, nil):
                return nil
            }
    })
    

    我们可能会消耗的东西,例如用于记录:

    for num in lazyInterleavedSeq { print(num) }
    /* 1
       2
       5
       7
       17
       17
       25
       29
       31
       38 */
    

    或者构造一个不可变数组:

    let interleaved = Array(lazyInterleavedSeq)
    // [1, 2, 5, 7, 17, 17, 25, 29, 31, 38]
    

    【讨论】:

      【解决方案5】:

      简单的功能解决方案

      我真的很喜欢Luca Angeletti 介绍的函数式方法。金字塔的想法也很好,但就我的口味而言,由于将reduce 函数与数组元组结合使用,因此代码不够可读/不直观。此外,金字塔的概念需要其他开发者额外解释。

      因此,我尝试使用 my original idea 并从前面慢慢“切掉”两个数组,并使其纯功能。结果非常简单:

      /// Merges two sorted arrays into a single sorted array in ascending order.
      ///
      /// - Precondition: This function assumes that both input parameters `orderedArray1` and 
      ///                 `orderedArray2` are already sorted using the predicate `<`.
      func mergeOrdered<T: Comparable>(orderedArray1: [T], orderedArray2: [T]) -> [T] {
      
          guard let first = orderedArray1.first else {
              return orderedArray2
          }
      
          guard let second = orderedArray2.first else {
              return orderedArray1
          }
      
          if first < second {
              return [first] + mergeOrdered(orderedArray1: Array(orderedArray1.dropFirst()),
                                            orderedArray2: orderedArray2)
          } else {
              return [second] + mergeOrdered(orderedArray1: orderedArray1,
                                             orderedArray2: Array(orderedArray2.dropFirst()))
          }
      }
      

      我会声称它比目前在此页面上建议的其他算法更容易阅读,而且根据我的判断,它甚至是 swifty! ?

      (应该注意,dfri 在 cmets 中提到的 Luca Angeletti's answer 的关注也适用于此处:在每个递归步骤中实例化一个新数组,这可能在计算上很昂贵 - 但它是数组的总数实例化将始终为 ,其中 mn 是要排序的数组中的元素数。)


      进一步思考……

      ? 这个解决方案可以扩展为与

      一起使用
      • 任何排序谓词
      • 泛型序列,而不仅仅是Ole Begemann 建议的数组

      基准测试

      ℹ️ 在所有这些方法中,Swift 标准排序算法是最快的。我用这两个数组测试了所有方法的运行时:

      let first  = Array(1...9999)
      let second = Array(5...500)
      

      结果:

      • 迭代器排序(由Ole Begemann 介绍):
        37.110 秒

      • 函数排序(如本答案所述):
        6.081 秒

      • 循环排序(在my other answer 中介绍过):
        0.695 秒

      • Swift 标准排序 ((first + second).sorted())
        0.013 秒

      当然,这始终取决于您要合并的特定数组,但从这些结果中我会声称,实际上使用 (first + second).sorted() 是您可以做的最快捷和最快的事情!

      【讨论】:

      • 您介意分享基准代码吗?结果有点可疑——我注意到在操场上执行的代码在操场代码中遭受了巨大的惩罚,这可以解释你测试的时间很慢。会是这样吗?如果我将它们作为命令行工具(不是游乐场)运行,我的测试显示出相当可比的时间
      【解决方案6】:

      这是我的一点...这是序列协议扩展和一个通用函数,允许合并两个相同类型的序列(协议扩展),甚至是任何两个具有相同 Element 类型的序列。

      import Cocoa
      
      struct MergeState<T> {
          var lastA: T?
          var iterA: AnyIterator<T>
          var lastB: T?
          var iterB: AnyIterator<T>
      
          mutating func consumeA() -> T? {
              let aux = lastA
              lastA = nil
              return aux ?? iterA.next()
          }
      
          mutating func consumeB() -> T? {
              let aux = lastB
              lastB = nil
              return aux ?? iterB.next()
          }
      }
      
      extension Sequence where Element: Comparable {
          func createMergeState(with other: Self) -> MergeState<Element> {
              let iterA = AnyIterator(self.makeIterator())
              let iterB = AnyIterator(other.makeIterator())
              return MergeState(lastA: nil, iterA: iterA, lastB: nil, iterB: iterB)
          }
      
          func mergeSequence(with other: Self) -> UnfoldSequence<Element, MergeState<Element>> {
              let state = createMergeState(with: other)
              return sequence(state: state) { (state) -> Element? in
                  guard let valueA = state.consumeA() else {
                      return state.consumeB()
                  }
                  guard let valueB = state.consumeB() else {
                      return valueA
                  }
                  if valueA < valueB {
                      state.lastB = valueB
                      return valueA
                  } else {
                      state.lastA = valueA
                      return valueB
                  }
              }
          }
      }
      
      func mergeSequence<S1: Sequence, S2: Sequence>(_ seq1: S1, _ seq2: S2) -> UnfoldSequence<S1.Element, MergeState<S1.Element>> where S1.Element == S2.Element, S1.Element: Comparable {
          return AnySequence(seq1).mergeSequence(with: AnySequence(seq2))
      }
      
      let a = [1, 9, 15, 55, 101]
      let b = [2, 4, 6, 8]
      
      //merge sequences of the same type
      for i in a.mergeSequence(with: b) {
          print("\(i)")
      }
      
      let c: IndexSet = [3, 9, 60]
      
      print("---")
      
      //merge any two sequences with the same Element
      for i in mergeSequence(c, a) {
          print("\(i)")
      }
      

      【讨论】:

        猜你喜欢
        • 2021-10-11
        • 1970-01-01
        • 2018-09-17
        • 1970-01-01
        • 1970-01-01
        • 2021-03-04
        • 1970-01-01
        相关资源
        最近更新 更多