Система Orphus
Версия для печати

Приоритетная очередь на основе бинарной, биномиальной и фибонначиевой куч и ее применение в многоагентных поисковых системах

Автор: Беляев Игорь Олегович
Опубликовано: 13.04.2012
Исправлено: 10.12.2016
Версия текста: 1.1
Введение
Базовый класс
Бинарная куча
Добавление нового элемента
Удаление минимального элемента
Изменение элемента
Удаление элемента
Объединение двух бинарных куч
Биномиальная куча
Поиск минимального элемента
Объединение двух пирамидальных деревьев
Добавление нового элемента
Объединение двух биномиальных куч
Удаление минимального элемента
Изменение элемента
Удаление элемента
Фибоначчиева куча
Поиск минимального элемента
Добавление нового элемента
Объединение двух фибоначчиевых куч
Объединение двух фибоначчиевых деревьев
Удаление минимального элемента
Изменение элемента
Удаление элемента
Сравнительный анализ
Тест #1.
Тест #2
Выводы
Демонстрационный проект
Список литературы

Введение

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

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

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

Документ – объект, на основе которого строится система информационного поиска. В общем случае под документом будет считать набор текстовой информации.

Запрос (Request) – набор лексем (терминов), по которым происходит поиск по коллекции документов.

Термин запроса – отдельное лексема (слово), составная часть запроса.

Релевантность (отклика) – числовая величина, характеризующая соответствие документа введенному запросу.

Введем следующие обозначения:


релевантность i-ого документа на запрос req


важность (вес) термина term в документе doc


важность (вес) термина term в запросе req

Существует различные методы оценки параметров fwterm,doc и wterm,req, одним из которых является подсчет tf-idf веса термина[7].

Запрос req можно представить в виде вектора


, где M – общее количество терминов в запросе req.

Также и произвольный документ doc можно представить в виде вектора


В дальнейшем для определения соответствия между вектором запроса Vreq и вектором документа Vdoc можно воспользоваться косинусной мерой сходства[7], которую можно считать релевантностью R(req,doci).

Одним из ключевых агентов в многоагентной поисковой системе является агент «хранитель знаний». Одной из важных операций для данного агента является получение K документов, имеющих наибольшую релевантность по отношению к введенному запросу. В общем случае для введенного запроса req необходимо получить N пар вида {doci, R(req, doci)} и найти среди них K пар с максимальным значением R(req, doci), где N – общее количество документов в коллекции. Значение K, как правило, невелико и задается константно в зависимости от задач, решаемых поисковой системой, и размеров обрабатываемой информации.

Для реализации поставленной задачи отлично подойдет приоритетная очередь с базовым набором операций:

  1. Добавление нового элемента.
  2. Получение минимального или максимального элемента.
  3. Удаление минимального или максимального элемента.

Так как количество документов в коллекции может быть велико, а получение релевантности для каждого документа является автономной задачей, то можно решить поставленную задачу, используя распределенные вычисления. Также можно независимо получать небольшие коллекции пар {doci, R(req, doci)} в виде отдельных приоритетных очередей, после чего объединять эти приоритетные очереди в одну большую.

Для популярных запросов можно предусмотреть двухуровневую меморизацию:

1 уровень. Хранить K документов, имеющих наибольшую релевантность для конкретного запроса req.

2 уровень. Хранить сериализированные копии небольших приоритетных очередей, которые получаются при распределенных вычислениях.

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

  1. Изменение приоритета произвольного элемента.
  2. Удаление произвольного элемента.
  3. Объединение двух приоритетных очередей.

Учитывая все вышесказанное, появляется актуальная задача обработки коллекции пар вида {doci, R(req, doci)}. В общем случае можно обобщить обработку такой коллекции до обработки коллекции однородных объектов, имеющих отношение порядка на множестве. Поэтому в дальнейшем будем оперировать понятием элемент коллекции. Для определенности в дальнейших рассуждениях будем искать минимальный элемент. Чтобы искать максимальный элемент, необходимо только изменить отношение порядка для элементов коллекции.

Далее будет рассмотрен механизм построения приоритетной очереди на основе трех различных куч: бинарной, биномиальной и фибоначчиевой.

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

Базовый класс

