Swift中的一个高效函数,它根据 predicate 将一个数组分成2个数组

3pvhb19x  于 12个月前  发布在  Swift
关注(0)|答案(7)|浏览(137)

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

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

选项1:使用filter

使用filter,你只得到能被3整除的数字列表,原始列表保持不变。然后你可以用相反的 predicate 再次过滤原始列表,但这是不必要的第二次。代码看起来像这样:

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]

字符串
这确实是非常可读的,但是它做了两次传递的事实对我来说似乎效率很低,特别是如果 predicate 更复杂的话。这是实际需要的检查的两倍。

选项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运行每个Option,迭代了1000次。结果如下:

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

选项5:基于negaipro的回答

我知道partition,但我不打算考虑它,因为它不保留排序顺序。negaipro的答案本质上是partition,但它让我思考。partition的想法是交换与枢轴点匹配的元素,这样就确保了在结束枢轴点一侧的所有内容都将与 predicate 匹配,而另一侧则不匹配。我采用了这个想法,并将操作改为“移动到结束”.所以火柴会从它们的位置移走,并添加到末尾。

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(同样,当我不关心过滤结果时,因为它只是使用相反的 predicate ),然后当我关心保留过滤和未过滤的结果时,使用separateremoveIf

问题:

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

bzzcjhmw

bzzcjhmw1#

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

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

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

5us2dqdw

5us2dqdw2#

Swift 4解决方案

partition(by:)
它重新排序原始数组并返回满足 predicate 的子数组的开始索引。
在这个例子中,它返回7。
0..<7的元素不能被3整除,7.. n-1的元素可以被3整除。

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]

字符串

9rygscc1

9rygscc13#

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
}

字符串

lc8prwob

lc8prwob4#

有一个新的Swift Algorithms open-source用于序列和集合算法,沿着它们的相关类型。
您可以从那里使用稳定分区
对可变集合执行稳定分区以及在已分区的集合中查找分区索引的方法。
标准库现有的partition(by:)方法,根据给定的 predicate 将集合中的元素重新排序到两个分区中,并不能保证任何一个分区的稳定性。也就是说,每个分区中元素的顺序并不一定与它们在原始集合中的相对顺序相匹配。这些新方法通过为一个或两个分区提供稳定性来扩展现有的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]

n8ghc7c1

n8ghc7c15#

// 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倍。

cgyqldqp

cgyqldqp6#

解决方案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)
    }
}

ykejflvf

ykejflvf7#

在WWDC 2018会议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)
    }
}

字符串

相关问题