Swift: second occurrence with indexOf

孤街醉人 提交于 2019-11-28 11:28:54

You can perform another search for the index of element at the remaining array slice as follow:

edit/update: Xcode 9 • Swift 4 or later

extension Collection where Element: Equatable {
    func secondIndex(of element: Element) -> Index? {
        guard let index = firstIndex(of: element) else { return nil }
        return self[self.index(after: index)...].firstIndex(of: element)
    }
    func indexes(of element: Element) -> [Index] {
        var indexes: [Index] = []
        var startIndex = self.startIndex
        while let index = self[startIndex...].firstIndex(of: element) {
            indexes.append(index)
            startIndex = self.index(after: index)
        }
        return indexes
    }
}

Testing:

let numbers = [1,3,4,5,5,9,0,1]
numbers.secondIndex(of: 5)         // 4
numbers.indexes(of: 5)             // [3,4]

I don't think you can do it with indexOf. Instead you'll have to use a for-loop. A shorthand version:

let numbers = [1,3,4,5,5,9,0,1]
var indexes = [Int]()
numbers.enumerate().forEach { if $0.element == 5 { indexes += [$0.index] } }

print(indexes) // [3, 4]

Once you've found the first occurrence, you can use indexOf on the remaining slice of the array to locate the second occurrence:

let numbers = [1,3,4,5,5,9,0,1]

if let firstFive = numbers.indexOf(5)  { // 3
    let secondFive = numbers[firstFive+1..<numbers.count].indexOf(5) // 4
}

Here's a general use extension of Array that will work for finding the nth element of a kind in any array:

extension Array where Element: Equatable {
    // returns nil if there is no nth occurence
    // or the index of the nth occurence if there is
    func findNthIndexOf(n: Int, thing: Element) -> Int? {
        guard n > 0 else { return nil }
        var count = 0
        for (index, item) in enumerate() where item == thing {
            count += 1
            if count == n {
                return index
            }
        }
        return nil
    }
}

let numbers = [1,3,4,5,5,9,0]

numbers.findNthIndexOf(2, thing: 5) // returns 4

EDIT: as per @davecom's comment, I've included a similar but slightly more complex solution at the bottom of the answer.

I see a couple of good solutions here, especially considering the limitations the relatively new language of Swift. There is a really concise way to do it too, but beware...it is rather quick-and-dirty. May not be the perfect solution, but it is pretty quick. Also very versatile (not to brag).

extension Array where Element: Equatable {
    func indexes(search: Element) -> [Int] {
        return enumerate().reduce([Int]()) { $1.1 == search ? $0 + [$1.0] : $0 }
    }
}

Using this extension, you could access the second index as follows:

let numbers = [1, 3, 4, 5, 5, 9, 0, 1]
let indexesOf5 = numbers.indexes(5) // [3, 4]

indexesOf5[1] // 4

And you're done!

Basically, the method works like this: enumerate() maps the array to tuples including the index of each element with the element itself. In this case, [1, 3, 4, 5, 5, 9, 0, 1].enumerate() returns a collection of the type EnumerateSequence<Array<Int>> which, translated to an Integer array, returns [(0,1), (1,3), (2,4), (3,5), (4,5), (5,9), (6,0), (7,1)].

The rest of the work is done using reduce (called 'inject' in some languages), which is an extremely powerful tool that many coders are not familiar with. If the reader is among those coders, I'd recommend checking out this article regarding use of the function in JS (keep in mind the placement of the non-block argument passed in is inputted after the block in JS, rather than before as seen here).

Thanks for reading.

P.S. not to be too long-winded on this relatively simple solution, but if the syntax for the indexes method shown above is a bit too quick-and-dirty, you could try something like this in the method body, where the closure's parameters are expanded for a bit more clarity:

return enumerate().reduce([Int]()) { memo, element in
    element.1 == search ? memo + [element.0] : memo
}

EDIT: Here's another option that allows the implementer to scan for a specific "index at index" (e.g. the second occurrence of 5) for a more efficient solution.

extension Array where Element: Equatable {
    func nIndex(search: Element, n: Int) -> Int? {
        let info = enumerate().reduce((count: 0, index: 0), combine: { memo, element in
            memo.count < n && element.1 == search ? (count: memo.count + 1, index: element.0) : memo
        })
        return info.count == n ? info.index : nil
    }
}

[1, 3, 4, 5, 5, 9, 0, 1].nIndex(5, n: 2) // 4
[1, 3, 4, 5, 5, 9, 0, 1].nIndex(5, n: 3) // nil

The new method still iterates over the entire array, but is much more efficient due to the lack of "array-building" in the previous method. That performance hit would be negligible with the 8-object array used for the majority. But consider a list of 10,000 random numbers from 0 to 99:

let randomNumbers = (1...10000).map{_ in Int(rand() % 100)}

let indexes = randomNumbers.indexes(93) // count -> 100 (in my first run)
let index1 = indexes[1] // 238
// executed in 29.6603130102158 sec

let index2 = randomNumbers.nIndex(93, n: 2) // 238
// executed in 3.82625496387482 sec

As can be seen, this new method is considerably faster with the (very) large dataset; it is a bit more cumbersome and confusing though, so depending on your application, you may prefer the simpler solution, or a different one entirely.

(Again) thanks for reading.

extension Collection where Element: Equatable {
    func nth(occurance: Int, of element: Element) -> Index? {
        var level : Int = occurance
        var position = self.startIndex
        while let index = self[position...].index(of: element) {
            level -= 1
            guard level >= 0 else { return nil }
            guard level != 0 else { return index }
            position = self.index(after: index)
        }
        return nil
    }
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!