Быстрый алгоритм поиска отсортированного массива с плавающей точкой, чтобы найти пару с плавающей точкой, заключающую в себе входное значение

10

У меня есть массив с плавающей точкой, отсортированный от наименьшего к наибольшему, и мне нужно иметь возможность выбрать ближайший с плавающей точкой больше или меньше, чем переданное входное значение. Это входное значение не обязательно присутствует в качестве значения в массиве.

Наивным подходом было бы сделать простой линейный поиск по массиву. Это может выглядеть так:

void FindClosestFloatsInArray( float input, std::vector<float> array, 
                               float *min_out, float *max_out )
{
    assert( input >= array[0] && input < array[ array.size()-1 ] );
    for( int i = 1; i < array.size(); i++ )
    {
        if ( array[i] >= input )
        {
            *min = array[i-1];
            *max = array[i];
        }
    }
}

Но очевидно, что по мере увеличения массива это будет становиться все медленнее и медленнее.

У кого-нибудь есть идея алгоритма, который позволил бы мне находить эти данные более оптимально? Я уже переключился на бинарный поиск, который несколько улучшил ситуацию, но он все еще намного медленнее, чем хотелось бы, и, поскольку я на самом деле не ищу конкретное значение, которое существует в массиве, оно никогда не может завершиться рано.

