Поиск локальных максимумов в динамическом диапазоне

Работая в С#, мне нужно найти все локальные пики в списке двойников и вернуть их в качестве другого списка. Это кажется достаточно простым, если у меня есть определенное количество значений, которые я сравниваю в любом заданном "окне" значений, но мне нужно иметь возможность фактически передать этот размер окна в саму функцию. Это может ввести в заблуждение, но в основном мне нужно что-то вроде этого:

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

где, если "rangeOfPeaks" равен 5, "текущее" значение сравнивается с 2 значениями с каждой стороны, чтобы определить, был ли он пиком или нет. Если "rangeOfPeaks" равно 11, текущее значение сравнивалось бы с 5 значениями с каждой стороны. Я думаю, что это был довольно простой алгоритм, однако я не смог найти хорошие методы для обнаружения такого пика. Кто-нибудь когда-либо делал это раньше? Любая помощь вообще будет оценена по достоинству. Спасибо заранее!

Ответы

Ответ 1

Есть, вероятно, более эффективные способы, но 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;
    }

Ответ 2

Я предлагаю несколько изменений в сообщении Леви...

1) Код Levy выдал исключение, когда указанные значения IList были почти прямой.

2) Я считаю, что индекс пиков в массиве является желаемым результатом. Рассмотрим, например, что произойдет, если бы у нас было два пика с одинаковыми удвоениями? Ops. Изменено для возврата индекса пиков в указанный 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;
    }

Ответ 3

Старый вопрос, который уже имеет принятый ответ, но мне нужно что-то лучше, чем O (n ^ 2). Эта функция O (n * m), где m - размер окна, и имеет то преимущество, что она действительно работает. Метод возвращает кортежи индексов локальных максимумов и их значение.

Вызов Enumerable.Repeat() обеспечивает максимальные максимумы в самом начале и конце набора.

Сравнение с очередью after использует >=, так что локальный максимум будет найден в начале плато значений. Побочным эффектом является то, что значение в индексе 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

Эта функция 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;
        }
    }

Ответ 5

Используя пакет интерактивных расширений из команды Rx, вы можете решить эту проблему довольно аккуратно. Пакет имеет множество функций, связанных с различными сценариями буферизации/окон.

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;
}

Ответ 6

Вот моя версия. Он использует 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)