比较两个数组并列出差异 - Swift

26

我想知道如何比较两个布尔数组,并列出不匹配的布尔值。

我写了一个简单的例子,包括两个数组。

let array1 = [true, false, true, false]
let array2 = [true, true, true, true]

我该如何比较数组1和数组2并显示不匹配的内容。我正在尝试这样做来检查智力竞赛游戏中用户的结果。

谢谢!


问题在于不清楚您认为的答案是什么。对于这个特定的输入,您想要/期望什么结果?您想要一个索引列表吗?一个数组?还是其他什么?如果一个布尔值比另一个多怎么办?如果其中一个为空呢? - matt
那个数组应该长什么样子?你必须要“明确”!在你的想法中,“正确答案”是什么? - matt
抱歉Matt!谢谢你的帮助 - 我意识到我应该更清楚地表达。我对编程非常新手,所以请耐心等待。我希望返回一个索引列表,但经过进一步研究,我相信我并不清楚我正在尝试实现什么。抱歉浪费你的时间。 - simlimsd3
我希望返回一个索引列表。这是非常合理的,但你需要明确指出。我会修改我的答案来展示这种方法。 - matt
“我不明白我试图实现什么。” 这是编程的第一步。你必须给计算机非常精确的指令,因此你必须清楚地知道在每种可能的情况下你想让它做什么。 - matt
5个回答

42

这里有一种实现方式,但是要确定它是否符合你的需求是完全不可能的,因为你没有明确指出你认为答案应该是什么:

let answer = zip(array1, array2).map {$0.0 == $0.1}

这将为您提供一个Bool值列表,如果答案与正确答案匹配,则为true,如果不匹配,则为false

但是假设您想要的是那些正确答案的索引列表。然后你可以这样说:


let answer = zip(array1, array2).enumerated().filter() {
    $1.0 == $1.1
}.map{$0.0}

如果你想要一份那些答案索引不正确的清单,只需将==改为!=


添加了另一种实现方式,其中我们假设您想要的是正确或错误答案的索引列表。 - matt
感谢您!非常感谢。 - simlimsd3
7
真正棒的是,这个答案成功地将 mapfilterzipenumerate 结合在一起使用——这些都是你需要了解的关键内容,以便在 Swift 中处理数组(只有 reduce 被省略了——在这个问题中找不到它的用处)。 :) - matt
必须要求array1和array2具有相同数量的元素(计数),还是它们可以不相等? - Frank Eno
1
为了好玩,使用 reduce 计算差异的数量...... .reduce(0) { (count, item) -> Int in return count + 1 } - bandejapaisa
显示剩余2条评论

24

Xcode 11 已支持 (仅适用于 iOS 13 或更高版本)

https://developer.apple.com/documentation/swift/array/3200716-difference

let oldNames = ["a", "b", "c", "d"]
    
let newNames = ["a", "b", "d", "e"]

let difference = newNames.difference(from: oldNames)

for change in difference {
  switch change {
  case let .remove(offset, oldElement, _):
    print("remove:", offset, oldElement)
  case let .insert(offset, newElement, _):
    print("insert:", offset, newElement)
  }
}

输出

remove: 2 c
insert: 3 e

4
在尝试使用差异函数时,我遇到了这个错误:“'difference(from:)'仅适用于iOS 13或更高版本”。 - GxocT

15

最简单的方法是使用 Set。Set 有一个 symmetricDifference() 方法,可以完美实现这一点,所以你只需要将两个数组转换为 Set,然后将结果转换回数组即可。

以下是一个扩展,可以使它更加容易:

extension Array where Element: Hashable {
    func difference(from other: [Element]) -> [Element] {
        let thisSet = Set(self)
        let otherSet = Set(other)
        return Array(thisSet.symmetricDifference(otherSet))
    } }

以下是一些示例代码,您可以使用它来尝试:

let names1 = ["student", "class", "teacher"]
let names2 = ["class", "teacher", "classroom"]
let difference = names1.difference(from: names2)

这将使差异被设置为["student", "classroom"],因为这两个名称只在其中一个数组中出现一次。


