Структуры данных и модели вычислений

Приоритетные очереди

Разбить на страницы
Показывать лекцию целиком

Основные определения

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

  • ВСТАВИТЬ в множество новый элемент со своим ключом.
  • НАЙТИ в множестве элемент с минимальным ключом. Если элементов с минимальным ключом несколько, то находится один из них. Найденный элемент не удаляется из множества.
  • УДАЛИТЬ из множества элемент с минимальным ключом. Если элементов с минимальным ключом несколько, то удаляется один из них.
  • Дополнительные операции над приоритетными очередями:

  • ОБЪЕДИНИТЬ два множества в одно.
  • УМЕНЬШИТЬ ключ указанного элемента множества на заданное положительное число.
  • Приоритетная очередь естественным образом используется в таких задачах, как сортировка элементов массива, поиск во взвешенном неориентированном графе минимального остовного дерева, поиск кратчайших путей от заданной вершины взвешенного графа до его остальных вершин, и во многих других.

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

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

    Соответствие между узлами дерева и элементами множества называется кучеобразным, если для каждого узла $$i$$ соблюдается следующее условие:

    Ключ элемента, приписанного узлу $$i$$, не превосходит ключей, приписанных его потомкам.

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

    Представление приоритетной очереди с помощью d-кучи

    Представление приоритетной очереди с помощью $$d$$ -кучи основано на использовании так называемых завершенных $$d$$ -арных деревьев ( $${d \ge 2}$$ ).

    Завершенное $$d$$ -арное дерево — это корневое дерево со следующими свойствами:

  • Каждый внутренний узел (то есть узел, не являющийся листом дерева), за исключением, быть может, только одного, имеет ровно $$d$$ потомков. Один узел-исключение может иметь от $$1$$ до $$d - 1$$ потомков.
  • Если $$k$$ — глубина дерева, то для любого $$i = 1\dts k - 1$$ такое дерево имеет ровно $$d^i$$ узлов глубины $$i$$.
  • Количество узлов глубины $$k$$ в дереве глубины $$k$$ может варьироваться от $$1$$ до $$d^k$$. Это свойство является следствием первых двух.
  • Узлы завершенного $$d$$ -арного дерева принято нумеровать следующим образом: корень получает номер 0, потомки узла с номером $$i$$ получают номера: $$i\cdot d + 1$$, $$i\cdot d + 2$$, $$\ldots$$, $$i\cdot d + d$$. Такая нумерация удобна тем, что позволяет разместить узлы дерева в массиве в порядке возрастания их номеров, при этом позиции потомков любого узла в массиве легко вычисляются по позиции самого узла. Так же легко по позиции узла вычислить позицию его родителя. Так, для узла, расположенного в позиции $$i$$, родительский узел располагается в позиции $$(i - 1) \mathop{\rm div} d$$, где $$\mathop{\rm div}$$ — операция деления нацело.

    В изображении завершенного $$d$$ -арного дерева узлы одинаковой глубины удобно располагать на одном уровне, при этом потомки одного узла располагаются слева направо в порядке объявленных номеров. При таком рисовании нижний уровень заполняется, возможно, не полностью.

    Отметим некоторые простые утверждения о завершенных $$d$$ -арных деревьях, которые будут полезны при анализе трудоемкости основных операций.

    Утверждение 1. Длина $$h$$ пути из корня завершенного $$d$$ -арного дерева с $$n > 1$$ узлами в любой лист удовлетворяет неравенствам:$$\eq*{ \log_d n - 1 < h < \log_d n + 1. }$$

    Доказательство Минимальное количество узлов в $$d$$ -куче высоты $$h$$ ( $$h > 0$$ ), по свойствам 2 и 3 $$d$$ -арного дерева, очевидно, равно $$1 + d + d^2 + \ldots + d^{h - 1} + 1$$ (последний уровень содержит лишь один узел).

    Максимальное количество узлов в такой $$d$$ -куче равно $$1 + d + d^2 + \ldots + d^h$$ (последний уровень содержит $$d^h$$ узлов). Отсюда имеем неравенства:$$\eq*{ (1 + d + d^2 + \ldots + d^h - 1 + 1) \le n \le (1 + d + d^2 + \ldots + d^h). }$$

    Суммируя левую и правую части как геометрические прогрессии, получим$$\eq*{ (d^h - 1)/(d - 1) + 1 \le n \le (d^{h + 1} - 1)/(d - 1), }$$ и после некоторых очевидных оценок с помощью логарифмирования получаем требуемые неравенства:$$\eq*{ \log_d n - 1 < h < \log_d n + 1. }$$

    Утверждение 2. Количество узлов высоты $$h$$ не превосходит $$n/d^h$$.

    Под высотой узла понимается расстояние от него до наиболее далекого потомка. Кучу, содержащую $$n$$ элементов, будем представлять двумя массивами — $$a[0 \ldots n - 1]$$ и $${\rm key}[0 \ldots n - 1]$$, — полагая что $$a[i]$$ — имя элемента, приписанного узлу $$i$$ ; $${\rm key}[i]$$ — его ключ. Иногда под $$a[i]$$ удобно понимать сам элемент исходного множества или ссылку на него. В некоторых прикладных задачах нет необходимости помещать в приоритетную очередь ни сами элементы, ни их имена, в таких случаях при организации кучи используется лишь массив $${\rm key}[0 \ldots n - 1]$$.

    На рис. 4.1 приведен пример кучи для $$d = 3$$, $$n = 18$$. Кружочками изображены узлы дерева, в них записаны элементы массива, представляющие имена элементов кучи.

    Пример кучи при $$d = 3$$, $$n = 7$$ для приоритетной очереди, содержащей элементы с ключами $$1, 2, 2, 2, 3, 4, 5$$, изображен на рис. 4.2, где пара чисел в каждом кружочке обозначает номер узла и ключ соответствующего элемента.

    (рис 4.2) (рис 4.1)

    Операции с d-кучей

    При реализации основных операций над кучами используются две вспомогательные операции — ВСПЛЫТИЕ и ПОГРУЖЕНИЕ. При реализации этих операций введем еще одну вспомогательную операцию — транспонирование, с помощью которой будем менять местами элементы, расположенные в двух разных узлах дерева. Ее реализация может быть представлена следующим образом:

    $$\formula{ \t{procedure}\ tr(i, j);\\ \t begin\\ \mbox{}\q {\rm temp}0:= a[i]; a[i]:= a[j];\ a[j]:= {\rm temp}0; \\ \mbox{}\q {\rm temp}1:= {\rm key}[i];\ {\rm key}[i]:= {\rm key}[j];\ {\rm key}[j]:= {\rm temp}1;\\ \t end; }$$

    Замечание. Если в кучу помещаются только ключи элементов, то процедура транспонирования модифицируется соответствующим образом.

    Операция ВСПЛЫТИЕ.Эта операция применяется в тех случаях, когда в некотором узле, например в $$i$$ -м, расположен элемент $$x$$, нарушающий кучеобразный порядок так, что его ключ меньше ключа его родителя $$y$$.

    Элементы $$x$$ и $$y$$ меняются местами. Если после этого элемент $$x$$ снова не удовлетворяет условиям кучи, то еще раз проводится аналогичная перестановка. И так до тех пор, пока $$x$$ не встанет на свое место.

    Рассмотрим 3-дерево на рис. 4.3. В этом дереве кучеобразный порядок нарушает узел $$17$$ с ключом $$14$$, так как его родительскому узлу приписан элемент с ключом $$31 > 14$$.

    (рис 4.3)

    Применим к узлу $$17$$ операцию ВСПЛЫТИЕ. Элементы с ключами $$31$$ и $$14$$ меняются местами. В результате получается дерево, представленное на рис. 4.4.

    (рис 4.4)

    Теперь нарушен кучеобразный порядок в узле $$5$$ ( $$21 > 14$$ ), меняем местами элементы c ключами $$21$$ и $$14$$. В результате получаем кучу, изображенную на рис. 4.5. Кучеобразный порядок восстановлен, операция ВСПЛЫТИЕ завершена.

    (рис 4.5)

    Вычислительная сложность этой операции пропорциональна числу сравнений элементов и их обменов. Это число, очевидно, не более чем удвоенное число узлов в пути от узла $$x$$ до корня дерева. Длина такого пути в $$d$$ -куче с $$n$$ узлами не превосходит ее высоты, а именно $$\log_d n + 1$$, в соответствии с доказанным выше утверждением 1. Значит, время выполнения данной операции — $$O(\log_d n)$$.

    Реализация операции ВСПЛЫТИЕ. Входным параметром этой операции является номер узла, в котором нарушен порядок:

    $$\formula{ \t{procedure ВСПЛЫТИЕ}(i);\\ \t{begin}\\ p:= (i - 1)\ \t div\ d;\\ \mbox{}\q\t while (i \ne 0)\ \t and\ ({\rm key}[p] > {\rm key}[i])\ \t do\ \{{\rm tr}(i, p);\ i:= p;\ p:= (i - 1)\ \t div\ d\};\\ \t end; }$$

    Замечание.

  • Операцию ВСПЛЫТИЕ можно применять не только к $$d$$ -куче, но и к другим видам куч.
  • Для более эффективного выполнения операции ВСПЛЫТИЕ можно поступить следующим образом: запомнить элемент, находящийся в узле $$i$$, переместить элемент из его родительского узла $$p = (i - 1) \mathop{\rm div}\nolimits d$$ в узел $$i$$, затем из узла $$(p - 1) \mathop{\rm div}\nolimits d$$ в узел $$p$$ и так до тех пор, пока не освободится узел для запомненного элемента. После этого поместить запомненный элемент на освободившееся место. Более точно это можно выразить с помощью следующих операторов:
  • $$\formula{ \t begin {\rm key}0:= {\rm key}[i];\ a0:= a[i];\ p:= (i - 1) \t div\ d; \\ \mbox{}\q \t while\ (i \ne 0)\ \t and\ ({\rm key}[p] > {\rm key}0\ \t do\\ \mbox{}\q\qq \{a[i]:=a[p];\ {\rm key}[i]:= {\rm key}[p];\ i:= p;\ p:= (i - 1)\ \t div\ d\};\\ \mbox{}\q a[i]:= a0;\ {\rm key}[i]:= {\rm key}0\\ \t end; }$$

    Операция ПОГРУЖЕНИЕ. Эта операция также применяется для восстановления свойства кучеобразности. Пусть, например, в $$i$$ -м узле расположен элемент $$x$$, нарушающий кучеобразный порядок таким образом, что ключ элемента $$x$$ больше ключа элемента $$y$$, приписанного потомку узла $$i$$. В этом случае среди непосредственных потомков узла $$i$$ выбирается элемент $$y$$ с наименьшим ключом, и элементы $$x$$ и $$y$$ меняются местами. Если после этого элемент $$x$$ снова не удовлетворяет условиям кучи, то еще раз проводим аналогичную перестановку. И так до тех пор, пока $$x$$ не встанет на свое место.

    Рассмотрим $$3$$ -дерево, представленное на рис. 4.6. В узле $$1$$ расположен элемент $$x$$ с ключом $$31$$, и этот узел имеет двух потомков с меньшими ключами, а именно $$30$$ и $$14$$. Применим к элементу $$x$$ операцию ПОГРУЖЕНИЕ.

    (рис 4.6)

    Среди непосредственных потомков узла 1 находим узел, которому приписан элемент $$y$$ с наименьшим ключом, в нашем случае это узел $$5$$ c ключом $$14$$. Меняем местами элементы $$x$$ и $$y$$. В результате получается дерево, изображенное на рис. 4.7.

    (рис 4.7)

    Теперь элемент $$x$$ снова имеет потомка с меньшим, чем у него, ключом (а точнее, оба его потомка имеют меньшие ключи). Снова находим непосредственного потомка элемента $$x$$ с наименьшим ключом, и меняем его и $$x$$ местами. Получается дерево, изображенное на рис. 4.8.

    (рис 4.8)

    Теперь $$x$$ находится в узле 17 и не имеет потомков с меньшим, чем у него, ключом (точнее, у него вообще нет потомков). Операция ПОГРУЖЕНИЕ завершена.

    Вычислительная сложность этой операции пропорциональна числу сравнений элементов и их обменов. Для каждого узла в пути следования данной операции производится $$d$$ сравнений (при поиске потомка с минимальным ключом) и один обмен. Длина этого пути в $$d$$ -куче с $$n$$ узлами не превосходит ее высоты, а именно $$\log_d n$$, по доказанному выше утверждению 1. Значит, время выполнения данной операции — $$O(d\cdot \log_d n)$$.

    Для реализации операции погружения воспользуемся функцией $${\rm minchild} (i)$$, позволяющей для любого узла $$i$$ находить его непосредственного потомка с минимальным ключом. Если у узла $$i$$ нет потомков, то $${\rm minchild} (i) = 0$$.

    Реализация операции ПОГРУЖЕНИЕ

    $$\formula{ \t{procedure ПОГРУЖЕНИЕ}(i);\\ \t begin \\ \mbox{}\q c:= {\rm minchild}(i);\\ \mbox{}\q \t while\ (c \ne 0)\ \t and\ ({\rm key}[c] < {\rm key}[i])\ \t do\ \{{\rm tr}(i, c);\ i:= c;\ c := {\rm minchild} (c)\}\\ \t end; }$$ $$\formula{ \t function\ {\rm minchild} (i);\\ \t begin\ \t if\ i\cdot d + 1 > n\ \t then\ {\rm minchild} := 0\ \t else\\ \mbox{}\q \t begin\\ \mbox{}\q\qq {\rm first\_child}:= i\cdot d + 1;\ {\rm last\_child}:= \min((i + 1)\cdot d - 1, n);\\ \mbox{}\q\qq{\rm min\_key} := {\rm key}[{\rm first\_child}];\\ \mbox{}\q\qq \t for\ i := {\rm first\_child}\ \t to\ {\rm last\_child}\ \t do\ \t if\ {\rm key}[i] > {\rm min\_key}\ \t then\\ \mbox{}\q\qq\qq \{{\rm min\_key} := {\rm key}[i];\ {\rm minchild} := i\}\\ \mbox{}\q \t end\\ \t end }$$

    Операция ВСТАВКА. Если перед выполнением этой операции куча содержала $$n$$ узлов (напомним, что они пронумерованы числами от $$0$$ до $${n - 1}$$ ), то добавляем к дереву $$(n + 1)$$ -й узел (его номер будет $$n$$ ) и приписываем ему элемент с именем $${\rm name}X$$ и ключом $${\rm key}X$$. Вставка нового элемента производится посредством отведения для него места в $$n$$ -ых позициях массивов $$a$$ и $${\rm key}$$ соответственно, после чего к добавленному узлу применяется операция ВСПЛЫТИЕ для восстановления кучеобразного порядка.

    Вставим в $$d$$ -кучу, изображенную на рис. 4.9, новый элемент с ключом $$14$$.

    (рис 4.9)

    Сначала добавляем к дереву новый узел с номером $$17$$ и приписываем ему элемент с ключом $$14$$. Получим дерево, представленное на рис. 4.10.

    (рис 4.10)

    Затем применяем к узлу $$17$$ операцию ВСПЛЫТИЕ. При описании этой операции использовался именно приведенный пример (см. рис. 4.3, 4.4, 4.5).

    Вычислительная сложность данной операции равна константе плюс вычислительная сложность операции ВСПЛЫТИЕ, то есть $$O(\log_d n)$$.

    Реализация операции ВСТАВКА

    $$\formula{ \t{procedure ВСТАВКА}\ ({\rm name}X, {\rm key}X);\\ \t begin\ {a}[{n}]:= {\rm name}X; {\rm key}[{n}]:= {\rm key}X;\ \t{ВСПЛЫТИЕ}\ ({n});\ {n}:= {n} + 1\ \t end; }$$

    Операция УДАЛЕНИЕ. Используется для удаления элемента, приписанного узлу с заданным номером $$i$$. Сначала элемент, приписанный последнему узлу дерева, переносится на место удаляемого элемента, последний узел при этом становится ненужным и поэтому удаляется из дерева. Далее, если узел $$i$$, в который помещен новый элемент, имеет родителя с большим ключом, то к узлу $$i$$ применяется операция ВСПЛЫТИЕ, в противном случае — ПОГРУЖЕНИЕ.

    Таким образом, ориентируясь на худший случай, вычислительную сложность операции УДАЛЕНИЕ оцениваем величиной $$O(d\cdot \log_d n)$$.

    Реализация операции УДАЛЕНИЕ

    $$\formula{ \t{procedure УДАЛЕНИЕ}(i);\\ \t begin\ {a}[{i}]:= {a}[{n} - 1];\ {\rm key}[{i}]:= {\rm key}[{n} - 1];\ {n}:= {n} - 1; \\ \mbox{}\q \t if\ i \ne 0\ \t and\ {\rm key}[{i}] ({\rm key}[(i - 1) \mathop{\rm div} {d}]\ \t then\ \t{ВСПЛЫТИЕ}({i})\\ \mbox{}\q \t else\ \t{ПОГРУЖЕНИЕ}({i})\\ \t end; }$$

    Операция УДАЛЕНИЕ_МИНИМУМА. Эта операция предназначена для взятия из кучи элемента с минимальным ключом (он находится в корне дерева) и удаления его из кучи с помощью операции УДАЛЕНИЕ.

    Реализация операции УДАЛЕНИЕ_МИНИМУМА

    $$\formula{ \t{procedure УДАЛЕНИЕ\_МИНИМУМА}\ ({\rm name}X, {\rm key}X);\\ \t begin\ {\rm name}X:= {a}[0];\ {\rm key}X:= {\rm key}[0];\ \t{УДАЛЕНИЕ}\ (0)\ \t end; }$$

    Функция MINKEY. Эта функция предназначена для определения минимального ключа без удаления соответствующего элемента.

    Реализация функции MINKEY

    $$\formula{ \t function\ {\rm MINKEY};\ \t begin\ {\rm MINKEY}: = {\rm key}[0]\ \t end; }$$

    Трудоемкость операции, очевидно, равна $${O}(1)$$.

    Операция УМЕНЬШЕНИЕ_КЛЮЧА. Предназначена для уменьшения ключа у элемента, приписанного узлу с заданным номером $$i$$, на заданную величину $$\Dl$$. Это действие может нарушить кучеобразный порядок лишь таким образом, что уменьшенный ключ элемента в узле $$i$$ станет меньше ключа элемента в родительском узле. Для восстановления порядка в куче используется операция ВСПЛЫТИЕ.

    Вычислительная сложность данной операции определяется временем, затрачиваемым на уменьшение ключа (то есть константой), и временем выполнения операции ВСПЛЫТИЕ (то есть $$O(\log_d n))$$. В итоге вычислительная сложность операции УМЕНЬШИТЬ_КЛЮЧ равна $$O(\log_d n)$$.

    Реализация операции УМЕНЬШЕНИЕ_КЛЮЧА

    $$\formula{ \t{procedure УМЕНЬШИТЬ\_КЛЮЧ}\ (i, delta);\\ \t begin\ {\rm key}[{i}] := {\rm key}[{i}] - {delta};\ \t{ВСПЛЫТИЕ}\ (i);\ \t end; }$$

    Операция ОКУЧИВАНИЕ. Заметим, что если $$d$$ -куча создается путем $$n$$ -кратного применения операции ВСТАВКА, то суммарная трудоемкость ее создания будет равна $$O(n\cdot \log_d n)$$. Если же все $$n$$ элементов сначала занимают в произвольном порядке массив $$a[0 \ldots (n - 1)]$$ и, соответственно, массив $${\rm key}[0 \ldots ({n} - 1)]$$, то можно превратить их в $$d$$ -кучу, применяя операцию ПОГРУЖЕНИЕ по очереди к узлам $$(n - 1),\,(n - 2),\,\ldots,\,0$$.

    Такой процесс будем называть окучиванием массива. Для доказательства того, что в результате действительно устанавливается кучеобразный порядок, достаточно заметить, что если поддеревья с корнями в узлах $$n - 1,\, n - 2,\, \ldots,\, i + 1$$ упорядочены по правилу кучи, то после применения процедуры ПОГРУЖЕНИЕ к узлу $$i$$ поддерево с корнем в этом узле также станет упорядоченным по правилу кучи. Итак, остановимся на следующей реализации.

    Реализация операции ОКУЧИВАНИЕ

    $$\formula{ \t{procedure ОКУЧИВАНИЕ};\\ \t begin\\ \mbox{}\q \t for\ i:= n - 1\ \t downto\ 0\ \t do\ \t{ПОГРУЖЕНИЕ}\ (i)\\ \t end; }$$

    Утверждение 3. Вычислительная сложность операции ОКУЧИВАНИЕ равна $$O(n)$$.

    Доказательство Заметим, что трудоемкость погружения с высоты $$h$$ равна $$O(h)$$, а количество узлов высоты $$h$$ не превосходит $$n/d^h$$. Осталось оценить сумму$$\eq*{ \suml_{h=1}^{H} h\frac{n}{d^{h}}, }$$ где $$H = \lceil \log_d n\rceil$$, и убедиться, что полученная сумма есть $$O(n)$$.

    Для суммирования можно воспользоваться формулой$$\eq*{ \suml_{i=1}^k \frac{i}{x^i} = \frac{x^{k+1} - (k-1) x + k}{x^k (x-1)^2}. }$$

    Предоставляем читателю возможность завершить доказательство.

    Операция СОЗДАТЬ_СПИСОК_МИНИМАЛЬНЫХ. Эта операция применяется для получения списка элементов, которые имеют ключи, меньшие заданного значения $${\rm key}0$$, и реализуется следующим образом. Если ключ элемента, находящегося в корне, больше, чем $${\rm key}0$$, то это дерево не имеет искомых элементов. В противном случае включаем его в выходной список $$S$$, а затем применяем ту же процедуру ко всем потомкам узла, включенного в список.

    Пусть куча содержит $$k$$ элементов с ключами, меньшими, чем $${\rm key}0$$. По свойству кучи, они все расположены на ее "верхушке". Данная процедура обходит эту верхушку за время, пропорциональное $$k$$, и для каждого из этих $$k$$ элементов просматривает все его $$d$$ (или меньше) непосредственных потомков. Получаем, что время выполнения данной процедуры является величиной $$O(d\cdot k)$$.

    Реализация операции СОЗДАТЬ_СПИСОК_МИНИМАЛЬНЫХ

    $$\formula{ \t{procedure СОЗДАТЬ\_СПИСОК\_МИНИМАЛЬНЫХ}(S, {\rm key}0);\\ \t begin\\ \mbox{}\q \t{Инициализируем пустой список} S; \\ \mbox{}\q \t{Инициализируем стек};\\ \mbox{}\q 0 \Rightarrow \t{стек};\\ \mbox{}\q \t{ while стек не пуст do}\\ \mbox{}\q\qq \t{begin стек} \Rightarrow i;\\ \mbox{}\q\qq\qq if ({\rm key}[i] < {\rm key}0)\ \t{then Добавить} a[i]\ \t{к списку}\ S;\\ \mbox{}\q\qq\qq \t for\ j:=d^\ast i + 1\ \t to\ d^\ast(i + 1)\ \t do if\ j \le (n - 1)\ \t then\ j \Rightarrow \t{стек};\\ \mbox{}\q\qq \t end\\ \t end }$$

    Сводные данные о трудоемкости операций с d-кучами

    ВСПЛЫТИЕ $$(i)$$ $$O(\log_d n)$$
    ПОГРУЖЕНИЕ $$(i)$$ $$O(d \log_d n)$$
    ВСТАВКА $$({\rm name}X, {\rm key}X)$$ $$O(\log_d n)$$
    УДАЛЕНИЕ $$(i)$$ $$O(d \log_d n)$$
    УДАЛЕНИЕ_МИН $$({\rm name}X, {\rm key}X)$$ $$O(d \log_d n)$$
    MINKEY $$O(1)$$
    УМЕНЬШЕНИЕ_КЛЮЧА $$(i, D)$$ $$O(\log_d n)$$
    ОБРАЗОВАTЬ_ОЧЕРЕДЬ $$O(n)$$
    СПИСОК_МИН $$(x, h)$$ $$O(d k)$$

    Замечание. Для $$d$$ -куч "неудобной" является операция слияния куч.

    Применение приоритетных очередей в задаче сортировки

    Под задачей сортировки в простейшем случае понимают следующее: дана последовательность $$({\rm key}[1],\,{\rm key}[2],\, \ldots,\, {\rm key}[{n}])$$ из $$n$$ элементов некоторого линейно упорядоченного множества, например целых или вещественных чисел, записанных в массив $${\rm key}$$. Требуется переставить элементы массива так, чтобы после перестановки выполнялись неравенства:$$\eq*{ {\rm key}[1] \le {\rm key}[2] \le \ldots \le {\rm key} [n]. }$$

    Уточнения этой задачи связаны с теми средствами, с помощью которых предполагается ее решение. Нас интересуют алгоритмы с точки зрения их компьютерной реализации. Оценивая качество различных алгоритмов, обычно интересуются тем, как зависит время счета от длины $$n$$ сортируемой последовательности и требуется ли для этого дополнительная память, размер которой определяется параметром $$n$$. Существенную роль при этом играет метод доступа к элементам памяти. При сортировке во внутренней (оперативной) памяти обычно используется прямой доступ, а во внешней — последовательный.

    Бесхитростная сортировка в памяти с прямым доступом.

    Бесхитростный алгоритм сортировки может заключаться в выполнении следующих операторов:

    $$\formula{ \t for\ {k}:= 1\ \t to\ n - 1\ \t do for\ i:= k+ 1\ \t to\ n\ \t do\ \t if\ {\rm key}[{i}] < {\rm key}[{k}]\ \t then\ {\rm tr}({i},{k}); }$$

    Здесь $${\rm tr}({i},{k})$$ — процедура, транспонирующая элементы $${\rm key}[{i}],\, {\rm key}[{k}]$$. Заметим, что число сравнений$$\eq*{ "{\rm key}[{i}] < {\rm key}[{k}]" }$$ при реализации такого алгоритма равно $$n(n - 1)/2$$. В частности, это означает, что время работы алгоритма равно $$O(n^2)$$.

    Сортировка методом "разделяй и властвуй".

    Предположим, что в нашем распоряжении имеется процедура РАЗДЕЛЯЙ $$(i, j, k)$$, которая по заданным значениям индексов $$i$$, $$j$$ находит некоторое промежуточное значение $$k$$ и переставляет элементы сегмента $${\rm key}[i \ldots j]$$ так, чтобы для $$s = {i},\,{i} + 1,\,\ldots,\,{k} - 1$$ выполнялось неравенство $${\rm key}[{s}] \le {\rm key}[k]$$, а для $$s = k + 1,\, k + 2,\, \ldots, j$$ — неравенство $${\rm key}[{k}] \le {\rm key}[{s}]$$.

    Тогда для сортировки сегмента $${\rm key}[{i} \ldots {j}]$$ может быть использована рекурсивная процедура СОРТИРУЙ.

    $$\formula{ \t{procedure СОРТИРУЙ}\ (i, j);\\ \t begin\ \t if\ {i} = {j}\ \t then\ \t{exit}\ \t else\\ \mbox{}\q \t{РАЗДЕЛЯЙ}\ ({i}, {j}, {k});\ \t{СОРТИРУЙ}\ (i, k - 1);\ \t{СОРТИРУЙ}\ (k + 1, j)\}\\ \t end; }$$

    Для сортировки всего исходного массива достаточно выполнить оператор СОРТИРУЙ $$(1, n)$$.

    Заметим, что если бы процедура РАЗДЕЛЯЙ работала линейное от длины сегмента время и давала значение $$k$$, близкое к середине между $$i$$ и $$j$$, то число обращений к ней приблизительно равнялось бы $$\log n$$ и сортировка всего массива проходила бы за время порядка $$O(n \cdot \log n)$$. Однако можно доказать, что при естественной реализации эта оценка справедлива лишь в среднем.

    Упражнения

  • Разработайте вариант процедуры СОРТИРУЙ без использования рекурсии. Сколько дополнительной памяти требуется для запоминания границ еще не отсортированных сегментов?
  • Охарактеризуйте работу процедуры СОРТИРУЙ на заранее отсортированном массиве.
  • Напишите на известном вам алгоритмическом языке программу сортировки числового массива с помощью процедуры СОРТИРУЙ и испытайте ее на массивах, сгенерированных с помощью датчика случайных чисел.
  • Составьте таблицу, отражающую время работы вашей программы на массивах разной длины. Каков максимальный размер массива, который можно отсортировать составленной программой на вашем компьютере?
  • Сортировка "слиянием".

    Этот метод является разновидностью метода "разделяй и властвуй"; впрочем, уместнее было бы назвать его "властвуй и объединяй".

    Предположим, что у нас есть процедура СЛИВАЙ ( $$i, j, k)$$, которая два уже отсортированных сегмента $${\rm key}[{i}\ldots (j - 1)]$$ и $${\rm key}[{j}\ldots {k}]$$ преобразует (сливает) в один сегмент $${\rm key}[{i}\ldots {k}]$$, делая его полностью отсортированным. Тогда рекурсивная процедура

    $$\formula{ \t{procedure СОРТИРУЙ}\ (i, j);\\ \t begin\ \t{if}\ {i} = {j}\ \t then\ \t{exit}\ \t else\\ \mbox{}\q \{m:= (i + j)\ \t div\ 2;\ \t{СОРТИРУЙ}\ (i, m);\ \t{СОРТИРУЙ}\ (m+1, j);\\ \mbox{}\q \t{СЛИВАЙ} (i, m, j)\}\\ \t end; }$$

    очевидно, сортирует сегмент $${\rm key}[{i}\ldots {j}]$$, а для сортировки всего исходного массива достаточно выполнить оператор СОРТИРУЙ $$(1, n)$$. Как видим, вопрос балансировки размера сегментов решается здесь просто. Число обращений к процедуре СЛИВАЙ $$(i, m, j)$$ равно $$\log n$$, а время ее выполнения легко сделать линейным от суммарной длины сливаемых сегментов.

    Упражнения

  • Разработайте процедуру СЛИВАЙ и вариант процедуры СОРТИРУЙ без использования рекурсии. Сколько дополнительной памяти требуется для ее реализации?
  • Оцените теоретически время работы алгоритма по методу слияния.
  • Напишите на известном вам алгоритмическом языке программу сортировки числового массива методом слияния и испытайте ее на массивах, сгенерированных с помощью датчика случайных чисел.
  • Составьте таблицу, отражающую время работы вашей программы на массивах разной длины. Каков максимальный размер массива, который можно отсортировать составленной программой на вашем компьютере?
  • Сортировка с помощью d-кучи.

    Для представления сортируемой последовательности используем структуру $$d$$-кучи. Сортировку можно провести в два этапа. Данный алгоритм (heapsort) работает для бинарных деревьев (d=2) и это необходимо указывать

  • Окучить сортируемый массив, применяя последовательно операцию ПОГРУЖЕНИЕ по очереди к узлам $$(n - 1),\, (n - 2),\, \ldots,\, 0$$ в предположении, что сначала все $$n$$ ключей занимают в произвольном порядке массив $${\rm key}[0 \ldots n - 1]$$.
  • Осуществить окончательную сортировку следующим образом. Первый (минимальный) элемент кучи меняем местами с последним, уменьшаем размер кучи на 1 (минимальный элемент остается в последней позиции массива $${\rm key}$$, не являясь уже элементом кучи) и применяем операцию ПОГРУЖЕНИЕ к корню, затем повторяем аналогичные действия, пока размер кучи не станет равным 1.
  • Эти два этапа реализуются с помощью процедуры SORT, которая сортирует массив по убыванию ключей:

    $$\formula{ \t{procedure SORT}(n);\\ \t begin\ \t{for}\ i:= n - 1\ \t downto\ 0\ \t do\ \t{ПОГРУЖЕНИЕ}\ (i);\\ \mbox{}\q \t while\ n > 1\ \t do\ \{{\rm tr} (1, n);\ n:= n - 1;\ \t{ПОГРУЖЕНИЕ} (1)\}\\ \t end; }$$

    Заметим, что процедура $$SORT$$ не требует дополнительной памяти, размер которой зависел бы от длины массива $$\rm key$$.

    Упражнения

  • Докажите, что оператор$$\eq*{ \t for\ i := n - 1\ \t downto\ 0\ \t do\ \t{ПОГРУЖЕНИЕ}\ (i); }$$

    в процедуре $$SORT$$ можно заменить оператором

    $$\eq*{ \t for\ i := n \t div\ 2\ \t downto\ 0\ \t do\ \t{ПОГРУЖЕНИЕ}\ (i).$$
  • Напишите программу сортировки числового массива с помощью процедуры $${\rm SORT}(n)$$ и испытайте ее на массивах, сгенерированных с помощью датчика случайных чисел. Составьте таблицу, отражающую время работы вашей программы на массивах разной длины. Каков максимальный размер массива, который можно отсортировать составленной программой на вашем компьютере?
  • Нахождение кратчайших путей в графе

    Входные данные:

  • Граф $$G$$ со взвешенными ребрами (под весами можно понимать длины ребер, если речь идет о геометрическом графе, или любые другие числовые характеристики ребер). Пусть $$L(i, j)$$ — вес ребра ( $$i,j$$ ).
  • Стартовая вершина $$s$$ (вершина, от которой вычисляются расстояния до всех остальных вершин).
  • Выходные данные:

  • Массив $${\rm dist}[1\ldots {n}]$$, $$({\rm dist}[i]$$ — кратчайшее расстояние от вершины $$s$$ до вершины $$i$$ ).
  • Массив $${\rm up}[1\ldots n]$$, $$({\rm up}[i]$$ — предпоследняя вершина в кратчайшем пути из вершины $$s$$ в вершину $$i$$ ).
  • Приводимый ниже алгоритм Дейкстры корректно решает задачу для графов с неотрицательными весами вершин. Если же в графе есть ребра с отрицательными весами, но нет циклов с отрицательным суммарным весом, то для решения задачи можно использовать алгоритм Форда, Беллмана.

    Алгоритм Дейкстры

  • Заполнить массив $${\rm up} [1\ldots n]$$ нулями.
  • Каждой вершине $$i$$ приписать в качестве ключа $${\rm dist} [i]$$ — максимально возможное число (оно должно быть больше, чем длина наибольшего из кратчайших путей в графе; в процессе вычислений это число будет уменьшаться и в итоге заменится на длину кратчайшего пути из вершины $$s$$ в вершину $$i$$ ).
  • Организовать приоритетную очередь из вершин графа, взяв в качестве ключей величины $${\rm dist}[i]$$, $$i= 1, 2 \dts n$$.
  • Заменить ключ вершины $$s$$ на 0.
  • Пока очередь не пуста, выполнять операции $$6,7$$.
  • Выбрать (с удалением) из приоритетной очереди элемент $$r_0$$ с минимальным ключом.
  • Для каждой вершины $$r$$, смежной с $$r_0$$, выполнить операции $$8, 9$$.
  • Вычислить величину $${\rm delta} = {\rm dist}[r] - ({\rm dist}[r_0] + L (r_0, r))$$.
  • Если $${\rm delta} > 0$$, то уменьшить ключ $${\rm dist}[r]$$ элемента $$r$$ на величину $${\rm delta}$$ и заменить старое значение величины $${\rm up}[r]$$ на $$r_0$$.
  • Упражнение

    Напишите на каком-либо алгоритмическом языке реализацию алгоритма Дейкстры с использованием $$d$$ -кучи и испытайте ее на тестовых примерах при различных значениях $$d$$.

    Страницы:

    Основные определения

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

  • ВСТАВИТЬ в множество новый элемент со своим ключом.
  • НАЙТИ в множестве элемент с минимальным ключом. Если элементов с минимальным ключом несколько, то находится один из них. Найденный элемент не удаляется из множества.
  • УДАЛИТЬ из множества элемент с минимальным ключом. Если элементов с минимальным ключом несколько, то удаляется один из них.
  • Дополнительные операции над приоритетными очередями:

  • ОБЪЕДИНИТЬ два множества в одно.
  • УМЕНЬШИТЬ ключ указанного элемента множества на заданное положительное число.
  • Приоритетная очередь естественным образом используется в таких задачах, как сортировка элементов массива, поиск во взвешенном неориентированном графе минимального остовного дерева, поиск кратчайших путей от заданной вершины взвешенного графа до его остальных вершин, и во многих других.

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

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

    Соответствие между узлами дерева и элементами множества называется кучеобразным, если для каждого узла $$i$$ соблюдается следующее условие:

    Ключ элемента, приписанного узлу $$i$$, не превосходит ключей, приписанных его потомкам.

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

    Представление приоритетной очереди с помощью d-кучи

    Представление приоритетной очереди с помощью $$d$$ -кучи основано на использовании так называемых завершенных $$d$$ -арных деревьев ( $${d \ge 2}$$ ).

    Завершенное $$d$$ -арное дерево — это корневое дерево со следующими свойствами:

  • Каждый внутренний узел (то есть узел, не являющийся листом дерева), за исключением, быть может, только одного, имеет ровно $$d$$ потомков. Один узел-исключение может иметь от $$1$$ до $$d - 1$$ потомков.
  • Если $$k$$ — глубина дерева, то для любого $$i = 1\dts k - 1$$ такое дерево имеет ровно $$d^i$$ узлов глубины $$i$$.
  • Количество узлов глубины $$k$$ в дереве глубины $$k$$ может варьироваться от $$1$$ до $$d^k$$. Это свойство является следствием первых двух.
  • Узлы завершенного $$d$$ -арного дерева принято нумеровать следующим образом: корень получает номер 0, потомки узла с номером $$i$$ получают номера: $$i\cdot d + 1$$, $$i\cdot d + 2$$, $$\ldots$$, $$i\cdot d + d$$. Такая нумерация удобна тем, что позволяет разместить узлы дерева в массиве в порядке возрастания их номеров, при этом позиции потомков любого узла в массиве легко вычисляются по позиции самого узла. Так же легко по позиции узла вычислить позицию его родителя. Так, для узла, расположенного в позиции $$i$$, родительский узел располагается в позиции $$(i - 1) \mathop{\rm div} d$$, где $$\mathop{\rm div}$$ — операция деления нацело.

    В изображении завершенного $$d$$ -арного дерева узлы одинаковой глубины удобно располагать на одном уровне, при этом потомки одного узла располагаются слева направо в порядке объявленных номеров. При таком рисовании нижний уровень заполняется, возможно, не полностью.

    Отметим некоторые простые утверждения о завершенных $$d$$ -арных деревьях, которые будут полезны при анализе трудоемкости основных операций.

    Утверждение 1. Длина $$h$$ пути из корня завершенного $$d$$ -арного дерева с $$n > 1$$ узлами в любой лист удовлетворяет неравенствам:$$\eq*{ \log_d n - 1 < h < \log_d n + 1. }$$

    Доказательство Минимальное количество узлов в $$d$$ -куче высоты $$h$$ ( $$h > 0$$ ), по свойствам 2 и 3 $$d$$ -арного дерева, очевидно, равно $$1 + d + d^2 + \ldots + d^{h - 1} + 1$$ (последний уровень содержит лишь один узел).

    Максимальное количество узлов в такой $$d$$ -куче равно $$1 + d + d^2 + \ldots + d^h$$ (последний уровень содержит $$d^h$$ узлов). Отсюда имеем неравенства:$$\eq*{ (1 + d + d^2 + \ldots + d^h - 1 + 1) \le n \le (1 + d + d^2 + \ldots + d^h). }$$

    Суммируя левую и правую части как геометрические прогрессии, получим$$\eq*{ (d^h - 1)/(d - 1) + 1 \le n \le (d^{h + 1} - 1)/(d - 1), }$$ и после некоторых очевидных оценок с помощью логарифмирования получаем требуемые неравенства:$$\eq*{ \log_d n - 1 < h < \log_d n + 1. }$$

    Утверждение 2. Количество узлов высоты $$h$$ не превосходит $$n/d^h$$.

    Под высотой узла понимается расстояние от него до наиболее далекого потомка. Кучу, содержащую $$n$$ элементов, будем представлять двумя массивами — $$a[0 \ldots n - 1]$$ и $${\rm key}[0 \ldots n - 1]$$, — полагая что $$a[i]$$ — имя элемента, приписанного узлу $$i$$ ; $${\rm key}[i]$$ — его ключ. Иногда под $$a[i]$$ удобно понимать сам элемент исходного множества или ссылку на него. В некоторых прикладных задачах нет необходимости помещать в приоритетную очередь ни сами элементы, ни их имена, в таких случаях при организации кучи используется лишь массив $${\rm key}[0 \ldots n - 1]$$.

    На рис. 4.1 приведен пример кучи для $$d = 3$$, $$n = 18$$. Кружочками изображены узлы дерева, в них записаны элементы массива, представляющие имена элементов кучи.

    Пример кучи при $$d = 3$$, $$n = 7$$ для приоритетной очереди, содержащей элементы с ключами $$1, 2, 2, 2, 3, 4, 5$$, изображен на рис. 4.2, где пара чисел в каждом кружочке обозначает номер узла и ключ соответствующего элемента.

    (рис 4.2) (рис 4.1)

    Операции с d-кучей

    При реализации основных операций над кучами используются две вспомогательные операции — ВСПЛЫТИЕ и ПОГРУЖЕНИЕ. При реализации этих операций введем еще одну вспомогательную операцию — транспонирование, с помощью которой будем менять местами элементы, расположенные в двух разных узлах дерева. Ее реализация может быть представлена следующим образом:

    $$\formula{ \t{procedure}\ tr(i, j);\\ \t begin\\ \mbox{}\q {\rm temp}0:= a[i]; a[i]:= a[j];\ a[j]:= {\rm temp}0; \\ \mbox{}\q {\rm temp}1:= {\rm key}[i];\ {\rm key}[i]:= {\rm key}[j];\ {\rm key}[j]:= {\rm temp}1;\\ \t end; }$$

    Замечание. Если в кучу помещаются только ключи элементов, то процедура транспонирования модифицируется соответствующим образом.

    Операция ВСПЛЫТИЕ.Эта операция применяется в тех случаях, когда в некотором узле, например в $$i$$ -м, расположен элемент $$x$$, нарушающий кучеобразный порядок так, что его ключ меньше ключа его родителя $$y$$.

    Элементы $$x$$ и $$y$$ меняются местами. Если после этого элемент $$x$$ снова не удовлетворяет условиям кучи, то еще раз проводится аналогичная перестановка. И так до тех пор, пока $$x$$ не встанет на свое место.

    Рассмотрим 3-дерево на рис. 4.3. В этом дереве кучеобразный порядок нарушает узел $$17$$ с ключом $$14$$, так как его родительскому узлу приписан элемент с ключом $$31 > 14$$.

    (рис 4.3)

    Применим к узлу $$17$$ операцию ВСПЛЫТИЕ. Элементы с ключами $$31$$ и $$14$$ меняются местами. В результате получается дерево, представленное на рис. 4.4.

    (рис 4.4)

    Теперь нарушен кучеобразный порядок в узле $$5$$ ( $$21 > 14$$ ), меняем местами элементы c ключами $$21$$ и $$14$$. В результате получаем кучу, изображенную на рис. 4.5. Кучеобразный порядок восстановлен, операция ВСПЛЫТИЕ завершена.

    (рис 4.5)

    Вычислительная сложность этой операции пропорциональна числу сравнений элементов и их обменов. Это число, очевидно, не более чем удвоенное число узлов в пути от узла $$x$$ до корня дерева. Длина такого пути в $$d$$ -куче с $$n$$ узлами не превосходит ее высоты, а именно $$\log_d n + 1$$, в соответствии с доказанным выше утверждением 1. Значит, время выполнения данной операции — $$O(\log_d n)$$.

    Реализация операции ВСПЛЫТИЕ. Входным параметром этой операции является номер узла, в котором нарушен порядок:

    $$\formula{ \t{procedure ВСПЛЫТИЕ}(i);\\ \t{begin}\\ p:= (i - 1)\ \t div\ d;\\ \mbox{}\q\t while (i \ne 0)\ \t and\ ({\rm key}[p] > {\rm key}[i])\ \t do\ \{{\rm tr}(i, p);\ i:= p;\ p:= (i - 1)\ \t div\ d\};\\ \t end; }$$

    Замечание.

  • Операцию ВСПЛЫТИЕ можно применять не только к $$d$$ -куче, но и к другим видам куч.
  • Для более эффективного выполнения операции ВСПЛЫТИЕ можно поступить следующим образом: запомнить элемент, находящийся в узле $$i$$, переместить элемент из его родительского узла $$p = (i - 1) \mathop{\rm div}\nolimits d$$ в узел $$i$$, затем из узла $$(p - 1) \mathop{\rm div}\nolimits d$$ в узел $$p$$ и так до тех пор, пока не освободится узел для запомненного элемента. После этого поместить запомненный элемент на освободившееся место. Более точно это можно выразить с помощью следующих операторов:
  • $$\formula{ \t begin {\rm key}0:= {\rm key}[i];\ a0:= a[i];\ p:= (i - 1) \t div\ d; \\ \mbox{}\q \t while\ (i \ne 0)\ \t and\ ({\rm key}[p] > {\rm key}0\ \t do\\ \mbox{}\q\qq \{a[i]:=a[p];\ {\rm key}[i]:= {\rm key}[p];\ i:= p;\ p:= (i - 1)\ \t div\ d\};\\ \mbox{}\q a[i]:= a0;\ {\rm key}[i]:= {\rm key}0\\ \t end; }$$

    Операция ПОГРУЖЕНИЕ. Эта операция также применяется для восстановления свойства кучеобразности. Пусть, например, в $$i$$ -м узле расположен элемент $$x$$, нарушающий кучеобразный порядок таким образом, что ключ элемента $$x$$ больше ключа элемента $$y$$, приписанного потомку узла $$i$$. В этом случае среди непосредственных потомков узла $$i$$ выбирается элемент $$y$$ с наименьшим ключом, и элементы $$x$$ и $$y$$ меняются местами. Если после этого элемент $$x$$ снова не удовлетворяет условиям кучи, то еще раз проводим аналогичную перестановку. И так до тех пор, пока $$x$$ не встанет на свое место.

    Рассмотрим $$3$$ -дерево, представленное на рис. 4.6. В узле $$1$$ расположен элемент $$x$$ с ключом $$31$$, и этот узел имеет двух потомков с меньшими ключами, а именно $$30$$ и $$14$$. Применим к элементу $$x$$ операцию ПОГРУЖЕНИЕ.

    (рис 4.6)

    Среди непосредственных потомков узла 1 находим узел, которому приписан элемент $$y$$ с наименьшим ключом, в нашем случае это узел $$5$$ c ключом $$14$$. Меняем местами элементы $$x$$ и $$y$$. В результате получается дерево, изображенное на рис. 4.7.

    (рис 4.7)

    Теперь элемент $$x$$ снова имеет потомка с меньшим, чем у него, ключом (а точнее, оба его потомка имеют меньшие ключи). Снова находим непосредственного потомка элемента $$x$$ с наименьшим ключом, и меняем его и $$x$$ местами. Получается дерево, изображенное на рис. 4.8.

    (рис 4.8)

    Теперь $$x$$ находится в узле 17 и не имеет потомков с меньшим, чем у него, ключом (точнее, у него вообще нет потомков). Операция ПОГРУЖЕНИЕ завершена.

    Вычислительная сложность этой операции пропорциональна числу сравнений элементов и их обменов. Для каждого узла в пути следования данной операции производится $$d$$ сравнений (при поиске потомка с минимальным ключом) и один обмен. Длина этого пути в $$d$$ -куче с $$n$$ узлами не превосходит ее высоты, а именно $$\log_d n$$, по доказанному выше утверждению 1. Значит, время выполнения данной операции — $$O(d\cdot \log_d n)$$.

    Для реализации операции погружения воспользуемся функцией $${\rm minchild} (i)$$, позволяющей для любого узла $$i$$ находить его непосредственного потомка с минимальным ключом. Если у узла $$i$$ нет потомков, то $${\rm minchild} (i) = 0$$.

    Реализация операции ПОГРУЖЕНИЕ

    $$\formula{ \t{procedure ПОГРУЖЕНИЕ}(i);\\ \t begin \\ \mbox{}\q c:= {\rm minchild}(i);\\ \mbox{}\q \t while\ (c \ne 0)\ \t and\ ({\rm key}[c] < {\rm key}[i])\ \t do\ \{{\rm tr}(i, c);\ i:= c;\ c := {\rm minchild} (c)\}\\ \t end; }$$ $$\formula{ \t function\ {\rm minchild} (i);\\ \t begin\ \t if\ i\cdot d + 1 > n\ \t then\ {\rm minchild} := 0\ \t else\\ \mbox{}\q \t begin\\ \mbox{}\q\qq {\rm first\_child}:= i\cdot d + 1;\ {\rm last\_child}:= \min((i + 1)\cdot d - 1, n);\\ \mbox{}\q\qq{\rm min\_key} := {\rm key}[{\rm first\_child}];\\ \mbox{}\q\qq \t for\ i := {\rm first\_child}\ \t to\ {\rm last\_child}\ \t do\ \t if\ {\rm key}[i] > {\rm min\_key}\ \t then\\ \mbox{}\q\qq\qq \{{\rm min\_key} := {\rm key}[i];\ {\rm minchild} := i\}\\ \mbox{}\q \t end\\ \t end }$$

    Операция ВСТАВКА. Если перед выполнением этой операции куча содержала $$n$$ узлов (напомним, что они пронумерованы числами от $$0$$ до $${n - 1}$$ ), то добавляем к дереву $$(n + 1)$$ -й узел (его номер будет $$n$$ ) и приписываем ему элемент с именем $${\rm name}X$$ и ключом $${\rm key}X$$. Вставка нового элемента производится посредством отведения для него места в $$n$$ -ых позициях массивов $$a$$ и $${\rm key}$$ соответственно, после чего к добавленному узлу применяется операция ВСПЛЫТИЕ для восстановления кучеобразного порядка.

    Вставим в $$d$$ -кучу, изображенную на рис. 4.9, новый элемент с ключом $$14$$.

    (рис 4.9)

    Сначала добавляем к дереву новый узел с номером $$17$$ и приписываем ему элемент с ключом $$14$$. Получим дерево, представленное на рис. 4.10.

    (рис 4.10)

    Затем применяем к узлу $$17$$ операцию ВСПЛЫТИЕ. При описании этой операции использовался именно приведенный пример (см. рис. 4.3, 4.4, 4.5).

    Вычислительная сложность данной операции равна константе плюс вычислительная сложность операции ВСПЛЫТИЕ, то есть $$O(\log_d n)$$.

    Реализация операции ВСТАВКА

    $$\formula{ \t{procedure ВСТАВКА}\ ({\rm name}X, {\rm key}X);\\ \t begin\ {a}[{n}]:= {\rm name}X; {\rm key}[{n}]:= {\rm key}X;\ \t{ВСПЛЫТИЕ}\ ({n});\ {n}:= {n} + 1\ \t end; }$$

    Операция УДАЛЕНИЕ. Используется для удаления элемента, приписанного узлу с заданным номером $$i$$. Сначала элемент, приписанный последнему узлу дерева, переносится на место удаляемого элемента, последний узел при этом становится ненужным и поэтому удаляется из дерева. Далее, если узел $$i$$, в который помещен новый элемент, имеет родителя с большим ключом, то к узлу $$i$$ применяется операция ВСПЛЫТИЕ, в противном случае — ПОГРУЖЕНИЕ.

    Таким образом, ориентируясь на худший случай, вычислительную сложность операции УДАЛЕНИЕ оцениваем величиной $$O(d\cdot \log_d n)$$.

    Реализация операции УДАЛЕНИЕ

    $$\formula{ \t{procedure УДАЛЕНИЕ}(i);\\ \t begin\ {a}[{i}]:= {a}[{n} - 1];\ {\rm key}[{i}]:= {\rm key}[{n} - 1];\ {n}:= {n} - 1; \\ \mbox{}\q \t if\ i \ne 0\ \t and\ {\rm key}[{i}] ({\rm key}[(i - 1) \mathop{\rm div} {d}]\ \t then\ \t{ВСПЛЫТИЕ}({i})\\ \mbox{}\q \t else\ \t{ПОГРУЖЕНИЕ}({i})\\ \t end; }$$

    Операция УДАЛЕНИЕ_МИНИМУМА. Эта операция предназначена для взятия из кучи элемента с минимальным ключом (он находится в корне дерева) и удаления его из кучи с помощью операции УДАЛЕНИЕ.

    Реализация операции УДАЛЕНИЕ_МИНИМУМА

    $$\formula{ \t{procedure УДАЛЕНИЕ\_МИНИМУМА}\ ({\rm name}X, {\rm key}X);\\ \t begin\ {\rm name}X:= {a}[0];\ {\rm key}X:= {\rm key}[0];\ \t{УДАЛЕНИЕ}\ (0)\ \t end; }$$

    Функция MINKEY. Эта функция предназначена для определения минимального ключа без удаления соответствующего элемента.

    Реализация функции MINKEY

    $$\formula{ \t function\ {\rm MINKEY};\ \t begin\ {\rm MINKEY}: = {\rm key}[0]\ \t end; }$$

    Трудоемкость операции, очевидно, равна $${O}(1)$$.

    Операция УМЕНЬШЕНИЕ_КЛЮЧА. Предназначена для уменьшения ключа у элемента, приписанного узлу с заданным номером $$i$$, на заданную величину $$\Dl$$. Это действие может нарушить кучеобразный порядок лишь таким образом, что уменьшенный ключ элемента в узле $$i$$ станет меньше ключа элемента в родительском узле. Для восстановления порядка в куче используется операция ВСПЛЫТИЕ.

    Вычислительная сложность данной операции определяется временем, затрачиваемым на уменьшение ключа (то есть константой), и временем выполнения операции ВСПЛЫТИЕ (то есть $$O(\log_d n))$$. В итоге вычислительная сложность операции УМЕНЬШИТЬ_КЛЮЧ равна $$O(\log_d n)$$.

    Реализация операции УМЕНЬШЕНИЕ_КЛЮЧА

    $$\formula{ \t{procedure УМЕНЬШИТЬ\_КЛЮЧ}\ (i, delta);\\ \t begin\ {\rm key}[{i}] := {\rm key}[{i}] - {delta};\ \t{ВСПЛЫТИЕ}\ (i);\ \t end; }$$

    Операция ОКУЧИВАНИЕ. Заметим, что если $$d$$ -куча создается путем $$n$$ -кратного применения операции ВСТАВКА, то суммарная трудоемкость ее создания будет равна $$O(n\cdot \log_d n)$$. Если же все $$n$$ элементов сначала занимают в произвольном порядке массив $$a[0 \ldots (n - 1)]$$ и, соответственно, массив $${\rm key}[0 \ldots ({n} - 1)]$$, то можно превратить их в $$d$$ -кучу, применяя операцию ПОГРУЖЕНИЕ по очереди к узлам $$(n - 1),\,(n - 2),\,\ldots,\,0$$.

    Такой процесс будем называть окучиванием массива. Для доказательства того, что в результате действительно устанавливается кучеобразный порядок, достаточно заметить, что если поддеревья с корнями в узлах $$n - 1,\, n - 2,\, \ldots,\, i + 1$$ упорядочены по правилу кучи, то после применения процедуры ПОГРУЖЕНИЕ к узлу $$i$$ поддерево с корнем в этом узле также станет упорядоченным по правилу кучи. Итак, остановимся на следующей реализации.

    Реализация операции ОКУЧИВАНИЕ

    $$\formula{ \t{procedure ОКУЧИВАНИЕ};\\ \t begin\\ \mbox{}\q \t for\ i:= n - 1\ \t downto\ 0\ \t do\ \t{ПОГРУЖЕНИЕ}\ (i)\\ \t end; }$$

    Утверждение 3. Вычислительная сложность операции ОКУЧИВАНИЕ равна $$O(n)$$.

    Доказательство Заметим, что трудоемкость погружения с высоты $$h$$ равна $$O(h)$$, а количество узлов высоты $$h$$ не превосходит $$n/d^h$$. Осталось оценить сумму$$\eq*{ \suml_{h=1}^{H} h\frac{n}{d^{h}}, }$$ где $$H = \lceil \log_d n\rceil$$, и убедиться, что полученная сумма есть $$O(n)$$.

    Для суммирования можно воспользоваться формулой$$\eq*{ \suml_{i=1}^k \frac{i}{x^i} = \frac{x^{k+1} - (k-1) x + k}{x^k (x-1)^2}. }$$

    Предоставляем читателю возможность завершить доказательство.

    Операция СОЗДАТЬ_СПИСОК_МИНИМАЛЬНЫХ. Эта операция применяется для получения списка элементов, которые имеют ключи, меньшие заданного значения $${\rm key}0$$, и реализуется следующим образом. Если ключ элемента, находящегося в корне, больше, чем $${\rm key}0$$, то это дерево не имеет искомых элементов. В противном случае включаем его в выходной список $$S$$, а затем применяем ту же процедуру ко всем потомкам узла, включенного в список.

    Пусть куча содержит $$k$$ элементов с ключами, меньшими, чем $${\rm key}0$$. По свойству кучи, они все расположены на ее "верхушке". Данная процедура обходит эту верхушку за время, пропорциональное $$k$$, и для каждого из этих $$k$$ элементов просматривает все его $$d$$ (или меньше) непосредственных потомков. Получаем, что время выполнения данной процедуры является величиной $$O(d\cdot k)$$.

    Реализация операции СОЗДАТЬ_СПИСОК_МИНИМАЛЬНЫХ

    $$\formula{ \t{procedure СОЗДАТЬ\_СПИСОК\_МИНИМАЛЬНЫХ}(S, {\rm key}0);\\ \t begin\\ \mbox{}\q \t{Инициализируем пустой список} S; \\ \mbox{}\q \t{Инициализируем стек};\\ \mbox{}\q 0 \Rightarrow \t{стек};\\ \mbox{}\q \t{ while стек не пуст do}\\ \mbox{}\q\qq \t{begin стек} \Rightarrow i;\\ \mbox{}\q\qq\qq if ({\rm key}[i] < {\rm key}0)\ \t{then Добавить} a[i]\ \t{к списку}\ S;\\ \mbox{}\q\qq\qq \t for\ j:=d^\ast i + 1\ \t to\ d^\ast(i + 1)\ \t do if\ j \le (n - 1)\ \t then\ j \Rightarrow \t{стек};\\ \mbox{}\q\qq \t end\\ \t end }$$

    Сводные данные о трудоемкости операций с d-кучами

    ВСПЛЫТИЕ $$(i)$$ $$O(\log_d n)$$
    ПОГРУЖЕНИЕ $$(i)$$ $$O(d \log_d n)$$
    ВСТАВКА $$({\rm name}X, {\rm key}X)$$ $$O(\log_d n)$$
    УДАЛЕНИЕ $$(i)$$ $$O(d \log_d n)$$
    УДАЛЕНИЕ_МИН $$({\rm name}X, {\rm key}X)$$ $$O(d \log_d n)$$
    MINKEY $$O(1)$$
    УМЕНЬШЕНИЕ_КЛЮЧА $$(i, D)$$ $$O(\log_d n)$$
    ОБРАЗОВАTЬ_ОЧЕРЕДЬ $$O(n)$$
    СПИСОК_МИН $$(x, h)$$ $$O(d k)$$

    Замечание. Для $$d$$ -куч "неудобной" является операция слияния куч.

    Применение приоритетных очередей в задаче сортировки

    Под задачей сортировки в простейшем случае понимают следующее: дана последовательность $$({\rm key}[1],\,{\rm key}[2],\, \ldots,\, {\rm key}[{n}])$$ из $$n$$ элементов некоторого линейно упорядоченного множества, например целых или вещественных чисел, записанных в массив $${\rm key}$$. Требуется переставить элементы массива так, чтобы после перестановки выполнялись неравенства:$$\eq*{ {\rm key}[1] \le {\rm key}[2] \le \ldots \le {\rm key} [n]. }$$

    Уточнения этой задачи связаны с теми средствами, с помощью которых предполагается ее решение. Нас интересуют алгоритмы с точки зрения их компьютерной реализации. Оценивая качество различных алгоритмов, обычно интересуются тем, как зависит время счета от длины $$n$$ сортируемой последовательности и требуется ли для этого дополнительная память, размер которой определяется параметром $$n$$. Существенную роль при этом играет метод доступа к элементам памяти. При сортировке во внутренней (оперативной) памяти обычно используется прямой доступ, а во внешней — последовательный.

    Бесхитростная сортировка в памяти с прямым доступом.

    Бесхитростный алгоритм сортировки может заключаться в выполнении следующих операторов:

    $$\formula{ \t for\ {k}:= 1\ \t to\ n - 1\ \t do for\ i:= k+ 1\ \t to\ n\ \t do\ \t if\ {\rm key}[{i}] < {\rm key}[{k}]\ \t then\ {\rm tr}({i},{k}); }$$

    Здесь $${\rm tr}({i},{k})$$ — процедура, транспонирующая элементы $${\rm key}[{i}],\, {\rm key}[{k}]$$. Заметим, что число сравнений$$\eq*{ "{\rm key}[{i}] < {\rm key}[{k}]" }$$ при реализации такого алгоритма равно $$n(n - 1)/2$$. В частности, это означает, что время работы алгоритма равно $$O(n^2)$$.

    Сортировка методом "разделяй и властвуй".

    Предположим, что в нашем распоряжении имеется процедура РАЗДЕЛЯЙ $$(i, j, k)$$, которая по заданным значениям индексов $$i$$, $$j$$ находит некоторое промежуточное значение $$k$$ и переставляет элементы сегмента $${\rm key}[i \ldots j]$$ так, чтобы для $$s = {i},\,{i} + 1,\,\ldots,\,{k} - 1$$ выполнялось неравенство $${\rm key}[{s}] \le {\rm key}[k]$$, а для $$s = k + 1,\, k + 2,\, \ldots, j$$ — неравенство $${\rm key}[{k}] \le {\rm key}[{s}]$$.

    Тогда для сортировки сегмента $${\rm key}[{i} \ldots {j}]$$ может быть использована рекурсивная процедура СОРТИРУЙ.

    $$\formula{ \t{procedure СОРТИРУЙ}\ (i, j);\\ \t begin\ \t if\ {i} = {j}\ \t then\ \t{exit}\ \t else\\ \mbox{}\q \t{РАЗДЕЛЯЙ}\ ({i}, {j}, {k});\ \t{СОРТИРУЙ}\ (i, k - 1);\ \t{СОРТИРУЙ}\ (k + 1, j)\}\\ \t end; }$$

    Для сортировки всего исходного массива достаточно выполнить оператор СОРТИРУЙ $$(1, n)$$.

    Заметим, что если бы процедура РАЗДЕЛЯЙ работала линейное от длины сегмента время и давала значение $$k$$, близкое к середине между $$i$$ и $$j$$, то число обращений к ней приблизительно равнялось бы $$\log n$$ и сортировка всего массива проходила бы за время порядка $$O(n \cdot \log n)$$. Однако можно доказать, что при естественной реализации эта оценка справедлива лишь в среднем.

    Упражнения

  • Разработайте вариант процедуры СОРТИРУЙ без использования рекурсии. Сколько дополнительной памяти требуется для запоминания границ еще не отсортированных сегментов?
  • Охарактеризуйте работу процедуры СОРТИРУЙ на заранее отсортированном массиве.
  • Напишите на известном вам алгоритмическом языке программу сортировки числового массива с помощью процедуры СОРТИРУЙ и испытайте ее на массивах, сгенерированных с помощью датчика случайных чисел.
  • Составьте таблицу, отражающую время работы вашей программы на массивах разной длины. Каков максимальный размер массива, который можно отсортировать составленной программой на вашем компьютере?
  • Сортировка "слиянием".

    Этот метод является разновидностью метода "разделяй и властвуй"; впрочем, уместнее было бы назвать его "властвуй и объединяй".

    Предположим, что у нас есть процедура СЛИВАЙ ( $$i, j, k)$$, которая два уже отсортированных сегмента $${\rm key}[{i}\ldots (j - 1)]$$ и $${\rm key}[{j}\ldots {k}]$$ преобразует (сливает) в один сегмент $${\rm key}[{i}\ldots {k}]$$, делая его полностью отсортированным. Тогда рекурсивная процедура

    $$\formula{ \t{procedure СОРТИРУЙ}\ (i, j);\\ \t begin\ \t{if}\ {i} = {j}\ \t then\ \t{exit}\ \t else\\ \mbox{}\q \{m:= (i + j)\ \t div\ 2;\ \t{СОРТИРУЙ}\ (i, m);\ \t{СОРТИРУЙ}\ (m+1, j);\\ \mbox{}\q \t{СЛИВАЙ} (i, m, j)\}\\ \t end; }$$

    очевидно, сортирует сегмент $${\rm key}[{i}\ldots {j}]$$, а для сортировки всего исходного массива достаточно выполнить оператор СОРТИРУЙ $$(1, n)$$. Как видим, вопрос балансировки размера сегментов решается здесь просто. Число обращений к процедуре СЛИВАЙ $$(i, m, j)$$ равно $$\log n$$, а время ее выполнения легко сделать линейным от суммарной длины сливаемых сегментов.

    Упражнения

  • Разработайте процедуру СЛИВАЙ и вариант процедуры СОРТИРУЙ без использования рекурсии. Сколько дополнительной памяти требуется для ее реализации?
  • Оцените теоретически время работы алгоритма по методу слияния.
  • Напишите на известном вам алгоритмическом языке программу сортировки числового массива методом слияния и испытайте ее на массивах, сгенерированных с помощью датчика случайных чисел.
  • Составьте таблицу, отражающую время работы вашей программы на массивах разной длины. Каков максимальный размер массива, который можно отсортировать составленной программой на вашем компьютере?
  • Сортировка с помощью d-кучи.

    Для представления сортируемой последовательности используем структуру $$d$$-кучи. Сортировку можно провести в два этапа. Данный алгоритм (heapsort) работает для бинарных деревьев (d=2) и это необходимо указывать

  • Окучить сортируемый массив, применяя последовательно операцию ПОГРУЖЕНИЕ по очереди к узлам $$(n - 1),\, (n - 2),\, \ldots,\, 0$$ в предположении, что сначала все $$n$$ ключей занимают в произвольном порядке массив $${\rm key}[0 \ldots n - 1]$$.
  • Осуществить окончательную сортировку следующим образом. Первый (минимальный) элемент кучи меняем местами с последним, уменьшаем размер кучи на 1 (минимальный элемент остается в последней позиции массива $${\rm key}$$, не являясь уже элементом кучи) и применяем операцию ПОГРУЖЕНИЕ к корню, затем повторяем аналогичные действия, пока размер кучи не станет равным 1.
  • Эти два этапа реализуются с помощью процедуры SORT, которая сортирует массив по убыванию ключей:

    $$\formula{ \t{procedure SORT}(n);\\ \t begin\ \t{for}\ i:= n - 1\ \t downto\ 0\ \t do\ \t{ПОГРУЖЕНИЕ}\ (i);\\ \mbox{}\q \t while\ n > 1\ \t do\ \{{\rm tr} (1, n);\ n:= n - 1;\ \t{ПОГРУЖЕНИЕ} (1)\}\\ \t end; }$$

    Заметим, что процедура $$SORT$$ не требует дополнительной памяти, размер которой зависел бы от длины массива $$\rm key$$.

    Упражнения

  • Докажите, что оператор$$\eq*{ \t for\ i := n - 1\ \t downto\ 0\ \t do\ \t{ПОГРУЖЕНИЕ}\ (i); }$$

    в процедуре $$SORT$$ можно заменить оператором

    $$\eq*{ \t for\ i := n \t div\ 2\ \t downto\ 0\ \t do\ \t{ПОГРУЖЕНИЕ}\ (i).$$
  • Напишите программу сортировки числового массива с помощью процедуры $${\rm SORT}(n)$$ и испытайте ее на массивах, сгенерированных с помощью датчика случайных чисел. Составьте таблицу, отражающую время работы вашей программы на массивах разной длины. Каков максимальный размер массива, который можно отсортировать составленной программой на вашем компьютере?
  • Нахождение кратчайших путей в графе

    Входные данные:

  • Граф $$G$$ со взвешенными ребрами (под весами можно понимать длины ребер, если речь идет о геометрическом графе, или любые другие числовые характеристики ребер). Пусть $$L(i, j)$$ — вес ребра ( $$i,j$$ ).
  • Стартовая вершина $$s$$ (вершина, от которой вычисляются расстояния до всех остальных вершин).
  • Выходные данные:

  • Массив $${\rm dist}[1\ldots {n}]$$, $$({\rm dist}[i]$$ — кратчайшее расстояние от вершины $$s$$ до вершины $$i$$ ).
  • Массив $${\rm up}[1\ldots n]$$, $$({\rm up}[i]$$ — предпоследняя вершина в кратчайшем пути из вершины $$s$$ в вершину $$i$$ ).
  • Приводимый ниже алгоритм Дейкстры корректно решает задачу для графов с неотрицательными весами вершин. Если же в графе есть ребра с отрицательными весами, но нет циклов с отрицательным суммарным весом, то для решения задачи можно использовать алгоритм Форда, Беллмана.

    Алгоритм Дейкстры

  • Заполнить массив $${\rm up} [1\ldots n]$$ нулями.
  • Каждой вершине $$i$$ приписать в качестве ключа $${\rm dist} [i]$$ — максимально возможное число (оно должно быть больше, чем длина наибольшего из кратчайших путей в графе; в процессе вычислений это число будет уменьшаться и в итоге заменится на длину кратчайшего пути из вершины $$s$$ в вершину $$i$$ ).
  • Организовать приоритетную очередь из вершин графа, взяв в качестве ключей величины $${\rm dist}[i]$$, $$i= 1, 2 \dts n$$.
  • Заменить ключ вершины $$s$$ на 0.
  • Пока очередь не пуста, выполнять операции $$6,7$$.
  • Выбрать (с удалением) из приоритетной очереди элемент $$r_0$$ с минимальным ключом.
  • Для каждой вершины $$r$$, смежной с $$r_0$$, выполнить операции $$8, 9$$.
  • Вычислить величину $${\rm delta} = {\rm dist}[r] - ({\rm dist}[r_0] + L (r_0, r))$$.
  • Если $${\rm delta} > 0$$, то уменьшить ключ $${\rm dist}[r]$$ элемента $$r$$ на величину $${\rm delta}$$ и заменить старое значение величины $${\rm up}[r]$$ на $$r_0$$.
  • Упражнение

    Напишите на каком-либо алгоритмическом языке реализацию алгоритма Дейкстры с использованием $$d$$ -кучи и испытайте ее на тестовых примерах при различных значениях $$d$$.

    Вернуться к учебному плану