1

Is it possible to find if a sequence of elements in an array exists? Lets take some digits from the Pi,

 let piDigits=[3,1,4,1,5,9,2,6,5,3,5,8,9,7,9,3,2,3,8,4,6,2,6,4,3,3,8,3,2,7,9,5,0,2,8,8,4,1,9,7,1,6,9,3,9,9,3,7,5,1,0,5,8,2,0,9,7,4,9,4,4] 

Now, i want to find if, 5 and 9 exist as sequence elements in the array- in this case they do, once, in positions 4 & 5.

Ideally, i wouldn't like to iterate over the array with a loop, i would like something similar to array.contains(element) .

@Bawpotter, the code snippet:

 for element in piDigits{  //check every element
  if element == 5 { //if element is equal with the element i want
    var currentPosition = piDigits.index(of: element) //get the position of that element
    if piDigits[currentPosition!+1] == 9 { //if the element at the next position is equal to the other element i want
        print("true")   // it prints true 7 times, instead of 1!
    }
  }
}
Do2
  • 1,751
  • 1
  • 16
  • 28
  • If your looking for something in swift's standard library to do this, I don't think that there is... – yxre Sep 28 '16 at 02:42
  • There are some specific text searching algorithms you could use, e.g. Aho-Corasic, if performance is critical. Consider that the array is a long text and the searched array is a substring. If performance is not critical, I would probably use a simple linear search using a `for`. On every index check that the next `n` items are equal to your searched array and if yes, output that index. – Sulthan Sep 28 '16 at 16:25

4 Answers4

3

You can filter your indices where its subsequence elementsEqual is true:

extension Collection where Element: Equatable {
    func firstIndex<C: Collection>(of collection: C) -> Index? where C.Element == Element {
        guard !collection.isEmpty else { return nil }
        let size = collection.count
        return indices.dropLast(size-1).first {
            self[$0..<index($0, offsetBy: size)].elementsEqual(collection)
        }
    }
    func indices<C: Collection>(of collection: C) -> [Index] where C.Element == Element {
        guard !collection.isEmpty else { return [] }
        let size = collection.count
        return indices.dropLast(size-1).filter {
            self[$0..<index($0, offsetBy: size)].elementsEqual(collection)
        }
    }
    func range<C: Collection>(of collection: C) -> Range<Index>? where C.Element == Element {
        guard !collection.isEmpty else { return nil }
        let size = collection.count
        var range: Range<Index>!
        guard let _ = indices.dropLast(size-1).first(where: {
            range = $0..<index($0, offsetBy: size)
            return self[range].elementsEqual(collection)
        }) else {
            return nil
        }
        return range
    }
    func ranges<C: Collection>(of collection: C) -> [Range<Index>] where C.Element == Element {
        guard !collection.isEmpty else { return [] }
        let size = collection.count
        return indices.dropLast(size-1).compactMap {
            let range = $0..<index($0, offsetBy: size)
            return self[range].elementsEqual(collection) ? range : nil
        }
    }
}

[1, 2, 3, 1, 2].indices(of: [1,2])   // [0,3]
[1, 2, 3, 1, 2].ranges(of: [1,2])    // [[0..<2], [3..<5]]

If you only need to check if a collection contains a subsequence:

extension Collection where Element: Equatable {
    func contains<C: Collection>(_ collection: C) -> Bool where C.Element == Element  {
        guard !collection.isEmpty else { return false }
        let size = collection.count
        for i in indices.dropLast(size-1) where self[i..<index(i, offsetBy: size)].elementsEqual(collection) {
            return true
        }
        return false
    }
}

[1, 2, 3].contains([1, 2])  // true
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
1

Inside the contains method iterates over the array and here you have to do the same thing. Here an example:

extension Array where Element: Equatable {
  func contains(array elements: [Element]) -> Int {
    guard elements.count > 0 else { return 0 }
    guard count > 0 else { return -1 }

    var ti = 0

    for (index, element) in self.enumerated() {
      ti = elements[ti] == element ? ti + 1 : 0

      if ti == elements.count {
        return index - elements.count + 1
      }
    }

    return -1
  }
}

And here how to use it:

let index = [1, 4, 5, 6, 6, 9, 6, 8, 10, 3, 4].contains(array: [6, 8, 10])
// index = 6

