我想有效地计算滚动的最大值和最小值。比每次窗口移动时重新计算所有使用值中的最大值/最小值更好。
这里有一篇帖子提出同样的问题,有人发布了一个涉及某种堆栈方法的解决方案,据说根据其评级工作。但是,对于我的生活,我再也找不到它。
在寻找解决方案或帖子时,我们将不胜感激。谢谢大家!
答案 0 :(得分:3)
您要使用的算法称为提升最小值 (C++ implementation)。
要在C#中执行此操作,您需要获得一个double ended queue类,并且在名称为Nito.Deque的NuGet上存在一个好类。
我已经使用Nito.Deque编写了一个快速的C#实现,但是我只是简单地检查了它,并且从头开始做了所以它可能是错的!
public static class AscendingMinima
{
private struct MinimaValue
{
public int RemoveIndex { get; set; }
public double Value { get; set; }
}
public static double[] GetMin(this double[] input, int window)
{
var queue = new Deque<MinimaValue>();
var result = new double[input.Length];
for (int i = 0; i < input.Length; i++)
{
var val = input[i];
// Note: in Nito.Deque, queue[0] is the front
while (queue.Count > 0 && i >= queue[0].RemoveIndex)
queue.RemoveFromFront();
while (queue.Count > 0 && queue[queue.Count - 1].Value >= val)
queue.RemoveFromBack();
queue.AddToBack(new MinimaValue{RemoveIndex = i + window, Value = val });
result[i] = queue[0].Value;
}
return result;
}
}
答案 1 :(得分:3)
这是提高效率的一种方法。您仍然需要偶尔计算值 ,但除了某些退化数据(不断减少的值)之外,在此解决方案中最小化。
我们将自己限制在最大限度以简化事情,但也可以将其扩展到最低限度。
您只需要以下内容:
max
),最初是任何值。maxcount
)的计数,最初为零。我们的想法是使用max
和maxcount
作为缓存来保持当前最大值。在缓存有效的情况下,您只需要返回其中的值,这是一个非常快速的恒定时间操作。
如果在请求最大值时缓存无效,则填充缓存然后返回该值。这比前一段中的方法慢,但是一旦缓存有效,后续的最大请求将使用更快的方法。
以下是维护窗口和相关数据的操作:
获取下一个值N
。
如果窗口已满,请删除最早的条目M
。如果maxcount大于0且M
等于max
,则递减maxcount
。一旦maxcount
达到0,缓存就无效,但我们不需要担心,直到用户请求最大值为止(没有重新填充的点)直到那时的缓存。)
将N
添加到滚动窗口。
如果窗口大小现在为1(N
是唯一的当前条目),请将max
设置为N
并将maxcount
设置为1,然后转到回到第1步。
如果maxcount
大于0且N
大于max
,请将max
设为N
,将maxcount
设为1,然后回到步骤1.
如果maxcount
大于0且N
等于max
,请增加maxcount
。
返回第1步。
现在,在窗口管理正在进行的任何点,您可以请求最大值。这是一个单独的操作,与窗口管理本身不同。这可以按顺序使用以下规则来完成。
如果窗口为空,则没有最大值:引发异常或返回一些合理的哨兵值。
如果maxcount
大于0,则缓存有效:只需返回max
。
否则,需要重新填充缓存。浏览整个列表,根据下面的代码段设置max
和maxcount
。
set max to window[0], maxcount to 0
for each x in window[]:
if x > max:
set max to x, maxcount to 1
else:
if x == max:
increment maxcount
大多数情况下,您需要维护一个最大值的缓存而仅在需要时重新计算,这使得这个解决方案比只是在添加条目时盲目地重新计算更有效。
对于一些明确的统计数据,我创建了以下Python程序。它使用大小为25的滑动窗口并使用0到999之间的随机数(您可以使用这些属性来查看它们如何影响结果)。
首先是一些初始化代码。请注意stat
个变量,它们将用于计算缓存命中和未命中数:
import random
window = []
max = 0
maxcount = 0
maxwin = 25
statCache = 0
statNonCache = 0
然后按照我上面的描述,在窗口中添加一个数字的函数:
def addNum(n):
global window
global max
global maxcount
if len(window) == maxwin:
m = window[0]
window = window[1:]
if maxcount > 0 and m == max:
maxcount = maxcount - 1
window.append(n)
if len(window) == 1:
max = n
maxcount = 1
return
if maxcount > 0 and n > max:
max = n
maxcount = 1
return
if maxcount > 0 and n == max:
maxcount = maxcount + 1
接下来,从窗口返回最大值的代码:
def getMax():
global max
global maxcount
global statCache
global statNonCache
if len(window) == 0:
return None
if maxcount > 0:
statCache = statCache + 1
return max
max = window[0]
maxcount = 0
for val in window:
if val > max:
max = val
maxcount = 1
else:
if val == max:
maxcount = maxcount + 1
statNonCache = statNonCache + 1
return max
最后,测试工具:
random.seed()
for i in range(1000000):
val = int(1000 * random.random())
addNum(val)
newmax = getMax()
print("%d cached, %d non-cached"%(statCache,statNonCache))
请注意,测试工具会尝试在向窗口添加数字的每个时间内获取的最大值。在实践中,可能不需要这样做。换句话说,这是生成随机数据的最坏情况。
出于伪统计目的运行该程序几次,我们得到(格式化和分析用于报告目的):
960579 cached, 39421 non-cached
960373 cached, 39627 non-cached
960395 cached, 39605 non-cached
960348 cached, 39652 non-cached
960441 cached, 39559 non-cached
960602 cached, 39398 non-cached
960561 cached, 39439 non-cached
960463 cached, 39537 non-cached
960409 cached, 39591 non-cached
960798 cached, 39202 non-cached
======= ======
9604969 395031
因此,您可以看到,对于随机数据,平均只有约3.95%的案例导致计算命中(缓存未命中)。绝大多数人使用缓存的值。这应该比每次插入窗口时重新计算最大值要好得多。
影响该百分比的一些事情将是:
0..999
缩小到0..9
,可以大大减少缓存未命中率(0.85%)。答案 2 :(得分:0)
我假设“窗口”是指范围a[start]
到a[start + len]
,start
移动。考虑最小值,最大值相似,以及移动到窗口a[start + 1]
到a[start + len + 1]
。然后,只有当(a)a[start + len + 1] < min
(较小的值进入)或(b)a[start] == min
(刚刚离开的最小值之一;重新计算最小值)时,窗口的最小值才会改变。
另一种可能更有效的方法是使用第一个窗口填充优先级队列,并使用每个值进入/离开进行更新,但我认为这不是更好(优先级队列不适合“从中间选择随机元素”(推进窗口时需要做的事情)。代码会更复杂。更好地坚持简单的解决方案,直到证明表现性能不高不可接受,并且此代码负责(大部分)资源消耗。
答案 3 :(得分:0)
昨天写了我自己的算法,并要求改进之后,我被提到了这里。确实这个算法更优雅。 我不确定它是否提供恒定速度计算,无论窗口大小如何,但无论如何,我测试了性能与我自己的缓存算法(相当简单,并且可能使用与其他人提出的相同的想法)。 缓存速度提高了8-15倍(使用5,50,300,1000的滚动窗口测试我不需要更多)。 以下是具有秒表和结果验证的替代方案。
static class Program
{
static Random r = new Random();
static int Window = 50; //(small to facilitate visual functional test). eventually could be 100 1000, but not more than 5000.
const int FullDataSize =1000;
static double[] InputArr = new double[FullDataSize]; //array prefilled with the random input data.
//====================== Caching algo variables
static double Low = 0;
static int LowLocation = 0;
static int CurrentLocation = 0;
static double[] Result1 = new double[FullDataSize]; //contains the caching mimimum result
static int i1; //incrementor, just to store the result back to the array. In real life, the result is not even stored back to array.
//====================== Ascending Minima algo variables
static double[] Result2 = new double[FullDataSize]; //contains ascending miminum result.
static double[] RollWinArray = new double[Window]; //array for the caching algo
static Deque<MinimaValue> RollWinDeque = new Deque<MinimaValue>(); //Niro.Deque nuget.
static int i2; //used by the struct of the Deque (not just for result storage)
//====================================== my initialy proposed caching algo
static void CalcCachingMin(double currentNum)
{
RollWinArray[CurrentLocation] = currentNum;
if (currentNum <= Low)
{
LowLocation = CurrentLocation;
Low = currentNum;
}
else if (CurrentLocation == LowLocation)
ReFindHighest();
CurrentLocation++;
if (CurrentLocation == Window) CurrentLocation = 0; //this is faster
//CurrentLocation = CurrentLocation % Window; //this is slower, still over 10 fold faster than ascending minima
Result1[i1++] = Low;
}
//full iteration run each time lowest is overwritten.
static void ReFindHighest()
{
Low = RollWinArray[0];
LowLocation = 0; //bug fix. missing from initial version.
for (int i = 1; i < Window; i++)
if (RollWinArray[i] < Low)
{
Low = RollWinArray[i];
LowLocation = i;
}
}
//======================================= Ascending Minima algo based on http://stackoverflow.com/a/14823809/2381899
private struct MinimaValue
{
public int RemoveIndex { get; set; }
public double Value { get; set; }
}
public static void CalcAscendingMinima (double newNum)
{ //same algo as the extension method below, but used on external arrays, and fed with 1 data point at a time like in the projected real time app.
while (RollWinDeque.Count > 0 && i2 >= RollWinDeque[0].RemoveIndex)
RollWinDeque.RemoveFromFront();
while (RollWinDeque.Count > 0 && RollWinDeque[RollWinDeque.Count - 1].Value >= newNum)
RollWinDeque.RemoveFromBack();
RollWinDeque.AddToBack(new MinimaValue { RemoveIndex = i2 + Window, Value = newNum });
Result2[i2++] = RollWinDeque[0].Value;
}
public static double[] GetMin(this double[] input, int window)
{ //this is the initial method extesion for ascending mimima
//taken from http://stackoverflow.com/a/14823809/2381899
var queue = new Deque<MinimaValue>();
var result = new double[input.Length];
for (int i = 0; i < input.Length; i++)
{
var val = input[i];
// Note: in Nito.Deque, queue[0] is the front
while (queue.Count > 0 && i >= queue[0].RemoveIndex)
queue.RemoveFromFront();
while (queue.Count > 0 && queue[queue.Count - 1].Value >= val)
queue.RemoveFromBack();
queue.AddToBack(new MinimaValue { RemoveIndex = i + window, Value = val });
result[i] = queue[0].Value;
}
return result;
}
//============================================ Test program.
static void Main(string[] args)
{ //this it the test program.
//it runs several attempts of both algos on the same data.
for (int j = 0; j < 10; j++)
{
Low = 12000;
for (int i = 0; i < Window; i++)
RollWinArray[i] = 10000000;
//Fill the data + functional test - generate 100 numbers and check them in as you go:
InputArr[0] = 12000;
for (int i = 1; i < FullDataSize; i++) //fill the Input array with random data.
//InputArr[i] = r.Next(100) + 11000;//simple data.
InputArr[i] = InputArr[i - 1] + r.NextDouble() - 0.5; //brownian motion data.
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < FullDataSize; i++) //run the Caching algo.
CalcCachingMin(InputArr[i]);
stopwatch.Stop();
Console.WriteLine("Caching : " + stopwatch.ElapsedTicks + " mS: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < FullDataSize; i++) //run the Ascending Minima algo
CalcAscendingMinima(InputArr[i]);
stopwatch.Stop();
Console.WriteLine("AscMimima: " + stopwatch.ElapsedTicks + " mS: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
i1 = 0; i2 = 0; RollWinDeque.Clear();
}
for (int i = 0; i < FullDataSize; i++) //test the results.
if (Result2[i] != Result1[i]) //this is a test that algos are valid. Errors (mismatches) are printed.
Console.WriteLine("Current:" + InputArr[i].ToString("#.00") + "\tLowest of " + Window + "last is " + Result1[i].ToString("#.00") + " " + Result2[i].ToString("#.00") + "\t" + (Result1[i] == Result2[i])); //for validation purposes only.
Console.ReadLine();
}
}
答案 4 :(得分:0)
我建议维护一个支持getMin()
或getMax()
的堆栈。
这可以通过两个堆栈来完成,并且只需花费恒定的时间。
fyi:https://www.geeksforgeeks.org/design-a-stack-that-supports-getmin-in-o1-time-and-o1-extra-space/