【问题标题】:Finding Local Maxima Over a Dynamic Range在动态范围内寻找局部最大值
【发布时间】:2011-03-11 04:37:38
【问题描述】:

在 C# 中工作时,我需要在双打列表中找到所有局部峰值,并将它们作为另一个列表双打返回。如果我在任何给定的值“窗口”中比较了一组值,这似乎很简单,但我需要能够实际将此窗口大小传递给函数本身。这可能会令人困惑,但基本上我需要这样的东西:

public List<double> FindPeaks(List<double> values, double rangeOfPeaks)

如果“rangeOfPeaks”为 5,则“当前”值将与其两侧的 2 个值进行比较,以确定它是否为峰值。如果“rangeOfPeaks”为 11,则当前值将与每侧的 5 个值进行比较。我认为这是一个非常基本的算法,但是,我一直未能找到任何好的方法来检测这样的峰值。以前有人做过吗?任何帮助将不胜感激。提前致谢!

【问题讨论】:

  • 你需要什么步骤? 1 或 rangeOfPeeks? (你想要 ~values.Length 结果还是 values.Length/rangeOfPeaks 结果?)

标签: c# algorithm


【解决方案1】:

我建议对 Levy 的帖子进行一些更改...

1) 当指定值 IList 几乎是一条直线时,Levy 的代码抛出异常。

2) 我认为数组中峰值的索引是期望的结果。例如,考虑如果我们有两个具有相同双峰的峰会发生什么?行动。更改为返回指定 IList 中的峰值索引。

    public static IList<int> FindPeaks(IList<double> values, int rangeOfPeaks)
    {
        List<int> peaks = new List<int>();
        double current;
        IEnumerable<double> range;

        int checksOnEachSide = rangeOfPeaks / 2;
        for (int i = 0; i < values.Count; i++)
        {
            current = values[i];
            range = values;

            if (i > checksOnEachSide)
            {
                range = range.Skip(i - checksOnEachSide);
            }

            range = range.Take(rangeOfPeaks);
            if ((range.Count() > 0) && (current == range.Max()))
            {
                peaks.Add(i);
            }
        }

        return peaks;
    }

