C#遍历IEnumerable进行使用前n个和后n个元素的计算

7
我发现自己经常需要处理一个IEnumerable对象,需要循环遍历每个元素,并对每个元素进行计算,该计算依赖于前后n个对象。一个常见的例子是计算滚动平均值,但有时计算比这更复杂,并依赖于列表中每个元素的几个字段。
我从未确定最佳的循环结构。效率很重要,但可维护性和可读性更重要。
有时我将其转换为List,然后使用for循环来获取元素[i-1],[i],[i+1],然后执行我的计算。
其他时候,我将其保留为IEnumerable,但是“缓存”了前几个元素,以便在foreach循环中到达[i+1]之前不会对i进行计算。
我还考虑过使用LinkedList,以便可以使用.Previous和.Next方法。
您有哪些建议最好使用哪种技术?

目前不打算发表意见(对于一个简短的答案来说,有太多的利弊得失,而我也没有时间写长篇大论),但在某些情况下另一种可能性是两个索引并行移动。i 从0开始,j 从3开始,你可以使用 k 进行操作,从 ij(半开区间,因此包括0、1和2),然后移动 ++i;++j; 并再次循环 k,直到 ++j > list.Count(所以 j 在最后一次遍历时指向列表之外)。 - Jon Hanna
2个回答

6

一种选择是创建一个扩展方法来提供一个可以使用的滚动“窗口”。这将使您能够以简单的方式编写循环:

IEnumerable<IList<T>> CreateRollingWindow(IEnumerable<T> items, int size)
{
    LinkedList<T> list = new LinkedList<T>();

    foreach(var item in items)
    {
        list.AddLast(item);
        if (list.Count == size)
        {
            yield return list.ToList();
            list.RemoveFirst();
        }
    }
}

这样一来,您可以将算法简单地编写为:
foreach(var window as collection.CreateRollingWindow(5))
{
    double rollingAverage = window.Average(); // window is IList<T> here
}

2
出于好奇,为什么要踩票呢? - Reed Copsey
可能是有人没有理解你的答案...不管怎样,我喜欢它通用且可重复使用于不同的场景,但我想知道是否有办法使它更有效率...如果你可以假设生成的列表在下一次迭代之前被消耗完,那么你可能可以直接返回它,这将避免将数据复制到新列表中。 - Thomas Levesque
@ThomasLevesque 是的。我故意使用了 LinkedList<T> 而不是 Queue<T> 来进行 ToList() 调用 - 但这使得它更加通用和安全,因为您可以在延迟执行场景、PLINQ等情况下使用它。 - Reed Copsey

2
这是一个简单的实现:
public static IEnumerable<double> RollingAverage(this IEnumerable<double> values, int count)
{
    var queue = new Queue<double>();
    foreach (var v in values)
    {
        queue.Enqueue(v);
        if (queue.Count == count)
        {
            yield return queue.Average();
            queue.Dequeue();
        }
    }
}

它可能还有改进的空间,但似乎已经可以工作了...

编辑:这里有一个稍微更好的版本(不需要枚举队列来计算平均值):

public static IEnumerable<double> RollingAverage(this IEnumerable<double> values, int count)
{
    var queue = new Queue<double>();
    double sum = 0.0;
    foreach (var v in values)
    {
        sum += v;
        queue.Enqueue(v);
        if (queue.Count == count)
        {
            yield return sum / count;
            sum -= queue.Dequeue();
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接