let index = [1, 4, 5, 6, 6, 9, 6, 8, 10, 3, 4].contains(array: [6, 8, 1])
// index = -1
Yannick Loriot
  • 7,107
  • 2
  • 33
  • 56
  • Hi Yannick, thank you for your answer. Your solution works fine as long as there is no repetition. But if 6,8,10 was repeated twice, it would only give me the position of the first one – Do2 Sep 28 '16 at 11:04
  • Ah ok, your question is not clear. You just ask for "if a sequence of elements in an array exists". If you want to have all the sequences you can then cut the array at the returned array and re-call this method until you have all the sequences. – Yannick Loriot Sep 28 '16 at 11:24
1

A very simple implementation using linear search:

let piDigits: [Int] = [3,1,4,1,5,9,2,6,5,3,5,8,9,7,9,3,2,3,8,4,6,2,6,4,3,3,8,3,2,7,9,5,0,2,8,8,4,1,9,7,1,6,9,3,9,9,3,7,5,1,0,5,8,2,0,9,7,4,9,4,4]

let searchedSequence: [Int] = [5, 9]

var index = 0
var resultIndices: [Int] = []

while index < (piDigits.count - searchedSequence.count) {
    let subarray = piDigits[index ..< (index + searchedSequence.count)]

    if subarray.elementsEqual(searchedSequence) {
        resultIndices.append(index)
    }

    index += 1
}

print("Result: \(resultIndices)")

There are other variants as well, you could, for example, keep dropping the first character from piDigits during iteration and check whether piDigits start with the searchedSequence.

If performance is critical, I recommend using a string searching algorithm, e.g. Aho-Corasick (see https://en.wikipedia.org/wiki/String_searching_algorithm) which builds a state machine first for fast comparison (similar to regular expressions).

Let's see how regular expressions can be used:

let searchedSequences: [[Int]] = [[5, 9], [7], [9, 2]]

let stringDigits = piDigits.map { String($0) }.joined()
let stringSearchedSequences = searchedSequences.map { sequence in sequence.map { String($0) }.joined() }

let regularExpressionPattern = stringSearchedSequences.joined(separator: "|")

let regularExpression = try! NSRegularExpression(pattern: regularExpressionPattern, options: [])

let matches = regularExpression.matches(in: stringDigits, options: [], range: NSRange(location: 0, length: stringDigits.characters.count))
let matchedIndices = matches.map { $0.range.location }

print("Matches: \(matchedIndices)")

The downside of the approach is that it won't search overlapping ranges (e.g. "592" matches two ranges but only one is reported).

Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • Neat! I am trying to take it one step further make searchSequence array of arrays and then the print result to be a dictionary(don't tell me the solution i wanna try it myself :) ) I tried finding a swift version of aho-corasick with no success.. – Do2 Sep 28 '16 at 16:51
  • @Do2 There are not many string searching algorithm libraries because strings already have the same algorithms in the standard libraries. The concept of the algorithm is still valid. – Sulthan Sep 28 '16 at 16:57
  • Converting your input to string and then searching for matches for regular expression `(subarray1|subarray2|...)` is also a way. – Sulthan Sep 28 '16 at 17:04
  • Sorry I didn't get that.. could you give me an example please? – Do2 Sep 28 '16 at 17:32
  • @Do2 I added an example of regular expressions used to search subarrays. Note that if you write the algorithm by yourself you can control the edge cases (e.g. overlapping intervals). – Sulthan Sep 28 '16 at 17:56
  • also the other flaw is that in your example it will not output 3 positions but 6 as it looks for combinations of the key e.g 9 & 7 – Do2 Sep 29 '16 at 00:45
  • I should fix that using parenthesis in the pattern. – Sulthan Sep 29 '16 at 06:14
  • Could you specify the fix please? Where are you supposed to use parenthesis? – Do2 Sep 30 '16 at 02:18
0
let firstSeqNum = 5
let secondSeqNum = 9
for (index, number) in array.enumerated() {
    if  number == firstSeqNum && array[index+1] == secondSeqNum {
        print("The sequence \(firstSeqNum), \(secondSeqNum) was found, starting at an index of \(index).")
    }
}

Since there's no built-in method for this, this would be your best option.

Bryan
  • 1,335
  • 1
  • 16
  • 32
  • Thanks! I played around with .enumerated but apparently not correctly.. I also tried this: for element in piDigits{ if element == 5 { var currentPosition = piDigits.index(of: element) if piDigits[currentPosition!+1] == 9 { print("true") } } } but it wouldn't output the correct results in this case it would print true 6 times, any idea why? – Do2 Sep 28 '16 at 03:42
  • Can you edit your original post to add that code? Much easier to troubleshoot it when it's formatted. – Bryan Sep 28 '16 at 15:20