【问题标题】:How do I insert an element at the correct position into a sorted array in Swift?如何将正确位置的元素插入到 Swift 中的排序数组中?
【发布时间】:2014-12-27 23:47:34
【问题描述】:

NSArray 具有- (NSUInteger)indexOfObject:(id)obj inSortedRange:(NSRange)r options:(NSBinarySearchingOptions)opts usingComparator:(NSComparator)cmp 来确定新对象在排序数组中的插入位置。

在纯 Swift 中执行此操作的最佳和高性能方法是什么?

类似的东西:

var myArray = ["b", "e", "d", "a"]
myArray.sort { $0 < $1 }

// myArray is now [a, b, d, e]

myArray.append("c")
myArray.sort { $0 < $1 }

// myArray is now [a, b, c, d, e]

我不想追加新元素然后对数组进行排序,而是想找出正确的位置并插入元素:

let index = [... how to calculate this index ??? ...]
myArray.insert("c", atIndex: index)

【问题讨论】:

    标签: arrays sorting swift


    【解决方案1】:

    @vadian's@Martin R's 答案的基础上,我注意到一些细微的差异,主要是插入索引与集合中等效元素的索引不匹配,或者它与 first 不匹配 em> 等价元素序列的索引。

    例如:

    • 如果您想在[4, 5, 6] 中查找5 的插入索引,则会返回索引2,如果您只想简单地搜索该值,这可能会出现问题。
    • [5, 5, 5] 中,再次搜索5 会返回索引1,这不是第一个插入索引。

    这与 NSArray 的实现行为及其各种选项不匹配,因此这里是另一个尝试考虑这一点的解决方案:

    extension RandomAccessCollection {
        /// Get the index of or an insertion index for a new element in
        /// a sorted collection in ascending order.
        /// - Parameter value: The element to insert into the array.
        /// - Returns: The index suitable for inserting the new element
        ///            into the array, or the first index of an existing element.
        @inlinable
        public func sortedInsertionIndex(
            of element: Element
        ) -> Index where Element: Comparable {
            sortedInsertionIndex(of: element, by: <)
        }
        
        /// Get the index of or an insertion index for a new element in
        /// a sorted collection that matches the rule defined by the predicate.
        /// - Parameters:
        ///   - value: The element to insert into the array.
        ///   - areInIncreasingOrder:
        ///       A closure that determines if the first element should
        ///       come before the second element. For instance: `<`.
        /// - Returns: The index suitable for inserting the new element
        ///            into the array, or the first index of an existing element.
        @inlinable
        public func sortedInsertionIndex(
             of element: Element,
             by areInIncreasingOrder: (Element, Element) throws -> Bool
        ) rethrows -> Index {
            try sortedInsertionIndex { try areInIncreasingOrder($0, element) }
        }
        
        /// Get the index of or an insertion index for a new element in
        /// a sorted collection that matches the rule defined by the predicate.
        ///
        /// This variation is useful when comparing an element that
        /// is of a different type than those already in the array.
        /// - Parameter isOrderedAfter:
        ///     Return `true` if the new element should come after the one
        ///     provided in the closure, or `false` otherwise. For instance
        ///     `{ $0 < newElement }` to sort elements in increasing order.
        /// - Returns: The index suitable for inserting the new element into
        ///            the array, or the first index of an existing element.
        @inlinable
        public func sortedInsertionIndex(
             where isOrderedAfter: (Element) throws -> Bool
        ) rethrows -> Index {
            var slice: SubSequence = self[...]
    
            while !slice.isEmpty {
                let middle = slice.index(slice.startIndex, offsetBy: slice.count/2)
                if try isOrderedAfter(slice[middle]) {
                    slice = slice[index(after: middle)...]
                } else {
                    slice = slice[..<middle]
                }
            }
            return slice.startIndex
        }
    }
    

    由于有时您不关心插入索引,而是关心与给定元素匹配的第一个或最后一个索引,因此以下是满足这些要求的上述变体:

    extension RandomAccessCollection {
        @inlinable
        public func sortedFirstIndex(
            of element: Element
        ) -> Index? where Element: Comparable {
            sortedFirstIndex(of: element, by: <)
        }
        
        @inlinable
        public func sortedFirstIndex(
             of element: Element,
             by areInIncreasingOrder: (Element, Element) throws -> Bool
        ) rethrows -> Index? where Element: Comparable {
            let insertionIndex = try sortedInsertionIndex(of: element, by: areInIncreasingOrder)
            guard insertionIndex < endIndex, self[insertionIndex] == element else { return nil }
            return insertionIndex
        }
        
        @inlinable
        public func sortedLastIndex(
            of element: Element
        ) -> Index? where Element: Comparable {
            sortedLastIndex(of: element, by: <)
        }
        
        @inlinable
        public func sortedLastIndex(
            of element: Element,
            by areInIncreasingOrder: (Element, Element) throws -> Bool
        ) rethrows -> Index? where Element: Comparable {
            let insertionIndex = try sortedInsertionIndex(of: element) { try areInIncreasingOrder($1, $0) }
            let finalIndex = index(insertionIndex, offsetBy: -1)
            guard finalIndex >= startIndex, self[finalIndex] == element else { return nil }
            return finalIndex
        }
    }
    

    【讨论】:

      【解决方案2】:

      在 Swift 5 中:

      var myArray = ["b", "e", "d", "a"]
      myArray.sort { $0 < $1 }
      
      // myArray is now [a, b, d, e]
      
      let newElement = "c"
      let index = myArray.firstIndex(where: { newElement < $0 })
      myArray.insert(newElement, at: index ?? myArray.endIndex)
      

      如果您想让呼叫站点更漂亮:

      extension Array where Element: Comparable {
          /// Insert element in the correct location of a sorted array
          mutating func insertSorted(_ element: Element) {
              let index = firstIndex(where: { element < $0 })
              insert(element, at: index ?? endIndex)
          }
      }
      
      myArray.insertSorted("c")
      

      【讨论】:

        【解决方案3】:

        这是一个在 Swift 中使用二进制搜索的可能实现(来自 http://rosettacode.org/wiki/Binary_search#Swift 稍作修改):

        extension Array {
            func insertionIndexOf(_ elem: Element, isOrderedBefore: (Element, Element) -> Bool) -> Int {
                var lo = 0
                var hi = self.count - 1
                while lo <= hi {
                    let mid = (lo + hi)/2
                    if isOrderedBefore(self[mid], elem) {
                        lo = mid + 1
                    } else if isOrderedBefore(elem, self[mid]) {
                        hi = mid - 1
                    } else {
                        return mid // found at position mid
                    }
                }
                return lo // not found, would be inserted at position lo
            }
        }
        

        indexOfObject:inSortedRange:options:usingComparator: 一样,假设 数组相对于比较器进行排序。 如果元素已经存在于 数组,或在保留顺序的同时可以插入的索引。这 对应NSArray方法的NSBinarySearchingInsertionIndex

        用法:

        let newElement = "c"
        let index = myArray.insertionIndexOf(newElement) { $0 < $1 } // Or: myArray.indexOf(c, <)
        myArray.insert(newElement, at: index)
        

        【讨论】:

        • 我在stackoverflow.com/questions/31904396/…中发布了一个更通用的插入索引二分搜索实现
        • 嗨,我很好奇 isOrderedBefore() 部分是如何工作的,它没有在任何地方实现,但它工作得很好。你能解释一下吗?
        • @sweepez: isOrderedBefore 是方法的一个参数。当您调用 myArray.indexOf(c, &lt;) 时,它会设置为 &lt; 运算符。它与sort() 方法中的机制相同,您可以传递比较函数。
        • 哇,谢谢,现在很有意义。现在我只是对我如何错过函数参数这一事实感到困惑。
        • “//在中间位置找到”之后应该有一个中断,否则你会陷入无限循环
        【解决方案4】:

        根据WWDC 2018 Session 406: Swift Generics (Expanded),可以通过切片集合对象以更高效、更通用的方式执行二分搜索。

        Slice 有两个相当大的好处:

        1. 切片始终是原始对象的子集,无需分配额外的内存。
        2. 切片的所有索引都引用原始对象。
          例如,如果您对包含 5 个对象的数组进行切片 let slice = array[2..&lt;4],则 slice.startIndex2 而不是 0

        RandomAccessCollection是一个协议(继承自BidirectionalCollection),各种结构/类都符合

        extension RandomAccessCollection where Element : Comparable {
            func insertionIndex(of value: Element) -> Index {
                var slice : SubSequence = self[...]
        
                while !slice.isEmpty {
                    let middle = slice.index(slice.startIndex, offsetBy: slice.count / 2)
                    if value < slice[middle] {
                        slice = slice[..<middle]
                    } else {
                        slice = slice[index(after: middle)...]
                    }
                }
                return slice.startIndex
            }
        }
        

        例子:

        let array = [1, 2, 4, 7, 8]
        let index = array.insertionIndex(of: 6) // 3
        

        您可以扩展该函数以检查谓词闭包而不是单个值

        extension RandomAccessCollection { // the predicate version is not required to conform to Comparable
            func insertionIndex(for predicate: (Element) -> Bool) -> Index {
                var slice : SubSequence = self[...]
        
                while !slice.isEmpty {
                    let middle = slice.index(slice.startIndex, offsetBy: slice.count / 2)
                    if predicate(slice[middle]) {
                        slice = slice[index(after: middle)...]
                    } else {
                        slice = slice[..<middle]
                    }
                }
                return slice.startIndex
            }
        }
        

        例子:

        struct Person { let name : String }
        
        let array = [Person(name: "Adam"), Person(name: "Cynthia"), Person(name: "Nancy"), Person(name: "Tom")]
        let index = array.insertionIndex{ $0.name < "Bruce" } // 1
        

        【讨论】:

        • 好主意!但是为什么这两个版本的行为不同呢?我希望insertionIndex { 4 &lt; $0 } 的行为与insertionIndex(of: 4) 相同(对于排序的整数数组)。比较和顺序颠倒是疏忽,还是有特定原因?
        • 第二个例子是用于具有多个属性的对象,这些属性可以以不同方式排序或用于特殊谓词(&lt; 除外)
        【解决方案5】:

        在 swift 3 中你可以使用index(where:):

        var myArray = ["a", "b", "d", "e"]
        let newElement = "c"
        if let index = myArray.index(where: { $0 > newElement }) {
            myArray.insert(newElement, at: index)
        }
        

        请注意,在这种情况下,您需要反转闭包内的条件(即 &gt; 而不是 &lt; 用于增加数组中的元素),因为您感兴趣的索引是第一个不匹配的元素谓词。此外,如果新插入的元素将是数组中的最后一个元素,则此方法将返回 nil(上例中为 newElement = "z"

        为方便起见,可以将其包装成一个单独的函数来处理所有这些问题:

        extension Collection {
            func insertionIndex(of element: Self.Iterator.Element,
                                using areInIncreasingOrder: (Self.Iterator.Element, Self.Iterator.Element) -> Bool) -> Index {
                return index(where: { !areInIncreasingOrder($0, element) }) ?? endIndex
            }
        }
        

        用法:

        var myArray = ["a", "b", "d", "e"]
        let newElement = "c"
        let index = myArray.insertionIndex(of: newElement, using: <)
        myArray.insert(newElement, at: index)
        

        【讨论】:

        • 在 Swift 4 中它的 index(where 被重命名为 firstIndex(where,但除此之外,效果很好。
        • 然而,在 sorted 集合中查找插入点的实现相当糟糕。
        • 注意元素必须具有可比性
        【解决方案6】:

        如果您知道您的数组已排序,则可以使用此方法——它适用于任何类型的数组。它每次都会遍历整个数组,所以不要将它用于大型数组 - 如果您有更大的需求,请使用另一种数据类型!

        func insertSorted<T: Comparable>(inout seq: [T], newItem item: T) {
            let index = seq.reduce(0) { $1 < item ? $0 + 1 : $0 }
            seq.insert(item, atIndex: index)
        }
        
        var arr = [2, 4, 6, 8]
        insertSorted(&arr, newItem: 5)
        insertSorted(&arr, newItem: 3)
        insertSorted(&arr, newItem: -3)
        insertSorted(&arr, newItem: 11)
        // [-3, 2, 3, 4, 5, 6, 8, 11]
        

        【讨论】:

        • 这可能会更快。如果您知道数组已排序,则可以进行二进制搜索以找到插入点。
        • 我认为这个答案很好。有时简单>性能
        【解决方案7】:

        这里是二叉搜索树。

        在一个有序数组上,取中间的元素,看看那个位置的对象是否大于你的新对象。这样一来,您就可以通过一次比较忘记一半的数组元素。

        用剩下的一半重复该步骤。同样,通过一次比较,您可以忘记剩余对象的一半。您的目标元素计数现在是开始时数组大小的四分之一,只有两次比较。

        重复此操作,直到找到插入新元素的正确位置。

        这是a good article on binary search trees with swift

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-03-22
          • 2017-08-14
          • 2011-09-11
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多