Забегая вперед, следует отметить, что внутренняя организация всех трех рассматриваемых куч отличается, хотя каждая из них и представляет древовидную структуру данных. Для обеспечения универсального механизма каждый элемент приоритетной очереди сопоставляется с уникальным идентификатором (id). При этом каждый id сопоставляется с древовидным узлом кучи. Данная надстройка увеличит асимптотическую сложность каждой операции на O(logN), но обеспечит выполнение всех функций по единому интерфейсу.

Таблица 1. Набор операций, которым должна обладать каждая приоритетная очередь

Операция

Описание

            bool empty()

Проверка наличия элементов в приоритетной очереди

            void pop()

Извлечение минимального элемента из приоритетной очереди

            int push(T obj)

Добавление нового элемента в приоритетную очередь

            int size()

Количество элементов в приоритетной очереди

            T top()

Минимальный элемент в приоритетной очереди

            bool change(int id)

Изменение значения элемента приоритетной очереди по id

            bool remove(int id)

Удаление элемента приоритетной очереди по id

            union_heap
          

Объединение двух приоритетных очередей, построенных на однородных кучах

Последнюю операцию не удалось включить в единый интерфейс.

      template <typename T, typename Node>
class base_heap {
public:
  base_heap(bool (*cmp) (const T&, const T&)) : _less(cmp) {}
public:
  // Базовые функцииvirtualbool  empty() = 0;                   // проверка кучи на наличие элементовvirtualvoid  pop() = 0;                     // извлечение минимального элементаvirtualint   push(T obj) = 0;               // добавление нового элементаvirtualint   size() = 0;                    // количество элементов в кучеvirtual T     top() = 0;                     // минимальный элемент кучи// Дополнительные функцииvirtualbool  change(int id, T new_obj) = 0; // изменение элемента по idvirtualbool  remove(int id) = 0;            // удаление элемента по idprotected:
  staticint            _last_id;                      // идентификатор последнего добавленного элементаbool                  (*_less) (const T&, const T&); // указатель на функцию-компаратор, определяющую отношение порядка
  std::map<int,Node>    node_by_id;                    // карта соответствия id с Node
  std::map<Node,int>    id_by_node;                    // карта соответсвия Node c idprotected:
  int get_next_id() {                                 
    return ++_last_id;
  }
  void add_links(int id, const Node &node) {    // добавление нового элемента в кучу сопровождается добавлением ссылок в обе карты
    node_by_id[id] = node;
    id_by_node[node] = id;
  }
  void remove_links(int id, const Node &node) { // удаление элемента из кучи сопровождается удалением ссылок из обоих карт.
    node_by_id.erase(id);
    id_by_node.erase(node);
  }
};
template <typename T, typename Node> int base_heap<T,Node>::_last_id = -1;

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

Для определения уникального идентификатора по узлу(Node) используется map<Node,int> id_by_node. Для обратной операции используется map<int,Node> node_by_id.

Бинарная куча

Данная куча является широко известной, т.к. достаточно проста в понимании и реализации. В литературе обычно умалчивается информация об авторе данной структуры данных, но доподлинно известно, что Р.Флойд и Д.Вильямс стояли у истоков ее создания, применив ее в алгоритме пирамидальной сортировки[1].

Не будем сильно вдаваться в подробности, а только опишем основные ее свойства. Двоичная куча представляет собой бинарное дерево. Для каждого элемента кучи выполняется два правила:

  1. Каждый элемент кучи имеет не более двух сыновей (правого и левого)
  2. Значения сыновей не превосходят значения родителя.

Элемент кучи, не имеющий родителя, называется корнем.

Элемент кучи, не имеющий ни одного сына, называется листом.

Минимальный элемент кучи всегда находится в ее корне, следовательно, доступ к нему осуществляется за O(1). Главной особенностью бинарной кучи является то, что для ее хранения не требуется дополнительной памяти, а сами значения элементов коллекции находятся в массиве.

На рисунке 1 представлена куча, сформированная по вышеизложенным правилам.


Рисунок 1. Пример бинарной кучи

Ту же самую кучу можно хранить в виде массива:


Рисунок 2. Хранение кучи в виде массива. Верхний ряд – индекс элемента, нижний – числовое значение.

Для элемента, находящегося на позиции i:

Добавление нового элемента

В качестве базовой кучи рассмотрим кучу, изображенную на рис. 1.

