Двоичная куча — различия между версиями
Ильнар (обсуждение | вклад)  (→Построение кучи за O(n))  | 
				м (rollbackEdits.php mass rollback)  | 
				||
| (не показано 18 промежуточных версий 15 участников) | |||
| Строка 4: | Строка 4: | ||
'''Двоичная куча''' или '''пирамида''' (англ. ''Binary heap'') — такое двоичное [[Дерево, эквивалентные определения|подвешенное дерево]], для которого выполнены следующие три условия:  | '''Двоичная куча''' или '''пирамида''' (англ. ''Binary heap'') — такое двоичное [[Дерево, эквивалентные определения|подвешенное дерево]], для которого выполнены следующие три условия:  | ||
| − | * Значение в любой вершине не   | + | * Значение в любой вершине не больше (если куча для минимума), чем значения её потомков.  | 
* На <tex>i</tex>-ом слое <tex>2^i</tex> вершин, кроме последнего. Слои нумеруются с нуля.  | * На <tex>i</tex>-ом слое <tex>2^i</tex> вершин, кроме последнего. Слои нумеруются с нуля.  | ||
* Последний слой заполнен слева направо (как показано на рисунке)  | * Последний слой заполнен слева направо (как показано на рисунке)  | ||
| Строка 22: | Строка 22: | ||
Если в куче изменяется один из элементов, то она может перестать удовлетворять свойству упорядоченности. Для восстановления этого свойства служат процедуры <tex> \mathrm {siftDown} </tex> (просеивание вниз)  | Если в куче изменяется один из элементов, то она может перестать удовлетворять свойству упорядоченности. Для восстановления этого свойства служат процедуры <tex> \mathrm {siftDown} </tex> (просеивание вниз)  | ||
и <tex> \mathrm {siftUp} </tex> (просеивание вверх).    | и <tex> \mathrm {siftUp} </tex> (просеивание вверх).    | ||
| + | |||
| + | ====siftDown====  | ||
Если значение измененного элемента увеличивается, то свойства кучи восстанавливаются функцией <tex> \mathrm {siftDown} </tex>.  | Если значение измененного элемента увеличивается, то свойства кучи восстанавливаются функцией <tex> \mathrm {siftDown} </tex>.  | ||
| + | |||
Работа процедуры: если <tex>i</tex>-й элемент меньше, чем его сыновья, всё поддерево уже является кучей, и делать ничего не надо. В противном случае меняем местами <tex>i</tex>-й элемент с наименьшим из его сыновей, после чего выполняем <tex> \mathrm {siftDown} </tex> для этого сына.  | Работа процедуры: если <tex>i</tex>-й элемент меньше, чем его сыновья, всё поддерево уже является кучей, и делать ничего не надо. В противном случае меняем местами <tex>i</tex>-й элемент с наименьшим из его сыновей, после чего выполняем <tex> \mathrm {siftDown} </tex> для этого сына.  | ||
Процедура выполняется за время <tex>O(\log{n})</tex>.  | Процедура выполняется за время <tex>O(\log{n})</tex>.  | ||
| − | + | ||
<code style="display:inline-block">  | <code style="display:inline-block">  | ||
  '''function''' siftDown(i : '''int'''):  |   '''function''' siftDown(i : '''int'''):  | ||
| − |       '''while''' 2 * i + 1 <  | + |       '''while''' 2 * i + 1 < a.heapSize     <font color = "green">// heapSize {{---}} количество элементов в куче</font>  | 
          left = 2 * i + 1             <font color = "green">// left {{---}} левый сын</font>  |           left = 2 * i + 1             <font color = "green">// left {{---}} левый сын</font>  | ||
          right = 2 * i + 2            <font color = "green">// right {{---}} правый сын</font>  |           right = 2 * i + 2            <font color = "green">// right {{---}} правый сын</font>  | ||
          j = left  |           j = left  | ||
| − |           '''if''' right <  | + |           '''if''' right < a.heapSize '''and''' a[right] < a[left]  | 
              j = right  |               j = right  | ||
