C#: How to iterate over a list with many conditions (filters and groups of items) [closed]

左心房为你撑大大i 提交于 2020-01-17 18:14:05

问题


Let's say I have this list of ints:

var numbers = new List<int> { 0, 0, 0, 27, 29, 24, 35, 33, 32, 1, 1, 1, 22, 55,
    44, 44, 55, 59, 0, 0, 0, 0 };

I want to implement the search algorithm described below. I am looking for the number 59.

  1. Some unspecified condition determines if I iterate from the left or from the right. In this example, let's say left to right is the direction of the iteration.
  2. We need to trim the start of the list:
    • 2.1. Leading 0s should be ignored.
    • 2.2. The next items are grouped together if they have the same tens. For example, 27, 29, 24 are grouped together. Next, 35, 33, 32 are grouped together. Next, 55. Etc.
    • 2.3. If the group contains an even number, it is ignored and we move on to the next until we find one that contains only odd numbers. This group is ignored as well and we move on to step 3 of this algorithm.
    • 2.3. 1s are ignored as well.
  3. Once the start of the list is cleared, we need to handle the end of the remaining items (44, 44, 55, 59, 0, 0, 0, 0):
    • 3.1. We are looking for the first group that contains an item ending with 9. We return this item. 59 is returned. Had we iterated from the other direction, 29 would have been found.

How would I go about implementing this algorithm in C#? These are some of my concerns:

  • I could iterate the whole list with a while or for construct, but the fact that sometimes I will have to start from the end of the list will lead to a messy code with the indexes. I have thought of implementing a custom IEnumerable/IEnumerator to hide this mess, but in this case, I should use a foreach statement. However, I think I will still have a mess when I try to handle the groups described above in this foreach.
  • How should I iterate the list and build those groups at the same time. What is a clean way to do this in C#?
  • For efficiency reasons, we should not do a first pass to filter out all 0s from the list. In the example, the list could be the start of a very long list of 10000000000 elements. There is no need to check the 9918477th element if the number we are looking for is the 15th.
  • Also, there are 2 distinct parts to this algorithm (the start of the sequence and the end). I don't know how I should handle them both in one iteration.

Note: This example is not an homework. This is a simplified problem meant to remove the unnecessary details of the real problem that involves complex objects and conditions.


回答1:


This problem is quite easy if you are familiar with LINQ, and you are comfortable with writing LINQ methods that are not built in.

using System.Linq;

var source = new List<int> { 0, 0, 0, 27, 29, 24, 35, 33, 32, 1, 1, 1,
    22, 55, 44, 44, 55, 59, 0, 0, 0, 0 };

var result = source
    .SkipWhile(n => n == 0) // Leading 0s ignored
    .GroupConsecutiveByKey(n => n / 10) // Next items having same tens grouped
    .SkipWhile(g => g.Any(n => n % 2 == 0)) // Group containing an even number ignored
    .Skip(1) // Next group ignored
    .Where(g => !g.All(n => n == 1)) // 1s are ignored as well
    .FirstOrDefault(g => g.Any(n => n % 10 == 9)) // Contains item ending in 9
    .FirstOrDefault(n => n % 10 == 9); // Item ending in 9

Console.WriteLine($"Result: {result}");

Output:

Result: 59

The only missing method is the GroupConsecutiveByKey. Here is an implementation:

public static IEnumerable<List<TSource>> GroupConsecutiveByKey<TSource, TKey>(
    this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
    var comparer = EqualityComparer<TKey>.Default;
    TKey prevKey = default;
    List<TSource> list = null;
    foreach (var item in source)
    {
        var key = keySelector(item);
        if (list == null)
        {
            list = new List<TSource>();
        }
        else if (!comparer.Equals(key, prevKey))
        {
            yield return list;
            list = new List<TSource>();
        }
        list.Add(item);
        prevKey = key;
    }
    if (list != null) yield return list;
}

The source sequence is enumerated only once. The only buffered elements are the ones belonging in a single group of consecutive numbers having same tens. This query could give a result for sequences with astronomical number of elements.




回答2:


You can use Linq, with some customization. You would need to implement a reverse enumerator so that you can keep the main logic the same, regardless if you are iterating left to right or vice versa. Don't use Linq's Reverse method with a big list, because that iterates the entire collection first.

How should I iterate the list and build those groups at the same time. What is a clean way to do this in C#?

The cleanest way is to use Linq's GroupBy, but that will cause the entire list to be iterated over while it is building the groups. Again, you can make your own enumerator and extension method that wraps the original enumerator and returns the groups, which would be more performant because you know your grouping function operates on elements that are already in order.

Once you have an enumerable of the groups, it's simply a SkipWhile to filter out zeros, another to filter groups with even numbers, a Skip to skip the next group, then First to find a group that ends with 9




回答3:


Let's say you have this list:

var numbers = new List<int> { 0, 0, 0, 27, 29, 24, 35, 33, 32, 1, 1, 1, 55, 44, 44, 55, 59, 0, 0, 0, 0 };

You can interpret the "unspecified condition" to be an input variable, which I shall call reversed:

public void DoAlgorithm(List<int> numbers, bool reversed)
{

To implement something that is reversible, you can hold the for conditions in variables, like this:

    var startIndex = reversed ? numbers.Count - 1 : 0;
    var endIndex   = reversed ? 0 : numbers.Count - 1;
    var increment  = reversed ? -1 : 1;

    for (int index = startIndex; index != endIndex; index += incremenet)
    {
        var currentNumber = numbers[index];

There are several ways to do the groups, but one way is to use a dictionary:

var groups = new Dictionary<int, List<int>>();

Then, given a number, you can figure out which list it would go in by dividing by 10.

Of course, you have to create the list first if it isn't present.

var tens = currentNumber / 10;
if (!groups.ContainsKey(tens)) groups.Add(tens, new List<int>());

Then add the number:

groups[tens].Add(currentNumber);

I won't write the rest of the code for you, but this should give you a pretty good idea how to proceed.




回答4:


@Theodor Zoulias gave an answer using LINQ.

The following solution is more manual:

@John Wu solved the index problem I had by using variables and a for loop.

As for the other part of the problem, ie how to build the groups while iterating the list, this should do it:

// This function finds the next group in numbers, starting at index startIndex.
IList<int> FindNextGroup(IList<int> numbers, int startIndex, bool reversed)
{
    var group = new List<int>();

    var firstNumberOfGroup = numbers[startIndex];

    // See @John Wu's answer
    var endIndex = reversed ? 0 : numbers.Count -1;
    var increment = reversed ? -1 : 1;
    for (int i = startIndex + 1; i != endIndex; i += increment)
    {
        if (numbers[i] is in same group as firstNumberOfGroup
            && numbers[i] != 0 && numbers[i] != 1 && ...)
            group.Add(numbers[i]);
        else
            return group;
    }
}


来源:https://stackoverflow.com/questions/58576497/c-how-to-iterate-over-a-list-with-many-conditions-filters-and-groups-of-items

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!