0

Function that checks if one array contains elements of another array but in order?

[1,2,3,4,5] -> [3,5] gives true
[1,2,3,4,5] -> [5,2] gives false

Is it possible to make the solution optimised?

5
  • 1
    Does your first array might contain duplicate elements? Commented Apr 2, 2022 at 17:34
  • 1
    Every computable function can be implemented as a higher-order apply function that calls that function, so it's not clear what "using higher order functions" here is trying to achieve, or why that would be related to "one liner" (higher order functions can be any length). The point of a higher-order function is that it can be passed different functions to adjust its behavior, but in this example, what would the "different functions" be? Anything can be a one-liner if you don't count the lines in the function you call. Commented Apr 2, 2022 at 17:47
  • 1
    I agree with that, by "one liner" i meant "minimal code" and less complex. thanks for pointing this out Commented Apr 2, 2022 at 18:00
  • 4
    Looking at the many good answers to this question, are any of them what you mean? If so, it may be worth rewording your question drop the mention of higher order functions, and just ask how to find a sequence in another sequence in order, but not necessarily consecutively. If these answers are correct (and I think they are), there's nothing "higher order" about them. And none are particularly "minimal code" (though they are all of an appropriate length). Commented Apr 2, 2022 at 18:29
  • This looks more like a codegolf question Commented Apr 4, 2022 at 16:23

4 Answers 4

3

The solution is to step through the arrays in a staggered way. That way the complexity is O(n) rather than O(n²):

extension Sequence where Element: Equatable {
    func containsInOrder<S1>(_ arr: S1) -> Bool where S1: Sequence, S1.Element == Element {
        var it = makeIterator()
        var it1 = arr.makeIterator()
        var v = it.next()
        var v1 = it1.next()
        while v != nil, v1 != nil {
            if v == v1 {
                v1 = it1.next()
            }
            v = it.next()
        }
        return v1 == nil
    }
}

Use it like this:

[1,2,3,4,5].containsInOrder([3, 5])  // true
[1,2,3,4,5].containsInOrder([5, 3])  // false
"Hello World!".containsInOrder("HW") // true

Updated for Sequences...

Sign up to request clarification or add additional context in comments.

5 Comments

Don't use Self Better to create a generic type that conform to Collection and constrain its Element to be equal to the collection element. This will allow you to mix Sequence and SubSequence when using that method
Nice. Also, it should be on Sequence instead of Collection... I'm not sure how to do that just yet.
Sequence is OK as long as you don't need an index. For the second collection is OK at least in my approach. Note that would also allow you to pass a collection of characters to search in a string. Check my last edit.
updated for sequences and made parameter generic so you can use different types.
Much better now
2

You can think of AnyIterator as a "pauseable sequence", which can resume execution after an operation on an element.

That's helpful for this problem, because you can think of the solution as, "for every element in the second sequence, search for a match in the remaining iterations of the first sequence".

extension Sequence where Element: Equatable {
  /// Whether this sequence contains all the elements of another, in order.
  func isOrderedSuperset<Elements: Sequence>(of elements: Elements) -> Bool
  where Elements.Element == Element {
    elements.allSatisfy(AnyIterator(makeIterator()).contains)
  }
}
XCTAssert([-10, 1, 2, 5, 2, 3, 0, 4, 6, 9, 5, 4].isOrderedSuperset(of: 1...5))
XCTAssert("🐱🐱".isOrderedSuperset(of: "🐱🐱"))
XCTAssertFalse("🦎🧙🏽‍♂️".isOrderedSuperset(of: "🧙🏽‍♂️🦎"))
XCTAssertFalse(
  CollectionOfOne(true).isOrderedSuperset(of: [true, false])
)

It's actually more complex, but AnyIterator effectively looks like the following. Behaviorally, it's the same as if IteratorSequence were a reference type.

struct PauseableSinglePassSequence<Element> {
  init<Iterator: IteratorProtocol>(_ iterator: Iterator)
  where Iterator.Element == Element {
    var iterator = iterator
    _next = { iterator.next() }
  }

  private let _next: () -> Element?
}

extension PauseableSinglePassSequence: IteratorProtocol & Sequence {
  func next() -> Element? {
    _next()
  }
}

4 Comments

If I understand correctly, the reason this works is because AnyIterator.contains advances the iterator? I had no idea it did that! TIL!
Sequence.contains has to advance an iterator. But unlike, say, IteratorSequence(makeIterator()), AnyIterator doesn't "reset" when you start iterating again. It's a useful hybrid of Sequence and IteratorProtocol. I consider this too much of a hidden feature, but it's a very handy one to have built-in.
Ah yes of course. I worded my comment badly. I meant to say that calling contains somehow doesn’t reset the iterator, but mutates its state permanently. But interestingly contains need not be marked mutating for this to work. I guess it’s time for me to check out the source code to see how they did this :-)
I don't know what _abstract() is. But I added a code snippet to demonstrate the reference semantics at work.
1