【讨论】:

    【解决方案2】:

    可能有更有效的方法,但 LINQ 让这变得非常简单

        static IList<double> FindPeaks(IList<double> values, int rangeOfPeaks)
        {
            List<double> peaks = new List<double>();
    
            int checksOnEachSide = rangeOfPeaks / 2;
            for (int i = 0; i < values.Count; i++)
            {
                double current = values[i];
                IEnumerable<double> range = values;
                if( i > checksOnEachSide )
                    range = range.Skip(i - checksOnEachSide);
                range = range.Take(rangeOfPeaks);
                if (current == range.Max())
                    peaks.Add(current);
            }
            return peaks;
        }
    

    【讨论】:

    • 是的,还有更有效的方法——.Skip 没有对IList 进行任何优化,所以最终结果为 O(n^2)。
    【解决方案3】:

    已经有一个公认答案的老问题,但我想要比 O(n^2) 更好的东西。这个函数是 O(n*m),其中 m 是窗口大小,并且也具有实际工作的优点。该方法返回局部最大值索引的元组及其相关值。

    Enumerable.Repeat() 的调用可确保在集合的开头和结尾找到最大值。

    after 队列的比较使用&gt;=,以便在值平台的开始处找到局部最大值。一个副作用是,如果集合中的所有值都相等,则返回索引 0 处的值,这可能是可取的,也可能不是可取的。

    public static IEnumerable<Tuple<int, double>> LocalMaxima( IEnumerable<double> source, int windowSize )
    {
        // Round up to nearest odd value
        windowSize = windowSize - windowSize % 2 + 1;
        int halfWindow = windowSize / 2;
    
        int index = 0;
        var before = new Queue<double>( Enumerable.Repeat( double.NegativeInfinity, halfWindow ) );
        var after = new Queue<double>( source.Take( halfWindow + 1 ) );
    
        foreach( double d in source.Skip( halfWindow + 1 ).Concat( Enumerable.Repeat( double.NegativeInfinity, halfWindow + 1 ) ) )
        {
            double curVal = after.Dequeue();
            if( before.All( x => curVal > x ) && after.All( x => curVal >= x ) )
            {
                yield return Tuple.Create( index, curVal );
            }
    
            before.Dequeue();
            before.Enqueue( curVal );
            after.Enqueue( d );
            index++;
        }
    }
    

    【讨论】:

      【解决方案4】:

      为了在不引发异常的情况下允许窗口大小为 1,我翻转了 Jeroen Cranendonk 贡献的 before.Enqueue( curVal );before.Dequeue(); 函数:

      public static IEnumerable<Tuple<int, double>> LocalMaxima( IEnumerable<double> source, int windowSize )
      {
          // Round up to nearest odd value
          windowSize = windowSize - windowSize % 2 + 1;
          int halfWindow = windowSize / 2;
      
          int index = 0;
          var before = new Queue<double>( Enumerable.Repeat( double.NegativeInfinity, halfWindow ) );
          var after = new Queue<double>( source.Take( halfWindow + 1 ) );
      
          foreach( double d in source.Skip( halfWindow + 1 ).Concat( Enumerable.Repeat( double.NegativeInfinity, halfWindow + 1 ) ) )
          {
              double curVal = after.Dequeue();
              if( before.All( x => curVal > x ) && after.All( x => curVal >= x ) )
              {
                  yield return Tuple.Create( index, curVal );
              }
              before.Enqueue( curVal );
              before.Dequeue();
              after.Enqueue( d );
              index++;
          }
      }
      

      【讨论】:

        【解决方案5】:

        这个函数是 O(n)。它会在执行过程中产生结果,因此它的内存开销也非常低。

            public static IEnumerable<double> FindPeaks(IEnumerable<double> values, int rangeOfPeaks)
            {
                double peak = 0;
                int decay = 0;
        
                foreach (var value in values)
                {
                    if (value > peak || decay > rangeOfPeaks / 2)
                    {
                        peak = value;
                        decay = 0;
                    }
                    else
                    {
                        decay++;
                    }
        
                    if (decay == rangeOfPeaks / 2)
                        yield return peak;
                }
            }
        

        【讨论】:

        • 使用Range = 5 和序列1,1,4,3,1,4,1,1,5 不会将4,4,5 作为峰值返回。
        【解决方案6】:

        使用来自 Rx 团队的Interactive Extensions package,您可以非常巧妙地解决这个问题。该包有很多功能与不同的缓冲/窗口场景有关。

        IEnumerable<double> FindPeaks(IEnumerable<double> numbers, int windowSize)
        {
            // Pad numbers to the left of <numbers> so that the first window of <windowSize> is centred on the first item in <numbers>
            // Eg if numbers = { 1, 2, 3, 4 }, windowSize = 3, the first window should be { MinValue, 1, 2 }, not { 1, 2, 3 }
            var paddedNumbers = Enumerable.Repeat(double.MinValue, windowSize / 2)
                                          .Concat(numbers);
        
            // Take buffers of size <windowSize>, stepping forward by one element each time
            var peaks = paddedNumbers.Buffer(windowSize, 1)
                                     .Select(range => range.Max())
                                     .DistinctUntilChanged();
        
            return peaks;
        }
        

        【讨论】:

          【解决方案7】:

          这是我的版本。它使用Queue 来保存最后一个windowSize 元素,同时枚举源。不幸的是,我不得不使用效率低下的ElementAt Linq 方法在Queue 中查找测试元素,因为Queue 实现没有公开其GetElement 方法(它是内部的)。对于小窗口尺寸,这应该不是问题。

          public static IEnumerable<(int, TSource)> LocalMaxima<TSource>(
              this IEnumerable<TSource> source, int windowSize)
          {
              var comparer = Comparer<TSource>.Default;
              var queue = new Queue<TSource>();
              var testedQueueIndex = (windowSize - 1) / 2;
              var index = testedQueueIndex;
              foreach (var item in source)
              {
                  queue.Enqueue(item);
                  if (queue.Count >= windowSize)
                  {
                      var testedItem = queue.ElementAt(testedQueueIndex);
                      var queueIndex = 0;
                      foreach (var queuedItem in queue)
                      {
                          if (queueIndex != testedQueueIndex
                              && comparer.Compare(queuedItem, testedItem) > 0) goto next;
                          queueIndex++;
                      }
                      yield return (index, testedItem);
                  next:
                      queue.Dequeue();
                      index++;
                  }
              }
          }
          

          使用示例:

          var source = "abbacdbbcac".ToCharArray();
          var indexes = Enumerable.Range(0, source.Length);
          var result = source.LocalMaxima(5);
          Console.WriteLine($"Source:  {String.Join(", ", source)}");
          Console.WriteLine($"Indexes: {String.Join("  ", indexes)}");
          Console.WriteLine($"Result:  {String.Join(", ", result)}");
          

          输出:

          Source:  a, b, b, a, c, d, b, b, c, a, c
          Indexes: 0  1  2  3  4  5  6  7  8  9  10
          Result:  (5, d), (8, c)
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2021-09-12
            相关资源
            最近更新 更多