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

本文关键字:对象 列表 最后 保留 | 更新日期: 2023-09-27 17:59:44

我想对特定方法进行一些性能测量,但我想平均完成所需的时间(这是一个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);
}

不知何故,这似乎效率低下,但也许并非如此。

建议?

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

您可以创建一个自定义集合:

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>的第一项成本很高。

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.
}

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

为了获得最佳性能,您可能只需要使用一个长数组而不是列表。

我们曾经有过类似的需求来实现下载时间估计器,我们使用了一个循环缓冲区来存储最后一个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.

这应该很容易适应您自己的需求。总和是一个很好的"缓存"信息的功能,这可能会使您的代码更快。我的意思是:如果你需要计算总和或平均值,只有当数据发生变化时,你才能计算出来,并使用最少的必要计算。

另一种选择是在请求时将所有十个数字相加的函数,这将比在将另一个值加载到缓冲区时的单个减法/加法慢。

您可能需要考虑使用队列数据结构。您可以使用一个简单的线性列表,但它的效率非常低。可以使用圆形数组,但必须不断调整其大小。因此,我建议您使用队列。

我需要在一个数组中保留最后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];
            }
        }
    }

对我来说似乎还可以。改为使用LinkedList怎么样?使用列表时,如果删除第一个项目,则所有其他项目都必须回退一个项目。使用LinkedList,您可以在列表中的任何位置添加或删除项目,而且成本很低。然而,我不知道这会有多大区别,因为我们只谈论十个项目。

链表的代价是,你无法有效地访问列表中的随机元素,因为链表必须沿着列表"行走",传递每个项目,直到它到达你需要的那个。但对于顺序访问,链表是可以的。

对于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

在得到最新答案多年后,我在寻找相同的解决方案时偶然发现了这些问题。我最后给出了以上答案的组合,特别是其中一个:由agent-j循环和由Thomas Levsque 使用队列

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

我不得不放弃j特工的一句非常优雅的话:ct = (ct + 1) % times.Length;因为我需要检测我们何时返回(通过hasCycled),以获得性能良好的枚举器。请注意,枚举器返回从最近最旧的值。