如何在Swift中合并两个排序的数组?

时间:2018-07-18 14:35:44

标签: arrays swift algorithm sorting

请考虑以下两个排序数组:

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]

这是完全可行的。

但是,我的问题是:

如果我们尝试通过更多“快捷”的速记解决方案来实现它,那会是什么?


请注意,其操作方式如下:

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

不会那么聪明,因为它应该像O(n)那样完成。

7 个答案:

答案 0 :(得分:9)

我试图在功能编程无变量中解决您的问题。

给出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]

enter image description here

理论

现在,如果我们一一挑选出边缘(左侧或右侧)上的最小元素,我们将保证按升序选择所有元素。

[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代码了。

代码!

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

这是代码

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 < last,我们需要:

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

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

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 中查看有价值的评论。

版本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

答案 1 :(得分:4)

我不确定您所说的“更多'Swifty'”是什么意思,但是到了。

我会像下面这样写函数。它并不短,但更通用:您可以合并任意两个Sequence,只要它们具有相同的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<S1.Element>。)

答案 2 :(得分:1)

也不确定您的定义,但是您可能会认为这更迅速:

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()

但是,当然,这取决于用例。

答案 3 :(得分:1)

引用@OleBegemann's answer

  

另一个有趣的改进是使序列变懒,   即定义一个MergedSequence及其伴随的迭代器结构   存储基本序列并按需生成下一个元素。

如果我们想使用某种“更快速”的方法,并且还想实现交错的惰性交错序列(基于用于元素逐项比较的<谓词),而不是像在您的示例数组中,我们可以利用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]

答案 4 :(得分:0)

简单的功能解决方案

我真的很喜欢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()))
    }
}

我认为到目前为止,它比本页上建议的其他算法更容易阅读,而且根据我的判断,它甚至

(尽管应注意,Luca Angeletti's answer的注释中提到的dfri的关注也适用于此:在每个递归步骤中实例化一个新数组,这可能在计算上是昂贵的–但是,数组实例化的总数始终为 ,其中 m n 是数组中要包含的元素数排序。)


进一步思考...

此解决方案可以扩展为与

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

基准测试

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

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

结果:

  • 迭代器排序(由Ole Begemann引入):
    37.110 s

  • 功能排序(如本答案所述):
    6.081 s

  • 循环排序(在my other answer中引入):
    0.695 s

  • 快速标准排序((first + second).sorted()
    0.013 s

当然,它总是取决于要合并的特定数组,但是从这些结果中,我认为实际上使用 (first + second).sorted()是您可以最快,最快地完成的事情!

答案 5 :(得分:0)

这是我的意思……这是序列协议扩展,是一个通用功能,可以合并两个相同类型的序列(协议扩展),甚至可以合并任何两个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)")
}

答案 6 :(得分:0)

SWIFT 5 干净的解决方案,带有 O(N),其中n是较小数组中元素的数量。使用数组切片。具有通用的最小最大功能。

  

这个想法是循环遍历数量较少的数组。比较每个相对的项目,然后将其插入结果中。循环结束后,从最​​长的数组中追加其余项目。

func merge(a: [Int] , b: [Int]) -> [Int] {
    var result: [Int] = []
    let count = min(a.count, b.count)
    for i in 0 ..< count {
        let first = min(a[i], b[i])
        let second = max(a[i], b[i])
        result.append(first)
        result.append(second)
    }
    let rest = a.count > count ? a[count...] : b[count...]
    result.append(contentsOf: Array(rest))
    return result
}