1
\$\begingroup\$

I have a collection of strings, all of which are three characters in length:

string[] test = { "abc", "bbc", "cbc", "aac", "bac", "ccc" };

I want to determine which char is present as the center character in the most number of elements. I've created a Linq query that accomplishes this goal:

var champion = test.GroupBy(g => g[1])
                   .ToDictionary(k => k.Key, v => v.Count())
                   .OrderByDescending(o => o.Value)
                   .First();
Console.WriteLine(champion.Key);

In this case, the output should be:

b

However, I can't help but feel like it could be simplified.


Is there a way to simplify my Linq query?

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

You don't need to use a dictionary. Instead, order by the count directly:

var champion = test.GroupBy(t => t[1])
                   .OrderByDescending(g => g.Count())
                   .First();

Note that this might do the count per group several times while sorting. For a very large number of items, you could improve it by storing the result in a ValueTuple before sorting.

var champion = test.GroupBy(t => t[1])
    .Select(g => (key: g.Key, count: g.Count()))
    .OrderByDescending(x => x.count)
    .First();

Console.WriteLine($"The champion '{champion.key}' occurred {champion.count} times.");

Select does not use an intermediate collection, but produces an IEnumerable<(char, int)> on the fly as it is iterated by OrderByDescending.

See also: Deferred Execution of LINQ Query

\$\endgroup\$
2
\$\begingroup\$

The following code part could be inefficient for large sets:

.OrderByDescending(x => x.count)
.First();

We don't need sorting at all, we need just to find an item with the maximum value.
Let's create a couple of extension methods for this.
For reference types:

private static TSource MaxItem<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
    where TSource : class
    where TKey : IComparable<TKey>
{
    // TODO: validate arguments

    TSource maxItem = null;
    TKey max = default(TKey);
    foreach (var element in source)
    {
        TKey value = keySelector(element);
        if (maxItem == null || value.CompareTo(max) > 0)
        {
            max = value;
            maxItem = element;
        }
    }

    return maxItem;
}

And for value types:

private static TSource? MaxValueItem<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
    where TSource : struct
    where TKey : IComparable<TKey>
{
    // TODO: validate arguments

    TSource? maxItem = null;
    TKey max = default(TKey);
    foreach (var element in source)
    {
        TKey value = keySelector(element);
        if (maxItem == null || value.CompareTo(max) > 0)
        {
            max = value;
            maxItem = element;
        }
    }

    return maxItem;
}

As you can see, there is no additional memory allocation unlike the OrderByDescending method.


Use it:

var champion = test.GroupBy(g => g[1])
                   .ToDictionary(k => k.Key, v => v.Count())
                   .MaxValueItem(o => o.Value);
Console.WriteLine(champion.Key);

The same for the @Olivier's answer:

var champion = test.GroupBy(t => t[1])
    .Select(g => (key: g.Key, count: g.Count()))
    .MaxValueItem(o => o.Value);
\$\endgroup\$
1
  • \$\begingroup\$ ...and if you're on .net-6 there is a new extension for it: MaxBy. \$\endgroup\$
    – t3chb0t
    Jan 1, 2022 at 15:13

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.