C# 高效滚动最大和最小窗口

C# 高效滚动最大和最小窗口,c#,performance,C#,Performance,我想有效地计算滚动最大值和最小值。这意味着在每次窗口移动时都要从使用的所有值中重新计算最大值/最小值 这里有一篇帖子问了同样的问题,有人发布了一个解决方案,涉及某种堆栈方法,根据其评级,这种方法应该是有效的。然而,为了我的生命,我再也找不到它了 如果您能帮助我们找到解决方案或帖子,我们将不胜感激。谢谢大家! 要使用的算法称为升序极小值 要在C#中实现这一点,您需要获得一个类,并且NuGet上有一个好的类,名称为 我已经使用Nito.Deque编写了一个快速的C#实现,但是我只是简单地检查了一下,

我想有效地计算滚动最大值和最小值。这意味着在每次窗口移动时都要从使用的所有值中重新计算最大值/最小值

这里有一篇帖子问了同样的问题,有人发布了一个解决方案,涉及某种堆栈方法,根据其评级,这种方法应该是有效的。然而,为了我的生命,我再也找不到它了


如果您能帮助我们找到解决方案或帖子,我们将不胜感激。谢谢大家!

要使用的算法称为升序极小值

要在C#中实现这一点,您需要获得一个类,并且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;
    }
}
公共静态类递增最小值
{
私有结构最小值
{
public int RemoveIndex{get;set;}
公共双值{get;set;}
}
公共静态双精度[]GetMin(此双精度[]输入,int窗口)
{
var queue=new Deque();
var结果=新的双精度[input.Length];
for(int i=0;i0&&i>=队列[0].RemoveIndex)
queue.RemoveFromFront();
while(queue.Count>0&&queue[queue.Count-1]。值>=val)
queue.RemoveFromBack();
AddToBack(新的最小值{RemoveIndex=i+window,Value=val});
结果[i]=队列[0]。值;
}
返回结果;
}
}

这里有一种更有效的方法。您仍然需要偶尔计算该值,但除了某些退化数据(不断减小的值),在该解决方案中,该值被最小化

我们将把自己限制到最大限度来简化事情,但将其扩展到最小限度也很简单

您只需要以下内容:

  • 窗口本身,最初为空
  • 当前最大值(
    max
    ),最初为任意值
  • 当前最大值(
    maxcount
    )的计数,最初为零
其思想是使用
max
maxcount
作为缓存来保存当前最大值。在缓存有效的地方,您只需要返回其中的值,这是一个非常快速的常量时间操作

如果请求最大值时缓存无效,它将填充缓存,然后返回该值。这比上一段中的方法慢,但缓存有效后的最大值请求再次使用更快的方法

以下是维护窗口和相关数据的方法:

  • 获取下一个值
    N

  • 如果窗口已满,请删除最早的条目
    M
    。如果最大计数大于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

  • 现在,在进行窗口管理的任何时候,您都可以请求最大值。这是一个独立的操作,不同于窗口管理本身。这可以按顺序使用以下规则来完成

  • 如果窗口为空,则没有最大值:引发异常或返回一些合理的sentinel值

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

  • 否则,需要重新填充缓存。浏览整个列表,按照下面的代码片段设置
    max
    maxcount



  • 事实上,您主要维护一个最大值的缓存,并且只在需要时重新计算,这比在添加条目时盲目地重新计算要有效得多

    为了获得一些明确的统计信息,我创建了以下Python程序。它使用大小为25的滑动窗口,并使用0到999(含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))
    
    请注意,每次向窗口添加数字时,测试线束都会尝试获得最大值。在实践中,可能不需要这样做。换句话说,这是生成的随机数据的最坏情况


    为了伪统计的目的运行该程序几次,我们得到(为报告目的格式化和分析):

    因此,您可以看到,对于随机数据,平均而言,只有大约3.95%的情况会导致计算命中(缓存未命中)。绝大多数使用缓存的值。这应该比每次插入窗口时必须重新计算最大值要好得多

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