>假设我有一个整数数组,我想把它分成几个部分,我想用零作为何时中断的条件。像这样:
[1,2,3,0,4,5,0,6,7] => [[1,2,3,0], [4,5,0], [6,7]]
好吧,使用两个 for 循环可以轻松完成此操作,但我想知道是否可以使用 LINQ 执行此操作。
有几个这样的问题[1],[2],但与这个问题相反,它们依赖于列表外部提供的条件。
注意:我知道在一个线程中问多个问题是不礼貌的,但如果有人熟悉函数式编程(因为它本质上是一个FP问题),我也想看看他们的观点和可能的解决方案这个问题。
集合的不同元素之间存在依赖关系,特别是对于您想知道的每个元素"上一个元素是否为零?一旦您的查询依赖于前一个元素(或者,更一般地说,一旦您的查询依赖于同一序列的其他元素),您应该达到Aggregate
(或更通用的函数式编程术语,fold
)。这是因为与其他 LINQ 运算符不同,Aggregate
允许您从一个迭代携带状态到下一个迭代。
因此,为了回答您的问题,我将在 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>()},
正如我之前所说,使用聚合,因为我们需要携带状态。将一个新的空列表放入我们的列表列表中会删除List<List<int>>
中没有列表的边缘情况。
(list, value) => {...}
同样,查看我们的 lambda 表达式的签名(Func<List<List<int>>, int, List<List<int>>
),我们可以看到状态显式传递:我们接受List<List<int>>
并返回相同的内容。
list.Last().Add(value);
由于我们总是想处理 最新的List<int>
,我们得到列表列表的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;
});
}
由于 Enumerables 的工作方式,使用 IEnumerable
's 而不是 List
's 的版本不太清楚,但同样,从上面的代码中创建并不是特别困难,看起来像(通过三元运算符简化):
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的splitOn实现很有趣,因为它完全符合你的要求。我会称它为不平凡(轻描淡写地说)。
这里有一个有帮助的扩展:
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。
我完全根据 Zack 的答案编译了两个扩展方法。
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());
}