| − |           '''if''' a[i] <  | + |           '''if''' a[i] <= a[j]  | 
              '''break'''  |               '''break'''  | ||
          swap(a[i], a[j])  |           swap(a[i], a[j])  | ||
| Строка 48: | Строка 51: | ||
<code style="display:inline-block">  | <code style="display:inline-block">  | ||
  '''function''' siftUp(i : '''int'''):  |   '''function''' siftUp(i : '''int'''):  | ||
| − |       '''while''' a[i] <  | + |       '''while''' a[i] < a[(i - 1) / 2]     <font color = "green">// i <tex>==</tex> 0 {{---}} мы в корне</font>  | 
          swap(a[i], a[(i - 1) / 2])  |           swap(a[i], a[(i - 1) / 2])  | ||
          i = (i - 1) / 2  |           i = (i - 1) / 2  | ||
| Строка 62: | Строка 65: | ||
# Сохранённый элемент возвращается.  | # Сохранённый элемент возвращается.  | ||
| − | |||
  '''int''' extractMin():  |   '''int''' extractMin():  | ||
      '''int''' min = a[0]  |       '''int''' min = a[0]  | ||
| Строка 69: | Строка 71: | ||
      siftDown(0)  |       siftDown(0)  | ||
      '''return''' min  |       '''return''' min  | ||
