如何保留最后n个对象的列表?

时间:2011-06-17 22:37:46

标签: c# collections circular-buffer

我想对特定方法进行一些性能测量,但我想平均完成所需的时间。 (这是一个C#Winforms应用程序,但这个问题很适用于其他框架。)

我有一个秒表,我在方法开始时重置并在结束时停止。 我想将最后10个值存储在列表或数组中。每个新增值都应该将最旧的值从列表中删除。

我会定期调用另一种方法来平均所有存储的值。

我认为这个构造是循环缓冲区吗?

如何创建具有最佳性能的缓冲区?现在我有以下内容:

List<long> PerfTimes = new List<long>(10);

// ...

private void DoStuff()
{
    MyStopWatch.Restart();
    // ...
    MyStopWatch.Stop();
    PerfTimes.Add(MyStopWatch.ElapsedMilliseconds);
    if (PerfTimes.Count > 10) PerfTimes.RemoveAt(0);
}

这在某种程度上似乎效率低下,但也许不是。

建议?

8 个答案:

答案 0 :(得分:19)

您可以创建自定义集合:

class SlidingBuffer<T> : IEnumerable<T>
{
    private readonly Queue<T> _queue;
    private readonly int _maxCount;

    public SlidingBuffer(int maxCount)
    {
        _maxCount = maxCount;
        _queue = new Queue<T>(maxCount);
    }

    public void Add(T item)
    {
        if (_queue.Count == _maxCount)
            _queue.Dequeue();
        _queue.Enqueue(item);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _queue.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

您当前的解决方案有效,但效率低,因为删除List<T>的第一项非常昂贵。

答案 1 :(得分:7)

private int ct = 0;
private long[] times = new long[10];

void DoStuff ()
{
   ...
   times[ct] = MyStopWatch.ElapsedMilliseconds;
   ct = (ct + 1) % times.Length; // Wrap back around to 0 when we reach the end.
}

这是一个简单的圆形结构。 这不需要其他解决方案的链表节点的数组复制或垃圾收集。

答案 2 :(得分:3)

为获得最佳性能,您可以使用long数组而不是列表。

我们在一个点上有类似的要求来实现下载时间估算器,我们使用循环缓冲区来存储过去N秒的每一个的速度。

我们对下载速度在整个时间内的速度并不感兴趣,只是根据最近的活动大致预计会花多长时间,而不是所以最近这个数字会全部跳过在这个地方(例如,如果我们只是用最后一秒来计算它)。

我们对整个时间框架不感兴趣的原因是下载速度可以达到1M / s半小时,然后在接下来的十分钟内切换到10M / s。尽管您现在下载速度非常快,但上半个小时将严重拖累平均速度。

我们创建了一个循环缓冲区,每个单元格在1秒钟内保存下载量。循环缓冲区大小为300,允许5分钟的历史数据,并且每个单元初始化为零。在您的情况下,您只需要十个单元格。

我们还维持了一个总数(缓冲区中所有条目的总和,因此最初也为零)和计数(显然最初为零)。

每一秒,我们都会知道自上一秒以来已经下载了多少数据,然后:

  • 从总数中减去当前单元格。
  • 将当前数字放入该单元格并推进单元格指针。
  • 将当前数字添加到总数中。
  • 如果不是300,则增加计数。
  • 根据总数/计数更新显示给用户的数字。

基本上,在伪代码中:

def init (sz):
    buffer = new int[sz]
    for i = 0 to sz - 1:
        buffer[i] = 0 
    total = 0
    count = 0
    index = 0
    maxsz = sz

def update (kbps):
    total = total - buffer[index] + kbps   # Adjust sum based on deleted/inserted values.
    buffer[index] = kbps                   # Insert new value.
    index = (index + 1) % maxsz            # Update pointer.
    if count < maxsz:                      # Update count.
        count = count + 1
    return total / count                   # Return average.

这应该很容易适应您自己的要求。总和是“缓存”信息的一个很好的功能,可以使您的代码更快。我的意思是:如果你需要计算总和或平均数,你可以只在数据发生变化时使用最小的必要计算。

替代方案是在请求时将所有十个数字相加的函数,在将另一个值加载到缓冲区时比单个减去/添加的速度慢。

答案 3 :(得分:1)

您可能希望查看使用队列数据结构。您可以使用简单的线性List,但它完全没有效率。可以使用圆形数组,但是必须不断调整它。因此,我建议你选择队列。

答案 4 :(得分:0)

似乎对我好。那么使用LinkedList呢?使用列表时,如果删除第一个项目,则必须将所有其他项目恢复为一个项目。使用LinkedList,您可以以极低的成本在列表中的任何位置添加或删除项目。但是,我不知道这会有多大区别,因为我们只讨论了十件事。

链接列表的权衡是你不能有效地访问列表中的随机元素,因为链表必须基本上沿着列表“走”,传递每个项目,直到它到达你需要的那个。但对于顺序访问,链接列表很好。

答案 5 :(得分:0)

我需要在数组中保留最后5个分数,然后我想出了这个简单的解决方案。 希望它会对某人有所帮助。

void UpdateScoreRecords(int _latestScore){
        latestScore = _latestScore;
        for (int cnt = 0; cnt < scoreRecords.Length; cnt++) {
            if (cnt == scoreRecords.Length - 1) {
                scoreRecords [cnt] = latestScore;
            } else {
                scoreRecords [cnt] = scoreRecords [cnt+1];
            }
        }
    }

答案 6 :(得分:0)

对于Java,可能就是这种方式

import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;

public class SlidingBuffer<T> implements Iterable<T>{
    private Queue<T> _queue;
    private int _maxCount;

    public SlidingBuffer(int maxCount) {
        _maxCount = maxCount;
        _queue =  new LinkedList<T>();
    }

    public void Add(T item) {
        if (_queue.size() == _maxCount)
            _queue.remove();
        _queue.add(item);
    }

    public Queue<T> getQueue() {
        return _queue;
    }

    public Iterator<T> iterator() {
        return  _queue.iterator();
    }
}

可以这样开始

public class ListT {

    public static void main(String[] args) {
        start();
    }

    private static void start() {
        SlidingBuffer<String> sb = new SlidingBuffer<>(5);
        sb.Add("Array1");
        sb.Add("Array2");
        sb.Add("Array3");
        sb.Add("Array4");
        sb.Add("Array5");
        sb.Add("Array6");
        sb.Add("Array7");
        sb.Add("Array8");
        sb.Add("Array9");

        //Test printout
        for (String s: sb) {
            System.out.println(s);
        }
    }
}

结果是

Array5

Array6

Array7

Array8

Array9

答案 7 :(得分:0)

在最新答案多年后,我在寻找相同的解决方案时偶然发现了这个问题。我以上述答案的组合结束,尤其是以下答案之一:cycling by agent-jusing a queue by Thomas Levesque

studentsRecyclerView = (RecyclerView) view.findViewById(R.id.rv_students);
studentsRecyclerView.setHasFixedSize(true);
studentsRecyclerView.setNestedScrollingEnabled(false); // this line prevents nested scrolling, you can set it to true / false depending on you want nested scroll or not
studentsRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));

我不得不放弃非常优雅的单行j-agentpublic class SlidingBuffer<T> : IEnumerable<T> { protected T[] items; protected int index = -1; protected bool hasCycled = false; public SlidingBuffer(int windowSize) { items = new T[windowSize]; } public void Add(T item) { index++; if (index >= items.Length) { hasCycled = true; index %= items.Length; } items[index] = item; } public IEnumerator<T> GetEnumerator() { if (index == -1) yield break; for (int i = index; i > -1; i--) { yield return items[i]; } if (hasCycled) { for (int i = items.Length-1; i > index; i--) { yield return items[i]; } } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } 因为我需要检测我们何时回旋(通过 ct = (ct + 1) % times.Length;)以获得一个行为良好的枚举器。请注意,枚举器返回从 most-recentoldest 值的值。

相关问题