【问题标题】:In Swift, an efficient function that separates an array into 2 arrays based on a predicate在 Swift 中,一个基于谓词将数组分成 2 个数组的高效函数
【发布时间】:2016-10-13 00:15:02
【问题描述】:

注意:我目前仍在使用 Swift 2.2,但也对 Swift 3 解决方案开放

我希望创建一个与filter 非常接近的函数,除了它还保留不匹配的结果并保持排序顺序。例如,假设您想过滤掉数组中可被 3 整除的数字,但仍保留不能被 3 整除的数字列表。


选项 1:使用filter

使用filter,您只能获得可被 3 整除的数字列表,而原始列表保持不变。然后,您可以使用相反的谓词再次过滤原始列表,但这是不必要的第二遍。代码如下所示:

let numbers = [1,2,3,4,5,6,7,8,9,10]
let divisibleBy3 = numbers.filter { $0 % 3 == 0 } // [3,6,9]
let theRest = numbers.filter { $0 % 3 != 0 }      // [1,2,4,5,7,8,10]

这确实是非常易读的,但它执行 2 遍的事实对我来说似乎效率低下,尤其是在谓词更复杂的情况下。这是实际需要的两倍。

选项 2:在 Collection 扩展中使用自定义 separate 函数

我的下一个尝试是扩展Collection 并创建一个我称为separate 的函数。此函数将获取集合并一次遍历一个元素并将它们添加到匹配列表或不匹配列表中。代码如下所示:

extension Collection {
    func separate(predicate: (Generator.Element) -> Bool) -> (matching: [Generator.Element], notMatching: [Generator.Element]) {
        var groups: ([Generator.Element],[Generator.Element]) = ([],[])
        for element in self {
            if predicate(element) {
                groups.0.append(element)
            } else {
                groups.1.append(element)
            }
        }
        return groups
    }
}

然后我可以像这样使用函数:

let numbers = [1,2,3,4,5,6,7,8,9,10]
let groups = numbers.separate { $0 % 3 == 0 }
let matching = groups.matching       // [3,6,9]
let notMatching = groups.notMatching // [1,2,4,5,7,8,10]

这也很干净,但我唯一不喜欢的是我使用元组作为返回类型。也许其他人会不同意,但我更喜欢返回与self 相同的类型进行链接。但从技术上讲,你可以抓住.matching.notMatching,这与self 的类型相同,你可以链接它们中的任何一个。

选项 3:在 Array 扩展中使用自定义的变异 removeIf 函数

separate 返回元组的问题导致我尝试创建一个函数来修改 self,方法是删除找到的匹配项并将它们添加到新列表中,并在最后返回匹配项列表.返回的列表是您的匹配项,并且该数组被修剪掉这些值。两个数组中都保留了顺序。代码如下所示:

extension Array {
    mutating func removeIf(predicate: (Element) -> Bool) -> [Element] {
        var removedCount: Int = 0
        var removed: [Element] = []

        for (index,element) in self.enumerated() {
            if predicate(element) {
                removed.append(self.remove(at: index-removedCount))
                removedCount += 1
            }
        }

        return removed
    }
}

它是这样使用的:

var numbers = [1,2,3,4,5,6,7,8,9,10]
let divisibleBy3 = numbers.removeIf { $0 % 3 == 0 }
// divisibleBy3: [3,6,9]
// numbers:      [1,2,4,5,7,8,10]

这个函数必须在Array的扩展中实现,因为删除特定索引处的元素的概念不适用于常规CollectionsArray定义为public struct Array<Element> : RandomAccessCollection, MutableCollection,并且它直接定义了remove(at:)函数,而不是从继承或协议中获取)。

选项 4:选项 2 和 3 的组合

我是代码重用的忠实拥护者,在提出选项 3 后,我意识到我可能可以重用选项 2 中的 separate 函数。我想出了这个:

extension Array {
    mutating func removeIf(predicate: (Element) -> Bool) -> [Element] {
        let groups = self.separate(predicate: predicate)
        self = groups.notMatching
        return groups.matching
    }
}

并且它的使用方式与选项 3 中一样。

