通过检查元素的条件将列表拆分为子列表

假设我有一个integeres数组,我想把它分成几个部分,我想用零作为何时破坏的条件。 像这样的东西:

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

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

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

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

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

那么,为了回答你的问题,我将在LINQ中编写如下查询。

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

我将其分解为部分,以便我能更好地解释我的想法。

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

正如我之前所说的那样,达到Aggregate是因为我们需要携带状态。 将新的空列表放入列表列表中会删除List>的边缘大小写,其中没有列表。

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

再看一下我们的lambda表达式的签名(它是Func>, int, List> ),我们可以看到显式传递的状态:我们接受List>并返回相同。

 list.Last().Add(value); 

因为我们总是想要处理最新的List ,所以我们得到ListLast()元素(由于上面的部分,它们永远不会为null)。

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

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

 return list; 

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


SplitOn方法中,这可以很容易地推广,如下所示:

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

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

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

您也可能会发现Haskell对splitOn的实现很有趣,因为它完全符合您的要求。 我会称之为不平凡(轻描淡写)。

这是一个有助于:

 public static IEnumerable> MarkWithLabels(this IEnumerable src, Predicate splittingCondition) { int label = 0; foreach (TIn item in src) { yield return new Tuple(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(); 

除了foreach之外,FP解决方案可以基本相同。

我完全根据Zack的答案编译了两个扩展方法。

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