通过检查元素的条件将列表拆分为子列表
假设我有一个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
,所以我们得到List
的Last()
元素(由于上面的部分,它们永远不会为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()); }