我正在寻找创建一个与filter
非常接近的函数,除了它保持不匹配的结果,并维护排序顺序。例如,假设您想过滤掉数组中可被3整除的数字,并且仍然保留不能被3整除的数字列表。
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次传递的事实对我来说似乎效率低下,特别是如果谓词更复杂的话。这是实际需要的支票的两倍。
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
相同,您可以将其中任何一个链接起来。
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
函数,而不是从继承或协议中获取它。
我是代码重用的忠实粉丝,在提出选项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
我知道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))
结果(同样地,当我不关心过滤结果时,因为它只是使用相反的谓词),然后使用filter
或separate
当我关心保持过滤结果时和未经过滤的结果。
那么,我是否遗漏了Swift内置的内容呢?有没有更好的方法来实现这一目标?我的扩展语法是否缺少任何内容(例如,任何可以将此概念应用于更多区域的内容)?
答案 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)
对于较少的元素,这可能是最快的。
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]
对于许多元素,这可能会更快,因为分配更少。我没有测量过表现。
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解决方案
它对原始数组重新排序,并返回满足谓词的子数组的起始索引。
在此示例中,它返回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]