Приоритетная очередь с ограниченным пространством: поиск хорошего алгоритма
Это не домашнее задание.
Я использую небольшую "очередь приоритетов" (реализованную как массив в данный момент) для хранения последних N элементов с наименьшим значением. Это немного медленнее - время вставки элемента O (N). Текущая реализация отслеживает наибольший элемент в массиве и отбрасывает любые элементы, которые не вписываются в массив, но я все же хотел бы уменьшить количество операций.
ищет алгоритм очереди приоритетов, который соответствует следующим требованиям:
- очередь может быть реализована как массив, который имеет фиксированный размер и _cannot_ растет. Динамическое распределение памяти во время любой операции в очереди строго запрещено.
- Все, что не вписывается в массив, отбрасывается, но очередь сохраняет все мельчайшие элементы, которые когда-либо встречались.
- O (log (N)) время ввода (т.е. добавление элемента в очередь должно занимать до O (log (N))).
- (необязательно) O (1) доступ к * наибольшему * элементу в очереди (в очереди хранятся * наименьшие * элементы, поэтому самый большой элемент будет отброшен первым, и мне понадобятся они, чтобы уменьшить количество операций)
- Легко реализовать/понять. В идеале - нечто похожее на двоичный поиск - как только вы это понимаете, вы помните его навсегда.
- Элементы не нужно сортировать. Мне просто нужно сохранить N наименьшее значение, которое когда-либо встречалось. Когда они мне понадобятся, я сразу же получу доступ ко всем. Так что технически это не обязательно должна быть очередь, мне просто нужно N последних наименьших значений, которые нужно сохранить.
Вначале я думал об использовании двоичных кучек (их можно легко реализовать с помощью массивов), но, по-видимому, они плохо себя ведут, когда массив больше не может расти. Связанные списки и массивы потребуют дополнительного времени для перемещения вещей. stl priority queue растет и использует динамическое распределение (я могу ошибаться, хотя).
Итак, любые другие идеи?
- EDIT--
Меня не интересует реализация STL. Реализация STL (предлагаемая несколькими людьми) работает немного медленнее, чем в настоящее время используемая линейная матрица из-за большого количества вызовов функций.
Мне интересны алгоритмы очереди приоритетов, а не реализации.
Ответы
Ответ 1
Навалочные массивы кажутся идеальными для вашей цели. Я не уверен, почему вы их отклонили.
Вы используете максимальную кучу.
Скажем, у вас есть куча элементов (реализована как массив), которая содержит N наименьших элементов, которые были просмотрены до сих пор.
Когда элемент входит, вы проверяете максимальное время (O (1)) и отклоняете, если оно больше.
Если значение, приходящее ниже, измените корень на новое значение и просеите это измененное значение - наихудшее время O (log N).
Процесс sift-down прост: начиная с root, на каждом шаге вы обмениваете это значение с ним большим дочерним до тех пор, пока свойство max-heap не будет восстановлено.
Таким образом, вам не придется делать никаких удалений, которые вам, вероятно, понадобятся, если вы используете std:: priority_queue. В зависимости от реализации std:: priority_queue это может привести к выделению/освобождению памяти.
Таким образом, вы можете получить код следующим образом:
- Выделенный массив размера N.
- Заполните его с помощью первых N элементов, которые вы видите.
- heapify (вы должны найти это в стандартных текстовых книгах, он использует sift-down). Это O (N).
- Теперь любой новый элемент, который вы получаете, вы либо отклоняете его в O (1), либо вставляете путем отсеивания в худшем случае O (logN).
В среднем, однако, вам, вероятно, не придется просеивать новое значение до конца и может стать лучше среднего времени вставки вставки (хотя я и не пробовал это доказать).
Вы выделяете массив размера N только один раз, и любая вставка выполняется путем обмена элементами массива, поэтому после этого не происходит динамического выделения памяти.
Проверьте страницу вики, в которой есть псевдокод для heapify и sift-down: http://en.wikipedia.org/wiki/Heapsort
Ответ 2
Используйте std::priority_queue
с самым большим элементом в голове. Для каждого нового элемента отбросьте его, если это >=
головной элемент, иначе поместите элемент головы и вставьте новый элемент.
Боковое примечание. Стандартные контейнеры будут расти только в том случае, если вы их вырастут. Пока вы удаляете один элемент перед вставкой нового элемента (конечно, если он достигнет максимального размера), этого не произойдет.
Ответ 3
Большинство приоритетных очередей, на которых я работаю, основаны на связанных списках. Если у вас есть предопределенное количество уровней приоритета, вы можете легко создать очередь приоритетов с вставкой O (1), имея массив связанных списков - один связанный список на уровень приоритета. Предметы того же приоритета, разумеется, вырождаются в FIFO, но это можно считать приемлемым.
Добавление и удаление затем становится чем-то вроде (ваш API может меняться)...
listItemAdd (&list[priLevel], &item); /* Add to tail */
pItem = listItemRemove (&list[priLevel]); /* Remove from head */
Получение первого элемента в очереди становится проблемой поиска непустого связанного списка с наивысшим приоритетом. Это может быть O (N), но есть несколько трюков, которые вы можете использовать для ускорения.
- В структуре очереди приоритетов держите указатель или индекс или что-то в связанном списке с текущим приоритетом. Это нужно обновлять каждый раз, когда элемент добавляется или удаляется из очереди приоритетов.
- Используйте растровое изображение, чтобы указать, какие связанные списки не пустые. В сочетании с поиском наиболее значимого бита или поиском алгоритма с наименьшим значащим битом вы обычно можете тестировать до 32 списков одновременно. Опять же, это нужно будет обновить при каждом добавлении/удалении.
Надеюсь, что это поможет.
Ответ 4
Если количество приоритетов невелико и фиксировано, вы можете использовать кольцевой буфер для каждого приоритета. Это приведет к расточительству пространства, если объекты большие, но если их размер сравним с указателем/индексом, чем варианты с сохранением дополнительных указателей в объектах, таким же образом может увеличить размер массива.
Или вы можете использовать простой односвязный список внутри массива и хранить указатели/индексы 2 * M + 1, один указывает на первый свободный node, а другие пары будут указывать на голову и хвост каждого приоритета. В этом случае вам придется сравнивать в средстве avg. O (M), прежде чем вывести следующий node с O (1). И вставка будет принимать O (1).
Ответ 5
Если вы создаете приоритетную очередь STL с максимальным размером (возможно, из вектора, инициализированного заполнителями), а затем проверяете размер перед вставкой (удаляя элемент, если необходимо заранее), вы никогда не будете иметь динамическое распределение во время операций вставки. Реализация STL довольно эффективна.
Ответ 6
"Вопросы вычислительной техники" см. стр. 158. Сама реализация очень хорошо, и вы можете даже немного ее настроить, не делая ее менее читаемой. Например, когда вы вычисляете левый дочерний элемент:
int left = i / 2;
Вы можете вычислить правую кнопку следующим образом:
int right = left + 1;
Ответ 7
Найдено решение ( "разница" означает "приоритет" в коде, а maxRememberedResults - 255 (может быть любым (2 ^ n - 1)):
template <typename T> inline void swap(T& a, T& b){
T c = a;
a = b;
b = c;
}
struct MinDifferenceArray{
enum{maxSize = maxRememberedResults};
int size;
DifferenceData data[maxSize];
void add(const DifferenceData& val){
if (size >= maxSize){
if(data[0].difference <= val.difference)
return;
data[0] = val;
for (int i = 0; (2*i+1) < maxSize; ){
int next = 2*i + 1;
if (data[next].difference < data[next+1].difference)
next++;
if (data[i].difference < data[next].difference)
swap(data[i], data[next]);
else
break;
i = next;
}
}
else{
data[size++] = val;
for (int i = size - 1; i > 0;){
int parent = (i-1)/2;
if (data[parent].difference < data[i].difference){
swap(data[parent], data[i]);
i = parent;
}
else
break;
}
}
}
void clear(){
size = 0;
}
MinDifferenceArray()
:size(0){
}
};
- создать максимальную очередь (корень самый большой)
- пока он не заполнится, заполните
- когда он заполнен, для каждого нового элемента
- Убедитесь, что новый элемент меньше, чем root.
- если он больше или равен root, отклоните.
- в противном случае замените корень на новый элемент и выполните обычную кучу "sift-down".
И мы получаем O (log (N)) как худший сценарий.
Это то же самое решение, что и предоставленное пользователем с псевдонимом "Морон".
Спасибо всем за ответы.
P.S. По-видимому, программирование без сна было неплохой идеей.