Just for fun. You can iterate your second collection elements and try to find the first index of each element in the first collection. If found just offset the startIndex of the next search otherwise return false:

extension Collection {
    func containsElementsInOrder<S: Sequence>(_ elements: S) -> Bool where S.Element == Element, Element: Equatable {
        var startIndex = self.startIndex
        for element in elements {
            if let index = self[startIndex...].firstIndex(of: element) {
                startIndex = self.index(after: index)
            } else {
                return false
            }
        }
        return true
    }
}

Usage:

[1,2,3,4,5].containsElementsInOrder([3, 4])   // true
[1,2,3,4,5].containsElementsInOrder([1, 4])   // true
[1,2,3,4,5].containsElementsInOrder([1])      // true
[1,2,3,4,5].containsElementsInOrder([])       // true
[Int]().containsElementsInOrder([])           // true
[Int]().containsElementsInOrder([1,2,3])      // false
[1,2,3,4,5].containsElementsInOrder([5,4,3])  // false
[1,2,3,4,5].containsElementsInOrder([4, 5])   // true
[1,2,3,4,5].containsElementsInOrder([4,4,5])  // false

Note that this would work for Strings and substrings as well:

"12345".containsElementsInOrder("34")               // true
"12345".containsElementsInOrder("14")               // true
"12345".containsElementsInOrder("1")                // true
"12345".containsElementsInOrder("")                 // true
"".containsElementsInOrder([])                      // true
"".containsElementsInOrder("123")                   // false
"12345".containsElementsInOrder("543")              // false
"12345".containsElementsInOrder("45")               // true
"12345"[...].containsElementsInOrder("445")         // false
"12345".containsElementsInOrder("4456".dropLast())  // false
"12345".containsElementsInOrder(["4","4","5"])      // false

Comments

1
extension RandomAccessCollection where Self: RangeReplaceableCollection, Element: Equatable {
    func containsInOrder(other: Self) -> Bool {
        (self.isEmpty && other.isEmpty) ||
        containsInOrderWithoutEdgeCases(other: other) ||
            (self.last == other.last && containsInOrderWithoutEdgeCases(other: other.dropLast()))
    }
    
    private func containsInOrderWithoutEdgeCases<C: RandomAccessCollection>(other: C) -> Bool
        where C.Element == Element {
        other.reduce(self[...]) { acc, next in
            acc.drop(while: { $0 != next }).dropFirst()
        }.isEmpty == false
    }
}

Explanation

You could find each element of the second array in the first array by using something like

firstArray.drop(while: { $0 != target }).dropFirst()

This can then be fed into a reduce function, with the first array as the initial result. If the resulting array is not empty, then the first array contains the second array's elements in order.

extension RandomAccessCollection where Self: RangeReplaceableCollection, Element: Equatable {

   func containsInOrderWithoutEdgeCases<C: RandomAccessCollection>(other: C) -> Bool
        where C.Element == Element {
        other.reduce(self[...]) { acc, next in
            acc.drop(while: { $0 != next }).dropFirst()
        }.isEmpty == false
    }
}

Note that I used self[...] to get an SubSequence from self. The reduce works on SubSequences.

There are some edge cases not handled by this, so those need to be considered too. For example, cases when at least one of the arrays are empty.

The most interesting case is that, when the last element of the second array matches the last element of the first array. In this case, reduce would give an empty result even when the first array contains the elements of the second array, in order.

Therefore, if self.last == other.last, then the elements of other except last must also be contained in self, in order. Hence we arrive at the code at the top of the answer.

Test cases

[1,2,3,4,5].containsInOrder(other: [3, 4]) // true
[1,2,3,4,5].containsInOrder(other: [1, 4]) // true
[1,2,3,4,5].containsInOrder(other: [1]) // true
[1,2,3,4,5].containsInOrder(other: []) // true
[Int]().containsInOrder(other: []) // true
[Int]().containsInOrder(other: [1,2,3]) // false
[1,2,3,4,5].containsInOrder(other: [5,4,3]) // false
[1,2,3,4,5].containsInOrder(other: [4, 5]) // true
[1,2,3,4,5].containsInOrder(other: [7, 7, 7]) // false
[1,2,3,4,5].containsInOrder(other: [7, 5]) // false
[1,2,3,4, 5, 5].containsInOrder(other: [5, 5, 5]) // false

3 Comments

I think this has trouble with duplicate elements. For example [1,2,3,4,5].containsInOrder(other: [4,4,5]) returns true when it should be false.
@RobNapier Arrgh, I had a hunch that my way of handling the self.last == other.last edge case is too “forceful” so to speak. I’ll try to think of something better…
@RobNapier I think I fixed it by going through the arrays a second time. It's not too much of a big deal asymptotically speaking :-)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.