在Swift中,一个有效的函数,它根据谓词

时间:2016-10-13 00:15:02

标签: arrays swift

注意:我目前仍在使用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:在separate扩展名

中使用自定义Collection功能

我的下一次尝试是扩展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:在removeIf扩展名

中使用自定义的变异Array函数

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的扩展名中实现,因为删除特定索引处的元素的概念不适用于常规Collections(定义{{​​1}}作为Array,它直接定义public struct Array<Element> : RandomAccessCollection, MutableCollection函数,而不是从继承或协议中获取它。

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

我是代码重用的忠实粉丝,在提出选项3之后,我意识到我可能会重用Option 2中的remove(at:)函数。我想出了这个:

separate

它就像在选项3中一样使用。

我关注性能,所以我通过XCTest的extension Array { mutating func removeIf(predicate: (Element) -> Bool) -> [Element] { let groups = self.separate(predicate: predicate) self = groups.notMatching return groups.matching } } 运行每个选项1000次迭代。结果如下:

measure

选项5:基于negaipro的回答

我知道Option 1: 9 ms Option 2: 7 ms Option 3: 10 ms Option 4: 8 ms ,但我不会考虑它,因为它没有保留排序顺序。 negaipro的回答基本上是partition,但它让我思考。 partition的想法是交换与枢轴点匹配的元素,从而确保端点枢轴点一侧的所有内容都与谓词匹配,而另一侧则不然。我接受了这个想法并将行动改为“走到尽头”。所以匹配从他们的位置移除并添加到最后。

partition

在我使用带有10个数字的数组的初始测试中,它与其他选项相当。但我关心的是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 } } 行的表现。所以我再次尝试了所有的选项,数组从1到1000,这个选项绝对是最慢的。

结论:

这些选项之间没有太大的性能差异。由于选项4比选项3快,并且重用了选项2中的代码,我倾向于抛弃选项3.所以当我不关心未经过滤时,我倾向于使用普通的append(remove(at: index))结果(同样地,当我不关心过滤结果时,因为它只是使用相反的谓词),然后使用filterseparate当我关心保持过滤结果时和未经过滤的结果。

问题:

那么,我是否遗漏了Swift内置的内容呢?有没有更好的方法来实现这一目标?我的扩展语法是否缺少任何内容(例如,任何可以将此概念应用于更多区域的内容)?

7 个答案:

答案 0 :(得分:3)

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
}

答案 1 :(得分:1)

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

答案 2 :(得分:1)

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

答案 3 :(得分:1)

在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)
    }
}

答案 4 :(得分:1)

Swift 4解决方案

partition(by:)

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

在此示例中,它返回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]

答案 5 :(得分:1)

从技术上讲,这不能保证保留顺序,但是可以。

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

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

答案 6 :(得分:1)

有一个新的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]