我关心性能,所以我通过 XCTest 的measure 运行每个选项,迭代 1000 次。结果如下:

Option 1: 9 ms
Option 2: 7 ms
Option 3: 10 ms
Option 4: 8 ms

选项 5:基于 negaipro 的回答

我知道partition,但我不打算考虑它,因为它没有保留排序顺序。 negaipro 的回答基本上是partition,但它让我思考。 partition 的想法是交换与枢轴点匹配的元素,从而确保结束枢轴点一侧的所有内容都与谓词匹配,而另一侧则不匹配。我接受了这个想法并将动作更改为“移动到最后”。因此,匹配项会从他们的位置移除并添加到末尾。

extension Array {
    mutating func swapIfModified(predicate: (Element) -> Bool) -> Int {
        var matchCount = 0
        var index = 0
        while index < (count-matchCount) {
            if predicate(self[index]) {
                append(remove(at: index))
                matchCount += 1
            } else {
                index += 1
            }
        }

        return count-matchCount
    }
}

在我使用包含 10 个数字的数组的初始测试中,它与其他选项相当。但我担心append(remove(at: index)) 行的性能。所以我再次尝试了所有选项,数组从 1 到 1000,这个选项绝对是最慢的。


结论:

这些选项之间没有很大的性能差异。而且由于选项 4 比选项 3 更快并且重用选项 2 中的代码,所以我倾向于抛弃选项 3。所以当我不关心未过滤的结果时,我倾向于使用普通的旧 filter (同样,当我不关心过滤结果时,因为它只是使用相反的谓词),然后在我关心保留过滤和未过滤结果时使用separateremoveIf

问题:

那么,我是否遗漏了 Swift 中已经内置的东西?有没有更好的方法来实现这一点?我的扩展语法是否缺少任何东西(例如,任何可以使它将此概念应用于更多领域的东西)?

【问题讨论】:

  • 减少!!!!!! :D
  • 请进一步解释。如何减少我正在寻找的东西?我需要 2 个数组。 Reduce 生成单个元素。
  • 如果你的单个元素是两个数组的数组会怎样
  • 在我看来,这并不比 2 元组好多少。实际上,我认为它更糟糕,因为 2 元组保证只有 2 个值,但数组数组中可以包含任意数量的数组。
  • 在这种情况下,一个元组会更好,那么如果你的单个元素是一个([Int], [Int])的元组呢?

标签: arrays swift


【解决方案1】:
let objects: [Int] = Array(1..<11)
let split = objects.reduce(([Int](), [Int]())) { (value, object) -> ([Int], [Int]) in

    var value = value

    if object % 2 == 0 {
        value.1.append(object)
    } else {
        value.0.append(object)
    }

    return value
}

【讨论】:

  • 这与我在选项 2 中提出的内容有何不同?
  • 好吧,它使用reduce函数,它们都是O(n),但这只是另一种方法
  • O(n) 与您将获得的一样好,除非您事先知道元素以特定方式排序
  • 这实际上以二次而非线性时间运行,因为 value 将在每次迭代时被复制 - 请参阅 Airspeed Velocity's blog post on the subject
  • O(n) 不是问题。我的问题的重点是看看是否已经有一些内置函数可以做到这一点。如果没有,那么我想知道是否有比我提出的解决方案更快或更清洁的方法。
【解决方案2】:

Swift 4 解决方案

partition(by:)

它重新排序原始数组并返回满足谓词的子数组的开始索引。

在本例中,它返回 7。

0..

 var numbers = [1,2,3,4,5,6,7,8,9,10]
 let partition = numbers.partition(by: { $0 % 3 == 0 })
 let divisibleBy3 = Array(numbers[..<partition]) //[3,6,9]
 let theRest = Array(numbers[partition...]) //[1,2,4,5,7,8,10]

【讨论】:

  • partition,正如我之前所说,不保留排序,因此不符合初始要求。
【解决方案3】:

从技术上讲,这并不能保证保持秩序,但确实如此。

Dictionary(grouping: numbers) { $0.isMultiple(of: 3) }

https://github.com/apple/swift/blob/master/stdlib/public/core/NativeDictionary.swift

