问题描述
我们有抽样数据。每次我们有不同的采样值时:
每个样本是 (time,value) -> 样本列表 [(t1,v1),...(tn,vn)]
我们的工作是实现moving_avg(data,window_sec)。 数据 -> 样本列表 [(t1,vn)] Windows_secs -> 以秒为单位的窗口大小
示例输入: 数据 = [(1,2),(2,4),(3,3),(4,(6,8),(8,(12,1)]
moving_avg(data,2) 返回: [(1,5),3) 返回: [(1,2.75),4.33),1)]
如何在 C# 中以最佳时间复杂度实现它?面试官告诉我我不能使用额外的空间(我在字典中的面试中使用过)
解决方法
假设具有更大 t
值的连续样本,一个简单的先验项目列表应该足以处理任务。将每个传入的值添加到缓冲区,删除窗口外的所有时间,然后对这些值求平均值。
像这样:
public class TimedValue
{
public readonly int Time;
public readonly double Value;
public TimedValue(int time,double value)
{
Time = time;
Value = value;
}
}
public static partial class Extensions
{
public static IEnumerable<TimedValue> MovingAverage(this IEnumerable<TimedValue> source,int numSeconds)
{
var buffer = new List<TimedValue>();
foreach (var item in source)
{
// Add next item to the buffer
buffer.Add(item);
// Remove expired items from buffer
int limit = item.Time - numSeconds;
while (buffer.Count > 0 && buffer[0].Time < limit)
buffer.RemoveAt(0);
// Calculate average and yield back
double average = buffer.Average(i => i.Value);
yield return new TimedValue(item.Time,average);
}
}
}
不确定标题中的“非连续”是什么意思,但如果您不想使用 IEnumerable<>
和 Linq 扩展(这并不总是最有效的方法)来做到这一点,那么有其他方式。
一种方法是分配结果缓冲区并在迭代源数据时填充它。对当前项目使用头指针,对最旧的包含项目使用尾指针,从总和中加减......等等。它看起来像:
static TimedValue[] MakeMovingAverage(TimedValue[] source,int numSeconds)
{
// Allocate the result buffer
var result = new TimedValue[source.Length];
// index of the first item to average
int tail = 0;
// the running total for the window
double sum = 0;
for (int head = 0; head < source.Length; head++)
{
// Add the current item to the running total
sum += source[head].Value;
// Remove values from the running total for expired items
var limit = source[head].Time - numSeconds;
while (source[tail].Time < limit)
sum -= source[tail++].Value;
// Set windowed average for this item
var average = sum / (head + 1 - tail);
result[head] = new TimedValue(source[head].Time,average);
}
return result;
}
对于数组输入,这更有效,因为每个输入项只处理一次。