21
Source - DoesData
4
我原本想发表同样的评论,但我意识到这个回答是在hackingwithswift发布之前发表的,所以也许这就是他们的消息来源。 - monstermac77
2
@monstermac77 好的,说得对。然而,那个链接仍然提供了额外的细节并且很有帮助。Apple Documentation 也提供了一个具体的例子。 - DoesData
看起来这个在返回数组时没有进行正确的转换。使用这个确切的例子,我得到了“无法将类型为'CollectionDifference<ItemObject>'的值转换为预期的参数类型'[ItemObject]'”。 - Rich Everts

2

我发现这篇文章是在寻找与我遇到问题非常相似的解决方案。然而,我发现使用Swift 5.1中引入的inferringMoves()来进行有序集合差异比较适合我的用例。如果其他人也可能会发现它有用,我添加了我所使用的内容:

let array1 = [true, false, true, true, false, true]
let array2 = [true, true, true, true, true, false]
        
let diff = array2.difference(from: array1).inferringMoves()
    for change in diff {
        switch change {
        case .insert(let offset, let element, _):
            print("insert offset \(offset) for element \(element)")
        case .remove(let offset, let element, _):
            print("remove offset \(offset) for element \(element)")
        }
    }

这将打印出数值:

remove offset 4 for element false
remove offset 1 for element false
insert offset 4 for element true
insert offset 5 for element false

1
它对我完美地起作用了,但是我需要添加在这里提到的扩展名:https://www.fivestars.blog/articles/swift-5-1-collection-diffing/ - Douglas Frari

1
从 Swift 源代码移植,适用于 Swift5、iOS 12。
使用方法:
let difference = newArray.differenceFrom(from: originalArray) { (s1: String, s2: String) in
                return s1 == s2
            }
// or

let difference = Array<String>.difference(from: originalArray, to: newArray) { (s1: String, s2: String) in
                return s1 == s2
            }

for change in diff {
                switch change {
                case let .remove(offset, oldElement, _):
                    print("remove:", offset, oldElement)
                case let .insert(offset, newElement, _):
                    print("insert:", offset, newElement)
                }
            }

来源

数组扩展

