从另一个数组中的顺序对 Swift 数组进行排序

37

假设我有一个自定义类的数组[Player],每个元素都包含一个称为player.position的字符串属性。

我还有一个任意的值数组,名为positionOrders,如下所示:

let positionOrders = ["QB", "WR", "RB", "TE"]

我的目标是将 [Player] 数组排序,让所有"QB"在前,然后是"WR"、"RB",最后是"TE"。

我目前的方法是循环遍历positionOrders中的每个元素,然后在其中循环遍历所有球员,将其附加到新数组中。但是,我无法想出更简单(更有效)的方法来做到这一点。任何提示或指针都将不胜感激。谢谢。


将订单转换为枚举或其他类型。在其中定义Comparable。使用该比较方法对玩家进行排序。 - Sulthan
数组positionOrders是常量还是在运行时会发生变化? - Sulthan
感谢评论。positionOrders是静态的(来自于常量“struct”)。@Alexander 谢谢您的建议。您提出的建议非常有效。 - daspianist
能否请给这个问题点踩的人留下一条评论,说明如何改进这个问题? - daspianist
1
@daspianist,如果数组是静态的,为什么不将您的位置定义为具有Int作为rawValue的枚举?只是好奇。因为这可能会使您的排序函数更容易实现。 - antonio081014
这是一个很好的建议。那样做也会更有效率 - 感谢你的参与 @antonio081014 - daspianist
10个回答

42

这是一个通用的Swift 5.2解决方案。

首先,我们需要定义一个协议,其中包含一个用于重新排序元素的属性:

protocol Reorderable {
    associatedtype OrderElement: Equatable
    var orderElement: OrderElement { get }
}

接下来,我们需要扩展 Array ,让其包含符合 Reorderable 接口的元素,并实现我们的重排序函数:

extension Array where Element: Reorderable {

    func reorder(by preferredOrder: [Element.OrderElement]) -> [Element] {
        sorted {
            guard let first = preferredOrder.firstIndex(of: $0.orderElement) else {
                return false
            }

            guard let second = preferredOrder.firstIndex(of: $1.orderElement) else {
                return true
            }

            return first < second
        }
    }
}

示例

假设您已经定义了:

struct Player {
    let position: String
}

let currentPositions = ["RB", "AA", "BB", "CC", "WR", "TE"]
let players = currentPositions.map { Player(position: $0) }

现在,您所需要做的就是将Player适配为Reorderable

extension Player: Reorderable {
    typealias OrderElement = String
    var orderElement: OrderElement { position }
}

现在您可以使用:

let sortedPlayers = players.reorder(by: ["QB", "WR", "RB", "TE"])

print(sortedPlayers.map { $0.position })
// ["WR", "RB", "TE", "AA", "BB", "CC"]

原始解决方案

这是一个通用的Swift 5.2解决方案,基于OuSS的代码,并要求数组元素是可比较的。


extension Array where Element: Equatable {

    func reorder(by preferredOrder: [Element]) -> [Element] {

        return self.sorted { (a, b) -> Bool in
            guard let first = preferredOrder.firstIndex(of: a) else {
                return false
            }

            guard let second = preferredOrder.firstIndex(of: b) else {
                return true
            }

            return first < second
        }
    }
}

let currentPositions = ["RB", "AA", "BB", "CC", "WR", "TE"]
let preferredOrder = ["QB", "WR", "RB", "TE"]
let sorted = currentPositions.reorder(by: preferredOrder)
print(sorted) // ["WR", "RB", "TE", "AA", "BB", "CC"]

3
好的解决方案。虽然您使用了相同类型的对象,但可以将其适应为在每个数组中使用不同类型的对象,例如按首选顺序表示要排序的较大对象中的一个字段。 - Cristi Băluță
@CristiBăluță 谢谢,很好的观察!我一开始提供的解决方案没有意识到这一点,但现在已经编辑过来按照您的建议去执行。 - RobertChals
1
我本来想发表一个答案,就像你最近包含的新解决方案一样,但我特别喜欢你包含守卫语句的方式。超级优雅。这绝对是现在最好的答案。 - Lyndsey Scott
很不幸,这是可怕且毫无意义的...因为.sort已经是通用的了。 - Fattie

39

编辑:我的原始方法很糟糕。这篇文章引起了很多关注,因此现在是时候给它更多的关注并改进它。


基本上,问题很简单。我们有两个元素,并且我们有一个数组(或任何有序Collection),其相对顺序决定它们的排序顺序。对于每个元素,我们找到它在有序集合中的位置,并比较两个索引以确定哪个“更大”。