Новый элемент добавляется в конец массива. При этом может произойти нарушения целостности кучи (см. п. 2). Если целостность кучи нарушена ровно одним элементом, то для восстановления общей целостности необходимо сделать просеивание данного элемента вверх (shift up) или вниз (shift down). В данном случае нужно применить просеивание вверх (рис. 3). Для ее проведения необходимо следовать только одному правилу: «Если текущий элемент меньше своего отца, то они меняются местами в куче».

Далее процесс повторяется для нового положения текущего элемента до тех пор, пока он меньше своего отца или до тех пор, пока текущий элемент не станет корнем кучи. Понятно, что в худшем случае при просеивании вверх новый элемент совершит количество обменов равных высоте дерева. Т.к. дерево является бинарным, то сложность данной операции O(log(N)).


Рисунок 3. Процесс добавления нового элемента в бинарную кучу.

Удаление минимального элемента

Удаление минимального элемента бинарной кучи можно разделить на следующие этапы:

  1. Обмен местами первого и последнего элемента.
  2. Уменьшение размера кучи на 1.
  3. Просеивание вниз(shift down) корневого элемента.
  4. Просеивание вниз осуществляется по следующему правилу: «Если текущий элемент меньше минимального из его сыновей, то они меняются местами».
  5. Далее процесс повторяется для нового положение текущего элемента до тех пор, пока минимальный сын меньше текущего элемента, или до тех пор, пока текущий элемент не станет листом. Аналогично, сложность данной операции O(logN).


Рисунок 4. Процесс удаление минимального элемента из бинарной кучи.

Изменение элемента

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

Удаление элемента

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

Объединение двух бинарных куч

Данную операцию можно выполнить со сложностью O(Nlog(N)), последовательно перебрав все элементы одной кучи за O(N) и добавив их в результирующую за O(log(N)). Низкая производительности данной операции обуславливает необходимость в рассмотрении двух последующих куч.

Биномиальная куча

Впервые данная структура данных была описана в работе Жана Вильемина в 1978[3].

Прежде чем рассмотреть устройство биномиальной кучи, введем понятие пирамидального дерева[4].

Пирамидальное дерево – это дерево размером 2N, все элементы которого, кроме корня и листьев, имеют ровно два дочерних элемента. При этом корневой элемент всегда имеет левого сына, кроме вырожденного случая, когда дерево состоит из одного элемента. Для каждого элемента дерева выполняется правило: «Все элементы левого поддерева текущего элемента не меньше его самого».


Рисунок 5. Примеры корректных пирамидальных деревьев.

Биномиальная куча – это структура данных, которая содержит в себе набор пирамидальных деревьев попарно различного размера. Можно провести прямую аналогию между размерами пирамидальных деревьев в биномиальной куче и двоичным представлением числа. Т.е. если известно, что биномиальная куча содержит 11 элементов, то это значит, в ее состав входят пирамидальные деревья с размерами 8, 2 и 1. Учитывая возможности современного компьютера и адекватных потребностей при использовании биномиальной кучи можно ограничить общее количество элементов в ней до 231-1.

      const
      int MAX_SIZE = 31;        // 2^MAX_SIZE – максимальное количество элементов в биномиальной кучеtemplate <typename T>
class binomial_heap_node {
public:
    T data;                              
    binomial_heap_node* left;            
    binomial_heap_node* right;
    binomial_heap_node* parent; // нужно для операции shift_up
};

template <typename T>
class binomial_heap : public base_heap<T,binomial_heap_node<T>*> {
private:
  binomial_heap_node<T>* mas[MAX_SIZE];  // массив указателей на корни пирамидальных деревьевint _size;                             // количество элементов
}

Поиск минимального элемента

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

Объединение двух пирамидальных деревьев

Два пирамидальных дерева A и B можно объединить только в том случае, если у них одинаковый размер. Для определенности считаем, что корень дерева A меньше корня дерева B. При этом корнем объединенного дерева будет корень дерева A, а его левое поддерево станет правым поддеревом корневого элемента дерева B, а само дерево B станет левым поддеревом корневого элемента A (рис. 6).


Рисунок 6. Объединение двух пирамидальных деревьев

Данная операция требует только аккуратного изменения ссылок, поэтому время ее работы O(1).

Добавление нового элемента

Данная операция полностью повторяет логику единичного инкремента. Пусть исходная биномиальная куча состоит из 11 элементов.

1110 + 110 = 10112 + 00012 = 11002 = 1210.

   1011