extension Array {

public func differenceFrom<C: BidirectionalCollection>(
        from other: C,
        by areEquivalent: (C.Element, Element) -> Bool
) -> CollectionDifference<Element>
        where C.Element == Self.Element {
    return Array<Element>.difference(from: other, to: self, using: areEquivalent)
}

public static func difference<C, D>(
        from old: C, to new: D,
        using cmp: (C.Element, D.Element) -> Bool
) -> CollectionDifference<C.Element>
        where
        C: BidirectionalCollection,
        D: BidirectionalCollection,
        C.Element == D.Element {

    // Core implementation of the algorithm described at http://www.xmailserver.org/diff2.pdf
    // Variable names match those used in the paper as closely as possible
    func _descent(from a: UnsafeBufferPointer<C.Element>, to b: UnsafeBufferPointer<D.Element>) -> [_V] {
        let n = a.count
        let m = b.count
        let max = n + m

        var result = [_V]()
        var v = _V(maxIndex: 1)
        v[1] = 0

        var x = 0
        var y = 0
        iterator: for d in 0...max {
            let prev_v = v
            result.append(v)
            v = _V(maxIndex: d)

            // The code in this loop is _very_ hot—the loop bounds increases in terms
            // of the iterator of the outer loop!
            for k in stride(from: -d, through: d, by: 2) {
                if k == -d {
                    x = prev_v[k &+ 1]
                } else {
                    let km = prev_v[k &- 1]

                    if k != d {
                        let kp = prev_v[k &+ 1]
                        if km < kp {
                            x = kp
                        } else {
                            x = km &+ 1
                        }
                    } else {
                        x = km &+ 1
                    }
                }
                y = x &- k

                while x < n && y < m {
                    if !cmp(a[x], b[y]) {
                        break;
                    }
                    x &+= 1
                    y &+= 1
                }

                v[k] = x

                if x >= n && y >= m {
                    break iterator
                }
            }
            if x >= n && y >= m {
                break
            }
        }

        //_internalInvariant(x >= n && y >= m)

        return result
    }

    // Backtrack through the trace generated by the Myers descent to produce the changes that make up the diff
    func _formChanges(
            from a: UnsafeBufferPointer<C.Element>,
            to b: UnsafeBufferPointer<C.Element>,
            using trace: [_V]
    ) -> [CollectionDifference<C.Element>.Change] {
        var changes = [CollectionDifference<C.Element>.Change]()
        changes.reserveCapacity(trace.count)

        var x = a.count
        var y = b.count
        for d in stride(from: trace.count &- 1, to: 0, by: -1) {
            let v = trace[d]
            let k = x &- y
            let prev_k = (k == -d || (k != d && v[k &- 1] < v[k &+ 1])) ? k &+ 1 : k &- 1
            let prev_x = v[prev_k]
            let prev_y = prev_x &- prev_k

            while x > prev_x && y > prev_y {
                // No change at this position.
                x &-= 1
                y &-= 1
            }

            //_internalInvariant((x == prev_x && y > prev_y) || (y == prev_y && x > prev_x))
            if y != prev_y {
                changes.append(.insert(offset: prev_y, element: b[prev_y], associatedWith: nil))
            } else {
                changes.append(.remove(offset: prev_x, element: a[prev_x], associatedWith: nil))
            }

            x = prev_x
            y = prev_y
        }

        return changes
    }

    /* Splatting the collections into contiguous storage has two advantages:
     *
     *   1) Subscript access is much faster
     *   2) Subscript index becomes Int, matching the iterator types in the algorithm
     *
     * Combined, these effects dramatically improves performance when
     * collections differ significantly, without unduly degrading runtime when
     * the parameters are very similar.
     *
     * In terms of memory use, the linear cost of creating a ContiguousArray (when
     * necessary) is significantly less than the worst-case n² memory use of the
     * descent algorithm.
     */
    func _withContiguousStorage<C: Collection, R>(
            for values: C,
            _ body: (UnsafeBufferPointer<C.Element>) throws -> R
    ) rethrows -> R {
        if let result = try values.withContiguousStorageIfAvailable(body) {
            return result
        }
        let array = ContiguousArray(values)
        return try array.withUnsafeBufferPointer(body)
    }

    return _withContiguousStorage(for: old) { a in
        return _withContiguousStorage(for: new) { b in
            return CollectionDifference(_formChanges(from: a, to: b, using: _descent(from: a, to: b)))!
        }
    }
}
}

CollectionDifference

public struct CollectionDifference<ChangeElement> {
/// A single change to a collection.
@frozen
public enum Change {
    /// An insertion.
    ///
    /// The `offset` value is the offset of the inserted element in the final
    /// state of the collection after the difference is fully applied.
    /// A non-`nil` `associatedWith` value is the offset of the complementary
    /// change.
    case insert(offset: Int, element: ChangeElement, associatedWith: Int?)

    /// A removal.
    ///
    /// The `offset` value is the offset of the element to be removed in the
    /// original state of the collection. A non-`nil` `associatedWith` value is
    /// the offset of the complementary change.
    case remove(offset: Int, element: ChangeElement, associatedWith: Int?)