| − | |||
===Добавление нового элемента===  | ===Добавление нового элемента===  | ||
| Строка 90: | Строка 91: | ||
Дан массив <tex>a[0.. n - 1].</tex> Требуется построить <tex>d</tex>-кучу с минимумом в корне. Наиболее очевидный способ построить такую кучу из неупорядоченного массива {{---}} сделать нулевой элемент массива корнем, а дальше по очереди добавить все его элементы в конец кучи и запускать от каждого добавленного элемента <math>\mathrm {siftUp}</math>. Временная оценка такого алгоритма <tex> O(n\log{n})</tex>. Однако можно построить кучу еще быстрее — за <tex> O(n) </tex>.    | Дан массив <tex>a[0.. n - 1].</tex> Требуется построить <tex>d</tex>-кучу с минимумом в корне. Наиболее очевидный способ построить такую кучу из неупорядоченного массива {{---}} сделать нулевой элемент массива корнем, а дальше по очереди добавить все его элементы в конец кучи и запускать от каждого добавленного элемента <math>\mathrm {siftUp}</math>. Временная оценка такого алгоритма <tex> O(n\log{n})</tex>. Однако можно построить кучу еще быстрее — за <tex> O(n) </tex>.    | ||
| − | Представим, что в массиве хранится дерево (<tex>a[0] - </tex>  корень, а потомками элемента <tex>a[i]</tex> являются <tex>a[  | + | Представим, что в массиве хранится дерево (<tex>a[0] - </tex>  корень, а потомками элемента <tex>a[i]</tex> являются <tex>a[di+1]...a[di+d]</tex>). Сделаем <tex> \mathrm {siftDown} </tex> для вершин, имеющих хотя бы одного потомка: от <tex dpi=140>\dfrac{n}{d}</tex> до <tex>0</tex>,{{---}} так как поддеревья, состоящие из одной вершины без потомков, уже упорядочены.  | 
{{Лемма  | {{Лемма  | ||
|statement= На выходе получим искомую кучу.    | |statement= На выходе получим искомую кучу.    | ||
| − | |proof=   | + | |proof= До вызова <tex> \mathrm {siftDown} </tex> для вершины, ее поддеревья являются кучами. После выполнения <tex> \mathrm {siftDown} </tex> эта вершина с ее поддеревьями будут также являться кучей.  Значит, после выполнения всех <tex> \mathrm {siftDown} </tex> получится куча.  | 
}}  | }}  | ||
{{Лемма  | {{Лемма  | ||
| Строка 120: | Строка 121: | ||
Псевдокод алгоритма:  | Псевдокод алгоритма:  | ||
<code style="display:inline-block">  | <code style="display:inline-block">  | ||
| − |   '''function'''   | + |   '''function''' buldHeap():  | 
      '''for''' i = a.heapSize / 2 '''downto''' 0  |       '''for''' i = a.heapSize / 2 '''downto''' 0  | ||
          siftDown(i)  |           siftDown(i)  | ||
| Строка 131: | Строка 132: | ||
<code style="display:inline-block">  | <code style="display:inline-block">  | ||
  '''function''' merge(a, b : '''Heap'''):  |   '''function''' merge(a, b : '''Heap'''):  | ||
| − |       '''while''' b.heapSize   | + |       '''while''' b.heapSize > 0     | 
          a.insert(b.extractMin())  |           a.insert(b.extractMin())  | ||
| − | </code>    | + | </code>  | 
| + | |||
====Реализация с помощью построения кучи====  | ====Реализация с помощью построения кучи====  | ||
Добавим все элементы кучи <tex>b</tex> в конец массива <tex>a</tex>, после чего вызовем функцию построения кучи. Процедура выполняется за время <tex>O(n + m)</tex>.    | Добавим все элементы кучи <tex>b</tex> в конец массива <tex>a</tex>, после чего вызовем функцию построения кучи. Процедура выполняется за время <tex>O(n + m)</tex>.    | ||
| Строка 145: | Строка 147: | ||
</code>    | </code>    | ||
| − | ===Поиск k-ого элемента===  | + | ===Поиск k-ого элемента (очень коряво расписано с неверными индексами)===  | 
Требуется найти <tex>k</tex>-ый по величине элемент в куче.  | Требуется найти <tex>k</tex>-ый по величине элемент в куче.  | ||
| Строка 153: | Строка 155: | ||
Время работы алгоритма {{---}} <tex>O(k \log k)</tex>.  | Время работы алгоритма {{---}} <tex>O(k \log k)</tex>.  | ||
| + | |||
| + | При <tex>n \lessapprox k \log k </tex> выгоднее запускать [[поиск k-ой порядковой статистики]].  | ||
[[Файл:Min_heap_kth.png|thumb|center|650px|Пример при <tex>k = 5</tex>, красные {{---}} уже удаленные из кучи элементы, зеленые находятся в куче, а голубые {{---}} еще не рассмотрены.]]  | [[Файл:Min_heap_kth.png|thumb|center|650px|Пример при <tex>k = 5</tex>, красные {{---}} уже удаленные из кучи элементы, зеленые находятся в куче, а голубые {{---}} еще не рассмотрены.]]  | ||
Текущая версия на 19:30, 4 сентября 2022
Содержание
Определение
| Определение: | 
Двоичная куча или пирамида (англ. Binary heap) — такое двоичное подвешенное дерево, для которого выполнены следующие три условия:
  | 
Удобнее всего двоичную кучу хранить в виде массива , у которого нулевой элемент, — элемент в корне, а потомками элемента являются и . Высота кучи определяется как высота двоичного дерева. То есть она равна количеству рёбер в самом длинном простом пути, соединяющем корень кучи с одним из её листьев. Высота кучи есть , где — количество узлов дерева.
Чаще всего используют кучи для минимума (когда предок не больше детей) и для максимума (когда предок не меньше детей).
Двоичные кучи используют, например, для того, чтобы извлекать минимум из набора чисел за . Они являются частным случаем приоритетных очередей.
Базовые процедуры
Восстановление свойств кучи
Если в куче изменяется один из элементов, то она может перестать удовлетворять свойству упорядоченности. Для восстановления этого свойства служат процедуры (просеивание вниз) и (просеивание вверх).
siftDown
Если значение измененного элемента увеличивается, то свойства кучи восстанавливаются функцией .
Работа процедуры: если -й элемент меньше, чем его сыновья, всё поддерево уже является кучей, и делать ничего не надо. В противном случае меняем местами -й элемент с наименьшим из его сыновей, после чего выполняем для этого сына. Процедура выполняется за время .
function siftDown(i : int):
    while 2 * i + 1 < a.heapSize     // heapSize — количество элементов в куче
        left = 2 * i + 1             // left — левый сын
        right = 2 * i + 2            // right — правый сын
        j = left
        if right < a.heapSize and a[right] < a[left]
            j = right
        if a[i] <= a[j]
            break
        swap(a[i], a[j])
        i = j
siftUp
Если значение измененного элемента уменьшается, то свойства кучи восстанавливаются функцией .
Работа процедуры: если элемент больше своего отца, условие 1 соблюдено для всего дерева, и больше ничего делать не нужно. Иначе, мы меняем местами его с отцом. После чего выполняем 
для этого отца. Иными словами, слишком маленький элемент всплывает наверх.
Процедура выполняется за время . 
function siftUp(i : int):
    while a[i] < a[(i - 1) / 2]     // i  0 — мы в корне
        swap(a[i], a[(i - 1) / 2])
        i = (i - 1) / 2
Извлечение минимального элемента
Выполняет извлечение минимального элемента из кучи за время . Извлечение выполняется в четыре этапа:
- Значение корневого элемента (он и является минимальным) сохраняется для последующего возврата.
 - Последний элемент копируется в корень, после чего удаляется из кучи.
 - Вызывается для корня.
 - Сохранённый элемент возвращается.
 
int extractMin():
    int min = a[0]
    a[0] = a[a.heapSize - 1]
    a.heapSize = a.heapSize - 1
    siftDown(0)
    return min
Добавление нового элемента
Выполняет добавление элемента в кучу за время . Добавление произвольного элемента в конец кучи, и восстановление свойства упорядоченности с помощью процедуры .
function insert(key : int):
    a.heapSize = a.heapSize + 1
    a[a.heapSize - 1] = key
    siftUp(a.heapSize - 1)
Построение кучи за O(n)
| Определение: | 
| -куча — это куча, в которой у каждого элемента, кроме, возможно, элементов на последнем уровне, ровно потомков. | 
Дан массив  Требуется построить -кучу с минимумом в корне. Наиболее очевидный способ построить такую кучу из неупорядоченного массива — сделать нулевой элемент массива корнем, а дальше по очереди добавить все его элементы в конец кучи и запускать от каждого добавленного элемента . Временная оценка такого алгоритма . Однако можно построить кучу еще быстрее — за . 
Представим, что в массиве хранится дерево ( корень, а потомками элемента являются ). Сделаем для вершин, имеющих хотя бы одного потомка: от до ,— так как поддеревья, состоящие из одной вершины без потомков, уже упорядочены.
| Лемма: | 
На выходе получим искомую кучу.  | 
| Доказательство: | 
| До вызова для вершины, ее поддеревья являются кучами. После выполнения эта вершина с ее поддеревьями будут также являться кучей. Значит, после выполнения всех получится куча. | 
| Лемма: | ||||||
Время работы этого алгоритма .  | ||||||
| Доказательство: | ||||||
| 
 Число вершин на высоте в куче из элементов не превосходит . Высота кучи не превосходит . Обозначим за высоту дерева, тогда время построения не превосходит 
 Докажем вспомогательную лемму о сумме ряда. 
  | ||||||
Псевдокод алгоритма:
function buldHeap():
    for i = a.heapSize / 2 downto 0
        siftDown(i)
Слияние двух куч
Даны две кучи и , размерами и , требуется объединить эти две кучи.
Наивная реализация
Поочередно добавим все элементы из  в . Время работы — .
function merge(a, b : Heap):
    while b.heapSize > 0  
        a.insert(b.extractMin())
Реализация с помощью построения кучи
Добавим все элементы кучи в конец массива , после чего вызовем функцию построения кучи. Процедура выполняется за время .
function merge(a, b : Heap):
    for i = 0 to b.heapSize - 1  
        a.heapSize = a.heapSize + 1
        a[a.heapSize - 1] = b[i]
    a.heapify()
Поиск k-ого элемента (очень коряво расписано с неверными индексами)
Требуется найти -ый по величине элемент в куче.
- Создаем новую кучу, в которой будем хранить пару , где — значение элемента, а — индекс элемента в основном массиве, и добавляем в нее корень кучи.
 - Возьмем корень новой кучи и добавим её детей из основной кучи, после чего удалим корень. Проделаем этот шаг раз.
 - В корне новой кучи будет находиться ответ.
 
Время работы алгоритма — .
При выгоднее запускать поиск k-ой порядковой статистики.