Швидкий алгоритм пошуку відсортованого масиву поплавків, щоб знайти пару поплавців, що сковують вхідне значення


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, щоб побачити, чи вони дужкою задають цільове значення. Я також повинен перевірити, чи 'я' був> = array.size () - 1, щоб я міг уникати вашого тесту, і чи було це <= 0, щоб я міг уникати тестування ... це насправді багато додаткові умови для виконання на кожному кроці, щоб перевірити наявність на ранньому етапі. Я думаю, що вони дуже сповільнить алгоритм, хоча, зізнаюся, я ще цього ще не перепрофілював.
Тревор Пауелл

3
Це не повинно бути настільки складним - якщо ваш масив розміром N, то вам просто потрібно ставитися до нього, як ніби розміром N - 1. Таким чином, у i + 1. завжди є дійсний елемент. Ви робите двійковий пошук по N - 1 для елемента i, який менший від вашого цільового значення, при цьому елемент i + 1 перевищує цільове значення.
Пол Р

Відповіді:


11

Код у запитанні (лінійний пошук), як ви правильно зазначаєте, буде малим для великих плаваючих масивів. Технічно це O (n), де n - кількість знаків float у вашому масиві.

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

Мій запропонований підхід був би простим двійковим пошуком масиву , тобто:

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

Це алгоритм O (log n), який повинен бути досить швидким для майже всіх ситуацій. Інтуїтивно це працює, зменшуючи вдвічі діапазон, який потрібно шукати на кожному кроці, поки ви не знайдете правильне значення.

Насправді важко перемогти простий двійковий пошук, тож якщо ви це вже правильно реалізували, то, можливо, ви вже досить близькі до оптимальних. Однак якщо ви знаєте розподіл даних та / або маєте обмежений діапазон значень пошуку (x), все ж є деякі інші більш досконалі хитрощі, які можна спробувати:

  • Bucketing - створити відряди (наприклад, для кожного інтервалу між двома цілими числами), кожне з яких містить менший відсортований список значень з плаваючою величиною між двома цілими граничними числами плюс два значення безпосередньо нижче та безпосередньо над кожним діапазоном. Потім ви можете розпочати пошук за адресою (trunc (x) +0.5). Це має забезпечити вам швидке прискорення, якщо ви виберете відра відповідного розміру (це ефективно збільшує коефіцієнт гіллястості дерева .....). Якщо цілі числа не працюють для вас, ви можете спробувати відра якоїсь іншої точності з фіксованою точкою (наприклад, кратні 1/16).
  • Бітове відображення - якщо діапазон можливих значень пошуку досить малий, ви можете спробувати створити велику таблицю пошуку, індексовану побітовим значенням x. Це буде O (1), але вам може знадобитися багато пам'яті, яка буде дуже недружньою у вашому кеші ... тому використовуйте з обережністю. Це особливо неприємно, тому що ви шукаєте плаваючі значення, тож вам може знадобитися кілька ГБ для обліку всіх менш значущих бітів ......
  • Округлення та хешування - хеш-таблиці, мабуть, не найкраща структура даних для цієї проблеми, але якщо ви можете пережити, втративши трохи точності, вони могли б працювати - просто заокруглюйте найнижчі біти значень пошуку і використовуйте хеш-карту, щоб безпосередньо шукати правильне значення. Вам доведеться поекспериментувати з правильним компромісом між розміром і точністю хешмапу, а також переконатися, що всі можливі значення хешу заповнені, так що це може бути трохи хитро ...
  • Балансування дерев - ваше ідеальне дерево повинно мати 50% шансів піти вліво або вправо. Отже, якщо ви створюєте дерево на основі розподілу значень пошуку (x), ви можете оптимізувати дерево для отримання відповідей з мінімальною кількістю тестів. Це, ймовірно, буде хорошим рішенням, якщо багато значень у вашому плаваючому масиві знаходяться дуже близько, оскільки це дозволить вам уникати пошуку цих гілок занадто часто.
  • Критичні дерева - це все-таки дерева (так як і раніше O (журнал n) ...), але деякі випадки: вам потрібно буде конвертувати свої поплавці у якийсь формат з фіксованою точкою, щоб порівняння спрацювали

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

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

1

Це здається досить простим:

Виконайте двійковий пошук поплавця, який ви хочете зв'язати, - O (log n) час.

Тоді елемент зліва від неї - нижня межа, а елемент праворуч від неї - верхня межа.


0

Очевидна відповідь - зберігати поплавці на дереві . Підтримка операцій "попередня" та "наступна" тривіальна в дереві. Тому просто зробіть "наступний" за вашим значенням, а потім зробіть "попередній" на значення, яке ви знайдете на першому кроці.


1
Це по суті те саме, що і двійковий пошук.
кевін клайн

-1

Ця стаття ("сулогіаритмічний пошук без множень") може представляти інтерес; він навіть містить якийсь вихідний код. Для цілей порівняння ви можете розглядати число з плаваючою точкою як ціле число з тим самим бітовим шаблоном; це було однією з цілей проектування стандарту IEEE з плаваючою точкою.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.