【讨论】:

  • 我喜欢这个。我很想找到不保留订单的示例。这当然是一个非常紧凑的解决方案。我也必须对其进行性能测试。
  • 我做了一些基准测试,我的选项 2 中的自定义 separate 函数的性能略高一些,但不是很多。我找不到任何说明“分组”初始化不能保证保留顺序的文档,但从查看源代码来看,它看起来确实如此。它只是将匹配的条目按顺序添加到 2 个存储桶之一。这是一个很好的初始化器。
  • 我认为这是我最接近简洁地回答“所以,我是否遗漏了 Swift 内置的东西已经这样做了?”,所以我将其标记为答案。我想让其他人知道分组初始化器的荣耀。
【解决方案4】:
// swap and return pivot
extension Array
{
    // return pivot
    mutating func swapIf(predicate: (Element) -> Bool) -> Int
    {
        var pivot = 0
        for i in 0..<self.count
        {
            if predicate( self[i] )
            {
                if i > 0
                {
                    swap(&self[i], &self[pivot])
                }

                pivot += 1
            }
        }

        return pivot
    }
}

这是我的代码,概念是..减少内存使用。

我检查了 'swapIf' 比 'removeIf' 快 4 倍。

【讨论】:

  • 关闭。您创建的内容实际上已经是 Swift 标准库的一部分。它被称为partition (developer.apple.com/reference/swift/array/2294611-partition)。虽然它很快,但它并不能保持秩序,这也是它如此之快的部分原因。而且由于不能保证我的元素按特定的排序顺序,我也不能只对 2 个结果数组进行排序。但是你给了我一个可能更快解决方案的想法。
  • 查看我的更新答案。保留顺序的解决方案仍然比选项 2 和 4 慢。但如果您不关心顺序,您的解决方案(或partition)肯定是要走的路。
【解决方案5】:

解决方案 A

对于较少的元素,这可能是最快的。

extension Array {
    func stablePartition(by condition: (Element) -> Bool) -> ([Element], [Element]) {
        var matching = [Element]()
        var nonMatching = [Element]()
        for element in self {
            if condition(element) {
                matching.append(element)
            } else {
                nonMatching.append(element)
            }
        }
        return (matching, nonMatching)
    }
}

用法

let numbers = [1,2,3,4,5,6,7,8,9,10]
let (divisibleBy3, theRest) = numbers.stablePartition { $0 % 3 == 0 }
print("divisible by 3: \(divisibleBy3), the rest: \(theRest)")
// divisible by 3: [3, 6, 9], the rest: [1, 2, 4, 5, 7, 8, 10]

解决方案 B

对于许多元素,这可能会更快,因为分配更少。我没有衡量性能。

extension Array {
    public func stablePartition(by condition: (Element) throws -> Bool) rethrows -> ([Element], [Element]) {
        var indexes = Set<Int>()
        for (index, element) in self.enumerated() {
            if try condition(element) {
                indexes.insert(index)
            }
        }
        var matching = [Element]()
        matching.reserveCapacity(indexes.count)
        var nonMatching = [Element]()
        nonMatching.reserveCapacity(self.count - indexes.count)
        for (index, element) in self.enumerated() {
            if indexes.contains(index) {
                matching.append(element)
            } else {
                nonMatching.append(element)
            }
        }
        return (matching, nonMatching)
    }
}

【讨论】:

  • 它是 O(n),我怀疑你能得到比这更有效的。
  • 数组有一个reserveCapacity(),可以减少分配的数量,也许可以提高性能。
  • 这本质上是我原始帖子中的选项 2,只是特定于数组而不是集合。我还展示了选项 4 如何结合使用选项 2 和选项 3 来专门针对数组执行。所以这个解决方案与已经提出的选项并没有什么不同。
  • @TimFuqua 我刚刚添加了另一个解决方案,所以现在有 A + B。我最初编写了解决方案 B。第二天早上我删除了 Set 代码并最终得到了解决方案 A,这非常类似于您的选项 2 代码。纯属巧合:-) 你认为CollectionArray 好吗?
  • 您会注意到我的选项 2 是 Collection 的扩展。这是因为您可以迭代任何集合。选项 3,以及随后的选项 4,它是选项 2 和选项 3 的组合,是对 Array 的扩展,因为它需要通过索引查找来修改元素组,这不能在每个 Collection 上执行,但你可以在一个数组上。