然而,如果我们天真地进行线性搜索(例如Array.firstIndex(of:)),我们将获得非常糟糕的性能(O(array.count)),特别是如果固定顺序非常大。为了解决这个问题,我们可以构建一个Dictionary,将元素映射到它们的索引。字典提供快速的O(1)查找,非常适合这项工作。

这正是HardCodedOrdering所做的。它预先计算元素到它们的顺序的字典,并提供一个接口来比较2个元素。更好的是,它可以配置为在遇到具有未知顺序的元素时以不同方式响应。它可以将它们放在所有其他元素之前,最后放在所有其他元素之后,或者完全崩溃(默认行为)。

HardCodedOrdering

public struct HardCodedOrdering<Element> where Element: Hashable {
    public enum UnspecifiedItemSortingPolicy {
        case first
        case last
        case assertAllItemsHaveDefinedSorting
    }

    private let ordering: [Element: Int]
    private let sortingPolicy: UnspecifiedItemSortingPolicy

    public init(
        ordering: Element...,
        sortUnspecifiedItems sortingPolicy: UnspecifiedItemSortingPolicy = .assertAllItemsHaveDefinedSorting
    ) {
        self.init(ordering: ordering, sortUnspecifiedItems: sortingPolicy)
    }

    public init<S: Sequence>(
        ordering: S,
        sortUnspecifiedItems sortingPolicy: UnspecifiedItemSortingPolicy = .assertAllItemsHaveDefinedSorting
    ) where S.Element == Element {

        self.ordering = Dictionary(uniqueKeysWithValues: zip(ordering, 1...))
        self.sortingPolicy = sortingPolicy
    }

    private func sortKey(for element: Element) -> Int {
        if let definedSortKey = self.ordering[element] { return definedSortKey }

        switch sortingPolicy {
            case .first:    return Int.min
            case .last:     return Int.max

            case .assertAllItemsHaveDefinedSorting:
                fatalError("Found an element that does not have a defined ordering: \(element)")
        }
    }

    public func contains(_ element: Element) -> Bool {
        return self.ordering.keys.contains(element)
    }

    // For use in sorting a collection of `T`s by the value's yielded by `keyDeriver`.
    // A throwing varient could be introduced, if necessary.
    public func areInIncreasingOrder<T>(by keyDeriver: @escaping (T) -> Element) -> (T, T) -> Bool {
        return { lhs, rhs in
            self.sortKey(for: keyDeriver(lhs)) < self.sortKey(for: keyDeriver(rhs))
        }   
    }

    // For use in sorting a collection of `Element`s
    public func areInIncreasingOrder(_ lhs: Element, rhs: Element) -> Bool {        
        return sortKey(for: lhs) < sortKey(for: rhs)
    }
}

示例用法:


let rankOrdering = HardCodedOrdering(ordering: "Private", "Lieutenant", "Captain", "Admiral") // ideally, construct this once, cache it and share it

let someRanks = [
    "Admiral", // Should be last (greatest)
    "Gallactic Overlord", // fake, should be removed
    "Private", // Should be first (least)
]
let realRanks = someRanks.lazy.filter(rankOrdering.contains)
let sortedRealRanks = realRanks.sorted(by: rankOrdering.areInIncreasingOrder) // works with mutating varient, `sort(by:)`, too.

print(sortedRealRanks) // => ["Private", "Admiral"]

这是完全错误的。在几乎所有领域中,对索引进行哈希处理的性能都较差!没有比不必要的“性能代码”更糟糕的工程了。如果令人难以置信地需要性能,则需要采用上游方法(例如空间哈希)。 - Fattie
此外,我必须说,在我看来,你在这个网站上的行为方式一直是批判性的、歇斯底里的和完全没有建设性的。这真的很不愉快。在所有评论中添加斜体和感叹号并不能更好地表达你的观点。如果你想要下投票,那么请提出更好的替代方案,如果你想要真正地为这里带来任何价值。我不认为这段代码是完美的,但在我的情况下它确实有明显的性能提升(也许我应该澄清什么时候达到平衡点),我已经进行了分析和测试。 - Alexander
排序列表每次都不同。请注意,例如,在 UI 中,您只需针对其他列表进行排序即可实现常见的情况,例如在选项卡或类似选项中,用户可以从十个列表中选择一些。您只需让用户编辑,然后根据“主列表”进行排序以保持列表有序。我的观点是,您所提供的(完全合理的)提示仅在某些环境中有用。请注意,这个问题只是“如何在 Swift 中排序”的问题。(答案是使用.sort :) :))当然,对于此站点上的任何问题, - Fattie
语气在文本中很难读懂,所以我可能完全错了,但原始语气只是毫无建设性地批评,没有建设性的精神。你暗示这是不必要的“糟糕工程”实际上是错误的。我有收据,我已经对此进行了分析,看到它在我的用例中有所不同。你可以争论我的用例(ordering中有很多项,被哈希一次并在整个应用程序生命周期内重复使用)是非典型的,如果你能以更具建设性的方式提出来,我会倾向于同意你的观点。 - Alexander
在这里也存在一种哲学差异。我的编程风格旨在简化业务逻辑调用点(例如,仅调用 realRanks.sorted(by: rankOrdering.areInIncreasingOrder),而不是带有内联块的sort)。即使没有特别的性能问题,我仍然更喜欢在我的项目中采用这种方法,因为这是我可以提取/测试一次、放在一边并无后顾之忧地使用的东西。部分原因是因为它更容易对该单独组件进行单元测试。我认为你的哲学更像Go社区所偏好的,这也很好。 - Alexander
显示剩余10条评论