    // Internal common field accessors
    internal var _offset: Int {
        get {
            switch self {
            case .insert(offset: let o, element: _, associatedWith: _):
                return o
            case .remove(offset: let o, element: _, associatedWith: _):
                return o
            }
        }
    }
    internal var _element: ChangeElement {
        get {
            switch self {
            case .insert(offset: _, element: let e, associatedWith: _):
                return e
            case .remove(offset: _, element: let e, associatedWith: _):
                return e
            }
        }
    }
    internal var _associatedOffset: Int? {
        get {
            switch self {
            case .insert(offset: _, element: _, associatedWith: let o):
                return o
            case .remove(offset: _, element: _, associatedWith: let o):
                return o
            }
        }
    }
}

/// The insertions contained by this difference, from lowest offset to
/// highest.
public let insertions: [Change]

/// The removals contained by this difference, from lowest offset to highest.
public let removals: [Change]

/// The public initializer calls this function to ensure that its parameter
/// meets the conditions set in its documentation.
///
/// - Parameter changes: a collection of `CollectionDifference.Change`
///   instances intended to represent a valid state transition for
///   `CollectionDifference`.
///
/// - Returns: whether the parameter meets the following criteria:
///
///   1. All insertion offsets are unique
///   2. All removal offsets are unique
///   3. All associations between insertions and removals are symmetric
///
/// Complexity: O(`changes.count`)
private static func _validateChanges<Changes: Collection>(
        _ changes: Changes
) -> Bool where Changes.Element == Change {
    if changes.isEmpty {
        return true
    }

    var insertAssocToOffset = Dictionary<Int, Int>()
    var removeOffsetToAssoc = Dictionary<Int, Int>()
    var insertOffset = Set<Int>()
    var removeOffset = Set<Int>()

    for change in changes {
        let offset = change._offset
        if offset < 0 {
            return false
        }

        switch change {
        case .remove(_, _, _):
            if removeOffset.contains(offset) {
                return false
            }
            removeOffset.insert(offset)
        case .insert(_, _, _):
            if insertOffset.contains(offset) {
                return false
            }
            insertOffset.insert(offset)
        }

        if let assoc = change._associatedOffset {
            if assoc < 0 {
                return false
            }
            switch change {
            case .remove(_, _, _):
                if removeOffsetToAssoc[offset] != nil {
                    return false
                }
                removeOffsetToAssoc[offset] = assoc
            case .insert(_, _, _):
                if insertAssocToOffset[assoc] != nil {
                    return false
                }
                insertAssocToOffset[assoc] = offset
            }
        }
    }

    return removeOffsetToAssoc == insertAssocToOffset
}

/// Creates a new collection difference from a collection of changes.
///
/// To find the difference between two collections, use the
/// `difference(from:)` method declared on the `BidirectionalCollection`
/// protocol.
///
/// The collection of changes passed as `changes` must meet these
/// requirements:
///
/// - All insertion offsets are unique
/// - All removal offsets are unique
/// - All associations between insertions and removals are symmetric
///
/// - Parameter changes: A collection of changes that represent a transition
///   between two states.
///
/// - Complexity: O(*n* * log(*n*)), where *n* is the length of the
///   parameter.
public init?<Changes: Collection>(
        _ changes: Changes
) where Changes.Element == Change {
    guard CollectionDifference<ChangeElement>._validateChanges(changes) else {
        return nil
    }

    self.init(_validatedChanges: changes)
}

/// Internal initializer for use by algorithms that cannot produce invalid
/// collections of changes. These include the Myers' diff algorithm,
/// self.inverse(), and the move inferencer.
///
/// If parameter validity cannot be guaranteed by the caller then
/// `CollectionDifference.init?(_:)` should be used instead.
///
/// - Parameter c: A valid collection of changes that represent a transition
///   between two states.
///
/// - Complexity: O(*n* * log(*n*)), where *n* is the length of the
///   parameter.
internal init<Changes: Collection>(
        _validatedChanges changes: Changes
) where Changes.Element == Change {
    let sortedChanges = changes.sorted { (a, b) -> Bool in
        switch (a, b) {
        case (.remove(_, _, _), .insert(_, _, _)):
            return true
        case (.insert(_, _, _), .remove(_, _, _)):
            return false
        default:
            return a._offset < b._offset
        }
    }

    // Find first insertion via binary search
    let firstInsertIndex: Int
    if sortedChanges.isEmpty {
        firstInsertIndex = 0
    } else {
        var range = 0...sortedChanges.count
        while range.lowerBound != range.upperBound {
            let i = (range.lowerBound + range.upperBound) / 2
            switch sortedChanges[i] {
            case .insert(_, _, _):
                range = range.lowerBound...i
            case .remove(_, _, _):
                range = (i + 1)...range.upperBound
            }
        }
        firstInsertIndex = range.lowerBound
    }

    removals = Array(sortedChanges[0..<firstInsertIndex])
    insertions = Array(sortedChanges[firstInsertIndex..<sortedChanges.count])
}

public func inverse() -> Self {
    return CollectionDifference(_validatedChanges: self.map { c in
        switch c {
        case .remove(let o, let e, let a):
            return .insert(offset: o, element: e, associatedWith: a)
        case .insert(let o, let e, let a):
            return .remove(offset: o, element: e, associatedWith: a)
        }
    })
}
}