【解决方案6】:

在 WWDC 2018 session Embracing Algorithm 他们提到了函数stablePartition,你可以看看这里https://github.com/apple/swift/blob/master/test/Prototypes/Algorithms.swift

extension Collection where Self : MutableCollectionAlgorithms {

  @discardableResult
  mutating func stablePartition(
    isSuffixElement: (Element) throws -> Bool
  ) rethrows -> Index {
        return try stablePartition(
            count: count, isSuffixElement: isSuffixElement)
  }

    /// Moves all elements satisfying `isSuffixElement` into a suffix of the collection,
    /// preserving their relative order, returning the start of the resulting suffix.
    ///
    /// - Complexity: O(n) where n is the number of elements.
    /// - Precondition: `n == self.count`
    fileprivate mutating func stablePartition(
        count n: Int, isSuffixElement: (Element) throws-> Bool
    ) rethrows -> Index {
        if n == 0 { return startIndex }
        if n == 1 {
            return try isSuffixElement(self[startIndex]) ? startIndex : endIndex
        }
        let h = n / 2, i = index(startIndex, offsetBy: h)
        let j = try self[..<i].stablePartition(
            count: h, isSuffixElement: isSuffixElement)
        let k = try self[i...].stablePartition(
            count: n - h, isSuffixElement: isSuffixElement)
        return self[j..<k].rotate(shiftingToStart: i)
    }
}

【讨论】:

  • Xcode10 中 Swift 4.2 的标准库中没有它,或者我遗漏了什么?
  • 我最初的问题是在考虑 Swift 2 或 3 的情况下编写的,因此随着 Swift 4 的继续发布,该语言很可能会有新功能。但是如果有人仍在使用旧的 Swift 代码,那么提供的其他信息仍然是相关的。但我会密切关注 Swift 4.2 的新功能。
  • 我在使用 Swift 4.2 时遇到 "Use of undeclared type 'MutableCollectionAlgorithms'" 错误。
【解决方案7】:

有一个新的Swift Algorithms open-source 用于序列和集合算法,以及它们的相关类型。

你可以从那里使用稳定的分区

在可变集合上执行稳定分区的方法,以及 在已分区的集合中查找分区索引。

标准库现有的partition(by:) 方法,它重新排序 基于给定谓词将集合中的元素分成两个分区,不 保证任一分区的稳定性。也就是元素的顺序 每个分区不一定匹配它们在原始分区中的相对顺序 收藏。这些新方法扩展了现有的partition(by:) 为一个或两个分区提供稳定性。

// existing partition(by:) - unstable ordering
var numbers = [10, 20, 30, 40, 50, 60, 70, 80]
let p1 = numbers.partition(by: { $0.isMultiple(of: 20) })
// p1 == 4
// numbers == [10, 70, 30, 50, 40, 60, 20, 80]

// new stablePartition(by:) - keeps the relative order of both partitions
numbers = [10, 20, 30, 40, 50, 60, 70, 80]
let p2 = numbers.stablePartition(by: { $0.isMultiple(of: 20) })
// p2 == 4
// numbers == [10, 30, 50, 70, 20, 40, 60, 80]

由于在分而治之的算法中经常使用分区,我们也 包括一个接受范围参数的变体,以避免在变异时复制 切片,以及现有标准库的基于范围的变体 分区。

partitioningIndex(where:) 方法返回开始的索引 在已分区的集合上调用第二个分区。

let numbers = [10, 30, 50, 70, 20, 40, 60]
let p = numbers.partitioningIndex(where: { $0.isMultiple(of: 20) })
// numbers[..<p] == [10, 30, 50, 70]
// numbers[p...] = [20, 40, 60]

【讨论】:

    猜你喜欢
    • 2018-01-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-17
    • 1970-01-01
    • 1970-01-01
    • 2019-03-15
    • 2019-05-03
    相关资源
    最近更新 更多