demiculus
demiculus

Reputation: 1333

How to stable sort an array in swift?

I've been using the sort() function but it mixes up the relative order.

This is how my code looks.

recipes.sort { $0.skill.value <= $1.skill.value }

Swift API says that:

The sorting algorithm is not stable. A nonstable sort may change the relative order of elements that compare equal.

How can I change this so that the relative order stays the same as before?

Upvotes: 21

Views: 4627

Answers (6)

jjrscott
jjrscott

Reputation: 1532

I appreciate the elegance of Tom's answer. Harking back to my Perl days I've reworked it to use ComparisonResult and the spaceship (<=>) operator:

extension Sequence {
    func sorted(with comparator: (Element, Element) throws -> ComparisonResult) rethrows -> [Element]
    {
        return try enumerated()
            .sorted { (try comparator($0.element, $1.element) || $0.offset <=> $1.offset) == .orderedAscending }
            .map { $0.element }
    }
}

extension Comparable {
    static func <=> (lhs: Self, rhs: Self) -> ComparisonResult {
        if lhs < rhs { return .orderedAscending }
        if lhs > rhs { return .orderedDescending }
        return .orderedSame
    }
}

extension ComparisonResult {
    static func || (lhs: Self, rhs: @autoclosure () -> Self) -> ComparisonResult {
        if lhs == .orderedSame {
            return rhs()
        }
        return lhs
    }
}

Upvotes: 0

Esqarrouth
Esqarrouth

Reputation: 39181

let sortedArray = (recipes as NSArray).sortedArray(options: .stable, usingComparator: { (lhs, rhs) -> ComparisonResult in
    let lhs = (lhs as! Recipe)
    let rhs = (rhs as! Recipe)
    if lhs.skill.value == rhs.skill.value {
        return ComparisonResult.orderedSame
    } else if lhs.skill.value < rhs.skill.value {
        return ComparisonResult.orderedAscending
    } else {
        return ComparisonResult.orderedDescending
    }
})

Took from here: https://medium.com/@cocotutch/a-swift-sorting-problem-e0ebfc4e46d4

Upvotes: 9

leavez
leavez

Reputation: 2218

The implementation below just work like the sorted method in the standard library, without additional limit.

extension RandomAccessCollection {

    /// return a sorted collection
    /// this use a stable sort algorithm
    ///
    /// - Parameter areInIncreasingOrder: return nil when two element are equal
    /// - Returns: the sorted collection
    public func stableSorted(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> [Element] {

        let sorted = try enumerated().sorted { (one, another) -> Bool in
            if try areInIncreasingOrder(one.element, another.element) {
                return true
            } else {
                return one.offset < another.offset
            }
        }
        return sorted.map { $0.element }
    }
}

A stable sort needs to preserve the original order. So we give every element a weight of order besides its value, the index, then the original sort method will just work, as there will never be 2 equal elements.

Upvotes: 21

kelin
kelin

Reputation: 11974

In Swift 5 sort() uses stable implementation and soon it will become officially guaranted to be stable.

From Swift forums:

...
On the other hand, the actual implementation calls

/// Sorts the elements of this buffer according to `areInIncreasingOrder`,
/// using a stable, adaptive merge sort.
///
/// The adaptive algorithm used is Timsort, modified to perform a straight
/// merge of the elements using a temporary buffer.
@inlinable
public mutating func _stableSortImpl(
    by areInIncreasingOrder: (Element, Element) throws -> Bool
) rethrows { ... }

And

If I recall, sort() is currently stable, but it is not yet guaranteed to be stable (meaning, the fact that it is stable is currently an implementation detail, and a future version of Swift could ship an unstable algorithm instead).

Upvotes: 2

Antzi
Antzi

Reputation: 13414

I use this wrapper

extension Array where Element: Comparable, Element: AnyObject {
    public func stableSorted() -> [Element] {
        let array = self as NSArray
        let result = array.sortedArray(options: .stable) { (left, right) -> ComparisonResult in
            let left = left as! Element
            let right = right as! Element
            if left < right {
                return ComparisonResult.orderedAscending
            }
            if left > right {
                return ComparisonResult.orderedDescending
            }
            return ComparisonResult.orderedSame
        }
        return result as! [Element]
    }
    public func stableReversed() -> [Element] {
        let array = self as NSArray
        let result = array.sortedArray(options: .stable) { (left, right) -> ComparisonResult in
            let left = left as! Element
            let right = right as! Element
            if left > right {
                return ComparisonResult.orderedAscending
            }
            if left < right {
                return ComparisonResult.orderedDescending
            }
            return ComparisonResult.orderedSame
        }
        return result as! [Element]
    }
}

Upvotes: 0

Tom
Tom

Reputation: 3861

I appreciate the elegance of leavez's answer. I adapted it to have the same signature as Sequence.sorted(by:):

extension Sequence {
  func stableSorted(
    by areInIncreasingOrder: (Element, Element) throws -> Bool)
    rethrows -> [Element]
  {
    return try enumerated()
      .sorted { a, b -> Bool in
        try areInIncreasingOrder(a.element, b.element) ||
          (a.offset < b.offset && !areInIncreasingOrder(b.element, a.element))
      }
      .map { $0.element }
  }
}

Upvotes: 17

Related Questions