+  0001
-------
   1100

Единичный разряд будет последовательно переноситься до ближайшего справа нулевого бита, обнуляя все биты на своем пути. Операция сложения двух единичных битов эквивалентна операции объединения двух пирамидальных деревьев одного размера. Сложность данной операции O(logN).

Объединение двух биномиальных куч

Аналогично добавлению одного элемента в биномиальную кучу работает и операция объединения двух биномиальных куч. Здесь можно провести полную аналогию со сложением двух двоичных чисел. Асимптотическая сложность данной операции O(logN).

Удаление минимального элемента

При удалении минимального элемента кучи сначала необходимо найти пирамидальное дерево, корнем которого он является, и исключить это дерево из общего списка деревьев биномиальной кучи. После чего необходимо разбить исходное дерево на log2(count) пирамидальных деревьев (где count – количество элементов дерева), которые являются поддеревьями исходного дерева (рис. 7). Эти деревья будут иметь размеры, равные степени двойки, а также будут попарно различны. Это обстоятельство дает право объединить эти деревья в отдельную биномиальную кучу T. После чего остается только объединить исходную биномиальную кучу с кучей T.


Рисунок 7. Удаление минимального элемента из пирамидального дерева

Данная операция требует последовательно применить две операции с асимптотической сложностью O(logN), поэтому общая сложность также будет O(logN).

Изменение элемента

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

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


Рисунок 8. Просеивание вверх в пирамидальном дереве.

Удаление элемента

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

Фибоначчиева куча

Последняя структура данных является самой «молодой» из рассматриваемых. Ее появление датируется 1984 годом. Авторами ее является М.Фредман и Р.Тарьян. Тем не менее, первая публикация о ней появилась только в 1987 году[5].

Фибоначчиева куча, как и биномиальная, представляет собой набор деревьев. Только на этот раз фибоначчиевых деревьев.


Рисунок 9. Пример корректного фибоначчиева дерева.

Фибоначчиево дерево – это k-ичное дерево, для каждого элемента которого выполняется правило: «дочерний элемент не превышает своего родителя». Корни фибоначчиевых деревьев хранятся в виде кольцевого списка. Также все дочерние элементы, имеющие общего предка, хранятся в виде кольцевого списка, поэтому элементу-родителю не нужно хранить ссылки на всех своих потомков. Для этого достаточно знать информацию только об одном из них (рис. 9).

      template <typename T>
class fibonacci_heap_node {
public:
    fibonacci_heap_node* parent;
    fibonacci_heap_node* child;
    fibonacci_heap_node* left;
    fibonacci_heap_node* right;
    // количество элементов в поддереве, вершиной которой является текущий    // элемент,используется в операции change для обеспечения     // логарифмической сложностиint degree;     
    // метка указывающая, были ли потери дочерних элементов,     // начиная с момента, когда текущий элемент стал дочерним узлом.bool mark;      
    T data;
};
template <typename T>
class fibonacci_heap: public base_heap<T,fibonacci_heap_node<T>*> {
private:
  fibonacci_heap_node<T>* min;  // указатель на минимальный элемент кучint _roots_amount;            // количество элементов в корневом спискеint _size;                    // количество элементов
}

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

Поиск минимального элемента

Как и в случае биномиальной кучи, минимальный элемент фибоначчиевой кучи будет одним из корневых узлов фибоначчиевых деревьев. Так как длина кольцевого списка корневых элементов может быть достаточно велика, то для достижения желаемой асимптотики проще хранить ссылку на минимальные из них. Точно так же можно добиться времени работы O(1) для биномиальной кучи.

Добавление нового элемента

Новый элемент всегда добавляется в корневой список элементов. При этом его удобно сделать левым или правым братом минимального элемента. При этом может получиться так, что новый элемент меньше минимального (рис. 10). В этом случае нужно обновить указатель на минимальный элемент всей кучи.


Рисунок 10. Добавление нового элемента в фибоначчиеву кучу.

Сложность выполнения данной операции O(1).

Объединение двух фибоначчиевых куч

Для объединения двух фибоначчиевых куч нужно объединить кольцевые списки их корневых элементов. Указатель на минимальный элемент объединенной кучи нужно выбрать между минимальными элементами первоначальных куч. Это можно сделать за константное время.

Объединение двух фибоначчиевых деревьев

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


