web-dev-qa-db-ja.com

Swift配列を別の配列からの順序で並べ替える

カスタムクラス[Player]の配列があり、それぞれにplayer.positionという文字列プロパティが含まれているとしましょう

次のように、positionOrdersと呼ばれる任意の値の配列もあります。

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

私の目標は、[Player]を並べ替えて、すべての「QB」を最初に、次に「WR」、「RB」、最後に「TE」を並べることです。

私が現在行っている方法は、positionOrdersの各要素をループし、その内部ですべてのプレーヤーをループして新しい配列に追加します。しかし、これを行うためのより簡単な(そしてより効率的な)方法を理解できませんでした。ヒントやポインタは大歓迎です。ありがとう。

18
daspianist

編集:私の最初のアプローチはたわごとでした。この投稿は多くの注目を集めたので、もっと注目して改善する時がきました。


基本的に、問題は簡単です。 2つの要素があり、配列(または任意の順序付けられたCollection)があり、その相対的な順序がソート順序を決定します。すべての要素について、順序付けられたコレクション内での位置を見つけ、2つのインデックスを比較して、どちらが「大きい」かを判断します。

ただし、線形検索(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"]

これは、一般的なSwift OuSS 'コードに基づく4ソリューションであり、配列要素がEquatableである必要があります。

extension Array where Element: Equatable {

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

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

            guard let second = preferredOrder.index(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"]
10
RobertChals