高效滚动最大和最小窗口

时间:2013-02-12 00:49:53

标签: c#

我想有效地计算滚动的最大值和最小值。比每次窗口移动时重新计算所有使用值中的最大值/最小值更好。

这里有一篇帖子提出同样的问题,有人发布了一个涉及某种堆栈方法的解决方案,据说根据其评级工作。但是,对于我的生活,我再也找不到它。

在寻找解决方案或帖子时,我们将不胜感激。谢谢大家!

5 个答案:

答案 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)的计数,最初为零。

我们的想法是使用maxmaxcount作为缓存来保持当前最大值。在缓存有效的情况下,您只需要返回其中的值,这是一个非常快速的恒定时间操作。

如果在请求最大值时缓存无效,则填充缓存然后返回该值。这比前一段中的方法慢,但是一旦缓存有效,后续的最大请求将使用更快的方法。

以下是维护窗口和相关数据的操作:

  1. 获取下一个值N

  2. 如果窗口已满,请删除最早的条目M。如果maxcount大于0且M等于max,则递减maxcount。一旦maxcount达到0,缓存就无效,但我们不需要担心,直到用户请求最大值为止(没有重新填充的点)直到那时的缓存。)

  3. N添加到滚动窗口。

  4. 如果窗口大小现在为1(N是唯一的当前条目),请将max设置为N并将maxcount设置为1,然后转到回到第1步。

  5. 如果maxcount大于0且N大于max,请将max设为N,将maxcount设为1,然后回到步骤1.

  6. 如果maxcount大于0且N等于max,请增加maxcount

  7. 返回第1步。

  8. 现在,在窗口管理正在进行的任何点,您可以请求最大值。这是一个单独的操作,与窗口管理本身不同。这可以按顺序使用以下规则来完成。

    1. 如果窗口为空,则没有最大值:引发异常或返回一些合理的哨兵值。

    2. 如果maxcount大于0,则缓存有效:只需返回max

    3. 否则,需要重新填充缓存。浏览整个列表,根据下面的代码段设置maxmaxcount


    4. 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%的案例导致计算命中(缓存未命中)。绝大多数人使用缓存的值。这应该比每次插入窗口时重新计算最大值要好得多。

      影响该百分比的一些事情将是:

      • 窗口大小。较大的大小意味着缓存命中的可能性更高,从而提高了百分比。例如,将窗口大小加倍会使缓存未命中几乎减半(达到1.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/