Рисунок 11. Объединение двух фибоначчиевых деревьев

Минимальный из корней объединяемых деревьев станет корнем объединенного дерева. Второй же корень станет сыном минимального корня (рис. 11).

Удаление минимального элемента

Данная операция разбивается на несколько стадий:

Если у удаляемого элемента есть потомки, то их кольцевой список объединяется с кольцевым списком корневых элементов (рис. 12).

Удаление минимального элемента из корневого списка. При этом ссылка на минимальный элемент всей кучи будет указывать на правого брата удаляемого элемента (рис. 12). Но не факт, что этот элемент является минимальным. Чтобы его найти, нужно выполнить п.3

Уплотнение кучи и нахождение минимального элемента.


Рисунок 12. Присоединение к корневому циклическому списку дочерних элементов удаляемого элемента.

Последний пункт рассмотрим очень подробно.

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


Рисунок 13.1. Начинаем просмотр деревьев с дерева «6»


Рисунок 13.2. Дерево «7-9-8» имеет уникальный размер, среди рассмотренных ранее.


Рисунок 13.3. Размер дерева «5» совпадает с дерево «6». Их следует объединить


Рисунок 13.4. Объединение деревьев «6» и «5». В результате минимальный элемент всей кучи обновляется.


Рисунок 13.5. Новое дерево «2» имеет уникальный размер. Обновляется минимальный элемент всей кучи.


Рисунок 13.6. Новое дерево «7» следует объединить с деревом «2».


Рисунок 13.7. Объединенное дерево «2-7» нужно объединить с деревом «5-6».


Рисунок 13.8. Не осталось больше нерассмотренных деревьев. Уплотнение кучи закончено.

Кажется, что уплотнение кучи является очень тяжеловесной операцией. Но, тем не менее, амортизированная сложность ее O(logN)[6].

Изменение элемента

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

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

Удаление элемента

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

Сравнительный анализ

Таблица 2. Сводная таблица оценок асимптотической сложности всех операций для трех видов куч

Операция

Бинарная куча

Биномиальная куча

Фибоначчиева куча

Добавление нового элемента

O(log(N))

O(log(N))

O(1)

Поиск минимального элемента

O(1)

O(1) / O(log(N))

O(1)

Удаление минимального элемента

O(log(N))

O(log(N))

O(log(N))

Изменение элемента

O(log(N))

O(log(N))

O(log(N))

Удаление элемента

O(log(N))

O(log(N))

O(log(N))

Объединение двух куч

O(Nlog(N))

O(log(N))

O(1)

Для оценки реализаций проведем два теста. Все тесты проводились на рабочей станции c процессором Intel Core2Duo T5250 1.5 Ггц, RAM до 2ГБ на платформе Windows 7. Все приведенные исходники проверялись с помощью компилятора Microsoft VC++ 2008(x86).

Тест #1.

В рамках первого теста проверим быстродействие связки операций:

  1. Добавление нового элемента (push)
  2. Удаление минимального элемента (pop)

Тест будет проводиться на случайном наборе целочисленных данных. Для каждого теста фиксируется количество элементов N и последовательно выполняются две операции:

  1. Добавление N элементов в кучу
  2. Удаление минимального элемента из кучи до полного ее опустошения.


Рисунок 14. Сравнительный анализ быстродействия связки операций: push и pop.

Результаты работы данного теста представлены на рисунке 14, где по оси X отображается количество элементов N, а по оси Y время выполнения теста в секундах.

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

Тест #2

В рамках второго теста проверим быстродействие связки операций:

  1. Объединение двух куч (union_heap)
  2. Удаление минимального элемента (pop)
  3. Добавление нового элемента (push)

Для каждого теста фиксируется общее количество обрабатываемых элементов N. После чего последовательно выполняются следующие операции:

  1. Создание log2(N) однородных куч
  2. Добавление в случайном порядке всех исходных элементов в эти кучи.
  3. Последовательное объединение двух различных куч.


Рисунок 15. Сравнительный анализ быстродействия связки операций: push, pop, union_heap.

Видно, что на предложенных ограничениях фибоначчиева и биномиальная куча ведут себя практически одинаково. С увеличением количества операций объединения двух однородных куч фибоначчиева куча становится более предпочтительной из-за лучшей асимптотики. В то время, как при уменьшении количества таких операций график на рис.15 становится более похожим на график с рис. 14, и следовательно в этом случае предпочтение отдается биномиальной куче.

