2

Context:

I have a variable numbers of type IEnumerable<int>.
I want to check if numbers are in ascending order.

Algorithm

So I was getting the first element storing it in prev and wanted to check that against next subsequent numbers.

// numbers can contain "10, 20, 60, 50"
IEnumerable<int> numbers = TraverseInOrder(root);
int prev = numbers.FirstOrDefault();
foreach (var curr in numbers.Skip(1))
{
    if (curr < prev) return false;
    prev = curr;
}
return true;

Question I am setting the value of prev using numbers.FirstOrDefault() and also skipping one element(numbers.Skip(1)) in foreach to start from next element.

So for following codes,

numbers.FirstOrDefault()
numbers.Skip(1)

Am I enumerating the numbers twice O(2N)? Meaning FirstOrDefault iterate the whole list?
-or-
Is it still O(N)? (O(1) constant time for FirstOrDefault + O(N) for Skip(1))

dance2die
  • 35,807
  • 39
  • 131
  • 194
  • 5
    Or better yet look at the source code https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,1033 – Nkosi Nov 28 '17 at 15:45
  • 7
    @Sung You are correct, there are lots of bad questions on SO that shouldn't have been asked because they're trivially answered by reading the documentation. That doesn't mean it's okay to ask questions that are trivially answered by reading the documentation. SO is not a place to ask questions because you can't be bothered to look up the answer in the documentation, or to run your own code and see what it does. – Servy Nov 28 '17 at 15:50
  • or just `int prev = int.MinValue` – Slai Nov 28 '17 at 15:55

3 Answers3

6

It's a bit subtle.

Enumerable.FirstOrDefault won't enumerate the whole sequence, as there's no point in doing that. Here's the implementation:

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source) {
    if (source == null) throw Error.ArgumentNull("source");
    IList<TSource> list = source as IList<TSource>;
    if (list != null) {
        if (list.Count > 0) return list[0];
    }
    else {
        using (IEnumerator<TSource> e = source.GetEnumerator()) {
            if (e.MoveNext()) return e.Current;
        }
    }
    return default(TSource);
}

You get an optimized version if the underlying sequence is an IList<T>, but otherwise an enumeration will be started.

Now imagine your IEnumerable<T> is actually an Entity Framework IQueryable<T> which represents a database query. What this means is that your code will still execute the query twice.

Better avoid enumerating IEnumerable<T> sequences multiple times as a rule of thumb. If you know that enumerating your sequence is cheap, you should use a more descriptive type, like ICollection<T> or IReadOnlyCollection<T>.

Lucas Trzesniewski
  • 50,214
  • 11
  • 107
  • 158
  • I've also read the source for `source.GetEnumerator()` https://github.com/Microsoft/referencesource/blob/master/mscorlib/system/collections/generic/list.cs#L1140 It looks like even though "an enumeration will be started", `FirstOrDefault` will only reach the first element. Thank you for the answer – dance2die Nov 28 '17 at 16:18
  • 1
    @Sung yes, but in the example of a database query, you'll only retrieve the first row, but the query will still be executed in order to get to that row. – Lucas Trzesniewski Nov 28 '17 at 16:28
2

I would subtly change your technique so that you don't need to know (although other answers address what you've specifically asked for):

IEnumerable<int> numbers = TraverseInOrder(root);
int prev = 0;
bool first = true;
foreach (var curr in numbers)
{
    if (!first && curr < prev) return false;
    prev = curr;
    first = false;
}
return true;

Now you know (even without reading the documentation!) that this code will only iterate the enumerable once.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
-1

FirstOrDefault() isn't iterating the whole collection.

IEnumerable and order

Just do:

var moreNumbers = numbers.ToList();
// ...stuff with the list. 
peteski
  • 1,455
  • 3
  • 18
  • 40