10

根据Alexander的回答,我已经实现了一个扩展来完成这个功能。

extension Array where Element == String {

func reordered() -> [String] {

    let defaultOrder = ["orange", "pear", "watermelon", "grapefruit", "apple", "lemon", "tomatoes"]

    return self.sorted { (a, b) -> Bool in
        if let first = defaultOrder.index(of: a), let second = defaultOrder.index(of: b) {
            return first < second
        }
        return false
    }
}

let arrayToSort = ["lemon", "watermelon", "tomatoes"]
let sortedArray = arrayToSort.reordered()
print(sortedArray) // ["watermelon", "lemon", "tomatoes"]

定义字符串数组的默认排序不是你应该做的事情,因为你不是StringArray的作者。如果其他人尝试使用它来重新排序他们自己的字符串会怎样呢?答案是:他们不能。扩展Array<YourOwnEnumType>就可以了。 - Alexander

8

SwifterSwift 有这个实现

/// SwifterSwift: Sort an array like another array based on a key path. If the other array doesn't contain a certain value, it will be sorted last.
    ///
    ///        [MyStruct(x: 3), MyStruct(x: 1), MyStruct(x: 2)].sorted(like: [1, 2, 3], keyPath: \.x)
    ///            -> [MyStruct(x: 1), MyStruct(x: 2), MyStruct(x: 3)]
    ///
    /// - Parameters:
    ///   - otherArray: array containing elements in the desired order.
    ///   - keyPath: keyPath indiciating the property that the array should be sorted by
    /// - Returns: sorted array.
    func sorted<T: Hashable>(like otherArray: [T], keyPath: KeyPath<Element, T>) -> [Element] {
        let dict = otherArray.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
        return sorted {
            guard let thisIndex = dict[$0[keyPath: keyPath]] else { return false }
            guard let otherIndex = dict[$1[keyPath: keyPath]] else { return true }
            return thisIndex < otherIndex
        }
    }

2
不使用库的一个很好的例子。该函数完全内置于Swift中。 - Fattie

8

这非常简单:

positionOrders.filter{player.contains($0)}

这就是全部内容。

更为明显的方法:

排序的方式就是这样的:

    player.sort{
        .. your formula for sorting goes here
    }

例如,如果您想按“银行余额”排序。
    player.sort{
        $0.bankBalance < $1.bankBalance
    }

按照"另一个数组"排序,只需要

    player.sort{
        let i = positionOrders.firstIndex(of: $0) ?? 0
        let j = positionOrders.firstIndex(of: $1) ?? 0
        return i < j
    }

就是这样。

(请注意,如果您希望“缺失”项出现在末尾而不是开头,可以使用Int.max而不是0,如果在您的用例中可能存在缺失项。)


(请注意,关于此页面性能的评论完全错误。在所述环境中,性能问题是不可想象的。如果令人难以置信地超越了所有信仰,性能成为一个问题,您只需添加一行代码来哈希索引即可。但要小心,实际上制作哈希在几乎所有情况下都会导致性能下降。再次强调,性能问题极不可能出现;请使用上面的代码。)


2
这个答案几乎是 https://dev59.com/8VgQ5IYBdhLWcg3wPxrc#65783023 的完全副本。 - Alexander
1
点赞因为这是一个解释清晰、格式良好的答案。感谢@Fattie的贡献。 - daspianist
我猜数组排序的最坏时间复杂度是 O(m n log n),其中 m 是排序数组的长度,n 是待排序数组的长度... - undefined

5

我将会做以下工作:

