Нетривіальний алгоритм для обчислення медіани розсувного вікна


25

Мені потрібно обчислити медіану бігу:

  • Вхід: n , k , вектор (x1,x2,,xn) .

  • Вихід: вектор (y1,y2,,ynk+1) , де yi є медіаною (xi,xi+1,,xi+k1) .

(Немає обману з наближеннями; я хотів би мати точні рішення. Елементи xi - великі цілі числа.)

Існує тривіальний алгоритм, який підтримує дерево пошуку розміром k ; загальний час роботи - O(nlogk) . (Тут "дерево пошуку" відноситься до деякої ефективної структури даних, яка підтримує вставки, видалення та медіанні запити в логарифмічний час.)

Однак це здається мені трохи дурним. Ми ефективно вивчимо всю статистику замовлень у всіх вікнах розміром k , а не тільки в медіанах. Більше того, це не надто привабливо на практиці, особливо якщо k велике (великі дерева пошуку мають тенденцію бути повільними, надмірне споживання пам’яті нетривіальне, ефективність кеш-пам'яті часто погана тощо).

Чи можемо ми зробити щось істотно краще?

Чи є нижні межі (наприклад, чи є тривіальний алгоритм асимптотично оптимальним для моделі порівняння)?


Редагувати: Девід Еппштейн дав приємну нижню межу для моделі порівняння! Цікаво, чи все-таки можливо зробити щось трохи розумніше, ніж тривіальний алгоритм?

Наприклад, чи могли ми зробити щось за цими лініями: розділити вхідний вектор на частини розміром k ; сортувати кожну частину (слідкуючи за вихідними положеннями кожного елемента); а потім використовувати кусково відсортований вектор, щоб ефективно знайти працюючі медіани без будь-яких допоміжних структур даних? Звичайно, це все-таки буде O(nlogk) , але на практиці сортування масивів, як правило, відбувається набагато швидше, ніж підтримка дерев пошуку.


Редагування 2: Саїд хотів побачити деякі причини, чому я вважаю, що сортування відбувається швидше, ніж операції з пошуковим деревом. Ось дуже швидкі орієнтири, для k=107 , n=108 :

  • ≈ 8s: сортування векторів з k елементами коженn/kk
  • ≈ 10s: сортування вектора з елементамиn
  • ≈ 80-ті: вставок та видалень у хеш-таблиці розміром knk
  • ≈ 390s: вставок та вилучень у збалансованому дереві пошуку розміром knk

Хеш-таблиця існує просто для порівняння; це не має прямого використання в цій програмі.

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

(Технічні деталі: Дані = випадкові 32-бітні цілі числа. Комп'ютер = типовий сучасний ноутбук. Тестовий код був написаний на C ++, використовуючи стандартні підпрограми бібліотеки (std :: сортування) та структури даних (std :: multiset, std :: unsorted_multiset) Я використав два різних компілятори C ++ (GCC та Clang) та дві різні реалізації стандартної бібліотеки (libstdc ++ та libc ++). Традиційно std :: multiset реалізований як високооптимізований червоно-чорний дерево.)


1
Я не думаю, що вам вдасться вдосконалити . Причина в тому, якщо ви подивитеся на вікно х т , . . . , x t + k - 1 , ви ніколи не можете виключати жодне з чисел x t + knlogkxt,...,xt+k1-це медіани майбутнього вікна. Це означає, що в будь-який час ви повинні тримати принаймніkxt+k2,...,xt+k1 цілих числа в структурі даних, і вона, здається, не оновлюється за менший час журналу. k2
РБ

Ваш тривіальний алгоритм мені здається не O ( n log k ) , я щось неправильно зрозумів? І я думаю, що через це у вас проблема з великим k , інакше логарифмічний фактор - це ніщо в практичних програмах, також немає великої прихованої константи в цьому алгоритмі. O((nk)klogk)O(nlogk)k
Саїд

@Saeed: у тривіальному алгоритмі ви обробляєте елементи по черзі; на кроці ви додаєте x i до дерева пошуку, а (якщо i > k ) ви також видаляєте x i - k з дерева пошуку. Це n кроків, кожен з яких займає час O ( log k ) . ixii>kxiknO(logk)
Jukka Suomela

Отже, ви маєте на увазі, що у вас збалансоване дерево пошуку, а не випадкове дерево пошуку?
Саїд

1
@Saeed: Зверніть увагу, що у своїх тестах я навіть не намагався знайти медіанів. Я тільки зробив вставок і n вилучень у дереві пошуку розміром k , і ці операції гарантовано займуть час O ( log k ) . Вам просто потрібно прийняти, що операції з пошуковим деревом на практиці дуже повільні, порівняно з сортуванням. Це ви побачите легко, якщо спробуєте написати алгоритм сортування, який працює, додаючи елементи до збалансованого дерева пошуку - це, безумовно, працює в O ( n log n ) час, але це буде смішно повільно на практиці, а також витрачає багато пам'яті. nnkO(logk)O(nlogn)
Jukka Suomela

Відповіді:


32

Ось нижня межа від сортування. Враховуючи набір вхідного сигналу довжини n, який потрібно сортувати, створіть вхід до вашої середньої задачі, що складається з n - 1 копій числа, меншого від мінімуму S , потім самого S , потім n - 1 копій числа, більшого ніж максимум S , а задаємо k = 2 n - 1 . Ходові медіани цього входу такі ж , як в відсортованому порядку S .Snn1SSn1Sk=2n1S

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


6
Ця відповідь насправді змушує мене замислитись, чи справедливим є і зворотне: з огляду на ефективний алгоритм сортування, чи отримуємо ми ефективний алгоритм роботи медіани? (Наприклад, чи ефективний алгоритм сортування цілих чисел має на увазі ефективний алгоритм середньої алгоритму виконання цілих чисел? Або алгоритм сортування, що діє на IO, забезпечує медіа-алгоритм ефективного виконання IO?)
Jukka Suomela

1
Ще раз, велике спасибі за вашу відповідь, це дійсно поставило мене на правильний шлях і надихнуло на алгоритм медіанного фільтра на основі сортування! Врешті-решт, мені вдалося знайти документ з 1991 року, який в основному представляв такий же аргумент, що і ви, що ви даєте тут, і Пат Морін дав вказівник на інший відповідний документ від 2005 року; див. реф. [6] та [9] тут .
Юкка Суомела

9

Редагувати: Цей алгоритм представлений тут: http://arxiv.org/abs/1406.1717


Так, для вирішення цієї проблеми достатньо виконати такі операції:

  • Сортуйте вектори, кожен з k елементів.n/kk
  • Виконайте лінійний час після обробки.

Дуже приблизно, ідея така:

  • Розглянемо два сусідні блоки введення, і b , обидва з k елементами; нехай елементи будуть 1 , 2 , . . . , До і б 1 , б 2 , . . . , b k у порядку появи у вхідному векторі x .abka1,a2,...,akb1,b2,...,bkx
  • Сортуйте ці блоки та вивчіть ранг кожного елемента в блоці.
  • Розширіть вектори і b за допомогою покажчиків попередника / наступника, щоб, слідуючи ланцюжкам вказівників, ми могли переміщувати елементи у порядку зростання. Таким чином, ми побудували подвійно пов'язані списки a ' і b ' .abab
  • Один за іншим, видалити всі елементи з пов'язаного списку , в зворотному порядку появи б до , б до - 1 , . . . , б 1 . Щоразу, коли ми видаляємо елемент, пам’ятайте, що було його наступником та попередником на момент видалення .bbk,bk1,...,b1
  • Тепер підтримуйте "середні покажчики" і q, які вказують на списки a ' і b ' відповідно. Initialise р до середньої точки ' , і Initialise ц до хвоста порожнього списку Ь ' .pqabpaqb
  • Для кожного :i

    • Видалення зі списку а ' (це O ( 1 ) час, просто видаліть з пов'язаного списку). Порівняйте з I з елементом вказав на р , щоб побачити , якщо ми видалили до або після того, як р .aiaO(1)aipp
    • Поставте назад до списку b ' у його початковому положенні (це O ( 1 ) час, ми запам'ятали попередника і наступника b i ). Порівняйте b i з елементом, вказаним q, щоб побачити, чи додали ми елемент до або після q .bibO(1)bibiqq
    • Оновіть покажчики і q так, що медіана об'єднаного списку a b або на p, або на q . (Це O ( 1 ) час, просто дотримуйтесь пов'язаних списків один або два кроки, щоб все виправити. Ми будемо відстежувати, скільки предметів до / після p і q у кожному списку, і ми будемо підтримувати інваріант, що обидва p і q вказують на елементи, максимально наближені до медіани.)pqabpqO(1)pqpq

