ShevO27
ShevO27

Reputation: 906

I need to compare 3 elements in a row in an array, but i get index out of range error

The problem: to return a number of zeros in an array that contains 0s and 1s, if there are 3 0s in a row, count them as one, for example [0, 1, 0, 0, 0, 1, 0, 1, 0] should return 4, but when i try to solve it like this

func findZeros(_ c: [Int]) -> Int {
var zeros = 0
    for var i in 0..<c.count {
        switch c[i] {
            case _ where c[i] == 0 && c[i+1] == 0 && c[i+2] == 0: // row 5
            zeros += 1
            i += 2
            case _ where c[i] == 0:
            zeros += 1
            default:
            break
        }
    }
    return zeros
}

i always get index out of range error in row 5 , although when i hardcode c[1], c[2], c[3] == 0, it just counts as false and goes through... i've just started to learn swift so maybe that's not optimal, but anyway i can't even get this one working :/

Upvotes: 0

Views: 120

Answers (3)

Alexander
Alexander

Reputation: 63271

I'm going to show a very different solution approach. It's not one that would come naturally to a new beginner, so don't worry if it's foreign to you. It's something that you might come up with from having a bit more experience and being able to relate scattered concepts with each other.

The over-all process is actually very straight-forward, and is an almost exact codification of the your english explanation to the solution:

  1. Identify all the sub-sequences of repeating elements. These are called "runs", and there's a concept called a "run-length encoding". It takes an input like ["A", "B, B", "C", "D", "D", "D"], and turns it into a sequence like [("A", 1), ("B", 2), ("C", 1), ("D", 3)]
  2. Identify the runs of 0s that repeat precisely 3 times, and treat them as runs of a single 0.
  3. Count the number of 0s in the runs.

Here's what the code to do that would look like:

let input = [0, 1, 0, 0, 0, 1, 0, 1, 0]

let result = input.runLengthEncoded()
    .lazy
    .filter(keepOnlyRunsOfZeros)
    .map(convertThreeCountRunsIntoOneCountRuns)
    .map { $0.count }
    .reduce(0, +)
    
print(result)

And here are the supporting functions that make it possible:

typealias Run = (element: Int, count: Int)

func keepOnlyRunsOfZeros(_ run: Run) -> Bool {
    return run.element == 0
}

func convertThreeCountRunsIntoOneCountRuns(_ run: Run) -> Run {
    if run.count == 3 {
        return (element: run.element, count: 1)
    }
    else {
        return run
    }
}

Knowing that the process in #1 is a known algorithm called run-length encoding, I can find and reuse an existing implementation. Over time as a developer, you build up a collection of useful functions/techniques that you reuse for future use. Often times, you find other people's libraries that you've come to find useful, thus have booked-marked and re-use.

In this case, I have an implementation of run-length encoding that I've written and used in previous projects. It's quite long, but it's generalized and lazy-evaluated (it doesn't need to make an array of all runs, it serves them one by one as you request them, which improves performance for huge inputs), which isn't strictly necessary in this case, but it's what I already have on hand.

There alternative implementations that you can find that are eagerly evaluated and less generic (which should be fine for a simple problem like this), feel free to substitute one of those, instead.

public extension Sequence where Self.Iterator.Element: Equatable {
    func runLengthEncoded() -> LazySequenceRunLengthEncoder<Self> {
        return LazySequenceRunLengthEncoder(encoding: self)
    }
}

public struct LazySequenceRunLengthEncoder<WrappedSequence: Sequence>: Sequence
where WrappedSequence.Element: Equatable {
    public let wrappedSequence: WrappedSequence
    
    public init(encoding wrappedSequence: WrappedSequence) {
        self.wrappedSequence = wrappedSequence
    }
    
    public func makeIterator() -> RunLengthEncodingIterator<WrappedSequence.Iterator> {
        return RunLengthEncodingIterator(encoding: wrappedSequence.makeIterator())
    }
}

public struct RunLengthEncodingIterator<WrappedIterator: IteratorProtocol>: IteratorProtocol
where WrappedIterator.Element: Equatable {
    
    public private(set) var wrappedIterator: WrappedIterator
    public private(set) var currentGrouping: (element: WrappedIterator.Element, count: Int)? = nil
    
    public init(encoding wrappedIterator: WrappedIterator) {
        self.wrappedIterator = wrappedIterator
    }
    
    public mutating func next() -> (element: WrappedIterator.Element, count: Int)? {
        
        while let newElement = wrappedIterator.next() { // Take all elements of this run
            if let currentGrouping = self.currentGrouping {
                if newElement == currentGrouping.element { // increment the current run
                    let newCount = currentGrouping.count + 1
                    self.currentGrouping = (element: newElement, count: newCount)
                } else { // Broke the streak
                    defer {
                        self.currentGrouping = (element: newElement, count: 1) // start a new group
                    }
                    return self.currentGrouping
                }
            } else { // There is no current group, this is the first element
                self.currentGrouping = (element: newElement, count: 1)
            }
        }
        
        // Reached end of the wrapped iterator
        
        // 2. Only return the current grouping once, return the `nil` next time to end this iterator.
        defer { self.currentGrouping = nil }
        
        // 1. Return current grouping, if there is one
        return self.currentGrouping
    }
}

Upvotes: 1

Leo Dabus
Leo Dabus

Reputation: 236370

You can group the consecutive elements and sum how many groups you have of those elements:

extension Collection where Element: Equatable {
    var grouped: [[Element]] {
        reduce(into: []) {
            // check if the last element of the last collection is equal to the current element
            $0.last?.last == $1 ?
            // append the element to the last collection
            $0[$0.index(before: $0.endIndex)].append($1) :
            // otherwise add a new collection with the new element
            $0.append([$1])
        }
    }
    func repeatedOccurences(of element: Element) -> Int {
        // if the collection first element is equal to the element add one otherwise return the current result
        grouped.reduce(0) { $1.first == element ? $0 + 1 : $0 }
    }
}

[0, 1, 0, 0, 0, 1, 0, 1, 0].repeatedOccurences(of: 0)  // 4
[0, 1, 0, 0, 0, 1, 0, 1, 0].repeatedOccurences(of: 1)  // 3

Upvotes: 2

Mathias R. Jessen
Mathias R. Jessen

Reputation: 174515

If you were only accessing c[i], then counting i up to c.count - 1 would be fine - but you're also trying to access c[i+1] and c[i+2], so you need to take that into account when setting the upper boundary of the range - and then verify that the array indeed has at least 3 elements:

if c.count < 3 {
  return 0;
}

for var i in 0..<c.count-2 {
  // now you can safely access c[i+2]
}

Upvotes: 1

Related Questions