Swift中优雅地拆分数组的方法

13

给定任何类型的数组和所需的子数组数量,我需要这个输出:

print([0, 1, 2, 3, 4, 5, 6].splitInSubArrays(into: 3))
// [[0, 3, 6], [1, 4], [2, 5]]

即使元素不足以填充子数组,输出必须包含正确数量的子数组:


print([0, 1, 2].splitInSubArrays(into: 4))
// [[0], [1], [2], []]

我现在有一个可行的实现,但是否有更好(更优雅)的方法来实现这个输出:

extension Array {

    func splitInSubArrays(into size: Int) -> [[Element]] {

        var output: [[Element]] = []

        (0..<size).forEach {

            var subArray: [Element] = []

            for elem in stride(from: $0, to: count, by: size) {
                subArray.append(self[elem])
            }

            output.append(subArray)
        }

        return output
    }
}

请注意,您所做的一切都是:按余数排序。你可以通过除法立即得到这些结果。(如果相关的话,您不需要实际制作这三个数组!) - Fattie
5个回答

21

您可以使用map()操作替换两个循环:

extension Array {
    func splitInSubArrays(into size: Int) -> [[Element]] {
        return (0..<size).map {
            stride(from: $0, to: count, by: size).map { self[$0] }
        }
    }
}

外部的 map() 将每个偏移量映射到相应的数组,内部的 map() 将索引映射到数组元素。

示例:

print([0, 1, 2, 3, 4, 5, 6].splitInSubArrays(into: 3))
// [[0, 3, 6], [1, 4], [2, 5]]

print([0, 1, 2].splitInSubArrays(into: 4))
// [[0], [1], [2], []]

1
干得好。我一直在想这个问题,而你提出了一个简单、优雅的解决方案。(投票) - Duncan C
1
这太美妙了,现在显而易见 :) - Manel
@Manel 在算法中,% 是你的好朋友 :) - Fattie
嘿@Fattie,抱歉,"%"是什么意思? :) - Manel
2
啊,我只是指“余数”。 - Fattie

6

仅供娱乐,这是一个通用实现,可与字符串一起使用:

extension Collection {
    func every(n: Int, start: Int = 0) -> UnfoldSequence<Element,Index> {
        sequence(state: dropFirst(start).startIndex) { index in
            guard index < endIndex else { return nil }
            defer { index = self.index(index, offsetBy: n, limitedBy: endIndex) ?? endIndex }
            return self[index]
        }
    }
}

extension RangeReplaceableCollection {
    func splitIn(subSequences n: Int) -> [SubSequence] {
        (0..<n).map { .init(every(n: n, start: $0)) }
    }
}

[0, 1, 2, 3, 4, 5, 6].splitIn(subSequences: 3)   // [[0, 3, 6], [1, 4], [2, 5]]
[0, 1, 2].splitIn(subSequences: 4)               // [[0], [1], [2], []]
"0123456".splitIn(subSequences: 3)               // ["036", "14", "25"]

6

KISS,算法匹配方法:

最直观的方法非常简单:

  • 对于每个索引
  • 求除以三的余数
  • 将数字放入那个数组中

因此,它实际上仅仅是这样:

arrays[i%n].append(item i)

以下是根据 @LeoDabus 评论提供的示例代码:

extension RangeReplaceableCollection {
    func moduloishtrancheization(n: Int) -> [SubSequence] {
        var r: [SubSequence] = .init(repeating: .init(), count: n)
        var i = 0
        forEach {
            r[i%n].append($0)
            i += 1
        }
        return r
    }
}

这就是整个事情。


1
扩展范围可替换集合{ func splitIn(subSequences n: Int) -> [SubSequence] { var result: [SubSequence] = .init(repeating: .init(), count: n) var index = 0 forEach { result[index%n].append($0) index += 1 } return result } } - Leo Dabus
我不确定这个“余数方法”是否比OP、Leo和我的方法更有效,我们的方法只是将一个固定数字反复*加到偏移量上。但正如你所说,这可能并不重要。 - Martin R
MartinR,关于Swift的效率问题,我恐怕对于像序列、子序列、追加、切片、跨度等非常复杂的结构/操作在Swift中是如何运作的几乎一无所知。因此,我完全不清楚。 - Fattie
@Fattie 请随意编辑您的问题并将其发布到您的答案中。 - Leo Dabus
不错的点子 @LeoDabus。你知道吗,我经常想知道在使用"$0"时是否有一种方法可以获取“那个”索引(例如,笨拙的“var i”将消失,变成类似于result[$0.magicIndex % n].append ..)。 - Fattie
并非所有的集合都是由整数索引的。你可以从startIndex获取距离,但这样做不如在每次迭代中递增一个整数快。另一个选择是枚举你的集合,并像Cristik的答案所示获取偏移量。 - Leo Dabus

