【问题标题】:Split a list into sublist by checking a condition on elements通过检查元素的条件将列表拆分为子列表
【发布时间】:2014-04-03 20:01:23
【问题描述】:

假设我有一个整数数组,我想将它分成几个部分,并且我想使用零作为何时中断的条件。像这样:

[1,2,3,0,4,5,0,6,7] => [[1,2,3,0], [4,5,0], [6,7]]

嗯,使用两个 for 循环可以轻松完成,但我想知道是否可以使用 LINQ 执行此操作。

有几个像这样的问题[1],[2],但与此相反,它们依赖于从列表之外提供的条件。

注意:我知道在一个线程中问多个问题是不礼貌的,但如果有人熟悉函数式编程(因为它本质上是一个 FP 问题),我也想看看他们的观点和这个问题的可能解决方案。

【问题讨论】:

    标签: c# arrays linq list functional-programming


    【解决方案1】:

    您在集合的不同元素之间存在依赖关系,特别是对于您想知道“前一个元素是否为零?”的每个元素。只要您的查询依赖于前一个元素(或者,更一般地说,只要您的查询依赖于相同序列的其他元素),您就应该使用Aggregate(或更一般的函数式编程术语,fold )。这是因为Aggregate 与其他 LINQ 运算符不同,它允许您将状态从一个迭代带到下一个迭代。

    所以,为了回答您的问题,我将在 LINQ 中编写如下查询。

    // assume our list of integers it called values
    var splitByZero = values.Aggregate(new List<List<int>>{new List<int>()},
                                       (list, value) => {
                                           list.Last().Add(value);
                                           if (value == 0) list.Add(new List<int>());
                                           return list;
                                       });
    

    我将把它分解成几个部分,以便更好地解释我的想法。

    values.Aggregate(new List<List<int>>{new List<int>()},
    

    正如我之前所说,因为我们需要携带状态,所以使用 Aggregate。将一个新的空列表放入我们的列表列表中可以消除 List&lt;List&lt;int&gt;&gt; 中没有列表的边缘情况。

    (list, value) => {...}
    

    再次查看 lambda 表达式的签名(Func&lt;List&lt;List&lt;int&gt;&gt;, int, List&lt;List&lt;int&gt;&gt;),我们可以看到状态显式传递:我们接受 List&lt;List&lt;int&gt;&gt; 并返回相同的值。

    list.Last().Add(value);
    

    由于我们总是希望处理最新的List&lt;int&gt;,因此我们得到了列表列表中的Last() 元素(由于上面的部分,它永远不会为空)。

    if (value == 0) list.Add(new List<int>());
    

    这是我们进行拆分的地方 - 在下一次迭代中,对 Last() 的调用将返回这个新列表。

    return list;
    

    我们最终将状态传递给下一次迭代。


    这可以用SplitOn 方法轻松概括,如下所示:

    public static IEnumerable<IEnumerable<T>> SplitOn<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        return source.Aggregate(new List<List<T>> {new List<T>()},
                                (list, value) =>
                                    {
                                        list.Last().Add(value);
                                        if (predicate(value)) list.Add(new List<T>());
                                        return list;
                                    });
    }
    

    使用IEnumerable's 而不是List's 的版本不太清楚,因为 Enumerables 的工作方式,但同样,从上面的代码创建并不是特别难,看起来像(简化只是通过三元运算符进行触摸):

    public static IEnumerable<IEnumerable<T>> SplitOn<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        return source.Aggregate(Enumerable.Repeat(Enumerable.Empty<T>(), 1),
                                (list, value) =>
                                    {
                                        list.Last().Concat(Enumerable.Repeat(value, 1));
                                        return predicate(value) ? list.Concat(Enumerable.Repeat(Enumerable.Empty<T>(), 1)) : list;
                                    });
    }
    

    您还可能会发现Haskell's implementation of splitOn 很有趣,因为它完全符合您的要求。我会称之为不平凡(说得轻巧)。

    【讨论】:

    • 不幸的是,IEnumerable&lt;IEnumerable&lt;T&gt;&gt; 版本不起作用,因为.Concat() 不会像.Add() 那样改变调用对象,所以这条线(没有return)什么都不做,结果总是空的。无数的空虚。为了在不使事情复杂化的情况下解决这个问题,我决定构建IEnumerable&lt;List&lt;T&gt;&gt;,使用.Add() 作为值,使用.Concat() 分割列表。尽管如此,还是感谢您的回答,这是最有见地的。
    【解决方案2】:

    这是一个有帮助的扩展:

    public static IEnumerable<Tuple<TIn, int>> MarkWithLabels<TIn>(this IEnumerable<TIn> src, Predicate<TIn> splittingCondition)
    {
        int label = 0;
        foreach (TIn item in src)
        {
            yield return new Tuple<TIn, int>(item, label);
            if (splittingCondition(item))
                label++;
        }
    }
    

    有了它,下面的技巧就可以了

    int breakingValue = 0;
    var subseq = seq.MarkWithLabels(i => i == breakingValue)
        .GroupBy(tup => tup.Item2)
        .Select(group => group.Select(tup => tup.Item1).ToArray())
        .ToArray();
    

    FP 解决方案可能基本相同,除了 foreach。

    【讨论】:

      【解决方案3】:

      我已经编译了两个完全基于Zack's答案的扩展方法。

      public static IEnumerable<List<T>> SplitBefore<T>(this IEnumerable<T> source, Func<T, bool> predicate)
      {
          return source.Aggregate(
              Enumerable.Repeat(new List<T>(), 1),
              (list, value) =>
              {
                  if (predicate(value))
                      list = list.Concat(Enumerable.Repeat(new List<T>(), 1));
                  list.Last().Add(value);
                  return list;
              }
          )
          .Where(list => list.Any());
      }
      
      
      public static IEnumerable<List<T>> SplitAfter<T>(this IEnumerable<T> source, Func<T, bool> predicate)
      {
          return source.Aggregate(
              Enumerable.Repeat(new List<T>(), 1),
              (list, value) =>
              {
                  list.Last().Add(value);
                  return predicate(value)
                      ? list.Concat(Enumerable.Repeat(new List<T>(), 1))
                      : list;
              }
          )
          .Where(list => list.Any());
      }
      

      【讨论】:

        猜你喜欢
        • 2019-04-08
        • 2015-05-19
        • 1970-01-01
        • 2015-12-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多