Зв'язані списки - це просто -елементні масиви індексів, тому вони легкі (за винятком того, що локальність доступу до пам'яті погана).k


Ось приклад реалізації та орієнтири:

Ось графік часу роботи (для ):n2106

  • Синій = сортування + післяобробка, .O(nlogk)
  • Зелений = підтримка двох купи, , реалізація від https://github.com/craffel/median-filterO(nlogk)
  • Червоний = підтримують два дерева пошуку, .O(nlogk)
  • Чорний = підтримувати відсортований вектор, .O(nk)
  • Вісь X = розмір вікна ( ).k/2
  • Вісь Y = час роботи в секундах.
  • Дані = 32-бітні цілі числа та випадкові 64-бітні цілі числа з різних розподілів.

running times


3

З огляду на обмеженість Девіда, навряд чи можна зробити гірший випадок, але є кращі алгоритми, що відрізняються від виходу. Зокрема, якщо у кількості медіанів у результаті, ми можемо вирішити задачу за час O ( n log m + m log n ) .mO(nlogm+mlogn)

Для цього замініть збалансоване бінарне дерево збалансованим бінарним деревом, що складається лише з тих елементів, які були медіанами в минулому, плюс два купи Фібоначчі між кожною парою попередніх медіанів (по одному для кожного напрямку), плюс рахується, щоб ми могли знайдіть, яка купка Фібоначчі містить певний елемент у порядку. Не заважайте ніколи видаляти елементи. Коли ми вставимо новий елемент, ми можемо оновити структуру даних за час . Якщо нові підрахунки вказують на те, що медіана знаходиться в одній із куб Фібоначчі, для витягування нової медіани потрібно додаткове O ( log n ) . Це О ( журнал n )O(logm)O(logn)O(logn) заряд відбувається лише один раз за медіану.

O(nlogm+mlogk)


Oops, this doesn't work as written, since if you don't delete elements the counts won't reflect the new window. I'm not sure if it can be fixed, but I will leave the answer in case there is a way.
Geoffrey Irving

Тому я думаю, що цей алгоритм може насправді зайняти О(нжурналм)якщо ви видалите вузли з куб Фібоначчі, оскільки глибина кучі Фібоначчі збільшується лише тоді, коли викликається min-min. Хтось знає приємні межі складності купи Фібоначчі з урахуванням кількості видалених хвилин дзвінків?
Джеффрі Ірвінг

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

1
I apologize for the incomplete work. I've asked the concrete question needed to fix this answer here: cstheory.stackexchange.com/questions/21778/…. If you think it's appropriate I can remove this answer until the secondary question is resolved.
Geoffrey Irving
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.