5

应该允许将其用于所有序列。

stride(from: 0, through: 6, by: 1).splitInSubArrays(into: 3)

如果它对许多应用程序都有用,像下面这个一样,请将其放入公共扩展中。

extension Sequence {
  func splitInSubArrays(into size: Int) -> [[Element]] {
    enumerated()
      .grouped { $0.offset % size }
      .map { $0.map(\.element) }
  }
}

  /// Group the elements by a transformation into an `Equatable`.
  /// - Note: Similar to `Dictionary(grouping values:)`,
  /// but preserves "key" ordering, and doesn't require hashability.
  func grouped<Equatable: Swift.Equatable>(
    by equatable: (Element) throws -> Equatable
  ) rethrows -> [[Element]] {
    try reduce(into: [(equatable: Equatable, elements: [Element])]()) {
      let equatable = try equatable($1)

      if let index = ( $0.firstIndex { $0.equatable == equatable } ) {
        $0[index].elements.append($1)
      } else {
        $0.append((equatable, [$1]))
      }
    }.map(\.elements)
  }

实际上,你只需要按余数分组。 - Fattie
Jessy,我可能误解了,但是你为什么要排序呢?在我能想到的任何情况下,你肯定希望保持原始数组中看到的顺序吧?如果我误解了,请原谅。 - Fattie
字典初始化器是Swift中唯一内置的分组算法。明白了,谢谢...但是...我太蠢了,不理解:实际上,生成的子数组是否与原始数组中看到的值顺序相同?谢谢 :O - Fattie
MartinR刚刚写的似乎是以Swift风格实现的最优雅的方式。我想。 - Fattie
你知道吗,我不太确定我能支持这个答案。在任何语言中,解决这个非常简单的问题的“通常方式”就是像L.Dabus提供的代码片段中那样,只需result[i%n].append($0)。简单地说,只是“基于余数向几个集合添加内容”。我想说,对我来说,像 result[i%n].append($0) 这样的表达方式最清楚地表达了你正在做 result[i%n].append($0)。 我稍微有点困扰,因为我必须执行复杂的编组操作才能使用当前(时髦的)分组功能或该语言。所以,这就是我的想法……干杯 - Fattie
显示剩余5条评论

5

为了完整性,这里提供一种基于reduce的解决方案,适用于所有Collection类型:

extension Collection {
    func splitInSubArrays(_ size: Int) -> [[Element]] {
        enumerated().reduce(into: [[Element]](repeating: [], count: size)) {
            $0[$1.offset % size].append($1.element)
        }
    }
}

函数的工作原理:它创建一个由[Element]条目组成的空数组,并将原始数组的每个元素附加到相应的子数组中。我们在这里使用reduce仅仅是为了传递结果数组,以避免显式地创建本地变量(虽然在内部reduce正在为我们执行这个操作)。

用法:

print([0, 1, 2, 3, 4, 5, 6].splitInSubArrays(3)) // [[0, 3, 6], [1, 4], [2, 5]]
print([0, 1, 2].splitInSubArrays(4))             // [[0], [1], [2], []]
print("ABCDEF".splitInSubArrays(3))              // ["A", "D"], ["B", "E"], ["C", "F"]]

请注意,正如Leo Dabus所指出的那样,在上面的最后一个示例中,2-D数组不是基于字符串的,而是一个2-D字符数组[[Character]]。要生成子字符串数组,可以扩展RangeReplaceableCollection,并将结果类型更改为[SubSequence]

1
请注意,这是执行此操作的绝大多数常见方式(例如,在游戏相关代码中经常看到)。@Cristik - 我相当确定L.D.只是在指出“啊,这就是Fattie提到的算法。” - Fattie
1
@Fattie 取决于饮料 :) - Cristik
@LeoDabus 这将有助于代码的大小,但不会帮助那些只是集合的许多其他类型。事实上,我可以在我的代码中简单地编写 enumerated().reduce(into: [[Element]](repeating: .init(), count: size)) {,并获得比你更少的字符 :) - Cristik
@Cristik,你没有理解我的意思。字符串的结果应该是子字符串的数组,而不是字符的数组 --> ["AD", "BE", "CF"],而不是 [["A", "D"], ["B", "E"], ["C", "F"]] 的数组。 - Leo Dabus
@Cristik 不同之处在于 RangeReplaceableCollection 需要一个空的初始化器。这就是你需要用来初始化空集合(如空字符串)的内容。这就是为什么我使用了 .init() 而不是 []。因此,您需要将 .init(repeating: [], count: size) 更改为 .init(repeating: .init(), count: size),并在扩展时更改返回类型。 - Leo Dabus
显示剩余11条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接