Дополнительная информация: Значения с плавающей запятой в массиве не обязательно распределены равномерно (то есть массив может состоять из значений "1.f, 2.f, 3.f, 4.f, 100.f, 1200.f. 1203.f, 1400.f ".

Я делаю эту операцию сотни тысяч раз, но могу выполнить любую предварительную обработку массива с плавающей точкой, если это улучшит время поиска. Я абсолютно могу изменить, чтобы использовать что-то кроме вектора для их хранения, если это поможет.

Тревор Пауэлл
источник
С чего вы взяли, что ваш бинарный поиск не может завершиться досрочно? Конечно, вы можете просто проверить элементы в i и i + 1, чтобы увидеть, заключают ли они в скобки целевое значение, и завершить, если они делают?
Пол Р
С другой стороны, я мог бы проверить элементы в i и i-1, чтобы увидеть, содержат ли они целевое значение. Мне также нужно проверить, было ли 'i'>> array.size () - 1, чтобы я мог избежать выполнения вашего теста, и было ли это <= 0, чтобы я мог избежать выполнения моего теста ... на самом деле это много дополнительные условия для выполнения на каждом этапе, чтобы проверить ранний выход. Я предполагаю, что они сильно замедлили бы алгоритм, хотя я должен признаться, что я фактически еще не профилировал это.
Тревор Пауэлл
3
Это не должно быть настолько сложным - если ваш массив имеет размер N, то вам просто нужно обработать его, как если бы он был размером N - 1. Таким образом, в i + 1 всегда есть действительный элемент. двоичный поиск по N - 1 элементу для элемента i, который меньше целевого значения, а элемент i + 1 больше целевого значения.
Пол Р

Ответы:

11

Код в вопросе (линейный поиск), как вы правильно заметили, будет работать медленно для больших массивов с плавающей точкой. Технически это O (n), где n - число значений с плавающей точкой в ​​вашем массиве.

В общем, лучшее, что вы можете сделать, чтобы найти значение в упорядоченном массиве, - это рекурсивный поиск дерева некоторого вида (например, двоичный поиск), и в этом случае вы можете достичь времени поиска O (log n) по количеству элементов. в вашем массиве. O (log n) намного лучше, чем O (n) для больших значений n.

Поэтому мой предложенный подход будет простым бинарным поиском массива , а именно:

  1. Установите мин / макс целочисленные индексы, чтобы охватить весь массив с плавающей точкой
  2. проверить значение в середине диапазона по индексу mid = (min + max / 2) с искомым значением x
  3. если x меньше этого значения, установите max на mid, иначе установите min на mid
  4. повторяйте (2-4), пока не найдете правильное значение

Это алгоритм O (log n), который должен быть достаточно быстрым практически для всех ситуаций. Интуитивно понятно, что он работает наполовину, чтобы искать диапазон на каждом шаге, пока вы не найдете правильное значение.

Простой бинарный поиск действительно трудно реализовать, поэтому, если вы уже правильно его реализовали, то, возможно, вы уже достаточно близки к оптимальному. Однако, если вы знаете распределение данных и / или имеете ограниченный диапазон значений поиска (x), есть еще некоторые более продвинутые приемы, которые вы можете попробовать:

  • Bucketing - создание сегментов (например, для каждого интервала между двумя целыми числами), каждое из которых содержит отсортированный список значений с плавающей запятой между двумя ограничивающими целыми числами плюс два значения непосредственно под и сразу над каждым диапазоном. Затем вы можете начать поиск с (trunc (x) +0.5). Это должно дать вам хорошее ускорение, если вы выберете подходящие по размеру сегменты (это эффективно увеличивает коэффициент ветвления дерева .....). Если целые числа не работают для вас, то вы можете попробовать сегменты с другой точностью с фиксированной точкой (например, кратные 1/16).
  • Битовое отображение - если диапазон возможных значений поиска достаточно мал, вы можете попробовать создать большую таблицу поиска, проиндексированную поразрядным значением x. Это будет O (1), но вам может понадобиться много памяти, которая будет очень недружелюбна в вашем кеше ... так что используйте с осторожностью. Это особенно неприятно, потому что вы ищете значения с плавающей запятой, поэтому вам может потребоваться несколько ГБ для учета всех менее значимых бит ......
  • Округление и хэширование - хеш-таблицы, вероятно, не самая лучшая структура данных для этой проблемы, но если вы сможете выжить, потеряв немного точности, они могут сработать - просто округлите младшие биты ваших значений поиска и используйте хэш-карту для прямого поиска правильное значение. Вам нужно будет поэкспериментировать над правильным компромиссом между размером и точностью хеш-карты, а также убедиться, что все возможные значения хеша заполнены, так что это может быть немного сложнее ......
  • Балансировка деревьев - у вашего идеального дерева должен быть 50% шанс влево или вправо. Таким образом, если вы создаете дерево на основе распределения значений поиска (x), то вы можете оптимизировать дерево для получения ответов с минимальным количеством тестов. Вероятно, это будет хорошим решением, если множество значений в вашем массиве с плавающей точкой очень близко друг к другу, поскольку это позволит вам избегать слишком частого поиска по этим ветвям.
  • Критически битовые деревья - это все еще деревья (поэтому все еще O (log n) ...), но в некоторых случаях: однако вам нужно будет преобразовать ваши плавающие числа в некоторый формат с фиксированной запятой, чтобы сравнения работали

Однако, если вы не находитесь в особой ситуации, я, вероятно, рекомендую придерживаться простого бинарного поиска. Причины:

  • это гораздо проще реализовать
  • это очень быстро для большинства распространенных случаев
  • дополнительные издержки более сложных подходов (например, более высокое использование памяти / нагрузка на кэш) часто перевешивают незначительные теоретические выгоды
  • это будет более устойчиво к будущим изменениям в распределении данных ....
mikera
источник
1

Это кажется достаточно простым:

Выполните бинарный поиск для поплавка, который вы хотите связать - O (log n) раз.

Тогда элемент слева от него является нижней границей, а элемент справа от него является верхней границей.

Анкит Сони
источник
0

Очевидный ответ - хранить поплавки в дереве . Поддержка операций «предыдущий» и «следующий» тривиальна в дереве. Так что просто сделайте 'next' для вашего значения, а затем сделайте 'previous' для значения, которое вы найдете на первом шаге.

Дэвид Шварц
источник
1
По сути, это то же самое, что бинарный поиск.
Кевин Клайн
-1

Эта статья («сублогарифмический поиск без умножений») может представлять интерес; он даже содержит некоторый исходный код. Для сравнения вы можете рассматривать число с плавающей запятой как целое число с одинаковым битовым шаблоном; это было одной из целей разработки стандарта IEEE с плавающей запятой.

zvrba
источник