Выводы

В ходе статьи были рассмотрены три вида куч: бинарная, биномиальная, фибоначчиева.

Таблица 3. Преимущества и недостатки трех видов куч

Плюсы

Минусы

Бинарная куча

  1. Простота реализации
  2. Экономия памяти при хранении древовидной структуры
  3. Асимптотическая сложность основных операций не превышает O(logN)
  1. Объединение двух бинарных куч выполняется со сложность O(Nlog(N))

Биномиальная куча

  1. Все операции выполняется за O(logN)
  1. Каждый элемент биномиальной кучи требует дополнительно до 12 байт памяти

Фибоначчиева куча

  1. Все операции не связанные с удалением или изменением элемента работают за O(1). Все остальные за O(logN)
  2. Объединение двух куч за O(1)
  1. Каждый элемент фибоначчиевой кучи требует дополнительно до 21 байта памяти
  2. Очень большая константа у логарифмических операций.

Из таблицы 2 можно сделать вывод, что фибоначчиева куча представляет наиболее выгодный вариант для реализации приоритетной очереди. Но в ходе проведенных тестов было выявлено, что все логарифмические операции в фибоначчиевой куче имеют очень большую константу в оценке асимптотической сложности. Тем не менее, только эта куча позволяет объединять две кучи в одну за константное время.

Возвращаясь к первоначальной задаче обработки пар вида {doci, R(req, doci)} нужно отметить, что выбор кучи для приоритетной очереди полностью будет зависеть от объема обрабатываемой информации и количества компьютеров в кластере. Если количество независимых потоков, в которых будут генерироваться первоначальные приоритетные очереди, будет меньше log2(N), где N – общее количество обрабатываемых элементов, то лучше использовать биномиальную кучу. В противном случае большей эффективностью обладает фибоначчиева куча. Использование бинарной кучи целесообразно только в том случае, когда необходимо экономить память и нет необходимости сливать кучи в одну.

Данные выводы и шаблонные классы будут использованы в реализации агента «хранителя знаний» в рамках разработки многоагентной поисковой системы.

Демонстрационный проект

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

Пусть имеется матрица смежности для графа. Ребра графа не отрицательные. Задаются начальная и конечная вершина.

Необходимо получать кратчайший путь между двумя парами вершин. Если пути не существует, вернуть -1.

      int Dijkstra(vector<vector<int> > &adj, int n, int beg, int end) {
  constint INF = 1e9;
  int len = 0;
  map<int,int> ids;
  dist = vector<int>(n,INF);
  fibonacci_heap<int> heap(_less);
  dist[beg] = 0;
  heap.push(beg);
  for (int j=0;j<n-1;++j) {
    int cur = heap.top(); heap.pop();
    if (cur == end) break;
    for (int i=0;i<adj[cur].size();++i) {
      if (adj[cur][i] == INF) continue;
      int nxt = i;
      if (dist[nxt] == INF)
        ids[nxt] = heap.push(nxt);
      if (dist[nxt] > dist[cur] + adj[cur][i]) {
        dist[nxt] = dist[cur] + adj[cur][i];
        heap.change(ids[nxt], nxt);
      }
    }
  }
  return dist[end] != INF ?  dist[end] : -1;
}

Список литературы

  1. J.W.J. Williams, Algorithm 232, Heapsort, CACM 7, 6 (June 1964), 347–348.
  2. R.W. Floyd, Algorithm 245, Treesort 3, CACM 7, 12 (Dec. 1964), 701.
  3. Vuillemin, J. A data structure for manipulating priority queues. СACM 21 (1978), 309–314.
  4. Р.Седжвик. Алгоритмы на С++: Пер. с англ. – М.: ООО «И.Д. Вильямс», 2011 – 1056 с.
  5. Fredman, M. L.; Tarjan (1987). Fibonacci heaps and their uses in improved network optimization algorithms. СACM 34 (3): 596–615.
  6. Т.Кормен, Ч.Лейзерсон, Р.Ривест, К.Штайн. Алгоритмы: построение и анализ — 2-е изд. — М.: Издательский дом «Вильямс», 2007. — С. 1296.
  7. К.Д.Маннинг, П.Рагхаван, Х.Шютце. Введение в информационный поиск. - “Вильямс”. 2011.


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