Структура поиска для обработки будущих событий (на основе времени) - PullRequest
4 голосов
/ 01 октября 2009

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

  • t = 20: через 420 секунд происходит A
  • t = 25: через 13 секунд происходит B
  • t = 27: через 735 секунд происходит C
  • ...

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

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

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

так что мне не терпится услышать ваше мнение ...:)


редактирование:

  • , чтобы быть более конкретным: я думаю, что n здесь составляет около 100K-1M, и я предполагаю, что у меня может быть около 1-100 событий в секунду ...
  • это не имеет особого значения ... это только для иллюстрации того, что будущее событие может быть поставлено в очередь в любое время ...

спасибо

back2dos

Ответы [ 4 ]

10 голосов
/ 01 октября 2009

Я полагаю, что вы ищете Очередь приоритетов с отметкой времени, когда событие происходит, является приоритетом (ну, более низкие отметки времени будут более приоритетными)

Просто небольшое разъяснение ваших случаев использования:

... где я могу положить в любом случае в в любое время в будущем ...

Вы вставляете в очередь приоритетов с помощью insertWithPriority, используя отметку времени, когда происходит событие. Это будет O (LGN)

... и где я могу получить и (делая итак) удалить все причитающиеся события ...

Вы бы неоднократно вызывали getTop (получает событие с самой низкой отметкой времени), собирая все элементы до интересующего вас времени.

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

Это было бы возможно, но было бы O (lgN) из-за перебалансировки.

3 голосов
/ 02 октября 2009

Насколько велика N? Как часто вам приходится вставлять и удалять предметы по сравнению со всем, что происходит? Если это более 10% от общего времени выполнения, и если N обычно больше 100 (скажем), может быть, , возможно, , пришло время позаботиться о big-O. Я видел программы с приоритетными очередями, реализованными с помощью причудливых контейнерных алгоритмов, выделяющих итераторы, хэш-карты, кучи и т. Д. И тратящих все свое время на создание и выпуск абстрактных объектов, где средняя длина очереди была равна three .

ДОБАВЛЕНО: ОК, поскольку N ~ 10 ^ 6 и частота ~ 100 Гц, вам, вероятно, нужно какое-то двоичное дерево или куча со временем вставки / удаления O (log (N)). Если вы готовы посвятить этому 1% процессорного времени, то это 10 ^ 6 микросекунд * 1% / 100 = 10 ^ 2 микросекунды / операция. Это не должно быть сложным, потому что если типичная глубина поиска равна 20, при ~ 50 нс на сравнение, то это ~ 1 микросекунда, чтобы выполнить поиск. Просто убедитесь, что все просто, не заворачивая все в абстрактные типы данных. Вам не нужно сильно беспокоиться о времени, затрачиваемом на выделение / освобождение узлов дерева, потому что вы выделяете / освобождаете только один узел на операцию. Перебалансирование не нужно делать часто, как, возможно, только после каждых 1000 операций. Если вы можете собирать вставки в пакетах, а затем вставлять их в случайном порядке, это может помешать дереву стать слишком несбалансированным. Если многие из ваших событий происходят одновременно, вы можете добавить небольшой шум к временному коду, чтобы части дерева не стали более похожими на линейный список.

2 голосов
/ 05 октября 2009

Хорошо, я хотел бы поблагодарить вас всех за ваши ответы - очень интересные и полезные. :)

PriorityQueue - определенно правильный термин, который я искал - спасибо за это. Теперь все дело в реализации.

Вот что я думаю:

