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



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

[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());
}

相关内容

  • 没有找到相关文章

最新更新