  1. 创建一个字典,以职位为键,以该职位的球员数组为值。时间复杂度为O(n),其中n为球员数量。
  2. 循环遍历你的positionOrders,并将每个键(职位)对应的值取出。

以下是代码:

    let preSortPlayerList = [Player]() // Filled with your players.
    let positionOrders = ["QB", "WR", "RB", "TE"]
    let dict = preSortPlayerList.reduce([String : [Player]]()) {
        var map = $0
        if var tmp = map[$1.position] {
            tmp.append($1)
            map[$1.position] = tmp
        } else {
            map[$1.position] = [$1]
        }
        return map
    }

    let playersArray: [Player] = positionOrders.flatMap { dict[$0] ?? [Player]() }
    print("\(playersArray)")

你可以使用 flatmap 来创建 playersArray - Alexander
@Alexander,实际上你说得很对。我刚刚更新了答案。谢谢。 - antonio081014

4

针对Swift 4版本,有一种非常简单的方法和勤奋的方式(欢迎您提出建议并纠正我)。

func reOrder(array : [String] , order : [String]) -> [String]{ 

    //common elments in order
    // order.filter{array.contains($0)}

    // the rest of the array that the order doesnt specify
    // array.filter{!order.contains($0)}

 return order.filter{array.contains($0)} + array.filter{!order.contains($0)}
}


let list =     ["A", "Z", "B", "H", "C", "T", "D", "E"] 
let newOrder = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"]




print("The ordered list is ",reOrder(array: list, order: newOrder))


 // The ordered list is  ["A", "B", "C", "D", "E", "H", "Z", "T"]

希望有人能将其作为通用类型的扩展,我对此并不擅长。


2
这是一个简单直接的解决方案,但如果你有大量的对象,它看起来并不是很高效。 - Cristi Băluță

3
我喜欢简洁的语句,这里有另一句:

我喜欢一行代码,这里又有一行:

positionOrders.compactMap { order in players.first(where: { $0.position == order })}

这种方法需要考虑的唯一问题是它将具有 O(n*m),或者说 O(positionOrders*players) 的时间复杂度。


它具有O^2的时间复杂度。 - user464230
通常情况下,为了进行排序,您可以使用Swift函数....sort :) :) - Fattie

1

基于Emily代码,我对扩展进行了一些更改,因为它不会将默认顺序中不存在的元素放在最后。

extension Array where Element == String {

func reordered() -> [String] {

    let defaultOrder = ["lemon", "watermelon", "tomatoes"]

    return self.sorted { (a, b) -> Bool in
        guard let first = defaultOrder.index(of: a) else {
            return false
        }

        guard let second = defaultOrder.index(of: b) else {
            return true
        }

        return first < second
    }
}

let arrayToSort = ["orange", "watermelon", "grapefruit", "lemon", "tomatoes"]
let sortedArray = arrayToSort.reordered()
print(sortedArray) // ["watermelon", "lemon", "tomatoes", "orange", "grapefruit"]

2
你不应将数据(默认顺序数组)添加到扩展中。 - Casper Zandbergen

0
自iOS15起,我们可以使用SortComparator轻松地按照自定义逻辑对对象进行排序。
例如,一个SortComparator可以按照预定义的位置顺序对球员进行排序。如果位置未包含在顺序中,则按照球员ID进行排序。
struct Player: Equatable {
    let id: Int
    let position: String
}

struct PlayerSort: SortComparator {
    let positionOrder: [String: Int]
    var order: SortOrder
    
    init(positionOrder: [String]) {
        self.order = .forward
        var order = [String: Int]()
        positionOrder.enumerated().forEach {
            order[$1] = $0
        }
        self.positionOrder = order
    }

    func compare(_ lhs: Player, _ rhs: Player) -> ComparisonResult {
        if positionOrder[lhs.position] ?? Int.max < positionOrder[rhs.position] ?? Int.max {
            return .orderedAscending
        } else if positionOrder[lhs.position] ?? Int.max > positionOrder[rhs.position] ?? Int.max {
            return .orderedDescending
        } else {
            return KeyPathComparator(\Player.id).compare(lhs, rhs)
        }
    }
}

使用方法

[player1, player2, player3, player4, player5].sorted(using: PlayerSort(positionOrder: ["QB", "WR", "RB", "TE"]))

文档:

  1. https://developer.apple.com/documentation/foundation/sortcomparator
  2. https://useyourloaf.com/blog/sortcomparator-and-sortdescriptor/

这是比微不足道的操作所需代码多得多的代码。 - Fattie

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