集合差异扩展。
extension CollectionDifference: Collection {
public typealias Element = Change

/// The position of a collection difference.
@frozen
public struct Index {
    // Opaque index type is isomorphic to Int
    @usableFromInline
    internal let _offset: Int

    internal init(_offset offset: Int) {
        _offset = offset
    }
}

public var startIndex: Index {
    return Index(_offset: 0)
}

public var endIndex: Index {
    return Index(_offset: removals.count + insertions.count)
}

public func index(after index: Index) -> Index {
    return Index(_offset: index._offset + 1)
}

public subscript(position: Index) -> Element {
    if position._offset < removals.count {
        return removals[removals.count - (position._offset + 1)]
    }
    return insertions[position._offset - removals.count]
}

public func index(before index: Index) -> Index {
    return Index(_offset: index._offset - 1)
}

public func formIndex(_ index: inout Index, offsetBy distance: Int) {
    index = Index(_offset: index._offset + distance)
}

public func distance(from start: Index, to end: Index) -> Int {
    return end._offset - start._offset
}
}

extension CollectionDifference.Index: Equatable {
@inlinable
public static func ==(
        lhs: CollectionDifference.Index,
        rhs: CollectionDifference.Index
) -> Bool {
    return lhs._offset == rhs._offset
}
}

extension CollectionDifference.Index: Comparable {
@inlinable
public static func <(
        lhs: CollectionDifference.Index,
        rhs: CollectionDifference.Index
) -> Bool {
    return lhs._offset < rhs._offset
}
}

extension CollectionDifference.Index: Hashable {
@inlinable
public func hash(into hasher: inout Hasher) {
    hasher.combine(_offset)
}
}

extension CollectionDifference.Change: Equatable where ChangeElement: Equatable {}

extension CollectionDifference: Equatable where ChangeElement: Equatable {}

extension CollectionDifference.Change: Hashable where ChangeElement: Hashable {}

extension CollectionDifference: Hashable where ChangeElement: Hashable {}

extension CollectionDifference where ChangeElement: Hashable {
/// Returns a new collection difference with associations between individual
/// elements that have been removed and inserted only once.
///
/// - Returns: A collection difference with all possible moves inferred.
///
/// - Complexity: O(*n*) where *n* is the number of collection differences.
public func inferringMoves() -> CollectionDifference<ChangeElement> {
    let uniqueRemovals: [ChangeElement: Int?] = {
        var result = [ChangeElement: Int?](minimumCapacity: Swift.min(removals.count, insertions.count))
        for removal in removals {
            let element = removal._element
            if result[element] != .none {
                result[element] = .some(.none)
            } else {
                result[element] = .some(removal._offset)
            }
        }
        return result.filter { (_, v) -> Bool in
            v != .none
        }
    }()

    let uniqueInsertions: [ChangeElement: Int?] = {
        var result = [ChangeElement: Int?](minimumCapacity: Swift.min(removals.count, insertions.count))
        for insertion in insertions {
            let element = insertion._element
            if result[element] != .none {
                result[element] = .some(.none)
            } else {
                result[element] = .some(insertion._offset)
            }
        }
        return result.filter { (_, v) -> Bool in
            v != .none
        }
    }()

    return CollectionDifference(_validatedChanges: map({ (change: Change) -> Change in
        switch change {
        case .remove(offset: let offset, element: let element, associatedWith: _):
            if uniqueRemovals[element] == nil {
                return change
            }
            if let assoc = uniqueInsertions[element] {
                return .remove(offset: offset, element: element, associatedWith: assoc)
            }
        case .insert(offset: let offset, element: let element, associatedWith: _):
            if uniqueInsertions[element] == nil {
                return change
            }
            if let assoc = uniqueRemovals[element] {
                return .insert(offset: offset, element: element, associatedWith: assoc)
            }
        }
        return change
    }))
}
}

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