【问题标题】:C#: How to iterate over a list with many conditions (filters and groups of items) [closed]C#:如何遍历具有许多条件的列表(过滤器和项目组)[关闭]
【发布时间】:2020-02-22 20:49:03
【问题描述】:

假设我有这个整数列表:

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 };

我想实现下面描述的搜索算法。我在找 59 号。

  1. 某些未指定的条件决定了我是从左侧还是从右侧进行迭代。在本例中,假设从左到右是迭代的方向。
  2. 我们需要修剪列表的开头:
    • 2.1。应该忽略前导 0。
    • 2.2。如果下一个项目具有相同的十位,则将它们组合在一起。例如,27、29、24 被组合在一起。接下来,将 35、33、32 组合在一起。接下来,55. 等等。
    • 2.3。如果该组包含偶数,则将其忽略并继续下一个,直到找到仅包含奇数的组。该组也被忽略,我们继续执行该算法的第 3 步。
    • 2.3。 1 也会被忽略。
  3. 一旦列表的开头被清除,我们需要处理剩余项目的结尾(44, 44, 55, 59, 0, 0, 0, 0):
    • 3.1。我们正在寻找第一个包含以 9 结尾的项目的组。我们返回这个项目。 59 被退回。如果我们从另一个方向迭代,就会找到 29 个。

我将如何在 C# 中实现这个算法?以下是我的一些担忧:

  • 我可以使用whilefor 构造迭代整个列表,但有时我必须从列表末尾开始这一事实会导致索引代码混乱。我曾想过实现自定义IEnumerable/IEnumerator 来隐藏这种混乱,但在这种情况下,我应该使用foreach 语句。但是,我想当我尝试处理上面这个foreach 中描述的组时,我仍然会一团糟。
  • 我应该如何迭代列表并同时构建这些组。在 C# 中执行此操作的简洁方法是什么?
  • 出于效率原因,我们不应该通过第一次从列表中过滤掉所有的 0。在示例中,该列表可能是一个非常长的 10000000000 个元素列表的开始。如果我们要查找的数字是第 15 个,则无需检查第 9918477 个元素。
  • 此外,此算法有 2 个不同的部分(序列的开头和结尾)。我不知道我应该如何在一次迭代中处理它们。

注意:这个例子不是家庭作业。这是一个简化的问题,旨在删除涉及复杂对象和条件的实际问题的不必要细节。

【问题讨论】:

  • 这是作业吗?
  • 不,这是一个虚构的例子。真实的事物涉及复杂对象的列表,并且条件和分组不同。我试图从一个更复杂的问题中创建一个非常简单的例子。

标签: c# algorithm conditional-statements


【解决方案1】:

您可以使用 Linq,并进行一些自定义。您将需要实现一个反向枚举器,以便您可以保持主逻辑相同,无论您是从左到右迭代还是反之亦然。不要对大列表使用 Linq 的 Reverse 方法,因为这会首先迭代整个集合。

我应该如何迭代列表并同时构建这些组。在 C# 中执行此操作的干净方法是什么?

最干净的方法是使用 Linq 的 GroupBy,但这会导致在构建组时迭代整个列表。同样,您可以创建自己的枚举器和扩展方法来包装原始枚举器并返回组,这会更高效,因为您知道分组函数对已经有序的元素进行操作。

一旦你有一个可枚举的组,它只是一个 SkipWhile 过滤掉零,另一个过滤偶数组,一个 Skip 跳过下一个组,然后 First 找到一个以 9 结尾的组

【讨论】:

  • 你有一些自定义 GroupBy 的示例代码吗?正如你所说,我不想迭代整个列表来构建组。此外,通常 GroupBy 会将不连续的元素组合在一起,这意味着 20, 21, 40, 28 会产生 2 个组,而不是我想要的 3 个组。
【解决方案2】:

假设你有这个列表:

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 };

您可以将“未指定条件”解释为输入变量,我将其称为reversed

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

要实现可逆的东西,您可以将for 条件保存在变量中,如下所示:

    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];

有几种方法可以进行分组,但一种方法是使用字典:

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

然后,给定一个数字,您可以通过除以 10 来确定它会进入哪个列表。

当然,如果列表不存在,您必须先创建列表。

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

然后添加数字:

groups[tens].Add(currentNumber);

我不会为您编写其余的代码,但这应该可以让您很好地了解如何继续。

【讨论】:

  • 你把所有的数字都用相同的十位分组,但我认为你应该只把连续的数字分组。
【解决方案3】:

@Theodor Zoulias 使用 LINQ 给出了答案。

下面的解决方案比较手动:

@John Wu 通过使用变量和for 循环解决了我遇到的索引问题。

至于问题的另一部分,即如何在迭代列表的同时构建组,应该这样做:

// 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;
    }
}

【讨论】:

    【解决方案4】:

    如果您熟悉 LINQ,并且熟悉编写非内置的 LINQ 方法,则此问题非常容易。

    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}");
    

    输出:

    结果:59

    唯一缺少的方法是GroupConsecutiveByKey。这是一个实现:

    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;
    }
    

    源序列仅枚举一次。唯一缓冲的元素是属于一组具有相同十位的连续数字的元素。这个查询可以给出具有天文数量元素的序列的结果。

    【讨论】:

    • 包含偶数的组应仅在开始时被忽略,而不是全部。
    • 我们正在寻找包含以 9 结尾的项目的第一个组 - 您正在寻找最后一个组
    • @AntonínLejsek 你是对的。我更新了我的答案。
    • 另外,如果我需要反向浏览列表并且我有一个 IList,我可以在另一个问题的答案中使用this FastReverse implementation
    • @AntonínLejsek 是的。实际上,LINQ 方法 First(OrDefault)Last(OrDefault)Single(OrDefault)ElementAt(OrDefault)CountContains 是使用优化的代码实现的,该代码利用了IList 接口(如果可用)。 Reverse 由于某种原因没有优化,可以通过检查 source code 来确认。
    猜你喜欢
    • 2017-12-10
    • 1970-01-01
    • 2021-06-07
    • 2016-01-09
    • 1970-01-01
    • 1970-01-01
    • 2021-06-18
    • 1970-01-01
    • 2017-09-11
    相关资源
    最近更新 更多