Пусть N - размер очереди, а M - среднее количество событий на метку времени (так называемые «параллельные» события) во время обработки (плотность событий не будет равномерно распределена, «далекое будущее» «Будучи гораздо более разреженным, но с течением времени эта область времени становится намного более плотной (на самом деле, я думаю, что максимальная плотность будет где-то в будущем через 4–12 часов)». Я ищу масштабируемое решение, которое хорошо работает для значительно больших M. Цель - действительно обработать эти события M в течение одной секунды, поэтому я хочу потратить как можно меньше времени на их поиск.

  1. Если перейти к простому подходу tree , как предлагалось несколько раз, у меня будет вставка O (log N), что, я думаю, весьма неплохо. Если я прав, стоимость обработки одной временной отметки будет O (M * log N), что уже не так хорошо.
    • Альтернативой может быть наличие дерева со списками событий вместо отдельных событий. должно быть возможным реализовать некоторую операцию getlistForGivenStampAndCreateIfNoneExists, которая была бы немного быстрее, чем дважды спускаться по дереву, если список не существует. Но в любом случае, с ростом М это не должно иметь большого значения. Таким образом, вставка будет O (log N), как и раньше, а обработка будет в O (M + log N), что, я думаю, также хорошо.
    • Подход хэш списков событий , я сформулировал. Это также должно иметь O (1) вставку и O (M) стоимость обработки, хотя это не слишком тривиально с хешами. Звучит круто, на самом деле. Или я что-то упустил? Конечно, не так просто заставить хеш работать хорошо, но кроме этого есть ли проблемы? Или проблема с хешем? Википедия заявляет:
      "В хеш-таблице с хорошими размерами средняя стоимость (количество инструкций) для каждого поиска не зависит от количества элементов, хранящихся в таблице. Многие конструкции хеш-таблиц также допускают произвольные вставки и удаления пар ключ-значение при постоянной средней (фактически амортизированной) стоимости за операцию. "
      Быстрый тест показал, что стандартная реализация для моей платформы, кажется, соответствует этому.
    • Подход массив списков событий , предоставленный DVK. Это имеет O (1) вставка. Теперь это хорошо. Но если я правильно понимаю, он имеет O (M + T) стоимость обработки, где T - это размер массива (если хотите, количество временных интервалов), потому что удаление из массивов происходит по линейной цене. Кроме того, это работает только при максимальном смещении времени.

На самом деле я хотел бы обсудить подход с использованием массива. O (M + T) не хорошо. Не за что. Но я вложил немного мозгов, и вот что я придумал:

Первая идея: лень

O (T) может быть раздавлен произвольным фактором, добавив немного лени, но в конце концов он останется O (T). Но насколько это плохо? Давайте T = 2419200, что составляет 28 дней. А потом, один раз в день, я бы его убрал (желательно, пока ожидается низкая нагрузка). Это потеряло бы меньше чем 5% массива. На моей целевой платформе операция копирования занимает 31 мсек на довольно старом 2 ГГц ядре, так что в конце концов это не кажется такой уж плохой идеей.

Вторая идея: куски

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

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

Для обработки я просто беру текущий интервал и обрабатываю должные события, обрабатывая текущий список ожидаемых событий, а затем уничтожая его. Массив остается постоянной длины, поэтому мы находимся в O (M) (что является лучшим, что вы можете получить для обработки M элементов). Как только текущий интервал полностью обработан (таким образом, если интервал теперь представляет «прошлое»), я просто располагаю его в O (1). Я могу сохранить дополнительную ссылку на текущий интервал, избавляя от необходимости искать его, но, полагаю, это не даст заметного улучшения.


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

Я был бы очень признателен за любую информацию по этому поводу.

1 голос
/ 01 октября 2009

Если ваши события имеют четко определенный верхний предел (например, нет событий позднее, чем через 2 дня в будущем), вы можете просто индексировать массив на # секунд от «начала времени». Значением массива является список событий с этим смещением.

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

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

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

Кроме того, из вашего вопроса неясно, нужно ли знать «t» - время, когда событие было возбуждено. Если вам нужно это знать, сохраните его как часть мероприятия. но ссылка на то, когда событие должно произойти, должна быть абсолютной по отношению к некоторой начальной точке (если это хэш-карта с неограниченным диапазоном, вы можете использовать секунды эпохи, и если события имеют границы, как в первом решении массива, которое я перечислил, вы должны вместо этого используйте «# секунд с начала диапазона» - например, с начала вчерашнего дня.

...